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  }