git.lukeshu.com/go/lowmemjson@v0.3.9-0.20230723050957-72f6d13f6fb2/compat/json/equiv_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 std "encoding/json" 10 "errors" 11 "io" 12 "strconv" 13 "strings" 14 "testing" 15 "unicode/utf8" 16 17 "github.com/stretchr/testify/assert" 18 19 low "git.lukeshu.com/go/lowmemjson/compat/json" 20 ) 21 22 func assertEquivErr(t *testing.T, stdErr, lowErr error) { 23 if (stdErr == nil) || (lowErr == nil) { 24 // Nil-equal. 25 assert.Equal(t, stdErr, lowErr) 26 return 27 } 28 switch stdErr.(type) { 29 case *std.SyntaxError: 30 if lowErr != nil { 31 stdMsg := stdErr.Error() 32 lowMsg := lowErr.Error() 33 34 // https://github.com/golang/go/issues/58680 35 if strings.HasPrefix(stdMsg, `invalid character ' ' `) && 36 (errors.Is(lowErr, io.ErrUnexpectedEOF) || lowMsg == "unexpected end of JSON input") { 37 return 38 } 39 40 // https://github.com/golang/go/issues/58713 41 prefix := `invalid character '` 42 if stdMsg != lowMsg && strings.HasPrefix(stdMsg, prefix) && strings.HasPrefix(lowMsg, prefix) { 43 stdRune, stdRuneSize := utf8.DecodeRuneInString(stdMsg[len(prefix):]) 44 lowByte := lowMsg[len(prefix)] 45 if lowByte == '\\' { 46 switch lowMsg[len(prefix)+1] { 47 case 'a': 48 lowByte = '\a' 49 case 'b': 50 lowByte = '\b' 51 case 'f': 52 lowByte = '\f' 53 case 'n': 54 lowByte = '\n' 55 case 'r': 56 lowByte = '\r' 57 case 't': 58 lowByte = '\t' 59 case 'v': 60 lowByte = '\v' 61 case '\\', '\'': 62 lowByte = lowMsg[len(prefix)+1] 63 case 'x': 64 lowByte64, _ := strconv.ParseUint(lowMsg[len(prefix)+2:][:2], 16, 8) 65 lowByte = byte(lowByte64) 66 case 'u': 67 lowRune, _ := strconv.ParseUint(lowMsg[len(prefix)+2:][:4], 16, 16) 68 var buf [4]byte 69 utf8.EncodeRune(buf[:], rune(lowRune)) 70 lowByte = buf[0] 71 case 'U': 72 lowRune, _ := strconv.ParseUint(lowMsg[len(prefix)+2:][:8], 16, 32) 73 var buf [4]byte 74 utf8.EncodeRune(buf[:], rune(lowRune)) 75 lowByte = buf[0] 76 } 77 } 78 if stdRune == rune(lowByte) { 79 lowRuneStr := lowMsg[len(prefix):] 80 lowRuneStr = lowRuneStr[:strings.IndexByte(lowRuneStr, '\'')] 81 stdMsg = prefix + lowRuneStr + stdMsg[len(prefix)+stdRuneSize:] 82 stdErr = errors.New(stdMsg) 83 } 84 } 85 86 // I'd file a ticket for this, but @dsnet (one of the encoding/json maintainers) says that he's 87 // working on a parser-rewrite that would fix a bunch of this type of issue. 88 // https://github.com/golang/go/issues/58680#issuecomment-1444224084 89 if strings.HasPrefix(stdMsg, `invalid character '\u00`) && strings.HasPrefix(lowMsg, `invalid character '\x`) { 90 stdMsg = `invalid character '\x` + strings.TrimPrefix(stdMsg, `invalid character '\u00`) 91 stdErr = errors.New(stdMsg) 92 } 93 } 94 // Text-equal. 95 assert.Equal(t, stdErr.Error(), lowErr.Error()) 96 // TODO: Assert that they are deep-equal (but be permissive of these not being type aliases). 97 case *std.MarshalerError: 98 // Text-equal. 99 assert.Equal(t, stdErr.Error(), lowErr.Error()) 100 // TODO: Assert that they are deep-equal (but be permissive of these not being type aliases). 101 default: 102 // Text-equal. 103 assert.Equal(t, stdErr.Error(), lowErr.Error()) 104 // TODO: Assert that they are deep-equal. 105 } 106 } 107 108 func FuzzEquiv(f *testing.F) { 109 f.Fuzz(func(t *testing.T, str []byte) { 110 t.Logf("str=%q", str) 111 t.Run("HTMLEscape", func(t *testing.T) { 112 var stdOut bytes.Buffer 113 std.HTMLEscape(&stdOut, str) 114 115 var lowOut bytes.Buffer 116 low.HTMLEscape(&lowOut, str) 117 118 assert.Equal(t, stdOut.String(), lowOut.String()) 119 }) 120 t.Run("Compact", func(t *testing.T) { 121 var stdOut bytes.Buffer 122 stdErr := std.Compact(&stdOut, str) 123 124 var lowOut bytes.Buffer 125 lowErr := low.Compact(&lowOut, str) 126 127 assert.Equal(t, stdOut.String(), lowOut.String()) 128 assertEquivErr(t, stdErr, lowErr) 129 }) 130 t.Run("Indent", func(t *testing.T) { 131 var stdOut bytes.Buffer 132 stdErr := std.Indent(&stdOut, str, "»", "\t") 133 134 var lowOut bytes.Buffer 135 lowErr := low.Indent(&lowOut, str, "»", "\t") 136 137 assert.Equal(t, stdOut.String(), lowOut.String()) 138 assertEquivErr(t, stdErr, lowErr) 139 }) 140 t.Run("Valid", func(t *testing.T) { 141 stdValid := std.Valid(str) && utf8.Valid(str) // https://github.com/golang/go/issues/58517 142 lowValid := low.Valid(str) 143 assert.Equal(t, stdValid, lowValid) 144 }) 145 t.Run("Decode-Encode", func(t *testing.T) { 146 var stdObj any 147 stdErr := std.NewDecoder(bytes.NewReader(str)).Decode(&stdObj) 148 149 var lowObj any 150 lowErr := low.NewDecoder(bytes.NewReader(str)).Decode(&lowObj) 151 152 assert.Equal(t, stdObj, lowObj) 153 assertEquivErr(t, stdErr, lowErr) 154 if t.Failed() { 155 return 156 } 157 158 var stdOut bytes.Buffer 159 stdErr = std.NewEncoder(&stdOut).Encode(stdObj) 160 161 var lowOut bytes.Buffer 162 lowErr = low.NewEncoder(&lowOut).Encode(lowObj) 163 164 assert.Equal(t, stdOut.String(), lowOut.String()) 165 assertEquivErr(t, stdErr, lowErr) 166 }) 167 t.Run("Unmarshal-Marshal", func(t *testing.T) { 168 var stdObj any 169 stdErr := std.Unmarshal(str, &stdObj) 170 171 var lowObj any 172 lowErr := low.Unmarshal(str, &lowObj) 173 174 assert.Equal(t, stdObj, lowObj) 175 assertEquivErr(t, stdErr, lowErr) 176 if t.Failed() { 177 return 178 } 179 180 stdOut, stdErr := std.Marshal(stdObj) 181 lowOut, lowErr := low.Marshal(lowObj) 182 183 assert.Equal(t, string(stdOut), string(lowOut)) 184 assertEquivErr(t, stdErr, lowErr) 185 }) 186 }) 187 }