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