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