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  }