// Copyright (C) 2022-2023 Luke Shumaker // // SPDX-License-Identifier: GPL-2.0-or-later package lowmemjson_test import ( "bytes" "errors" "fmt" "io" "strings" "testing" "github.com/stretchr/testify/assert" "git.lukeshu.com/go/lowmemjson" ) type ShortSum string func (s ShortSum) EncodeJSON(w io.Writer) error { // Test that it's OK to call lowmemjson.Encoder.Encode for the top-level value in a method. return lowmemjson.NewEncoder(w).Encode(string(s)) } type SumRun struct { ChecksumSize int `json:",omitempty"` Addr int64 `json:",omitempty"` Sums ShortSum } func (run SumRun) Size() int64 { return int64(len(run.Sums)/(2*run.ChecksumSize)) * (4 * 1024) } type SumRunWithGaps struct { Addr int64 Size int64 Runs []SumRun } func (sg SumRunWithGaps) EncodeJSON(w io.Writer) error { if _, err := fmt.Fprintf(w, `{"Addr":%d,"Size":%d,"Runs":[`, sg.Addr, sg.Size); err != nil { return err } cur := sg.Addr for i, run := range sg.Runs { if i > 0 { if _, err := w.Write([]byte{','}); err != nil { return err } } switch { case run.Addr < cur: return fmt.Errorf("invalid %T: addr went backwards: %v < %v", sg, run.Addr, cur) case run.Addr > cur: if _, err := fmt.Fprintf(w, `{"Gap":%d},`, run.Addr-cur); err != nil { return err } fallthrough default: if err := lowmemjson.NewEncoder(w).Encode(run); err != nil { return err } cur = run.Addr + run.Size() } } end := sg.Addr + sg.Size switch { case end < cur: return fmt.Errorf("invalid %T: addr went backwards: %v < %v", sg, end, cur) case end > cur: if _, err := fmt.Fprintf(w, `,{"Gap":%d}`, end-cur); err != nil { return err } } if _, err := w.Write([]byte("]}")); err != nil { return err } return nil } func (sg *SumRunWithGaps) DecodeJSON(r io.RuneScanner) error { *sg = SumRunWithGaps{} var name string return lowmemjson.DecodeObject(r, func(r io.RuneScanner) error { return lowmemjson.NewDecoder(r).Decode(&name) }, func(r io.RuneScanner) error { switch name { case "Addr": return lowmemjson.NewDecoder(r).Decode(&sg.Addr) case "Size": return lowmemjson.NewDecoder(r).Decode(&sg.Size) case "Runs": return lowmemjson.DecodeArray(r, func(r io.RuneScanner) error { var run SumRun if err := lowmemjson.NewDecoder(r).Decode(&run); err != nil { return err } if run.ChecksumSize > 0 { sg.Runs = append(sg.Runs, run) } return nil }) default: return fmt.Errorf("unknown key %q", name) } }) } func TestMethods(t *testing.T) { t.Parallel() in := SumRunWithGaps{ Addr: 13631488, Size: 416033783808, Runs: []SumRun{ { ChecksumSize: 4, Addr: 1095761920, Sums: "c160817cb5c72bbb", }, }, } var buf bytes.Buffer assert.NoError(t, lowmemjson.NewEncoder(&buf).Encode(in)) assert.Equal(t, `{"Addr":13631488,"Size":416033783808,"Runs":[{"Gap":1082130432},{"ChecksumSize":4,"Addr":1095761920,"Sums":"c160817cb5c72bbb"},{"Gap":414951645184}]}`, buf.String()) var out SumRunWithGaps assert.NoError(t, lowmemjson.NewDecoder(&buf).Decode(&out)) assert.Equal(t, in, out) } type strEncoder string func (s strEncoder) EncodeJSON(w io.Writer) error { _, err := io.WriteString(w, string(s)) return err } type strMarshaler string func (s strMarshaler) MarshalJSON() ([]byte, error) { return []byte(s), nil } type strTextMarshaler struct { str string err string } func (m strTextMarshaler) MarshalText() (txt []byte, err error) { if len(m.str) > 0 { txt = []byte(m.str) } if len(m.err) > 0 { err = errors.New(m.err) } return } func TestMethodsEncode(t *testing.T) { t.Parallel() type testcase struct { In string ExpectedErr string } testcases := map[string]testcase{ "basic": {In: `{}`}, "empty": {In: ``, ExpectedErr: `syntax error at input byte 0: EOF`}, "short": {In: `{`, ExpectedErr: `syntax error at input byte 1: unexpected EOF`}, "long": {In: `{}{}`, ExpectedErr: `syntax error at input byte 2: invalid character '{' after top-level value`}, } t.Run("encodable", func(t *testing.T) { t.Parallel() for tcName, tc := range testcases { tc := tc t.Run(tcName, func(t *testing.T) { t.Parallel() var buf strings.Builder err := lowmemjson.NewEncoder(&buf).Encode([]any{strEncoder(tc.In)}) if tc.ExpectedErr == "" { assert.NoError(t, err) assert.Equal(t, "["+tc.In+"]", buf.String()) } else { assert.EqualError(t, err, `json: error calling EncodeJSON for type lowmemjson_test.strEncoder: `+ tc.ExpectedErr) } }) } }) t.Run("marshaler", func(t *testing.T) { t.Parallel() for tcName, tc := range testcases { tc := tc t.Run(tcName, func(t *testing.T) { t.Parallel() var buf strings.Builder err := lowmemjson.NewEncoder(&buf).Encode([]any{strMarshaler(tc.In)}) if tc.ExpectedErr == "" { assert.NoError(t, err) assert.Equal(t, "["+tc.In+"]", buf.String()) } else { assert.EqualError(t, err, `json: error calling MarshalJSON for type lowmemjson_test.strMarshaler: `+ tc.ExpectedErr) } }) } }) t.Run("text", func(t *testing.T) { t.Parallel() type testcase struct { Str string Err string } testcases := map[string]testcase{ "basic": {Str: `a`}, "err": {Err: `xxx`}, "both": {Str: `a`, Err: `xxx`}, } for tcName, tc := range testcases { tc := tc t.Run(tcName, func(t *testing.T) { t.Parallel() var buf strings.Builder err := lowmemjson.NewEncoder(&buf).Encode([]any{strTextMarshaler{str: tc.Str, err: tc.Err}}) if tc.Err == "" { assert.NoError(t, err) assert.Equal(t, `["`+tc.Str+`"]`, buf.String()) } else { assert.EqualError(t, err, `json: error calling MarshalText for type lowmemjson_test.strTextMarshaler: `+ tc.Err) assert.Equal(t, "[", buf.String()) } }) } }) } type tstDecoder struct { n int err string } func (d *tstDecoder) DecodeJSON(r io.RuneScanner) error { for i := 0; i < d.n; i++ { if _, _, err := r.ReadRune(); err != nil { if err == io.EOF { break } return err } } if len(d.err) > 0 { return errors.New(d.err) } return nil } type strUnmarshaler struct { err string } func (u *strUnmarshaler) UnmarshalJSON([]byte) error { if u.err == "" { return nil } return errors.New(u.err) } type textUnmarshaler struct { err string } func (u *textUnmarshaler) UnmarshalText([]byte) error { if u.err == "" { return nil } return errors.New(u.err) } type errTextUnmarshaler struct { S string } func (u *errTextUnmarshaler) UnmarshalText(dat []byte) error { u.S = string(dat) return errors.New("eee") } func TestMethodsDecode(t *testing.T) { t.Parallel() type testcase struct { In string Obj any ExpectedErr string } testcases := map[string]testcase{ "decode-basic": {In: `{}`, Obj: &tstDecoder{n: 2}}, "decode-basic-eof": {In: `{}`, Obj: &tstDecoder{n: 5}}, "decode-syntax-error": {In: `{x}`, Obj: &tstDecoder{n: 5}, ExpectedErr: `json: v: syntax error at input byte 1: invalid character 'x' looking for beginning of object key string`}, "unmarshal-syntax-error": {In: `{x}`, Obj: &strUnmarshaler{}, ExpectedErr: `json: v: syntax error at input byte 1: invalid character 'x' looking for beginning of object key string`}, "decode-short": {In: `{}`, Obj: &tstDecoder{n: 1}, ExpectedErr: `json: v: cannot decode JSON object at input byte 0 into Go *lowmemjson_test.tstDecoder: did not consume entire object`}, "decode-err": {In: `{}`, Obj: &tstDecoder{err: "xxx"}, ExpectedErr: `json: v: cannot decode JSON object at input byte 0 into Go *lowmemjson_test.tstDecoder: xxx`}, "decode-err2": {In: `{}`, Obj: &tstDecoder{n: 1, err: "yyy"}, ExpectedErr: `json: v: cannot decode JSON object at input byte 0 into Go *lowmemjson_test.tstDecoder: yyy`}, "unmarshal-err": {In: `{}`, Obj: &strUnmarshaler{err: "zzz"}, ExpectedErr: `json: v: cannot decode JSON object at input byte 0 into Go *lowmemjson_test.strUnmarshaler: zzz`}, "unmarshaltext": {In: `""`, Obj: &textUnmarshaler{}}, "unmarshaltext-nonstr": {In: `{}`, Obj: &textUnmarshaler{}, ExpectedErr: `json: v: cannot decode JSON object at input byte 0 into Go *lowmemjson_test.textUnmarshaler`}, "unmarshaltext-err": {In: `""`, Obj: &textUnmarshaler{err: "zzz"}, ExpectedErr: `json: v: cannot decode JSON string at input byte 0 into Go *lowmemjson_test.textUnmarshaler: zzz`}, "unmarshaltext-mapkey": {In: `{"a":1}`, Obj: new(map[errTextUnmarshaler]int), ExpectedErr: `json: v: cannot decode JSON string at input byte 1 into Go *lowmemjson_test.errTextUnmarshaler: eee`}, } for tcName, tc := range testcases { tc := tc t.Run(tcName, func(t *testing.T) { t.Parallel() obj := tc.Obj err := lowmemjson.NewDecoder(strings.NewReader(tc.In)).Decode(&obj) if tc.ExpectedErr == "" { assert.NoError(t, err) } else { assert.EqualError(t, err, tc.ExpectedErr) } }) } }