// Copyright (C) 2022-2023 Luke Shumaker // // SPDX-License-Identifier: GPL-2.0-or-later package lowmemjson import ( "fmt" "io" "strings" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "git.lukeshu.com/go/lowmemjson/internal" ) type ReadRuneTypeResult struct { r rune s int t internal.RuneType e error } func (r ReadRuneTypeResult) String() string { return fmt.Sprintf("{%q, %d, %#v, %v}", r.r, r.s, r.t, r.e) } type runeTypeScannerTestcase struct { Input string ExpRemainder string Exp []ReadRuneTypeResult } func testRuneTypeScanner(t *testing.T, testcases map[string]runeTypeScannerTestcase, factory func(io.RuneScanner) runeTypeScanner) { for tcName, tc := range testcases { tc := tc t.Run(tcName, func(t *testing.T) { t.Parallel() reader := strings.NewReader(tc.Input) sc := factory(reader) var exp, act []string for _, iExp := range tc.Exp { var iAct ReadRuneTypeResult if iExp.s < 0 { iAct.s = iExp.s iAct.e = sc.UnreadRune() } else { iAct.r, iAct.s, iAct.t, iAct.e = sc.ReadRuneType() } exp = append(exp, iExp.String()) act = append(act, iAct.String()) } assert.Equal(t, exp, act) assert.Equal(t, tc.ExpRemainder, tc.Input[len(tc.Input)-reader.Len():]) }) } } func TestRuneTypeScanner(t *testing.T) { t.Parallel() testcases := map[string]runeTypeScannerTestcase{ "basic": {`{"foo": 12.0}`, ``, []ReadRuneTypeResult{ {'{', 1, internal.RuneTypeObjectBeg, nil}, {'"', 1, internal.RuneTypeStringBeg, nil}, {'f', 1, internal.RuneTypeStringChar, nil}, {'o', 1, internal.RuneTypeStringChar, nil}, {'o', 1, internal.RuneTypeStringChar, nil}, {'"', 1, internal.RuneTypeStringEnd, nil}, {':', 1, internal.RuneTypeObjectColon, nil}, {' ', 1, internal.RuneTypeSpace, nil}, {'1', 1, internal.RuneTypeNumberIntDig, nil}, {'2', 1, internal.RuneTypeNumberIntDig, nil}, {'.', 1, internal.RuneTypeNumberFracDot, nil}, {'0', 1, internal.RuneTypeNumberFracDig, nil}, {'}', 1, internal.RuneTypeObjectEnd, nil}, {0, 0, internal.RuneTypeEOF, nil}, {0, 0, internal.RuneTypeEOF, nil}, }}, "unread": {`{"foo": 12.0}`, ``, []ReadRuneTypeResult{ {'{', 1, internal.RuneTypeObjectBeg, nil}, {'"', 1, internal.RuneTypeStringBeg, nil}, {'f', 1, internal.RuneTypeStringChar, nil}, {'o', 1, internal.RuneTypeStringChar, nil}, {'o', 1, internal.RuneTypeStringChar, nil}, {'"', 1, internal.RuneTypeStringEnd, nil}, {':', 1, internal.RuneTypeObjectColon, nil}, {' ', 1, internal.RuneTypeSpace, nil}, {'1', 1, internal.RuneTypeNumberIntDig, nil}, {0, -1, 0, nil}, {'1', 1, internal.RuneTypeNumberIntDig, nil}, {'2', 1, internal.RuneTypeNumberIntDig, nil}, {'.', 1, internal.RuneTypeNumberFracDot, nil}, {'0', 1, internal.RuneTypeNumberFracDig, nil}, {'}', 1, internal.RuneTypeObjectEnd, nil}, {0, 0, internal.RuneTypeEOF, nil}, {0, 0, internal.RuneTypeEOF, nil}, }}, "unread2": {`{"foo": 12.0}`, ``, []ReadRuneTypeResult{ {'{', 1, internal.RuneTypeObjectBeg, nil}, {'"', 1, internal.RuneTypeStringBeg, nil}, {'f', 1, internal.RuneTypeStringChar, nil}, {'o', 1, internal.RuneTypeStringChar, nil}, {'o', 1, internal.RuneTypeStringChar, nil}, {'"', 1, internal.RuneTypeStringEnd, nil}, {':', 1, internal.RuneTypeObjectColon, nil}, {' ', 1, internal.RuneTypeSpace, nil}, {'1', 1, internal.RuneTypeNumberIntDig, nil}, {0, -1, 0, nil}, {0, -1, 0, ErrInvalidUnreadRune}, {'1', 1, internal.RuneTypeNumberIntDig, nil}, {'2', 1, internal.RuneTypeNumberIntDig, nil}, {'.', 1, internal.RuneTypeNumberFracDot, nil}, {'0', 1, internal.RuneTypeNumberFracDig, nil}, {'}', 1, internal.RuneTypeObjectEnd, nil}, {0, 0, internal.RuneTypeEOF, nil}, {0, 0, internal.RuneTypeEOF, nil}, }}, "unread-eof": {`{"foo": 12.0}`, ``, []ReadRuneTypeResult{ {'{', 1, internal.RuneTypeObjectBeg, nil}, {'"', 1, internal.RuneTypeStringBeg, nil}, {'f', 1, internal.RuneTypeStringChar, nil}, {'o', 1, internal.RuneTypeStringChar, nil}, {'o', 1, internal.RuneTypeStringChar, nil}, {'"', 1, internal.RuneTypeStringEnd, nil}, {':', 1, internal.RuneTypeObjectColon, nil}, {' ', 1, internal.RuneTypeSpace, nil}, {'1', 1, internal.RuneTypeNumberIntDig, nil}, {'2', 1, internal.RuneTypeNumberIntDig, nil}, {'.', 1, internal.RuneTypeNumberFracDot, nil}, {'0', 1, internal.RuneTypeNumberFracDig, nil}, {'}', 1, internal.RuneTypeObjectEnd, nil}, {0, 0, internal.RuneTypeEOF, nil}, {0, -1, 0, ErrInvalidUnreadRune}, {0, 0, internal.RuneTypeEOF, nil}, {0, 0, internal.RuneTypeEOF, nil}, }}, "syntax-error": {`[[0,]`, ``, []ReadRuneTypeResult{ {'[', 1, internal.RuneTypeArrayBeg, nil}, {'[', 1, internal.RuneTypeArrayBeg, nil}, {'0', 1, internal.RuneTypeNumberIntZero, nil}, {',', 1, internal.RuneTypeArrayComma, nil}, {']', 1, internal.RuneTypeError, &DecodeSyntaxError{Offset: 4, Err: fmt.Errorf("invalid character %q looking for beginning of value", ']')}}, {']', 1, internal.RuneTypeError, &DecodeSyntaxError{Offset: 4, Err: fmt.Errorf("invalid character %q looking for beginning of value", ']')}}, {']', 1, internal.RuneTypeError, &DecodeSyntaxError{Offset: 4, Err: fmt.Errorf("invalid character %q looking for beginning of value", ']')}}, }}, "multi-value": {`1{}`, `}`, []ReadRuneTypeResult{ {'1', 1, internal.RuneTypeNumberIntDig, nil}, {'{', 1, internal.RuneTypeEOF, nil}, {'{', 1, internal.RuneTypeEOF, nil}, {'{', 1, internal.RuneTypeEOF, nil}, }}, "early-eof": {`{`, ``, []ReadRuneTypeResult{ {'{', 1, internal.RuneTypeObjectBeg, nil}, {0, 0, internal.RuneTypeError, &DecodeSyntaxError{Offset: 1, Err: io.ErrUnexpectedEOF}}, {0, 0, internal.RuneTypeError, &DecodeSyntaxError{Offset: 1, Err: io.ErrUnexpectedEOF}}, {0, 0, internal.RuneTypeError, &DecodeSyntaxError{Offset: 1, Err: io.ErrUnexpectedEOF}}, }}, "empty": {``, ``, []ReadRuneTypeResult{ {0, 0, internal.RuneTypeError, &DecodeSyntaxError{Offset: 0, Err: io.EOF}}, {0, 0, internal.RuneTypeError, &DecodeSyntaxError{Offset: 0, Err: io.EOF}}, {0, 0, internal.RuneTypeError, &DecodeSyntaxError{Offset: 0, Err: io.EOF}}, }}, } testRuneTypeScanner(t, testcases, func(reader io.RuneScanner) runeTypeScanner { return &runeTypeScannerImpl{ inner: reader, } }) } func TestNoWSRuneTypeScanner(t *testing.T) { t.Parallel() testcases := map[string]runeTypeScannerTestcase{ "basic": {`{"foo": 12.0}`, ``, []ReadRuneTypeResult{ {'{', 1, internal.RuneTypeObjectBeg, nil}, {'"', 1, internal.RuneTypeStringBeg, nil}, {'f', 1, internal.RuneTypeStringChar, nil}, {'o', 1, internal.RuneTypeStringChar, nil}, {'o', 1, internal.RuneTypeStringChar, nil}, {'"', 1, internal.RuneTypeStringEnd, nil}, {':', 1, internal.RuneTypeObjectColon, nil}, {'1', 1, internal.RuneTypeNumberIntDig, nil}, {'2', 1, internal.RuneTypeNumberIntDig, nil}, {'.', 1, internal.RuneTypeNumberFracDot, nil}, {'0', 1, internal.RuneTypeNumberFracDig, nil}, {'}', 1, internal.RuneTypeObjectEnd, nil}, {0, 0, internal.RuneTypeEOF, nil}, {0, 0, internal.RuneTypeEOF, nil}, }}, "unread": {`{"foo": 12.0}`, ``, []ReadRuneTypeResult{ {'{', 1, internal.RuneTypeObjectBeg, nil}, {'"', 1, internal.RuneTypeStringBeg, nil}, {'f', 1, internal.RuneTypeStringChar, nil}, {'o', 1, internal.RuneTypeStringChar, nil}, {'o', 1, internal.RuneTypeStringChar, nil}, {'"', 1, internal.RuneTypeStringEnd, nil}, {':', 1, internal.RuneTypeObjectColon, nil}, {'1', 1, internal.RuneTypeNumberIntDig, nil}, {0, -1, 0, nil}, {'1', 1, internal.RuneTypeNumberIntDig, nil}, {'2', 1, internal.RuneTypeNumberIntDig, nil}, {'.', 1, internal.RuneTypeNumberFracDot, nil}, {'0', 1, internal.RuneTypeNumberFracDig, nil}, {'}', 1, internal.RuneTypeObjectEnd, nil}, {0, 0, internal.RuneTypeEOF, nil}, {0, 0, internal.RuneTypeEOF, nil}, }}, "tail": {`{"foo": 12.0} `, ``, []ReadRuneTypeResult{ {'{', 1, internal.RuneTypeObjectBeg, nil}, {'"', 1, internal.RuneTypeStringBeg, nil}, {'f', 1, internal.RuneTypeStringChar, nil}, {'o', 1, internal.RuneTypeStringChar, nil}, {'o', 1, internal.RuneTypeStringChar, nil}, {'"', 1, internal.RuneTypeStringEnd, nil}, {':', 1, internal.RuneTypeObjectColon, nil}, {'1', 1, internal.RuneTypeNumberIntDig, nil}, {'2', 1, internal.RuneTypeNumberIntDig, nil}, {'.', 1, internal.RuneTypeNumberFracDot, nil}, {'0', 1, internal.RuneTypeNumberFracDig, nil}, {'}', 1, internal.RuneTypeObjectEnd, nil}, {0, 0, internal.RuneTypeEOF, nil}, {0, 0, internal.RuneTypeEOF, nil}, }}, "multi-value": {`1{}`, `}`, []ReadRuneTypeResult{ {'1', 1, internal.RuneTypeNumberIntDig, nil}, {'{', 1, internal.RuneTypeEOF, nil}, {'{', 1, internal.RuneTypeEOF, nil}, {'{', 1, internal.RuneTypeEOF, nil}, }}, "early-eof": {` {`, ``, []ReadRuneTypeResult{ {'{', 1, internal.RuneTypeObjectBeg, nil}, {0, 0, internal.RuneTypeError, &DecodeSyntaxError{Offset: 2, Err: io.ErrUnexpectedEOF}}, {0, 0, internal.RuneTypeError, &DecodeSyntaxError{Offset: 2, Err: io.ErrUnexpectedEOF}}, {0, 0, internal.RuneTypeError, &DecodeSyntaxError{Offset: 2, Err: io.ErrUnexpectedEOF}}, }}, } testRuneTypeScanner(t, testcases, func(reader io.RuneScanner) runeTypeScanner { return &noWSRuneTypeScanner{ inner: &runeTypeScannerImpl{ inner: reader, }, } }) } func TestElemRuneTypeScanner(t *testing.T) { t.Parallel() toplevelTestcases := map[string]runeTypeScannerTestcase{ "basic": {`1`, ``, []ReadRuneTypeResult{ {'1', 1, internal.RuneTypeNumberIntDig, nil}, {0, 0, internal.RuneTypeEOF, nil}, {0, 0, internal.RuneTypeEOF, nil}, {0, 0, internal.RuneTypeEOF, nil}, }}, "syntax-error": {`[[0,]`, ``, []ReadRuneTypeResult{ {'[', 1, internal.RuneTypeArrayBeg, nil}, {'[', 1, internal.RuneTypeArrayBeg, nil}, {'0', 1, internal.RuneTypeNumberIntZero, nil}, {',', 1, internal.RuneTypeArrayComma, nil}, {']', 1, internal.RuneTypeError, &DecodeSyntaxError{Offset: 5, Err: fmt.Errorf("invalid character %q looking for beginning of value", ']')}}, {']', 1, internal.RuneTypeError, &DecodeSyntaxError{Offset: 5, Err: fmt.Errorf("invalid character %q looking for beginning of value", ']')}}, {']', 1, internal.RuneTypeError, &DecodeSyntaxError{Offset: 5, Err: fmt.Errorf("invalid character %q looking for beginning of value", ']')}}, }}, "multi-value": {`1{}`, `{}`, []ReadRuneTypeResult{ {'1', 1, internal.RuneTypeNumberIntDig, nil}, {0, 0, internal.RuneTypeEOF, nil}, {0, 0, internal.RuneTypeEOF, nil}, {0, 0, internal.RuneTypeEOF, nil}, }}, "fragment": {`1,`, `,`, []ReadRuneTypeResult{ {'1', 1, internal.RuneTypeNumberIntDig, nil}, {0, 0, internal.RuneTypeEOF, nil}, {0, 0, internal.RuneTypeEOF, nil}, {0, 0, internal.RuneTypeEOF, nil}, }}, "early-eof": {`{`, ``, []ReadRuneTypeResult{ {'{', 1, internal.RuneTypeObjectBeg, nil}, {0, 0, internal.RuneTypeError, &DecodeSyntaxError{Offset: 1, Err: io.ErrUnexpectedEOF}}, {0, 0, internal.RuneTypeError, &DecodeSyntaxError{Offset: 1, Err: io.ErrUnexpectedEOF}}, {0, 0, internal.RuneTypeError, &DecodeSyntaxError{Offset: 1, Err: io.ErrUnexpectedEOF}}, }}, } childTestcases := make(map[string]runeTypeScannerTestcase, len(toplevelTestcases)) for tcName, tc := range toplevelTestcases { tc.Input = `[` + tc.Input tc.Exp = append([]ReadRuneTypeResult(nil), tc.Exp...) // copy for i, res := range tc.Exp { if se, ok := res.e.(*DecodeSyntaxError); ok { seCopy := *se seCopy.Offset++ tc.Exp[i].e = &seCopy } } childTestcases[tcName] = tc } t.Run("top-level", func(t *testing.T) { t.Parallel() testRuneTypeScanner(t, toplevelTestcases, func(reader io.RuneScanner) runeTypeScanner { return &elemRuneTypeScanner{ inner: &noWSRuneTypeScanner{ inner: &runeTypeScannerImpl{ inner: reader, }, }, } }) }) t.Run("child", func(t *testing.T) { t.Parallel() testRuneTypeScanner(t, childTestcases, func(reader io.RuneScanner) runeTypeScanner { inner := &noWSRuneTypeScanner{ inner: &runeTypeScannerImpl{ inner: reader, }, } var res ReadRuneTypeResult res.r, res.s, res.t, res.e = inner.ReadRuneType() require.Equal(t, ReadRuneTypeResult{'[', 1, internal.RuneTypeArrayBeg, nil}.String(), res.String()) return &elemRuneTypeScanner{ inner: inner, } }) }) } func TestElemRuneTypeScanner2(t *testing.T) { t.Parallel() parent := &noWSRuneTypeScanner{ inner: &runeTypeScannerImpl{ inner: strings.NewReader(` { "foo" : 12.0 } `), }, } exp := []ReadRuneTypeResult{ {'{', 1, internal.RuneTypeObjectBeg, nil}, {'"', 1, internal.RuneTypeStringBeg, nil}, {'f', 1, internal.RuneTypeStringChar, nil}, {'o', 1, internal.RuneTypeStringChar, nil}, {'o', 1, internal.RuneTypeStringChar, nil}, {'"', 1, internal.RuneTypeStringEnd, nil}, {':', 1, internal.RuneTypeObjectColon, nil}, } expStr := make([]string, 0, len(exp)) actStr := make([]string, 0, len(exp)) for _, iExp := range exp { var iAct ReadRuneTypeResult iAct.r, iAct.s, iAct.t, iAct.e = parent.ReadRuneType() expStr = append(expStr, iExp.String()) actStr = append(actStr, iAct.String()) require.Equal(t, expStr, actStr) } child := &elemRuneTypeScanner{ inner: parent, } exp = []ReadRuneTypeResult{ {'1', 1, internal.RuneTypeNumberIntDig, nil}, {'2', 1, internal.RuneTypeNumberIntDig, nil}, {'.', 1, internal.RuneTypeNumberFracDot, nil}, {'0', 1, internal.RuneTypeNumberFracDig, nil}, {0, 0, internal.RuneTypeEOF, nil}, {0, 0, internal.RuneTypeEOF, nil}, } expStr, actStr = nil, nil for _, iExp := range exp { var iAct ReadRuneTypeResult iAct.r, iAct.s, iAct.t, iAct.e = child.ReadRuneType() expStr = append(expStr, iExp.String()) actStr = append(actStr, iAct.String()) require.Equal(t, expStr, actStr) } exp = []ReadRuneTypeResult{ {'}', 1, internal.RuneTypeObjectEnd, nil}, {0, 0, internal.RuneTypeEOF, nil}, {0, 0, internal.RuneTypeEOF, nil}, } expStr, actStr = nil, nil for _, iExp := range exp { var iAct ReadRuneTypeResult iAct.r, iAct.s, iAct.t, iAct.e = parent.ReadRuneType() expStr = append(expStr, iExp.String()) actStr = append(actStr, iAct.String()) require.Equal(t, expStr, actStr) } }