github.com/Elate-DevOps/migrate/v4@v4.0.12/database/pgx/v5/pgx_test.go (about) 1 package pgx 2 3 // error codes https://github.com/jackc/pgerrcode/blob/master/errcode.go 4 5 import ( 6 "context" 7 "database/sql" 8 sqldriver "database/sql/driver" 9 "errors" 10 "fmt" 11 "io" 12 "log" 13 "strconv" 14 "strings" 15 "sync" 16 "testing" 17 18 "github.com/Elate-DevOps/migrate/v4" 19 20 "github.com/dhui/dktest" 21 22 "github.com/Elate-DevOps/migrate/v4/database" 23 dt "github.com/Elate-DevOps/migrate/v4/database/testing" 24 "github.com/Elate-DevOps/migrate/v4/dktesting" 25 _ "github.com/Elate-DevOps/migrate/v4/source/file" 26 ) 27 28 const ( 29 pgPassword = "postgres" 30 ) 31 32 var ( 33 opts = dktest.Options{ 34 Env: map[string]string{"POSTGRES_PASSWORD": pgPassword}, 35 PortRequired: true, ReadyFunc: isReady, 36 } 37 // Supported versions: https://www.postgresql.org/support/versioning/ 38 specs = []dktesting.ContainerSpec{ 39 {ImageName: "postgres:9.5", Options: opts}, 40 {ImageName: "postgres:9.6", Options: opts}, 41 {ImageName: "postgres:10", Options: opts}, 42 {ImageName: "postgres:11", Options: opts}, 43 {ImageName: "postgres:12", Options: opts}, 44 {ImageName: "postgres:13", Options: opts}, 45 {ImageName: "postgres:14", Options: opts}, 46 {ImageName: "postgres:15", Options: opts}, 47 } 48 ) 49 50 func pgConnectionString(host, port string, options ...string) string { 51 options = append(options, "sslmode=disable") 52 return fmt.Sprintf("postgres://postgres:%s@%s:%s/postgres?%s", pgPassword, host, port, strings.Join(options, "&")) 53 } 54 55 func isReady(ctx context.Context, c dktest.ContainerInfo) bool { 56 ip, port, err := c.FirstPort() 57 if err != nil { 58 return false 59 } 60 61 db, err := sql.Open("pgx", pgConnectionString(ip, port)) 62 if err != nil { 63 return false 64 } 65 defer func() { 66 if err := db.Close(); err != nil { 67 log.Println("close error:", err) 68 } 69 }() 70 if err = db.PingContext(ctx); err != nil { 71 switch err { 72 case sqldriver.ErrBadConn, io.EOF: 73 return false 74 default: 75 log.Println(err) 76 } 77 return false 78 } 79 80 return true 81 } 82 83 func mustRun(t *testing.T, d database.Driver, statements []string) { 84 for _, statement := range statements { 85 if err := d.Run(strings.NewReader(statement)); err != nil { 86 t.Fatal(err) 87 } 88 } 89 } 90 91 func Test(t *testing.T) { 92 dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) { 93 ip, port, err := c.FirstPort() 94 if err != nil { 95 t.Fatal(err) 96 } 97 98 addr := pgConnectionString(ip, port) 99 p := &Postgres{} 100 d, err := p.Open(addr) 101 if err != nil { 102 t.Fatal(err) 103 } 104 defer func() { 105 if err := d.Close(); err != nil { 106 t.Error(err) 107 } 108 }() 109 dt.Test(t, d, []byte("SELECT 1")) 110 }) 111 } 112 113 func TestMigrate(t *testing.T) { 114 dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) { 115 ip, port, err := c.FirstPort() 116 if err != nil { 117 t.Fatal(err) 118 } 119 120 addr := pgConnectionString(ip, port) 121 p := &Postgres{} 122 d, err := p.Open(addr) 123 if err != nil { 124 t.Fatal(err) 125 } 126 defer func() { 127 if err := d.Close(); err != nil { 128 t.Error(err) 129 } 130 }() 131 m, err := migrate.NewWithDatabaseInstance("file://../examples/migrations", "pgx", d) 132 if err != nil { 133 t.Fatal(err) 134 } 135 dt.TestMigrate(t, m) 136 }) 137 } 138 139 func TestMultipleStatements(t *testing.T) { 140 dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) { 141 ip, port, err := c.FirstPort() 142 if err != nil { 143 t.Fatal(err) 144 } 145 146 addr := pgConnectionString(ip, port) 147 p := &Postgres{} 148 d, err := p.Open(addr) 149 if err != nil { 150 t.Fatal(err) 151 } 152 defer func() { 153 if err := d.Close(); err != nil { 154 t.Error(err) 155 } 156 }() 157 if err := d.Run(strings.NewReader("CREATE TABLE foo (foo text); CREATE TABLE bar (bar text);")); err != nil { 158 t.Fatalf("expected err to be nil, got %v", err) 159 } 160 161 // make sure second table exists 162 var exists bool 163 if err := d.(*Postgres).conn.QueryRowContext(context.Background(), "SELECT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'bar' AND table_schema = (SELECT current_schema()))").Scan(&exists); err != nil { 164 t.Fatal(err) 165 } 166 if !exists { 167 t.Fatalf("expected table bar to exist") 168 } 169 }) 170 } 171 172 func TestMultipleStatementsInMultiStatementMode(t *testing.T) { 173 dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) { 174 ip, port, err := c.FirstPort() 175 if err != nil { 176 t.Fatal(err) 177 } 178 179 addr := pgConnectionString(ip, port, "x-multi-statement=true") 180 p := &Postgres{} 181 d, err := p.Open(addr) 182 if err != nil { 183 t.Fatal(err) 184 } 185 defer func() { 186 if err := d.Close(); err != nil { 187 t.Error(err) 188 } 189 }() 190 if err := d.Run(strings.NewReader("CREATE TABLE foo (foo text); CREATE INDEX CONCURRENTLY idx_foo ON foo (foo);")); err != nil { 191 t.Fatalf("expected err to be nil, got %v", err) 192 } 193 194 // make sure created index exists 195 var exists bool 196 if err := d.(*Postgres).conn.QueryRowContext(context.Background(), "SELECT EXISTS (SELECT 1 FROM pg_indexes WHERE schemaname = (SELECT current_schema()) AND indexname = 'idx_foo')").Scan(&exists); err != nil { 197 t.Fatal(err) 198 } 199 if !exists { 200 t.Fatalf("expected table bar to exist") 201 } 202 }) 203 } 204 205 func TestErrorParsing(t *testing.T) { 206 dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) { 207 ip, port, err := c.FirstPort() 208 if err != nil { 209 t.Fatal(err) 210 } 211 212 addr := pgConnectionString(ip, port) 213 p := &Postgres{} 214 d, err := p.Open(addr) 215 if err != nil { 216 t.Fatal(err) 217 } 218 defer func() { 219 if err := d.Close(); err != nil { 220 t.Error(err) 221 } 222 }() 223 224 wantErr := `migration failed: syntax error at or near "TABLEE" (column 37) in line 1: CREATE TABLE foo ` + 225 `(foo text); CREATE TABLEE bar (bar text); (details: ERROR: syntax error at or near "TABLEE" (SQLSTATE 42601))` 226 if err := d.Run(strings.NewReader("CREATE TABLE foo (foo text); CREATE TABLEE bar (bar text);")); err == nil { 227 t.Fatal("expected err but got nil") 228 } else if err.Error() != wantErr { 229 t.Fatalf("expected '%s' but got '%s'", wantErr, err.Error()) 230 } 231 }) 232 } 233 234 func TestFilterCustomQuery(t *testing.T) { 235 dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) { 236 ip, port, err := c.FirstPort() 237 if err != nil { 238 t.Fatal(err) 239 } 240 241 addr := pgConnectionString(ip, port, "x-custom=foobar") 242 p := &Postgres{} 243 d, err := p.Open(addr) 244 if err != nil { 245 t.Fatal(err) 246 } 247 defer func() { 248 if err := d.Close(); err != nil { 249 t.Error(err) 250 } 251 }() 252 }) 253 } 254 255 func TestWithSchema(t *testing.T) { 256 dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) { 257 ip, port, err := c.FirstPort() 258 if err != nil { 259 t.Fatal(err) 260 } 261 262 addr := pgConnectionString(ip, port) 263 p := &Postgres{} 264 d, err := p.Open(addr) 265 if err != nil { 266 t.Fatal(err) 267 } 268 defer func() { 269 if err := d.Close(); err != nil { 270 t.Fatal(err) 271 } 272 }() 273 274 // create foobar schema 275 if err := d.Run(strings.NewReader("CREATE SCHEMA foobar AUTHORIZATION postgres")); err != nil { 276 t.Fatal(err) 277 } 278 if err := d.SetVersion(1, false); err != nil { 279 t.Fatal(err) 280 } 281 282 // re-connect using that schema 283 d2, err := p.Open(pgConnectionString(ip, port, "search_path=foobar")) 284 if err != nil { 285 t.Fatal(err) 286 } 287 defer func() { 288 if err := d2.Close(); err != nil { 289 t.Fatal(err) 290 } 291 }() 292 293 version, _, err := d2.Version() 294 if err != nil { 295 t.Fatal(err) 296 } 297 if version != database.NilVersion { 298 t.Fatal("expected NilVersion") 299 } 300 301 // now update version and compare 302 if err := d2.SetVersion(2, false); err != nil { 303 t.Fatal(err) 304 } 305 version, _, err = d2.Version() 306 if err != nil { 307 t.Fatal(err) 308 } 309 if version != 2 { 310 t.Fatal("expected version 2") 311 } 312 313 // meanwhile, the public schema still has the other version 314 version, _, err = d.Version() 315 if err != nil { 316 t.Fatal(err) 317 } 318 if version != 1 { 319 t.Fatal("expected version 2") 320 } 321 }) 322 } 323 324 func TestMigrationTableOption(t *testing.T) { 325 dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) { 326 ip, port, err := c.FirstPort() 327 if err != nil { 328 t.Fatal(err) 329 } 330 331 addr := pgConnectionString(ip, port) 332 p := &Postgres{} 333 d, _ := p.Open(addr) 334 defer func() { 335 if err := d.Close(); err != nil { 336 t.Fatal(err) 337 } 338 }() 339 340 // create migrate schema 341 if err := d.Run(strings.NewReader("CREATE SCHEMA migrate AUTHORIZATION postgres")); err != nil { 342 t.Fatal(err) 343 } 344 345 // bad unquoted x-migrations-table parameter 346 wantErr := "x-migrations-table must be quoted (for instance '\"migrate\".\"schema_migrations\"') when x-migrations-table-quoted is enabled, current value is: migrate.schema_migrations" 347 d, err = p.Open(fmt.Sprintf("postgres://postgres:%s@%v:%v/postgres?sslmode=disable&x-migrations-table=migrate.schema_migrations&x-migrations-table-quoted=1", 348 pgPassword, ip, port)) 349 if (err != nil) && (err.Error() != wantErr) { 350 t.Fatalf("expected '%s' but got '%s'", wantErr, err.Error()) 351 } 352 353 // too many quoted x-migrations-table parameters 354 wantErr = "\"\"migrate\".\"schema_migrations\".\"toomany\"\" MigrationsTable contains too many dot characters" 355 d, err = p.Open(fmt.Sprintf("postgres://postgres:%s@%v:%v/postgres?sslmode=disable&x-migrations-table=\"migrate\".\"schema_migrations\".\"toomany\"&x-migrations-table-quoted=1", 356 pgPassword, ip, port)) 357 if (err != nil) && (err.Error() != wantErr) { 358 t.Fatalf("expected '%s' but got '%s'", wantErr, err.Error()) 359 } 360 361 // good quoted x-migrations-table parameter 362 d, err = p.Open(fmt.Sprintf("postgres://postgres:%s@%v:%v/postgres?sslmode=disable&x-migrations-table=\"migrate\".\"schema_migrations\"&x-migrations-table-quoted=1", 363 pgPassword, ip, port)) 364 if err != nil { 365 t.Fatal(err) 366 } 367 368 // make sure migrate.schema_migrations table exists 369 var exists bool 370 if err := d.(*Postgres).conn.QueryRowContext(context.Background(), "SELECT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'schema_migrations' AND table_schema = 'migrate')").Scan(&exists); err != nil { 371 t.Fatal(err) 372 } 373 if !exists { 374 t.Fatalf("expected table migrate.schema_migrations to exist") 375 } 376 377 d, err = p.Open(fmt.Sprintf("postgres://postgres:%s@%v:%v/postgres?sslmode=disable&x-migrations-table=migrate.schema_migrations", 378 pgPassword, ip, port)) 379 if err != nil { 380 t.Fatal(err) 381 } 382 if err := d.(*Postgres).conn.QueryRowContext(context.Background(), "SELECT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'migrate.schema_migrations' AND table_schema = (SELECT current_schema()))").Scan(&exists); err != nil { 383 t.Fatal(err) 384 } 385 if !exists { 386 t.Fatalf("expected table 'migrate.schema_migrations' to exist") 387 } 388 }) 389 } 390 391 func TestFailToCreateTableWithoutPermissions(t *testing.T) { 392 dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) { 393 ip, port, err := c.FirstPort() 394 if err != nil { 395 t.Fatal(err) 396 } 397 398 addr := pgConnectionString(ip, port) 399 400 // Check that opening the postgres connection returns NilVersion 401 p := &Postgres{} 402 403 d, err := p.Open(addr) 404 if err != nil { 405 t.Fatal(err) 406 } 407 408 defer func() { 409 if err := d.Close(); err != nil { 410 t.Error(err) 411 } 412 }() 413 414 // create user who is not the owner. Although we're concatenating strings in an sql statement it should be fine 415 // since this is a test environment and we're not expecting to the pgPassword to be malicious 416 mustRun(t, d, []string{ 417 "CREATE USER not_owner WITH ENCRYPTED PASSWORD '" + pgPassword + "'", 418 "CREATE SCHEMA barfoo AUTHORIZATION postgres", 419 "GRANT USAGE ON SCHEMA barfoo TO not_owner", 420 "REVOKE CREATE ON SCHEMA barfoo FROM PUBLIC", 421 "REVOKE CREATE ON SCHEMA barfoo FROM not_owner", 422 }) 423 424 // re-connect using that schema 425 d2, err := p.Open(fmt.Sprintf("postgres://not_owner:%s@%v:%v/postgres?sslmode=disable&search_path=barfoo", 426 pgPassword, ip, port)) 427 428 defer func() { 429 if d2 == nil { 430 return 431 } 432 if err := d2.Close(); err != nil { 433 t.Fatal(err) 434 } 435 }() 436 437 var e *database.Error 438 if !errors.As(err, &e) || err == nil { 439 t.Fatal("Unexpected error, want permission denied error. Got: ", err) 440 } 441 442 if !strings.Contains(e.OrigErr.Error(), "permission denied for schema barfoo") { 443 t.Fatal(e) 444 } 445 446 // re-connect using that x-migrations-table and x-migrations-table-quoted 447 d2, err = p.Open(fmt.Sprintf("postgres://not_owner:%s@%v:%v/postgres?sslmode=disable&x-migrations-table=\"barfoo\".\"schema_migrations\"&x-migrations-table-quoted=1", 448 pgPassword, ip, port)) 449 450 if !errors.As(err, &e) || err == nil { 451 t.Fatal("Unexpected error, want permission denied error. Got: ", err) 452 } 453 454 if !strings.Contains(e.OrigErr.Error(), "permission denied for schema barfoo") { 455 t.Fatal(e) 456 } 457 }) 458 } 459 460 func TestCheckBeforeCreateTable(t *testing.T) { 461 dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) { 462 ip, port, err := c.FirstPort() 463 if err != nil { 464 t.Fatal(err) 465 } 466 467 addr := pgConnectionString(ip, port) 468 469 // Check that opening the postgres connection returns NilVersion 470 p := &Postgres{} 471 472 d, err := p.Open(addr) 473 if err != nil { 474 t.Fatal(err) 475 } 476 477 defer func() { 478 if err := d.Close(); err != nil { 479 t.Error(err) 480 } 481 }() 482 483 // create user who is not the owner. Although we're concatenating strings in an sql statement it should be fine 484 // since this is a test environment and we're not expecting to the pgPassword to be malicious 485 mustRun(t, d, []string{ 486 "CREATE USER not_owner WITH ENCRYPTED PASSWORD '" + pgPassword + "'", 487 "CREATE SCHEMA barfoo AUTHORIZATION postgres", 488 "GRANT USAGE ON SCHEMA barfoo TO not_owner", 489 "GRANT CREATE ON SCHEMA barfoo TO not_owner", 490 }) 491 492 // re-connect using that schema 493 d2, err := p.Open(fmt.Sprintf("postgres://not_owner:%s@%v:%v/postgres?sslmode=disable&search_path=barfoo", 494 pgPassword, ip, port)) 495 if err != nil { 496 t.Fatal(err) 497 } 498 499 if err := d2.Close(); err != nil { 500 t.Fatal(err) 501 } 502 503 // revoke privileges 504 mustRun(t, d, []string{ 505 "REVOKE CREATE ON SCHEMA barfoo FROM PUBLIC", 506 "REVOKE CREATE ON SCHEMA barfoo FROM not_owner", 507 }) 508 509 // re-connect using that schema 510 d3, err := p.Open(fmt.Sprintf("postgres://not_owner:%s@%v:%v/postgres?sslmode=disable&search_path=barfoo", 511 pgPassword, ip, port)) 512 if err != nil { 513 t.Fatal(err) 514 } 515 516 version, _, err := d3.Version() 517 if err != nil { 518 t.Fatal(err) 519 } 520 521 if version != database.NilVersion { 522 t.Fatal("Unexpected version, want database.NilVersion. Got: ", version) 523 } 524 525 defer func() { 526 if err := d3.Close(); err != nil { 527 t.Fatal(err) 528 } 529 }() 530 }) 531 } 532 533 func TestParallelSchema(t *testing.T) { 534 dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) { 535 ip, port, err := c.FirstPort() 536 if err != nil { 537 t.Fatal(err) 538 } 539 540 addr := pgConnectionString(ip, port) 541 p := &Postgres{} 542 d, err := p.Open(addr) 543 if err != nil { 544 t.Fatal(err) 545 } 546 defer func() { 547 if err := d.Close(); err != nil { 548 t.Error(err) 549 } 550 }() 551 552 // create foo and bar schemas 553 if err := d.Run(strings.NewReader("CREATE SCHEMA foo AUTHORIZATION postgres")); err != nil { 554 t.Fatal(err) 555 } 556 if err := d.Run(strings.NewReader("CREATE SCHEMA bar AUTHORIZATION postgres")); err != nil { 557 t.Fatal(err) 558 } 559 560 // re-connect using that schemas 561 dfoo, err := p.Open(pgConnectionString(ip, port, "search_path=foo")) 562 if err != nil { 563 t.Fatal(err) 564 } 565 defer func() { 566 if err := dfoo.Close(); err != nil { 567 t.Error(err) 568 } 569 }() 570 571 dbar, err := p.Open(pgConnectionString(ip, port, "search_path=bar")) 572 if err != nil { 573 t.Fatal(err) 574 } 575 defer func() { 576 if err := dbar.Close(); err != nil { 577 t.Error(err) 578 } 579 }() 580 581 if err := dfoo.Lock(); err != nil { 582 t.Fatal(err) 583 } 584 585 if err := dbar.Lock(); err != nil { 586 t.Fatal(err) 587 } 588 589 if err := dbar.Unlock(); err != nil { 590 t.Fatal(err) 591 } 592 593 if err := dfoo.Unlock(); err != nil { 594 t.Fatal(err) 595 } 596 }) 597 } 598 599 func TestPostgres_Lock(t *testing.T) { 600 dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) { 601 ip, port, err := c.FirstPort() 602 if err != nil { 603 t.Fatal(err) 604 } 605 606 addr := pgConnectionString(ip, port) 607 p := &Postgres{} 608 d, err := p.Open(addr) 609 if err != nil { 610 t.Fatal(err) 611 } 612 613 dt.Test(t, d, []byte("SELECT 1")) 614 615 ps := d.(*Postgres) 616 617 err = ps.Lock() 618 if err != nil { 619 t.Fatal(err) 620 } 621 622 err = ps.Unlock() 623 if err != nil { 624 t.Fatal(err) 625 } 626 627 err = ps.Lock() 628 if err != nil { 629 t.Fatal(err) 630 } 631 632 err = ps.Unlock() 633 if err != nil { 634 t.Fatal(err) 635 } 636 }) 637 } 638 639 func TestWithInstance_Concurrent(t *testing.T) { 640 dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) { 641 ip, port, err := c.FirstPort() 642 if err != nil { 643 t.Fatal(err) 644 } 645 646 // The number of concurrent processes running WithInstance 647 const concurrency = 30 648 649 // We can instantiate a single database handle because it is 650 // actually a connection pool, and so, each of the below go 651 // routines will have a high probability of using a separate 652 // connection, which is something we want to exercise. 653 db, err := sql.Open("pgx", pgConnectionString(ip, port)) 654 if err != nil { 655 t.Fatal(err) 656 } 657 defer func() { 658 if err := db.Close(); err != nil { 659 t.Error(err) 660 } 661 }() 662 663 db.SetMaxIdleConns(concurrency) 664 db.SetMaxOpenConns(concurrency) 665 666 var wg sync.WaitGroup 667 defer wg.Wait() 668 669 wg.Add(concurrency) 670 for i := 0; i < concurrency; i++ { 671 go func(i int) { 672 defer wg.Done() 673 _, err := WithInstance(db, &Config{}) 674 if err != nil { 675 t.Errorf("process %d error: %s", i, err) 676 } 677 }(i) 678 } 679 }) 680 } 681 682 func Test_computeLineFromPos(t *testing.T) { 683 testcases := []struct { 684 pos int 685 wantLine uint 686 wantCol uint 687 input string 688 wantOk bool 689 }{ 690 { 691 15, 2, 6, "SELECT *\nFROM foo", true, // foo table does not exists 692 }, 693 { 694 16, 3, 6, "SELECT *\n\nFROM foo", true, // foo table does not exists, empty line 695 }, 696 { 697 25, 3, 7, "SELECT *\nFROM foo\nWHERE x", true, // x column error 698 }, 699 { 700 27, 5, 7, "SELECT *\n\nFROM foo\n\nWHERE x", true, // x column error, empty lines 701 }, 702 { 703 10, 2, 1, "SELECT *\nFROMM foo", true, // FROMM typo 704 }, 705 { 706 11, 3, 1, "SELECT *\n\nFROMM foo", true, // FROMM typo, empty line 707 }, 708 { 709 17, 2, 8, "SELECT *\nFROM foo", true, // last character 710 }, 711 { 712 18, 0, 0, "SELECT *\nFROM foo", false, // invalid position 713 }, 714 } 715 for i, tc := range testcases { 716 t.Run("tc"+strconv.Itoa(i), func(t *testing.T) { 717 run := func(crlf bool, nonASCII bool) { 718 var name string 719 if crlf { 720 name = "crlf" 721 } else { 722 name = "lf" 723 } 724 if nonASCII { 725 name += "-nonascii" 726 } else { 727 name += "-ascii" 728 } 729 t.Run(name, func(t *testing.T) { 730 input := tc.input 731 if crlf { 732 input = strings.Replace(input, "\n", "\r\n", -1) 733 } 734 if nonASCII { 735 input = strings.Replace(input, "FROM", "FRÖM", -1) 736 } 737 gotLine, gotCol, gotOK := computeLineFromPos(input, tc.pos) 738 739 if tc.wantOk { 740 t.Logf("pos %d, want %d:%d, %#v", tc.pos, tc.wantLine, tc.wantCol, input) 741 } 742 743 if gotOK != tc.wantOk { 744 t.Fatalf("expected ok %v but got %v", tc.wantOk, gotOK) 745 } 746 if gotLine != tc.wantLine { 747 t.Fatalf("expected line %d but got %d", tc.wantLine, gotLine) 748 } 749 if gotCol != tc.wantCol { 750 t.Fatalf("expected col %d but got %d", tc.wantCol, gotCol) 751 } 752 }) 753 } 754 run(false, false) 755 run(true, false) 756 run(false, true) 757 run(true, true) 758 }) 759 } 760 }