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