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  }