// Test GoAWK Lexer package lexer_test import ( "fmt" "strconv" "strings" "testing" . "github.com/benhoyt/goawk/lexer" ) func TestLexer(t *testing.T) { tests := []struct { input string output string }{ // Comments, whitespace, line continuations {"+# foo \n- #foo", `1:1 + "", 1:8 "", 2:1 - ""`}, {"+\\\n-", `1:1 + "", 2:1 - ""`}, {"+\\\r\n-", `1:1 + "", 2:1 - ""`}, {"+\\-", `1:1 + "", 1:3 "expected \\n after \\ line continuation"`}, // Names and keywords {"x", `1:1 name "x"`}, {"x y0", `1:1 name "x", 1:3 name "y0"`}, {"x 0y", `1:1 name "x", 1:3 number "0", 1:4 name "y"`}, {"sub SUB", `1:1 sub "", 1:5 name "SUB"`}, // String tokens {`"foo"`, `1:1 string "foo"`}, {`"a\t\r\n\z\'\"\a\b\f\vb"`, `1:1 string "a\t\r\nz'\"\a\b\f\vb"`}, {`"x`, `1:3 "didn't find end quote in string"`}, {`"foo\"`, `1:7 "didn't find end quote in string"`}, {"\"x\n\"", `1:3 "can't have newline in string"`}, {`'foo'`, `1:1 string "foo"`}, {`'a\t\r\n\z\'\"b'`, `1:1 string "a\t\r\nz'\"b"`}, {`'x`, `1:3 "didn't find end quote in string"`}, {"'x\n'", `1:3 "can't have newline in string"`}, {`"\x0.\x00.\x0A\x10\xff\xFF\x41"`, `1:1 string "\x00.\x00.\n\x10\xff\xffA"`}, {`"\xg"`, `1:4 "1 or 2 hex digits expected"`}, {`"\0\78\7\77\777\0 \141 "`, `1:1 string "\x00\a8\a?\xff\x00 a "`}, {`"\ufg\uffg\uabcg\u201C\u1F602"`, `1:1 string "\x0fgÿg઼g“😂"`}, {`"\u201C"`, `1:1 string "“"`}, {`"\u00000041Z"`, `1:1 string "AZ"`}, {`"\ug"`, `1:4 "1-8 hex digits expected"`}, {`"\uffffffffg"`, `1:12 "invalid Unicode character"`}, // Number tokens {"0", `1:1 number "0"`}, {"9", `1:1 number "9"`}, {" 0 ", `1:2 number "0"`}, {"\n 1", `1:1 "", 2:3 number "1"`}, {"1234", `1:1 number "1234"`}, {".5", `1:1 number ".5"`}, {".5e1", `1:1 number ".5e1"`}, {"5e+1", `1:1 number "5e+1"`}, {"5e-1", `1:1 number "5e-1"`}, {"0.", `1:1 number "0."`}, {"42e", `1:1 number "42", 1:3 name "e"`}, {"4.2e", `1:1 number "4.2", 1:4 name "e"`}, {"1.e3", `1:1 number "1.e3"`}, {"1.e3", `1:1 number "1.e3"`}, {"1e3foo", `1:1 number "1e3", 1:4 name "foo"`}, {"1e3+", `1:1 number "1e3", 1:4 + ""`}, {"1e3.4", `1:1 number "1e3", 1:4 number ".4"`}, {"1e-", `1:1 number "1", 1:2 name "e", 1:3 - ""`}, {"1e+", `1:1 number "1", 1:2 name "e", 1:3 + ""`}, {"42`", `1:1 number "42", 1:3 "unexpected char"`}, {"0..", `1:1 number "0.", 1:4 "expected digits"`}, {".", `1:2 "expected digits"`}, // Misc errors {"&=", `1:2 "unexpected char after '&'"`}, } for _, test := range tests { t.Run(test.input, func(t *testing.T) { l := NewLexer([]byte(test.input)) strs := []string{} for { pos, tok, val := l.Scan() if tok == EOF { break } if tok == NUMBER { // Ensure ParseFloat() works, as that's what our // parser uses to convert trimmed := strings.TrimRight(val, "eE") _, err := strconv.ParseFloat(trimmed, 64) if err != nil { t.Fatalf("couldn't parse float: %q", val) } } strs = append(strs, fmt.Sprintf("%d:%d %s %q", pos.Line, pos.Column, tok, val)) if tok == ILLEGAL { break } } output := strings.Join(strs, ", ") if output != test.output { t.Errorf("expected %q, got %q", test.output, output) } }) } } func TestRegex(t *testing.T) { tests := []struct { input string output string }{ {`/foo/`, `1:1 regex "foo"`}, {`/=foo/`, `1:1 regex "=foo"`}, {`/a\/b/`, `1:1 regex "a/b"`}, {`/a\/\zb/`, `1:1 regex "a/\\zb"`}, {`/a`, `1:3 "didn't find end slash in regex"`}, {"/a\n", `1:3 "can't have newline in regex"`}, } for _, test := range tests { t.Run(test.input, func(t *testing.T) { l := NewLexer([]byte(test.input)) l.Scan() // Scan first token (probably DIV) pos, tok, val := l.ScanRegex() output := fmt.Sprintf("%d:%d %s %q", pos.Line, pos.Column, tok, val) if output != test.output { t.Errorf("expected %q, got %q", test.output, output) } }) } } func TestScanRegexInvalid(t *testing.T) { defer func() { r := recover() if message, ok := r.(string); ok { expected := "ScanRegex should only be called after DIV or DIV_ASSIGN token" if message != expected { t.Fatalf("expected %q, got %q", expected, message) } } else { t.Fatalf("expected panic of string type") } }() l := NewLexer([]byte("foo/")) l.Scan() // Scan first token (NAME foo) l.ScanRegex() } func TestHadSpace(t *testing.T) { tests := []struct { input string tokens []Token spaces []bool }{ {`foo(x)`, []Token{NAME, LPAREN, NAME, RPAREN}, []bool{false, false, false, false}}, {`foo (x) `, []Token{NAME, LPAREN, NAME, RPAREN}, []bool{false, true, false, false}}, {` foo ( x ) `, []Token{NAME, LPAREN, NAME, RPAREN}, []bool{true, true, true, true}}, } for _, test := range tests { t.Run(test.input, func(t *testing.T) { l := NewLexer([]byte(test.input)) for i := 0; ; i++ { _, tok, _ := l.Scan() if tok == EOF { break } if tok != test.tokens[i] { t.Errorf("expected %s for token %d, got %s", test.tokens[i], i, tok) } if l.HadSpace() != test.spaces[i] { t.Errorf("expected %v for space %d, got %v", test.spaces[i], i, l.HadSpace()) } } }) } } func TestPeekByte(t *testing.T) { l := NewLexer([]byte("foo()")) b := l.PeekByte() if b != 'f' { t.Errorf("expected 'f', got %q", b) } _, tok, _ := l.Scan() if tok != NAME { t.Errorf("expected name, got %s", tok) } b = l.PeekByte() if b != '(' { t.Errorf("expected '(', got %q", b) } _, tok, _ = l.Scan() if tok != LPAREN { t.Errorf("expected (, got %s", tok) } _, tok, _ = l.Scan() if tok != RPAREN { t.Errorf("expected ), got %s", tok) } b = l.PeekByte() if b != 0 { t.Errorf("expected 0, got %q", b) } } func TestKeywordToken(t *testing.T) { tests := []struct { name string tok Token }{ {"print", PRINT}, {"split", F_SPLIT}, {"BEGIN", BEGIN}, {"foo", ILLEGAL}, {"GoAWK", ILLEGAL}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { tok := KeywordToken(test.name) if tok != test.tok { t.Errorf("expected %v, got %v", test.tok, tok) } }) } } func TestAllTokens(t *testing.T) { input := "# comment line\n" + "+ += && = : , -- /\n/= $ @ == >= > >> ++ { [ < ( #\n" + "<= ~ % %= * *= !~ ! != | || ^ ^= ** **= ? } ] ) ; - -= " + "BEGIN break continue delete do else END exit " + "for function getline if in next nextfile print printf return while " + "atan2 close cos exp fflush gsub index int length log match rand " + "sin split sprintf sqrt srand sub substr system tolower toupper " + "x \"str\\n\" 1234\n" + "` ." strs := make([]string, 0, LAST+1) seen := make([]bool, LAST+1) l := NewLexer([]byte(input)) for { _, tok, _ := l.Scan() strs = append(strs, tok.String()) seen[int(tok)] = true if tok == EOF { break } } output := strings.Join(strs, " ") expected := " " + "+ += && = : , -- / /= $ @ == >= > >> ++ { [ < ( " + "<= ~ % %= * *= !~ ! != | || ^ ^= ^ ^= ? } ] ) ; - -= " + "BEGIN break continue delete do else END exit " + "for function getline if in next nextfile print printf return while " + "atan2 close cos exp fflush gsub index int length log match rand " + "sin split sprintf sqrt srand sub substr system tolower toupper " + "name string number " + " EOF" if output != expected { t.Errorf("expected %q, got %q", expected, output) } for i, s := range seen { if !s && Token(i) != CONCAT && Token(i) != REGEX { t.Errorf("token %s (%d) not seen", Token(i), i) } } l = NewLexer([]byte(`/foo/`)) _, tok1, _ := l.Scan() _, tok2, val := l.ScanRegex() if tok1 != DIV || tok2 != REGEX || val != "foo" { t.Errorf(`expected / regex "foo", got %s %s %q`, tok1, tok2, val) } l = NewLexer([]byte(`/=foo/`)) _, tok1, _ = l.Scan() _, tok2, val = l.ScanRegex() if tok1 != DIV_ASSIGN || tok2 != REGEX || val != "=foo" { t.Errorf(`expected /= regex "=foo", got %s %s %q`, tok1, tok2, val) } } func TestUnescape(t *testing.T) { tests := []struct { input string output string error string }{ {``, "", ""}, {`foo bar`, "foo bar", ""}, {`foo\tbar`, "foo\tbar", ""}, {"foo\nbar", "", "can't have newline in string"}, {`foo"`, "foo\"", ""}, {`O'Connor`, "O'Connor", ""}, {`foo\`, "foo\\", ""}, // Other cases tested in TestLexer string handling. } for _, test := range tests { t.Run(test.input, func(t *testing.T) { got, err := Unescape(test.input) if err != nil { if err.Error() != test.error { t.Fatalf("expected error %q, got %q", test.error, err) } } else { if test.error != "" { t.Fatalf("expected error %q, got %q", test.error, "") } if got != test.output { t.Fatalf("expected %q, got %q", test.output, got) } } }) } } func benchmarkLexer(b *testing.B, repeat int, source string) { fullSource := []byte(strings.Repeat(source+"\n", repeat)) b.ResetTimer() for i := 0; i < b.N; i++ { l := NewLexer(fullSource) for { _, tok, _ := l.Scan() if tok == EOF || tok == ILLEGAL { break } } } } func BenchmarkProgram(b *testing.B) { benchmarkLexer(b, 5, `{ print $1, ($3+$4)*$5 }`) } func BenchmarkNames(b *testing.B) { benchmarkLexer(b, 5, `x y i foobar abcdefghij0123456789 _`) } func BenchmarkKeywords(b *testing.B) { benchmarkLexer(b, 5, `BEGIN END print sub if length`) } func BenchmarkSimpleTokens(b *testing.B) { benchmarkLexer(b, 5, "\n : , { [ ( } ] ) ~ ? ; $") } func BenchmarkChoiceTokens(b *testing.B) { benchmarkLexer(b, 5, `/ /= % %= + ++ += * ** **= *= = == ^ ^= ! != !~ < <= > >= >> && | ||`) } func BenchmarkNumbers(b *testing.B) { benchmarkLexer(b, 5, `0 1 .5 1234 1234567890 1234.56789e-50`) } func BenchmarkStrings(b *testing.B) { benchmarkLexer(b, 5, `"x" "y" "xyz" "foo" "foo bar baz" "foo\tbar\rbaz\n"`) } func BenchmarkRegex(b *testing.B) { source := `/x/ /./ /foo/ /bar/ /=equals=/ /\/\/\/\//` fullSource := []byte(strings.Repeat(source+" ", 5)) b.ResetTimer() for i := 0; i < b.N; i++ { l := NewLexer(fullSource) for { _, tok, _ := l.Scan() if tok == EOF { break } if tok != DIV && tok != DIV_ASSIGN { b.Fatalf("expected / or /=, got %s", tok) } _, tok, _ = l.ScanRegex() if tok != REGEX { b.Fatalf("expected regex, got %s", tok) } } } } func Example() { lexer := NewLexer([]byte(`$0 { print $1 }`)) for { pos, tok, val := lexer.Scan() if tok == EOF { break } fmt.Printf("%d:%d %s %q\n", pos.Line, pos.Column, tok, val) } // Output: // 1:1 $ "" // 1:2 number "0" // 1:4 { "" // 1:6 print "" // 1:12 $ "" // 1:13 number "1" // 1:15 } "" }