git.lukeshu.com/go/lowmemjson@v0.3.9-0.20230723050957-72f6d13f6fb2/compat/json/compat_test.go (about) 1 // Copyright (C) 2023 Luke Shumaker <lukeshu@lukeshu.com> 2 // 3 // SPDX-License-Identifier: GPL-2.0-or-later 4 5 package json_test 6 7 import ( 8 "bytes" 9 "reflect" 10 "strings" 11 "testing" 12 13 "github.com/stretchr/testify/assert" 14 // When adding new testcases, comment this out and import 15 // "encoding/json", to validate your testcase. 16 "git.lukeshu.com/go/lowmemjson/compat/json" 17 ) 18 19 func TestCompatHTMLEscape(t *testing.T) { 20 t.Parallel() 21 type testcase struct { 22 In string 23 Out string 24 } 25 testcases := map[string]testcase{ 26 "invalid": {In: `x`, Out: `x`}, 27 "hex-lower": {In: `"\uabcd"`, Out: `"\uabcd"`}, 28 "hex-upper": {In: `"\uABCD"`, Out: `"\uABCD"`}, 29 "hex-mixed": {In: `"\uAbCd"`, Out: `"\uAbCd"`}, 30 } 31 for tcName, tc := range testcases { 32 tc := tc 33 t.Run(tcName, func(t *testing.T) { 34 t.Parallel() 35 t.Logf("in=%q", tc.In) 36 var dst bytes.Buffer 37 json.HTMLEscape(&dst, []byte(tc.In)) 38 assert.Equal(t, tc.Out, dst.String()) 39 }) 40 } 41 } 42 43 func TestCompatValid(t *testing.T) { 44 t.Parallel() 45 type testcase struct { 46 In string 47 Exp bool 48 } 49 testcases := map[string]testcase{ 50 "empty": {In: ``, Exp: false}, 51 "num": {In: `1`, Exp: true}, 52 "trunc": {In: `{`, Exp: false}, 53 "object": {In: `{}`, Exp: true}, 54 "non-utf8": {In: "\"\x85\xcd\"", Exp: false}, // https://github.com/golang/go/issues/58517 55 "hex-lower": {In: `"\uabcd"`, Exp: true}, 56 "hex-upper": {In: `"\uABCD"`, Exp: true}, 57 "hex-mixed": {In: `"\uAbCd"`, Exp: true}, 58 } 59 for tcName, tc := range testcases { 60 tc := tc 61 t.Run(tcName, func(t *testing.T) { 62 t.Parallel() 63 t.Logf("in=%q", tc.In) 64 act := json.Valid([]byte(tc.In)) 65 assert.Equal(t, tc.Exp, act) 66 }) 67 } 68 } 69 70 func TestCompatCompact(t *testing.T) { 71 t.Parallel() 72 type testcase struct { 73 In string 74 Out string 75 Err string 76 } 77 testcases := map[string]testcase{ 78 "empty": {In: ``, Out: ``, Err: `unexpected end of JSON input`}, 79 "trunc": {In: `{`, Out: ``, Err: `unexpected end of JSON input`}, 80 "object": {In: `{}`, Out: `{}`}, 81 "non-utf8": {In: "\"\x85\xcd\"", Out: "\"\x85\xcd\""}, 82 "float": {In: `1.200e003`, Out: `1.200e003`}, 83 "hex-lower": {In: `"\uabcd"`, Out: `"\uabcd"`}, 84 "hex-upper": {In: `"\uABCD"`, Out: `"\uABCD"`}, 85 "hex-mixed": {In: `"\uAbCd"`, Out: `"\uAbCd"`}, 86 "invalid-utf8": {In: "\x85", Err: `invalid character '\x85' looking for beginning of value`}, 87 } 88 for tcName, tc := range testcases { 89 tc := tc 90 t.Run(tcName, func(t *testing.T) { 91 t.Parallel() 92 t.Logf("in=%q", tc.In) 93 var out bytes.Buffer 94 err := json.Compact(&out, []byte(tc.In)) 95 assert.Equal(t, tc.Out, out.String()) 96 if tc.Err == "" { 97 assert.NoError(t, err) 98 } else { 99 assert.EqualError(t, err, tc.Err) 100 } 101 }) 102 } 103 } 104 105 func TestCompatIndent(t *testing.T) { 106 t.Parallel() 107 type testcase struct { 108 In string 109 Out string 110 Err string 111 } 112 testcases := map[string]testcase{ 113 "empty": {In: ``, Out: ``, Err: `unexpected end of JSON input`}, 114 "trunc": {In: `{`, Out: ``, Err: `unexpected end of JSON input`}, 115 "object": {In: `{}`, Out: `{}`}, 116 "non-utf8": {In: "\"\x85\xcd\"", Out: "\"\x85\xcd\""}, 117 "float": {In: `1.200e003`, Out: `1.200e003`}, 118 "tailws0": {In: `0`, Out: `0`}, 119 "tailws1": {In: `0 `, Out: `0 `}, 120 "tailws2": {In: `0 `, Out: `0 `}, 121 "tailws3": {In: "0\n", Out: "0\n"}, 122 "headws1": {In: ` 0`, Out: `0`}, 123 "objws1": {In: `{"a" : 1}`, Out: "{\n>.\"a\": 1\n>}"}, 124 "objws2": {In: "{\"a\"\n:\n1}", Out: "{\n>.\"a\": 1\n>}"}, 125 "hex-lower": {In: `"\uabcd"`, Out: `"\uabcd"`}, 126 "hex-upper": {In: `"\uABCD"`, Out: `"\uABCD"`}, 127 "hex-mixed": {In: `"\uAbCd"`, Out: `"\uAbCd"`}, 128 "invalid-utf8": {In: "\x85", Err: `invalid character '\x85' looking for beginning of value`}, 129 } 130 for tcName, tc := range testcases { 131 tc := tc 132 t.Run(tcName, func(t *testing.T) { 133 t.Parallel() 134 t.Logf("in=%q", tc.In) 135 var out bytes.Buffer 136 err := json.Indent(&out, []byte(tc.In), ">", ".") 137 assert.Equal(t, tc.Out, out.String()) 138 if tc.Err == "" { 139 assert.NoError(t, err) 140 } else { 141 assert.EqualError(t, err, tc.Err) 142 } 143 }) 144 } 145 } 146 147 func TestCompatMarshal(t *testing.T) { 148 t.Parallel() 149 type testcase struct { 150 In any 151 Out string 152 Err string 153 } 154 testcases := map[string]testcase{ 155 "non-utf8": {In: "\x85\xcd", Out: "\"\\ufffd\\ufffd\""}, 156 "urc": {In: "\ufffd", Out: "\"\ufffd\""}, 157 "float": {In: 1.2e3, Out: `1200`}, 158 "obj": {In: map[string]any{"": 1, " ": 2}, Out: `{"":1," ":2}`}, 159 "byte-ary": {In: struct{ Label [5]byte }{}, Out: `{"Label":[0,0,0,0,0]}`}, 160 } 161 for tcName, tc := range testcases { 162 tc := tc 163 t.Run(tcName, func(t *testing.T) { 164 t.Parallel() 165 out, err := json.Marshal(tc.In) 166 assert.Equal(t, tc.Out, string(out)) 167 if tc.Err == "" { 168 assert.NoError(t, err) 169 } else { 170 assert.EqualError(t, err, tc.Err) 171 } 172 }) 173 } 174 } 175 176 func TestCompatUnmarshal(t *testing.T) { 177 t.Parallel() 178 type testcase struct { 179 In string 180 InPtr any 181 ExpOut any 182 ExpErr string 183 } 184 testcases := map[string]testcase{ 185 "empty-obj": {In: `{}`, ExpOut: map[string]any{}}, 186 "partial-obj": {In: `{"foo":"bar",`, ExpOut: nil, ExpErr: `unexpected end of JSON input`}, 187 "existing-obj": {In: `{"baz":"quz"}`, InPtr: &map[string]string{"foo": "bar"}, ExpOut: map[string]string{"foo": "bar", "baz": "quz"}}, 188 "existing-obj-partial": {In: `{"baz":"quz"`, InPtr: &map[string]string{"foo": "bar"}, ExpOut: map[string]string{"foo": "bar"}, ExpErr: "unexpected end of JSON input"}, 189 "empty-ary": {In: `[]`, ExpOut: []any{}}, 190 "two-objs": {In: `{} {}`, ExpOut: nil, ExpErr: `invalid character '{' after top-level value`}, 191 "two-numbers1": {In: `00`, ExpOut: nil, ExpErr: `invalid character '0' after top-level value`}, 192 "two-numbers2": {In: `1 2`, ExpOut: nil, ExpErr: `invalid character '2' after top-level value`}, 193 "invalid-utf8": {In: "\x85", ExpErr: `invalid character '\x85' looking for beginning of value`}, 194 "byte-ary": {In: `{"Label":[1,0,0,0,0]}`, InPtr: new(struct{ Label [5]byte }), ExpOut: struct{ Label [5]byte }{Label: [5]byte{1, 0, 0, 0, 0}}}, 195 // 2e308 is slightly more than math.MaxFloat64 (~1.79e308) 196 "obj-overflow": {In: `{"foo":"bar", "baz":2e308, "qux": "orb"}`, ExpOut: map[string]any{"foo": "bar", "baz": nil, "qux": "orb"}, ExpErr: `json: cannot unmarshal number 2e308 into Go value of type float64`}, 197 "ary-overflow": {In: `["foo",2e308,"bar",3e308]`, ExpOut: []any{"foo", nil, "bar", nil}, ExpErr: `json: cannot unmarshal number 2e308 into Go value of type float64`}, 198 "existing-overflow": {In: `2e308`, InPtr: func() any { x := 4; return &x }(), ExpOut: 4, ExpErr: `json: cannot unmarshal number 2e308 into Go value of type int`}, 199 // syntax error messages 200 "syntax-01": {In: `{}x`, ExpErr: `invalid character 'x' after top-level value`}, 201 "syntax-02": {In: `x`, ExpErr: `invalid character 'x' looking for beginning of value`}, 202 "syntax-03": {In: `{x`, ExpErr: `invalid character 'x' looking for beginning of object key string`}, 203 "syntax-18": {In: `{"":0,}`, ExpErr: `invalid character '}' looking for beginning of object key string`}, 204 "syntax-04": {In: `{""x`, ExpErr: `invalid character 'x' after object key`}, 205 "syntax-05": {In: `{"":0x`, ExpErr: `invalid character 'x' after object key:value pair`}, 206 "syntax-06": {In: `[0x`, ExpErr: `invalid character 'x' after array element`}, 207 "syntax-07": {In: "\"\x01\"", ExpErr: `invalid character '\x01' in string literal`}, 208 "syntax-08": {In: `"\x`, ExpErr: `invalid character 'x' in string escape code`}, 209 "syntax-09": {In: `"\ux`, ExpErr: `invalid character 'x' in \u hexadecimal character escape`}, 210 "syntax-10": {In: `"\u0x`, ExpErr: `invalid character 'x' in \u hexadecimal character escape`}, 211 "syntax-11": {In: `"\u00x`, ExpErr: `invalid character 'x' in \u hexadecimal character escape`}, 212 "syntax-12": {In: `"\u000x`, ExpErr: `invalid character 'x' in \u hexadecimal character escape`}, 213 "syntax-13": {In: `-x`, ExpErr: `invalid character 'x' in numeric literal`}, 214 "syntax-14": {In: `0.x`, ExpErr: `invalid character 'x' after decimal point in numeric literal`}, 215 "syntax-15": {In: `1ex`, ExpErr: `invalid character 'x' in exponent of numeric literal`}, 216 "syntax-16": {In: `1e+x`, ExpErr: `invalid character 'x' in exponent of numeric literal`}, 217 "syntax-17": {In: `fx`, ExpErr: `invalid character 'x' in literal false (expecting 'a')`}, 218 } 219 for tcName, tc := range testcases { 220 tc := tc 221 t.Run(tcName, func(t *testing.T) { 222 t.Parallel() 223 ptr := tc.InPtr 224 if ptr == nil { 225 var out any 226 ptr = &out 227 } 228 err := json.Unmarshal([]byte(tc.In), ptr) 229 assert.Equal(t, tc.ExpOut, reflect.ValueOf(ptr).Elem().Interface()) 230 if tc.ExpErr == "" { 231 assert.NoError(t, err) 232 } else { 233 assert.EqualError(t, err, tc.ExpErr) 234 } 235 }) 236 } 237 } 238 239 func TestCompatDecode(t *testing.T) { 240 t.Parallel() 241 type testcase struct { 242 In string 243 InPtr any 244 ExpOut any 245 ExpErr string 246 } 247 testcases := map[string]testcase{ 248 "empty-obj": {In: `{}`, ExpOut: map[string]any{}}, 249 "partial-obj": {In: `{"foo":"bar",`, ExpOut: nil, ExpErr: `unexpected EOF`}, 250 "existing-obj": {In: `{"baz":"quz"}`, InPtr: &map[string]string{"foo": "bar"}, ExpOut: map[string]string{"foo": "bar", "baz": "quz"}}, 251 "existing-obj-partial": {In: `{"baz":"quz"`, InPtr: &map[string]string{"foo": "bar"}, ExpOut: map[string]string{"foo": "bar"}, ExpErr: "unexpected EOF"}, 252 "empty-ary": {In: `[]`, ExpOut: []any{}}, 253 "two-objs": {In: `{} {}`, ExpOut: map[string]any{}}, 254 "two-numbers1": {In: `00`, ExpOut: float64(0)}, 255 "two-numbers2": {In: `1 2`, ExpOut: float64(1)}, 256 "invalid-utf8": {In: "\x85", ExpErr: `invalid character '\x85' looking for beginning of value`}, 257 // 2e308 is slightly more than math.MaxFloat64 (~1.79e308) 258 "obj-overflow": {In: `{"foo":"bar", "baz":2e308, "qux": "orb"}`, ExpOut: map[string]any{"foo": "bar", "baz": nil, "qux": "orb"}, ExpErr: `json: cannot unmarshal number 2e308 into Go value of type float64`}, 259 "ary-overflow": {In: `["foo",2e308,"bar",3e308]`, ExpOut: []any{"foo", nil, "bar", nil}, ExpErr: `json: cannot unmarshal number 2e308 into Go value of type float64`}, 260 "existing-overflow": {In: `2e308`, InPtr: func() any { x := 4; return &x }(), ExpOut: 4, ExpErr: `json: cannot unmarshal number 2e308 into Go value of type int`}, 261 } 262 for tcName, tc := range testcases { 263 tc := tc 264 t.Run(tcName, func(t *testing.T) { 265 t.Parallel() 266 ptr := tc.InPtr 267 if ptr == nil { 268 var out any 269 ptr = &out 270 } 271 err := json.NewDecoder(strings.NewReader(tc.In)).Decode(ptr) 272 assert.Equal(t, tc.ExpOut, reflect.ValueOf(ptr).Elem().Interface()) 273 if tc.ExpErr == "" { 274 assert.NoError(t, err) 275 } else { 276 assert.EqualError(t, err, tc.ExpErr) 277 } 278 }) 279 } 280 }