git.lukeshu.com/go/lowmemjson@v0.3.9-0.20230723050957-72f6d13f6fb2/reencode_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 6 7 import ( 8 "errors" 9 "io" 10 "strings" 11 "testing" 12 13 "github.com/stretchr/testify/assert" 14 15 "git.lukeshu.com/go/lowmemjson/internal/fastio" 16 ) 17 18 func TestEncodeReEncode(t *testing.T) { 19 t.Parallel() 20 type testcase struct { 21 enc ReEncoderConfig 22 in any 23 exp string 24 } 25 testcases := map[string]testcase{ 26 "basic": { 27 enc: ReEncoderConfig{ 28 Indent: "\t", 29 CompactIfUnder: 10, 30 }, 31 in: map[string][]string{ 32 "a": {"b", "c"}, 33 "d": {"eeeeeeeeeeeeeee"}, 34 }, 35 exp: `{ 36 "a": ["b","c"], 37 "d": [ 38 "eeeeeeeeeeeeeee" 39 ] 40 }`, 41 }, 42 "arrays1": { 43 enc: ReEncoderConfig{ 44 Indent: "\t", 45 CompactIfUnder: 10, 46 ForceTrailingNewlines: true, 47 }, 48 in: []any{ 49 map[string]any{ 50 "generation": 123456, 51 }, 52 map[string]any{ 53 "a": 1, 54 }, 55 map[string]any{ 56 "generation": 7891011213, 57 }, 58 }, 59 exp: `[ 60 { 61 "generation": 123456 62 }, 63 {"a":1}, 64 { 65 "generation": 7891011213 66 } 67 ] 68 `, 69 }, 70 "arrays2": { 71 enc: ReEncoderConfig{ 72 Indent: "\t", 73 CompactIfUnder: 15, 74 ForceTrailingNewlines: true, 75 }, 76 in: []any{ 77 map[string]any{ 78 "a": 1, 79 "b": 2, 80 }, 81 map[string]any{ 82 "generation": 123456, 83 }, 84 map[string]any{ 85 "generation": 7891011213, 86 }, 87 }, 88 exp: `[ 89 {"a":1,"b":2}, 90 { 91 "generation": 123456 92 }, 93 { 94 "generation": 7891011213 95 } 96 ] 97 `, 98 }, 99 "arrays3": { 100 enc: ReEncoderConfig{ 101 Indent: "\t", 102 ForceTrailingNewlines: true, 103 }, 104 in: []any{ 105 map[string]any{ 106 "a": 1, 107 }, 108 map[string]any{ 109 "generation": 123456, 110 }, 111 map[string]any{ 112 "generation": 7891011213, 113 }, 114 }, 115 exp: `[ 116 { 117 "a": 1 118 }, 119 { 120 "generation": 123456 121 }, 122 { 123 "generation": 7891011213 124 } 125 ] 126 `, 127 }, 128 "indent-unicode": { 129 enc: ReEncoderConfig{ 130 Prefix: "—", 131 Indent: "»", 132 }, 133 in: []int{9}, 134 exp: `[ 135 —»9 136 —]`, 137 }, 138 "numbers": { 139 enc: ReEncoderConfig{ 140 Compact: true, 141 CompactFloats: true, 142 }, 143 in: []any{ 144 Number("1.200e003"), 145 }, 146 exp: `[1.2e3]`, 147 }, 148 "numbers-zero": { 149 enc: ReEncoderConfig{ 150 Compact: true, 151 CompactFloats: true, 152 }, 153 in: []any{ 154 Number("1.000e000"), 155 }, 156 exp: `[1.0e0]`, 157 }, 158 } 159 for tcName, tc := range testcases { 160 tc := tc 161 t.Run(tcName, func(t *testing.T) { 162 t.Parallel() 163 var out strings.Builder 164 enc := NewEncoder(NewReEncoder(&out, tc.enc)) 165 assert.NoError(t, enc.Encode(tc.in)) 166 assert.Equal(t, tc.exp, out.String()) 167 }) 168 } 169 } 170 171 func TestReEncode(t *testing.T) { 172 t.Parallel() 173 type testcase struct { 174 Cfg ReEncoderConfig 175 In string 176 ExpOut string 177 ExpWriteErr string 178 ExpCloseErr string 179 } 180 testcases := map[string]testcase{ 181 "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"}, 182 "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`}, 183 "partial-utf8-error": {Cfg: ReEncoderConfig{InvalidUTF8: InvalidUTF8Error}, In: "\xf0\xbf", ExpOut: ``, ExpCloseErr: `json: syntax error at input byte 0: truncated UTF-8: "\xf0\xbf"`}, 184 } 185 for tcName, tc := range testcases { 186 tc := tc 187 t.Run(tcName, func(t *testing.T) { 188 t.Parallel() 189 var out strings.Builder 190 enc := NewReEncoder(&out, tc.Cfg) 191 _, err := enc.WriteString(tc.In) 192 assert.Equal(t, tc.ExpOut, out.String()) 193 if tc.ExpWriteErr == "" { 194 assert.NoError(t, err) 195 } else { 196 assert.EqualError(t, err, tc.ExpWriteErr) 197 } 198 err = enc.Close() 199 if tc.ExpCloseErr == "" { 200 assert.NoError(t, err) 201 } else { 202 assert.EqualError(t, err, tc.ExpCloseErr) 203 } 204 }) 205 } 206 } 207 208 func TestReEncodeWriteSize(t *testing.T) { 209 t.Parallel() 210 211 multibyteRune := `😂` 212 assert.Len(t, multibyteRune, 4) 213 214 input := `"` + multibyteRune + `"` 215 216 t.Run("bytes-bigwrite", func(t *testing.T) { 217 t.Parallel() 218 var out strings.Builder 219 enc := NewReEncoder(&out, ReEncoderConfig{}) 220 221 n, err := enc.Write([]byte(input)) 222 assert.NoError(t, err) 223 assert.Equal(t, len(input), n) 224 225 assert.Equal(t, input, out.String()) 226 }) 227 t.Run("string-bigwrite", func(t *testing.T) { 228 t.Parallel() 229 var out strings.Builder 230 enc := NewReEncoder(&out, ReEncoderConfig{}) 231 232 n, err := enc.WriteString(input) 233 assert.NoError(t, err) 234 assert.Equal(t, len(input), n) 235 236 assert.Equal(t, input, out.String()) 237 }) 238 239 t.Run("bytes-smallwrites", func(t *testing.T) { 240 t.Parallel() 241 var out strings.Builder 242 enc := NewReEncoder(&out, ReEncoderConfig{}) 243 244 var buf [1]byte 245 for i := 0; i < len(input); i++ { 246 buf[0] = input[i] 247 n, err := enc.Write(buf[:]) 248 assert.NoError(t, err) 249 assert.Equal(t, 1, n) 250 } 251 252 assert.Equal(t, input, out.String()) 253 }) 254 t.Run("string-smallwrites", func(t *testing.T) { 255 t.Parallel() 256 var out strings.Builder 257 enc := NewReEncoder(&out, ReEncoderConfig{}) 258 259 for i := 0; i < len(input); i++ { 260 n, err := enc.WriteString(input[i : i+1]) 261 assert.NoError(t, err) 262 assert.Equal(t, 1, n) 263 } 264 265 assert.Equal(t, input, out.String()) 266 }) 267 } 268 269 func TestReEncoderStackSize(t *testing.T) { 270 t.Parallel() 271 272 enc := NewReEncoder(fastio.Discard, ReEncoderConfig{}) 273 assert.Equal(t, 0, enc.stackSize()) 274 275 for i := 0; i < 5; i++ { 276 assert.NoError(t, enc.WriteByte('[')) 277 assert.Equal(t, i+1, enc.stackSize()) 278 enc.pushWriteBarrier() 279 assert.Equal(t, i+2, enc.stackSize()) 280 } 281 } 282 283 var errNoSpace = errors.New("no space left on device") 284 285 type limitedWriter struct { 286 Limit int 287 Inner io.Writer 288 289 n int 290 } 291 292 func (w *limitedWriter) Write(p []byte) (int, error) { 293 switch { 294 case w.n >= w.Limit: 295 return 0, errNoSpace 296 case w.n+len(p) > w.Limit: 297 n, err := w.Inner.Write(p[:w.Limit-w.n]) 298 if n > 0 { 299 w.n += n 300 } 301 if err == nil { 302 err = errNoSpace 303 } 304 return n, err 305 default: 306 n, err := w.Inner.Write(p) 307 if n > 0 { 308 w.n += n 309 } 310 return n, err 311 } 312 } 313 314 func TestReEncodeIOErr(t *testing.T) { 315 t.Parallel() 316 317 input := `"😀"` 318 assert.Len(t, input, 6) 319 320 t.Run("bytes", func(t *testing.T) { 321 t.Parallel() 322 323 var out strings.Builder 324 enc := NewReEncoder(&limitedWriter{Limit: 5, Inner: &out}, ReEncoderConfig{}) 325 326 n, err := enc.Write([]byte(input[:2])) 327 assert.NoError(t, err) 328 assert.Equal(t, 2, n) 329 // Of the 2 bytes "written", only one should be in 330 // `out` yet; the other should be in the UTF-8 buffer. 331 assert.Equal(t, input[:1], out.String()) 332 333 n, err = enc.Write([]byte(input[2:])) 334 assert.ErrorIs(t, err, errNoSpace) 335 // Check that the byte in the UTF-8 buffer from the 336 // first .Write didn't count toward the total for this 337 // .Write. 338 assert.Equal(t, 3, n) 339 assert.Equal(t, input[:5], out.String()) 340 }) 341 t.Run("string", func(t *testing.T) { 342 t.Parallel() 343 344 var out strings.Builder 345 enc := NewReEncoder(&limitedWriter{Limit: 5, Inner: &out}, ReEncoderConfig{}) 346 347 n, err := enc.WriteString(input[:2]) 348 assert.NoError(t, err) 349 assert.Equal(t, 2, n) 350 // Of the 2 bytes "written", only one should be in 351 // `out` yet; the other should be in the UTF-8 buffer. 352 assert.Equal(t, input[:1], out.String()) 353 354 n, err = enc.WriteString(input[2:]) 355 assert.ErrorIs(t, err, errNoSpace) 356 // Check that the byte in the UTF-8 buffer from the 357 // first .Write didn't count toward the total for this 358 // .Write. 359 assert.Equal(t, 3, n) 360 assert.Equal(t, input[:5], out.String()) 361 }) 362 }