// Copyright (C) 2022-2023 Luke Shumaker // // SPDX-License-Identifier: GPL-2.0-or-later package lowmemjson import ( "errors" "io" "strings" "testing" "github.com/stretchr/testify/assert" "git.lukeshu.com/go/lowmemjson/internal/fastio" ) func TestEncodeReEncode(t *testing.T) { t.Parallel() type testcase struct { enc ReEncoderConfig in any exp string } testcases := map[string]testcase{ "basic": { enc: ReEncoderConfig{ Indent: "\t", CompactIfUnder: 10, }, in: map[string][]string{ "a": {"b", "c"}, "d": {"eeeeeeeeeeeeeee"}, }, exp: `{ "a": ["b","c"], "d": [ "eeeeeeeeeeeeeee" ] }`, }, "arrays1": { enc: ReEncoderConfig{ Indent: "\t", CompactIfUnder: 10, ForceTrailingNewlines: true, }, in: []any{ map[string]any{ "generation": 123456, }, map[string]any{ "a": 1, }, map[string]any{ "generation": 7891011213, }, }, exp: `[ { "generation": 123456 }, {"a":1}, { "generation": 7891011213 } ] `, }, "arrays2": { enc: ReEncoderConfig{ Indent: "\t", CompactIfUnder: 15, ForceTrailingNewlines: true, }, in: []any{ map[string]any{ "a": 1, "b": 2, }, map[string]any{ "generation": 123456, }, map[string]any{ "generation": 7891011213, }, }, exp: `[ {"a":1,"b":2}, { "generation": 123456 }, { "generation": 7891011213 } ] `, }, "arrays3": { enc: ReEncoderConfig{ Indent: "\t", ForceTrailingNewlines: true, }, in: []any{ map[string]any{ "a": 1, }, map[string]any{ "generation": 123456, }, map[string]any{ "generation": 7891011213, }, }, exp: `[ { "a": 1 }, { "generation": 123456 }, { "generation": 7891011213 } ] `, }, "indent-unicode": { enc: ReEncoderConfig{ Prefix: "—", Indent: "»", }, in: []int{9}, exp: `[ —»9 —]`, }, "numbers": { enc: ReEncoderConfig{ Compact: true, CompactFloats: true, }, in: []any{ Number("1.200e003"), }, exp: `[1.2e3]`, }, "numbers-zero": { enc: ReEncoderConfig{ Compact: true, CompactFloats: true, }, in: []any{ Number("1.000e000"), }, exp: `[1.0e0]`, }, } for tcName, tc := range testcases { tc := tc t.Run(tcName, func(t *testing.T) { t.Parallel() var out strings.Builder enc := NewEncoder(NewReEncoder(&out, tc.enc)) assert.NoError(t, enc.Encode(tc.in)) assert.Equal(t, tc.exp, out.String()) }) } } func TestReEncode(t *testing.T) { t.Parallel() type testcase struct { Cfg ReEncoderConfig In string ExpOut string ExpWriteErr string ExpCloseErr string } testcases := map[string]testcase{ "partial-utf8-replace": {Cfg: ReEncoderConfig{InvalidUTF8: InvalidUTF8Replace}, In: "\xf0\xbf", ExpOut: ``, ExpCloseErr: "json: syntax error at input byte 0: invalid character '\uFFFD' looking for beginning of value"}, "partial-utf8-preserve": {Cfg: ReEncoderConfig{InvalidUTF8: InvalidUTF8Preserve}, In: "\xf0\xbf", ExpOut: ``, ExpCloseErr: `json: syntax error at input byte 0: invalid character '\xf0' looking for beginning of value`}, "partial-utf8-error": {Cfg: ReEncoderConfig{InvalidUTF8: InvalidUTF8Error}, In: "\xf0\xbf", ExpOut: ``, ExpCloseErr: `json: syntax error at input byte 0: truncated UTF-8: "\xf0\xbf"`}, } for tcName, tc := range testcases { tc := tc t.Run(tcName, func(t *testing.T) { t.Parallel() var out strings.Builder enc := NewReEncoder(&out, tc.Cfg) _, err := enc.WriteString(tc.In) assert.Equal(t, tc.ExpOut, out.String()) if tc.ExpWriteErr == "" { assert.NoError(t, err) } else { assert.EqualError(t, err, tc.ExpWriteErr) } err = enc.Close() if tc.ExpCloseErr == "" { assert.NoError(t, err) } else { assert.EqualError(t, err, tc.ExpCloseErr) } }) } } func TestReEncodeWriteSize(t *testing.T) { t.Parallel() multibyteRune := `😂` assert.Len(t, multibyteRune, 4) input := `"` + multibyteRune + `"` t.Run("bytes-bigwrite", func(t *testing.T) { t.Parallel() var out strings.Builder enc := NewReEncoder(&out, ReEncoderConfig{}) n, err := enc.Write([]byte(input)) assert.NoError(t, err) assert.Equal(t, len(input), n) assert.Equal(t, input, out.String()) }) t.Run("string-bigwrite", func(t *testing.T) { t.Parallel() var out strings.Builder enc := NewReEncoder(&out, ReEncoderConfig{}) n, err := enc.WriteString(input) assert.NoError(t, err) assert.Equal(t, len(input), n) assert.Equal(t, input, out.String()) }) t.Run("bytes-smallwrites", func(t *testing.T) { t.Parallel() var out strings.Builder enc := NewReEncoder(&out, ReEncoderConfig{}) var buf [1]byte for i := 0; i < len(input); i++ { buf[0] = input[i] n, err := enc.Write(buf[:]) assert.NoError(t, err) assert.Equal(t, 1, n) } assert.Equal(t, input, out.String()) }) t.Run("string-smallwrites", func(t *testing.T) { t.Parallel() var out strings.Builder enc := NewReEncoder(&out, ReEncoderConfig{}) for i := 0; i < len(input); i++ { n, err := enc.WriteString(input[i : i+1]) assert.NoError(t, err) assert.Equal(t, 1, n) } assert.Equal(t, input, out.String()) }) } func TestReEncoderStackSize(t *testing.T) { t.Parallel() enc := NewReEncoder(fastio.Discard, ReEncoderConfig{}) assert.Equal(t, 0, enc.stackSize()) for i := 0; i < 5; i++ { assert.NoError(t, enc.WriteByte('[')) assert.Equal(t, i+1, enc.stackSize()) enc.pushWriteBarrier() assert.Equal(t, i+2, enc.stackSize()) } } var errNoSpace = errors.New("no space left on device") type limitedWriter struct { Limit int Inner io.Writer n int } func (w *limitedWriter) Write(p []byte) (int, error) { switch { case w.n >= w.Limit: return 0, errNoSpace case w.n+len(p) > w.Limit: n, err := w.Inner.Write(p[:w.Limit-w.n]) if n > 0 { w.n += n } if err == nil { err = errNoSpace } return n, err default: n, err := w.Inner.Write(p) if n > 0 { w.n += n } return n, err } } func TestReEncodeIOErr(t *testing.T) { t.Parallel() input := `"😀"` assert.Len(t, input, 6) t.Run("bytes", func(t *testing.T) { t.Parallel() var out strings.Builder enc := NewReEncoder(&limitedWriter{Limit: 5, Inner: &out}, ReEncoderConfig{}) n, err := enc.Write([]byte(input[:2])) assert.NoError(t, err) assert.Equal(t, 2, n) // Of the 2 bytes "written", only one should be in // `out` yet; the other should be in the UTF-8 buffer. assert.Equal(t, input[:1], out.String()) n, err = enc.Write([]byte(input[2:])) assert.ErrorIs(t, err, errNoSpace) // Check that the byte in the UTF-8 buffer from the // first .Write didn't count toward the total for this // .Write. assert.Equal(t, 3, n) assert.Equal(t, input[:5], out.String()) }) t.Run("string", func(t *testing.T) { t.Parallel() var out strings.Builder enc := NewReEncoder(&limitedWriter{Limit: 5, Inner: &out}, ReEncoderConfig{}) n, err := enc.WriteString(input[:2]) assert.NoError(t, err) assert.Equal(t, 2, n) // Of the 2 bytes "written", only one should be in // `out` yet; the other should be in the UTF-8 buffer. assert.Equal(t, input[:1], out.String()) n, err = enc.WriteString(input[2:]) assert.ErrorIs(t, err, errNoSpace) // Check that the byte in the UTF-8 buffer from the // first .Write didn't count toward the total for this // .Write. assert.Equal(t, 3, n) assert.Equal(t, input[:5], out.String()) }) }