git.lukeshu.com/go/lowmemjson@v0.3.9-0.20230723050957-72f6d13f6fb2/methods_test.go (about) 1 // Copyright (C) 2022-2023 Luke Shumaker <lukeshu@lukeshu.com> 2 // 3 // SPDX-License-Identifier: GPL-2.0-or-later 4 5 package lowmemjson_test 6 7 import ( 8 "bytes" 9 "errors" 10 "fmt" 11 "io" 12 "strings" 13 "testing" 14 15 "github.com/stretchr/testify/assert" 16 17 "git.lukeshu.com/go/lowmemjson" 18 ) 19 20 type ShortSum string 21 22 func (s ShortSum) EncodeJSON(w io.Writer) error { 23 // Test that it's OK to call lowmemjson.Encoder.Encode for the top-level value in a method. 24 return lowmemjson.NewEncoder(w).Encode(string(s)) 25 } 26 27 type SumRun struct { 28 ChecksumSize int `json:",omitempty"` 29 Addr int64 `json:",omitempty"` 30 Sums ShortSum 31 } 32 33 func (run SumRun) Size() int64 { 34 return int64(len(run.Sums)/(2*run.ChecksumSize)) * (4 * 1024) 35 } 36 37 type SumRunWithGaps struct { 38 Addr int64 39 Size int64 40 Runs []SumRun 41 } 42 43 func (sg SumRunWithGaps) EncodeJSON(w io.Writer) error { 44 if _, err := fmt.Fprintf(w, `{"Addr":%d,"Size":%d,"Runs":[`, sg.Addr, sg.Size); err != nil { 45 return err 46 } 47 cur := sg.Addr 48 for i, run := range sg.Runs { 49 if i > 0 { 50 if _, err := w.Write([]byte{','}); err != nil { 51 return err 52 } 53 } 54 switch { 55 case run.Addr < cur: 56 return fmt.Errorf("invalid %T: addr went backwards: %v < %v", sg, run.Addr, cur) 57 case run.Addr > cur: 58 if _, err := fmt.Fprintf(w, `{"Gap":%d},`, run.Addr-cur); err != nil { 59 return err 60 } 61 fallthrough 62 default: 63 if err := lowmemjson.NewEncoder(w).Encode(run); err != nil { 64 return err 65 } 66 cur = run.Addr + run.Size() 67 } 68 } 69 end := sg.Addr + sg.Size 70 switch { 71 case end < cur: 72 return fmt.Errorf("invalid %T: addr went backwards: %v < %v", sg, end, cur) 73 case end > cur: 74 if _, err := fmt.Fprintf(w, `,{"Gap":%d}`, end-cur); err != nil { 75 return err 76 } 77 } 78 if _, err := w.Write([]byte("]}")); err != nil { 79 return err 80 } 81 return nil 82 } 83 84 func (sg *SumRunWithGaps) DecodeJSON(r io.RuneScanner) error { 85 *sg = SumRunWithGaps{} 86 var name string 87 return lowmemjson.DecodeObject(r, 88 func(r io.RuneScanner) error { 89 return lowmemjson.NewDecoder(r).Decode(&name) 90 }, 91 func(r io.RuneScanner) error { 92 switch name { 93 case "Addr": 94 return lowmemjson.NewDecoder(r).Decode(&sg.Addr) 95 case "Size": 96 return lowmemjson.NewDecoder(r).Decode(&sg.Size) 97 case "Runs": 98 return lowmemjson.DecodeArray(r, func(r io.RuneScanner) error { 99 var run SumRun 100 if err := lowmemjson.NewDecoder(r).Decode(&run); err != nil { 101 return err 102 } 103 if run.ChecksumSize > 0 { 104 sg.Runs = append(sg.Runs, run) 105 } 106 return nil 107 }) 108 default: 109 return fmt.Errorf("unknown key %q", name) 110 } 111 }) 112 } 113 114 func TestMethods(t *testing.T) { 115 t.Parallel() 116 in := SumRunWithGaps{ 117 Addr: 13631488, 118 Size: 416033783808, 119 Runs: []SumRun{ 120 { 121 ChecksumSize: 4, 122 Addr: 1095761920, 123 Sums: "c160817cb5c72bbb", 124 }, 125 }, 126 } 127 var buf bytes.Buffer 128 assert.NoError(t, lowmemjson.NewEncoder(&buf).Encode(in)) 129 assert.Equal(t, 130 `{"Addr":13631488,"Size":416033783808,"Runs":[{"Gap":1082130432},{"ChecksumSize":4,"Addr":1095761920,"Sums":"c160817cb5c72bbb"},{"Gap":414951645184}]}`, 131 buf.String()) 132 var out SumRunWithGaps 133 assert.NoError(t, lowmemjson.NewDecoder(&buf).Decode(&out)) 134 assert.Equal(t, in, out) 135 } 136 137 type strEncoder string 138 139 func (s strEncoder) EncodeJSON(w io.Writer) error { 140 _, err := io.WriteString(w, string(s)) 141 return err 142 } 143 144 type strMarshaler string 145 146 func (s strMarshaler) MarshalJSON() ([]byte, error) { 147 return []byte(s), nil 148 } 149 150 type strTextMarshaler struct { 151 str string 152 err string 153 } 154 155 func (m strTextMarshaler) MarshalText() (txt []byte, err error) { 156 if len(m.str) > 0 { 157 txt = []byte(m.str) 158 } 159 if len(m.err) > 0 { 160 err = errors.New(m.err) 161 } 162 return 163 } 164 165 func TestMethodsEncode(t *testing.T) { 166 t.Parallel() 167 type testcase struct { 168 In string 169 ExpectedErr string 170 } 171 testcases := map[string]testcase{ 172 "basic": {In: `{}`}, 173 "empty": {In: ``, ExpectedErr: `syntax error at input byte 0: EOF`}, 174 "short": {In: `{`, ExpectedErr: `syntax error at input byte 1: unexpected EOF`}, 175 "long": {In: `{}{}`, ExpectedErr: `syntax error at input byte 2: invalid character '{' after top-level value`}, 176 } 177 t.Run("encodable", func(t *testing.T) { 178 t.Parallel() 179 for tcName, tc := range testcases { 180 tc := tc 181 t.Run(tcName, func(t *testing.T) { 182 t.Parallel() 183 var buf strings.Builder 184 err := lowmemjson.NewEncoder(&buf).Encode([]any{strEncoder(tc.In)}) 185 if tc.ExpectedErr == "" { 186 assert.NoError(t, err) 187 assert.Equal(t, "["+tc.In+"]", buf.String()) 188 } else { 189 assert.EqualError(t, err, 190 `json: error calling EncodeJSON for type lowmemjson_test.strEncoder: `+ 191 tc.ExpectedErr) 192 } 193 }) 194 } 195 }) 196 t.Run("marshaler", func(t *testing.T) { 197 t.Parallel() 198 for tcName, tc := range testcases { 199 tc := tc 200 t.Run(tcName, func(t *testing.T) { 201 t.Parallel() 202 var buf strings.Builder 203 err := lowmemjson.NewEncoder(&buf).Encode([]any{strMarshaler(tc.In)}) 204 if tc.ExpectedErr == "" { 205 assert.NoError(t, err) 206 assert.Equal(t, "["+tc.In+"]", buf.String()) 207 } else { 208 assert.EqualError(t, err, 209 `json: error calling MarshalJSON for type lowmemjson_test.strMarshaler: `+ 210 tc.ExpectedErr) 211 } 212 }) 213 } 214 }) 215 t.Run("text", func(t *testing.T) { 216 t.Parallel() 217 type testcase struct { 218 Str string 219 Err string 220 } 221 testcases := map[string]testcase{ 222 "basic": {Str: `a`}, 223 "err": {Err: `xxx`}, 224 "both": {Str: `a`, Err: `xxx`}, 225 } 226 for tcName, tc := range testcases { 227 tc := tc 228 t.Run(tcName, func(t *testing.T) { 229 t.Parallel() 230 var buf strings.Builder 231 err := lowmemjson.NewEncoder(&buf).Encode([]any{strTextMarshaler{str: tc.Str, err: tc.Err}}) 232 if tc.Err == "" { 233 assert.NoError(t, err) 234 assert.Equal(t, `["`+tc.Str+`"]`, buf.String()) 235 } else { 236 assert.EqualError(t, err, 237 `json: error calling MarshalText for type lowmemjson_test.strTextMarshaler: `+ 238 tc.Err) 239 assert.Equal(t, "[", buf.String()) 240 } 241 }) 242 } 243 }) 244 } 245 246 type tstDecoder struct { 247 n int 248 err string 249 } 250 251 func (d *tstDecoder) DecodeJSON(r io.RuneScanner) error { 252 for i := 0; i < d.n; i++ { 253 if _, _, err := r.ReadRune(); err != nil { 254 if err == io.EOF { 255 break 256 } 257 return err 258 } 259 } 260 if len(d.err) > 0 { 261 return errors.New(d.err) 262 } 263 return nil 264 } 265 266 type strUnmarshaler struct { 267 err string 268 } 269 270 func (u *strUnmarshaler) UnmarshalJSON([]byte) error { 271 if u.err == "" { 272 return nil 273 } 274 return errors.New(u.err) 275 } 276 277 type textUnmarshaler struct { 278 err string 279 } 280 281 func (u *textUnmarshaler) UnmarshalText([]byte) error { 282 if u.err == "" { 283 return nil 284 } 285 return errors.New(u.err) 286 } 287 288 type errTextUnmarshaler struct { 289 S string 290 } 291 292 func (u *errTextUnmarshaler) UnmarshalText(dat []byte) error { 293 u.S = string(dat) 294 return errors.New("eee") 295 } 296 297 func TestMethodsDecode(t *testing.T) { 298 t.Parallel() 299 type testcase struct { 300 In string 301 Obj any 302 ExpectedErr string 303 } 304 testcases := map[string]testcase{ 305 "decode-basic": {In: `{}`, Obj: &tstDecoder{n: 2}}, 306 "decode-basic-eof": {In: `{}`, Obj: &tstDecoder{n: 5}}, 307 "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`}, 308 "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`}, 309 "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`}, 310 "decode-err": {In: `{}`, Obj: &tstDecoder{err: "xxx"}, ExpectedErr: `json: v: cannot decode JSON object at input byte 0 into Go *lowmemjson_test.tstDecoder: xxx`}, 311 "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`}, 312 "unmarshal-err": {In: `{}`, Obj: &strUnmarshaler{err: "zzz"}, ExpectedErr: `json: v: cannot decode JSON object at input byte 0 into Go *lowmemjson_test.strUnmarshaler: zzz`}, 313 "unmarshaltext": {In: `""`, Obj: &textUnmarshaler{}}, 314 "unmarshaltext-nonstr": {In: `{}`, Obj: &textUnmarshaler{}, ExpectedErr: `json: v: cannot decode JSON object at input byte 0 into Go *lowmemjson_test.textUnmarshaler`}, 315 "unmarshaltext-err": {In: `""`, Obj: &textUnmarshaler{err: "zzz"}, ExpectedErr: `json: v: cannot decode JSON string at input byte 0 into Go *lowmemjson_test.textUnmarshaler: zzz`}, 316 "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`}, 317 } 318 for tcName, tc := range testcases { 319 tc := tc 320 t.Run(tcName, func(t *testing.T) { 321 t.Parallel() 322 obj := tc.Obj 323 err := lowmemjson.NewDecoder(strings.NewReader(tc.In)).Decode(&obj) 324 if tc.ExpectedErr == "" { 325 assert.NoError(t, err) 326 } else { 327 assert.EqualError(t, err, tc.ExpectedErr) 328 } 329 }) 330 } 331 }