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