github.com/pingcap/tidb/parser@v0.0.0-20231013125129-93a834a6bf8d/lexer_test.go (about) 1 // Copyright 2016 PingCAP, Inc. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // See the License for the specific language governing permissions and 12 // limitations under the License. 13 14 package parser 15 16 import ( 17 "fmt" 18 "testing" 19 "unicode" 20 21 "github.com/pingcap/tidb/parser/mysql" 22 requires "github.com/stretchr/testify/require" 23 ) 24 25 func TestTokenID(t *testing.T) { 26 for str, tok := range tokenMap { 27 l := NewScanner(str) 28 var v yySymType 29 tok1 := l.Lex(&v) 30 requires.Equal(t, tok1, tok) 31 } 32 } 33 34 func TestSingleChar(t *testing.T) { 35 table := []byte{'|', '&', '-', '+', '*', '/', '%', '^', '~', '(', ',', ')'} 36 for _, tok := range table { 37 l := NewScanner(string(tok)) 38 var v yySymType 39 tok1 := l.Lex(&v) 40 requires.Equal(t, tok1, int(tok)) 41 } 42 } 43 44 type testCaseItem struct { 45 str string 46 tok int 47 } 48 49 type testLiteralValue struct { 50 str string 51 val interface{} 52 } 53 54 func TestSingleCharOther(t *testing.T) { 55 table := []testCaseItem{ 56 {"AT", identifier}, 57 {"?", paramMarker}, 58 {"PLACEHOLDER", identifier}, 59 {"=", eq}, 60 {".", int('.')}, 61 } 62 runTest(t, table) 63 } 64 65 func TestAtLeadingIdentifier(t *testing.T) { 66 table := []testCaseItem{ 67 {"@", singleAtIdentifier}, 68 {"@''", singleAtIdentifier}, 69 {"@1", singleAtIdentifier}, 70 {"@.1_", singleAtIdentifier}, 71 {"@-1.", singleAtIdentifier}, 72 {"@~", singleAtIdentifier}, 73 {"@$", singleAtIdentifier}, 74 {"@a_3cbbc", singleAtIdentifier}, 75 {"@`a_3cbbc`", singleAtIdentifier}, 76 {"@-3cbbc", singleAtIdentifier}, 77 {"@!3cbbc", singleAtIdentifier}, 78 {"@@global.test", doubleAtIdentifier}, 79 {"@@session.test", doubleAtIdentifier}, 80 {"@@local.test", doubleAtIdentifier}, 81 {"@@test", doubleAtIdentifier}, 82 {"@@global.`test`", doubleAtIdentifier}, 83 {"@@session.`test`", doubleAtIdentifier}, 84 {"@@local.`test`", doubleAtIdentifier}, 85 {"@@`test`", doubleAtIdentifier}, 86 } 87 runTest(t, table) 88 } 89 90 func TestUnderscoreCS(t *testing.T) { 91 var v yySymType 92 scanner := NewScanner(`_utf8"string"`) 93 tok := scanner.Lex(&v) 94 requires.Equal(t, underscoreCS, tok) 95 tok = scanner.Lex(&v) 96 requires.Equal(t, stringLit, tok) 97 98 scanner.reset("N'string'") 99 tok = scanner.Lex(&v) 100 requires.Equal(t, underscoreCS, tok) 101 tok = scanner.Lex(&v) 102 requires.Equal(t, stringLit, tok) 103 } 104 105 func TestLiteral(t *testing.T) { 106 table := []testCaseItem{ 107 {`'''a'''`, stringLit}, 108 {`''a''`, stringLit}, 109 {`""a""`, stringLit}, 110 {`\'a\'`, int('\\')}, 111 {`\"a\"`, int('\\')}, 112 {"0.2314", decLit}, 113 {"1234567890123456789012345678901234567890", decLit}, 114 {"132.313", decLit}, 115 {"132.3e231", floatLit}, 116 {"132.3e-231", floatLit}, 117 {"001e-12", floatLit}, 118 {"23416", intLit}, 119 {"123test", identifier}, 120 {"123" + string(unicode.ReplacementChar) + "xxx", identifier}, 121 {"0", intLit}, 122 {"0x3c26", hexLit}, 123 {"x'13181C76734725455A'", hexLit}, 124 {"0b01", bitLit}, 125 {fmt.Sprintf("t1%c", 0), identifier}, 126 {"N'some text'", underscoreCS}, 127 {"n'some text'", underscoreCS}, 128 {"\\N", null}, 129 {".*", int('.')}, // `.`, `*` 130 {".1_t_1_x", decLit}, // `.1`, `_t_1_x` 131 {"9e9e", floatLit}, // 9e9e = 9e9 + e 132 {".1e", invalid}, 133 // Issue #3954 134 {".1e23", floatLit}, // `.1e23` 135 {".123", decLit}, // `.123` 136 {".1*23", decLit}, // `.1`, `*`, `23` 137 {".1,23", decLit}, // `.1`, `,`, `23` 138 {".1 23", decLit}, // `.1`, `23` 139 {".1$23", decLit}, // `.1`, `$23` 140 {".1a23", decLit}, // `.1`, `a23` 141 {".1e23$23", floatLit}, // `.1e23`, `$23` 142 {".1e23a23", floatLit}, // `.1e23`, `a23` 143 {".1C23", decLit}, // `.1`, `C23` 144 {".1\u0081", decLit}, // `.1`, `\u0081` 145 {".1\uff34", decLit}, // `.1`, `\uff34` 146 {`b''`, bitLit}, 147 {`b'0101'`, bitLit}, 148 {`0b0101`, bitLit}, 149 } 150 runTest(t, table) 151 } 152 153 func TestLiteralValue(t *testing.T) { 154 table := []testLiteralValue{ 155 {`'''a'''`, `'a'`}, 156 {`''a''`, ``}, 157 {`""a""`, ``}, 158 {`\'a\'`, `\`}, 159 {`\"a\"`, `\`}, 160 {"0.2314", "0.2314"}, 161 {"1234567890123456789012345678901234567890", "1234567890123456789012345678901234567890"}, 162 {"132.313", "132.313"}, 163 {"132.3e231", 1.323e+233}, 164 {"132.3e-231", 1.323e-229}, 165 {"001e-12", 1e-12}, 166 {"23416", int64(23416)}, 167 {"123test", "123test"}, 168 {"123" + string(unicode.ReplacementChar) + "xxx", "123" + string(unicode.ReplacementChar) + "xxx"}, 169 {"0", int64(0)}, 170 {"0x3c26", "[60 38]"}, 171 {"x'13181C76734725455A'", "[19 24 28 118 115 71 37 69 90]"}, 172 {"0b01", "[1]"}, 173 {fmt.Sprintf("t1%c", 0), "t1"}, 174 {"N'some text'", "utf8"}, 175 {"n'some text'", "utf8"}, 176 {"\\N", `\N`}, 177 {".*", `.`}, // `.`, `*` 178 {".1_t_1_x", "0.1"}, // `.1`, `_t_1_x` 179 {"9e9e", float64(9000000000)}, // 9e9e = 9e9 + e 180 {".1e", ""}, 181 // Issue #3954 182 {".1e23", float64(10000000000000000000000)}, // `.1e23` 183 {".123", "0.123"}, // `.123` 184 {".1*23", "0.1"}, // `.1`, `*`, `23` 185 {".1,23", "0.1"}, // `.1`, `,`, `23` 186 {".1 23", "0.1"}, // `.1`, `23` 187 {".1$23", "0.1"}, // `.1`, `$23` 188 {".1a23", "0.1"}, // `.1`, `a23` 189 {".1e23$23", float64(10000000000000000000000)}, // `.1e23`, `$23` 190 {".1e23a23", float64(10000000000000000000000)}, // `.1e23`, `a23` 191 {".1C23", "0.1"}, // `.1`, `C23` 192 {".1\u0081", "0.1"}, // `.1`, `\u0081` 193 {".1\uff34", "0.1"}, // `.1`, `\uff34` 194 {`b''`, "[]"}, 195 {`b'0101'`, "[5]"}, 196 {`0b0101`, "[5]"}, 197 } 198 runLiteralTest(t, table) 199 } 200 201 func runTest(t *testing.T, table []testCaseItem) { 202 var val yySymType 203 for _, v := range table { 204 l := NewScanner(v.str) 205 tok := l.Lex(&val) 206 requires.Equal(t, v.tok, tok, v.str) 207 } 208 } 209 210 func runLiteralTest(t *testing.T, table []testLiteralValue) { 211 for _, v := range table { 212 l := NewScanner(v.str) 213 val := l.LexLiteral() 214 switch val.(type) { 215 case int64: 216 requires.Equal(t, v.val, val, v.str) 217 case float64: 218 requires.Equal(t, v.val, val, v.str) 219 case string: 220 requires.Equal(t, v.val, val, v.str) 221 default: 222 requires.Equal(t, v.val, fmt.Sprint(val), v.str) 223 } 224 } 225 } 226 227 func TestComment(t *testing.T) { 228 table := []testCaseItem{ 229 {"-- select --\n1", intLit}, 230 {"/*!40101 SET character_set_client = utf8 */;", set}, 231 {"/* SET character_set_client = utf8 */;", int(';')}, 232 {"/* some comments */ SELECT ", selectKwd}, 233 {`-- comment continues to the end of line 234 SELECT`, selectKwd}, 235 {`# comment continues to the end of line 236 SELECT`, selectKwd}, 237 {"#comment\n123", intLit}, 238 {"--5", int('-')}, 239 {"--\nSELECT", selectKwd}, 240 {"--\tSELECT", 0}, 241 {"--\r\nSELECT", selectKwd}, 242 {"--", 0}, 243 244 // The odd behavior of '*/' inside conditional comment is the same as 245 // that of MySQL. 246 {"/*T![unsupported] '*/0 -- ' */", intLit}, // equivalent to 0 247 {"/*T![auto_rand] '*/0 -- ' */", stringLit}, // equivalent to '*/0 -- ' 248 } 249 runTest(t, table) 250 } 251 252 func TestScanQuotedIdent(t *testing.T) { 253 l := NewScanner("`fk`") 254 l.r.peek() 255 tok, pos, lit := scanQuotedIdent(l) 256 requires.Zero(t, pos.Offset) 257 requires.Equal(t, quotedIdentifier, tok) 258 requires.Equal(t, "fk", lit) 259 } 260 261 func TestScanString(t *testing.T) { 262 table := []struct { 263 raw string 264 expect string 265 }{ 266 {`' \n\tTest String'`, " \n\tTest String"}, 267 {`'\x\B'`, "xB"}, 268 {`'\0\'\"\b\n\r\t\\'`, "\000'\"\b\n\r\t\\"}, 269 {`'\Z'`, "\x1a"}, 270 {`'\%\_'`, `\%\_`}, 271 {`'hello'`, "hello"}, 272 {`'"hello"'`, `"hello"`}, 273 {`'""hello""'`, `""hello""`}, 274 {`'hel''lo'`, "hel'lo"}, 275 {`'\'hello'`, "'hello"}, 276 {`"hello"`, "hello"}, 277 {`"'hello'"`, "'hello'"}, 278 {`"''hello''"`, "''hello''"}, 279 {`"hel""lo"`, `hel"lo`}, 280 {`"\"hello"`, `"hello`}, 281 {`'disappearing\ backslash'`, "disappearing backslash"}, 282 {"'한국의中文UTF8およびテキストトラック'", "한국의中文UTF8およびテキストトラック"}, 283 {"'\\a\x90'", "a\x90"}, 284 {"'\\a\x18èàø»\x05'", "a\x18èàø»\x05"}, 285 } 286 287 for _, v := range table { 288 l := NewScanner(v.raw) 289 tok, pos, lit := l.scan() 290 requires.Zero(t, pos.Offset) 291 requires.Equal(t, stringLit, tok) 292 requires.Equal(t, v.expect, lit) 293 } 294 } 295 296 func TestScanStringWithNoBackslashEscapesMode(t *testing.T) { 297 table := []struct { 298 raw string 299 expect string 300 }{ 301 {`' \n\tTest String'`, ` \n\tTest String`}, 302 {`'\x\B'`, `\x\B`}, 303 {`'\0\\''"\b\n\r\t\'`, `\0\\'"\b\n\r\t\`}, 304 {`'\Z'`, `\Z`}, 305 {`'\%\_'`, `\%\_`}, 306 {`'hello'`, "hello"}, 307 {`'"hello"'`, `"hello"`}, 308 {`'""hello""'`, `""hello""`}, 309 {`'hel''lo'`, "hel'lo"}, 310 {`'\'hello'`, `\`}, 311 {`"hello"`, "hello"}, 312 {`"'hello'"`, "'hello'"}, 313 {`"''hello''"`, "''hello''"}, 314 {`"hel""lo"`, `hel"lo`}, 315 {`"\"hello"`, `\`}, 316 {"'한국의中文UTF8およびテキストトラック'", "한국의中文UTF8およびテキストトラック"}, 317 } 318 l := NewScanner("") 319 l.SetSQLMode(mysql.ModeNoBackslashEscapes) 320 for _, v := range table { 321 l.reset(v.raw) 322 tok, pos, lit := l.scan() 323 requires.Zero(t, pos.Offset) 324 requires.Equal(t, stringLit, tok) 325 requires.Equal(t, v.expect, lit) 326 } 327 } 328 329 func TestIdentifier(t *testing.T) { 330 table := [][2]string{ 331 {`哈哈`, "哈哈"}, 332 {"`numeric`", "numeric"}, 333 {"\r\n \r \n \tthere\t \n", "there"}, 334 {`5number`, `5number`}, 335 {"1_x", "1_x"}, 336 {"0_x", "0_x"}, 337 {string(unicode.ReplacementChar) + "xxx", string(unicode.ReplacementChar) + "xxx"}, 338 {"9e", "9e"}, 339 {"0b", "0b"}, 340 {"0b123", "0b123"}, 341 {"0b1ab", "0b1ab"}, 342 {"0B01", "0B01"}, 343 {"0x", "0x"}, 344 {"0x7fz3", "0x7fz3"}, 345 {"023a4", "023a4"}, 346 {"9eTSs", "9eTSs"}, 347 {fmt.Sprintf("t1%cxxx", 0), "t1"}, 348 } 349 l := &Scanner{} 350 for _, item := range table { 351 l.reset(item[0]) 352 var v yySymType 353 tok := l.Lex(&v) 354 requires.Equal(t, identifier, tok, item) 355 requires.Equal(t, item[1], v.ident, item) 356 } 357 } 358 359 func TestSpecialComment(t *testing.T) { 360 l := NewScanner("/*!40101 select\n5*/") 361 tok, pos, lit := l.scan() 362 requires.Equal(t, identifier, tok) 363 requires.Equal(t, "select", lit) 364 requires.Equal(t, Pos{1, 9, 9}, pos) 365 366 tok, pos, lit = l.scan() 367 requires.Equal(t, intLit, tok) 368 requires.Equal(t, "5", lit) 369 requires.Equal(t, Pos{2, 1, 16}, pos) 370 } 371 372 func TestFeatureIDsComment(t *testing.T) { 373 l := NewScanner("/*T![auto_rand] auto_random(5) */") 374 tok, pos, lit := l.scan() 375 requires.Equal(t, identifier, tok) 376 requires.Equal(t, "auto_random", lit) 377 requires.Equal(t, Pos{1, 16, 16}, pos) 378 tok, _, _ = l.scan() 379 requires.Equal(t, int('('), tok) 380 _, pos, lit = l.scan() 381 requires.Equal(t, "5", lit) 382 requires.Equal(t, Pos{1, 28, 28}, pos) 383 tok, _, _ = l.scan() 384 requires.Equal(t, int(')'), tok) 385 386 l = NewScanner("/*T![unsupported_feature] unsupported(123) */") 387 tok, _, _ = l.scan() 388 requires.Equal(t, 0, tok) 389 } 390 391 func TestOptimizerHint(t *testing.T) { 392 l := NewScanner("SELECT /*+ BKA(t1) */ 0;") 393 tokens := []struct { 394 tok int 395 ident string 396 pos int 397 }{ 398 {selectKwd, "SELECT", 0}, 399 {hintComment, "/*+ BKA(t1) */", 7}, 400 {intLit, "0", 22}, 401 {';', ";", 23}, 402 } 403 for i := 0; ; i++ { 404 var sym yySymType 405 tok := l.Lex(&sym) 406 if tok == 0 { 407 return 408 } 409 requires.Equal(t, tokens[i].tok, tok, i) 410 requires.Equal(t, tokens[i].ident, sym.ident, i) 411 requires.Equal(t, tokens[i].pos, sym.offset, i) 412 } 413 } 414 415 func TestOptimizerHintAfterCertainKeywordOnly(t *testing.T) { 416 tests := []struct { 417 input string 418 tokens []int 419 }{ 420 { 421 input: "SELECT /*+ hint */ *", 422 tokens: []int{selectKwd, hintComment, '*', 0}, 423 }, 424 { 425 input: "UPDATE /*+ hint */", 426 tokens: []int{update, hintComment, 0}, 427 }, 428 { 429 input: "INSERT /*+ hint */", 430 tokens: []int{insert, hintComment, 0}, 431 }, 432 { 433 input: "REPLACE /*+ hint */", 434 tokens: []int{replace, hintComment, 0}, 435 }, 436 { 437 input: "DELETE /*+ hint */", 438 tokens: []int{deleteKwd, hintComment, 0}, 439 }, 440 { 441 input: "CREATE /*+ hint */", 442 tokens: []int{create, hintComment, 0}, 443 }, 444 { 445 input: "/*+ hint */ SELECT *", 446 tokens: []int{selectKwd, '*', 0}, 447 }, 448 { 449 input: "SELECT /* comment */ /*+ hint */ *", 450 tokens: []int{selectKwd, hintComment, '*', 0}, 451 }, 452 { 453 input: "SELECT * /*+ hint */", 454 tokens: []int{selectKwd, '*', 0}, 455 }, 456 { 457 input: "SELECT /*T![auto_rand] * */ /*+ hint */", 458 tokens: []int{selectKwd, '*', 0}, 459 }, 460 { 461 input: "SELECT /*T![unsupported] * */ /*+ hint */", 462 tokens: []int{selectKwd, hintComment, 0}, 463 }, 464 { 465 input: "SELECT /*+ hint1 */ /*+ hint2 */ *", 466 tokens: []int{selectKwd, hintComment, '*', 0}, 467 }, 468 { 469 input: "SELECT * FROM /*+ hint */", 470 tokens: []int{selectKwd, '*', from, 0}, 471 }, 472 { 473 input: "`SELECT` /*+ hint */", 474 tokens: []int{identifier, 0}, 475 }, 476 { 477 input: "'SELECT' /*+ hint */", 478 tokens: []int{stringLit, 0}, 479 }, 480 } 481 482 for _, tc := range tests { 483 scanner := NewScanner(tc.input) 484 var sym yySymType 485 for i := 0; ; i++ { 486 tok := scanner.Lex(&sym) 487 requires.Equalf(t, tc.tokens[i], tok, "input = [%s], i = %d", tc.input, i) 488 if tok == 0 { 489 break 490 } 491 } 492 } 493 } 494 495 func TestInt(t *testing.T) { 496 tests := []struct { 497 input string 498 expect uint64 499 }{ 500 {"01000001783", 1000001783}, 501 {"00001783", 1783}, 502 {"0", 0}, 503 {"0000", 0}, 504 {"01", 1}, 505 {"10", 10}, 506 } 507 scanner := NewScanner("") 508 for _, test := range tests { 509 var v yySymType 510 scanner.reset(test.input) 511 tok := scanner.Lex(&v) 512 requires.Equal(t, intLit, tok) 513 switch i := v.item.(type) { 514 case int64: 515 requires.Equal(t, test.expect, uint64(i)) 516 case uint64: 517 requires.Equal(t, test.expect, i) 518 default: 519 t.Fail() 520 } 521 } 522 } 523 524 func TestSQLModeANSIQuotes(t *testing.T) { 525 tests := []struct { 526 input string 527 tok int 528 ident string 529 }{ 530 {`"identifier"`, identifier, "identifier"}, 531 {"`identifier`", identifier, "identifier"}, 532 {`"identifier""and"`, identifier, `identifier"and`}, 533 {`'string''string'`, stringLit, "string'string"}, 534 {`"identifier"'and'`, identifier, "identifier"}, 535 {`'string'"identifier"`, stringLit, "string"}, 536 } 537 scanner := NewScanner("") 538 scanner.SetSQLMode(mysql.ModeANSIQuotes) 539 for _, test := range tests { 540 var v yySymType 541 scanner.reset(test.input) 542 tok := scanner.Lex(&v) 543 requires.Equal(t, test.tok, tok) 544 requires.Equal(t, test.ident, v.ident) 545 } 546 scanner.reset(`'string' 'string'`) 547 var v yySymType 548 tok := scanner.Lex(&v) 549 requires.Equal(t, stringLit, tok) 550 requires.Equal(t, "string", v.ident) 551 tok = scanner.Lex(&v) 552 requires.Equal(t, stringLit, tok) 553 requires.Equal(t, "string", v.ident) 554 } 555 556 func TestIllegal(t *testing.T) { 557 table := []testCaseItem{ 558 {"'", invalid}, 559 {"'fu", invalid}, 560 {"'\\n", invalid}, 561 {"'\\", invalid}, 562 {fmt.Sprintf("%c", 0), invalid}, 563 {"`", invalid}, 564 {`"`, invalid}, 565 {"@`", invalid}, 566 {"@'", invalid}, 567 {`@"`, invalid}, 568 {"@@`", invalid}, 569 {"@@global.`", invalid}, 570 } 571 runTest(t, table) 572 } 573 574 func TestVersionDigits(t *testing.T) { 575 tests := []struct { 576 input string 577 min int 578 max int 579 nextChar byte 580 }{ 581 { 582 input: "12345", 583 min: 5, 584 max: 5, 585 nextChar: 0, 586 }, 587 { 588 input: "12345xyz", 589 min: 5, 590 max: 5, 591 nextChar: 'x', 592 }, 593 { 594 input: "1234xyz", 595 min: 5, 596 max: 5, 597 nextChar: '1', 598 }, 599 { 600 input: "123456", 601 min: 5, 602 max: 5, 603 nextChar: '6', 604 }, 605 { 606 input: "1234", 607 min: 5, 608 max: 5, 609 nextChar: '1', 610 }, 611 { 612 input: "", 613 min: 5, 614 max: 5, 615 nextChar: 0, 616 }, 617 { 618 input: "1234567xyz", 619 min: 5, 620 max: 6, 621 nextChar: '7', 622 }, 623 { 624 input: "12345xyz", 625 min: 5, 626 max: 6, 627 nextChar: 'x', 628 }, 629 { 630 input: "12345", 631 min: 5, 632 max: 6, 633 nextChar: 0, 634 }, 635 { 636 input: "1234xyz", 637 min: 5, 638 max: 6, 639 nextChar: '1', 640 }, 641 } 642 643 scanner := NewScanner("") 644 for _, test := range tests { 645 scanner.reset(test.input) 646 scanner.scanVersionDigits(test.min, test.max) 647 nextChar := scanner.r.readByte() 648 requires.Equalf(t, test.nextChar, nextChar, "input = %s", test.input) 649 } 650 } 651 652 func TestFeatureIDs(t *testing.T) { 653 tests := []struct { 654 input string 655 featureIDs []string 656 nextChar byte 657 }{ 658 { 659 input: "[feature]", 660 featureIDs: []string{"feature"}, 661 nextChar: 0, 662 }, 663 { 664 input: "[feature] xx", 665 featureIDs: []string{"feature"}, 666 nextChar: ' ', 667 }, 668 { 669 input: "[feature1,feature2]", 670 featureIDs: []string{"feature1", "feature2"}, 671 nextChar: 0, 672 }, 673 { 674 input: "[feature1,feature2,feature3]", 675 featureIDs: []string{"feature1", "feature2", "feature3"}, 676 nextChar: 0, 677 }, 678 { 679 input: "[id_en_ti_fier]", 680 featureIDs: []string{"id_en_ti_fier"}, 681 nextChar: 0, 682 }, 683 { 684 input: "[invalid, whitespace]", 685 featureIDs: nil, 686 nextChar: '[', 687 }, 688 { 689 input: "[unclosed_brac", 690 featureIDs: nil, 691 nextChar: '[', 692 }, 693 { 694 input: "unclosed_brac]", 695 featureIDs: nil, 696 nextChar: 'u', 697 }, 698 { 699 input: "[invalid_comma,]", 700 featureIDs: nil, 701 nextChar: '[', 702 }, 703 { 704 input: "[,]", 705 featureIDs: nil, 706 nextChar: '[', 707 }, 708 { 709 input: "[]", 710 featureIDs: nil, 711 nextChar: '[', 712 }, 713 } 714 scanner := NewScanner("") 715 for _, test := range tests { 716 scanner.reset(test.input) 717 featureIDs := scanner.scanFeatureIDs() 718 requires.Equalf(t, test.featureIDs, featureIDs, "input = %s", test.input) 719 nextChar := scanner.r.readByte() 720 requires.Equalf(t, test.nextChar, nextChar, "input = %s", test.input) 721 } 722 }