git.lukeshu.com/go/lowmemjson@v0.3.9-0.20230723050957-72f6d13f6fb2/methods_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_test
     6  
     7  import (
     8  	"bytes"
     9  	"errors"
    10  	"fmt"
    11  	"io"
    12  	"strings"
    13  	"testing"
    14  
    15  	"github.com/stretchr/testify/assert"
    16  
    17  	"git.lukeshu.com/go/lowmemjson"
    18  )
    19  
    20  type ShortSum string
    21  
    22  func (s ShortSum) EncodeJSON(w io.Writer) error {
    23  	// Test that it's OK to call lowmemjson.Encoder.Encode for the top-level value in a method.
    24  	return lowmemjson.NewEncoder(w).Encode(string(s))
    25  }
    26  
    27  type SumRun struct {
    28  	ChecksumSize int   `json:",omitempty"`
    29  	Addr         int64 `json:",omitempty"`
    30  	Sums         ShortSum
    31  }
    32  
    33  func (run SumRun) Size() int64 {
    34  	return int64(len(run.Sums)/(2*run.ChecksumSize)) * (4 * 1024)
    35  }
    36  
    37  type SumRunWithGaps struct {
    38  	Addr int64
    39  	Size int64
    40  	Runs []SumRun
    41  }
    42  
    43  func (sg SumRunWithGaps) EncodeJSON(w io.Writer) error {
    44  	if _, err := fmt.Fprintf(w, `{"Addr":%d,"Size":%d,"Runs":[`, sg.Addr, sg.Size); err != nil {
    45  		return err
    46  	}
    47  	cur := sg.Addr
    48  	for i, run := range sg.Runs {
    49  		if i > 0 {
    50  			if _, err := w.Write([]byte{','}); err != nil {
    51  				return err
    52  			}
    53  		}
    54  		switch {
    55  		case run.Addr < cur:
    56  			return fmt.Errorf("invalid %T: addr went backwards: %v < %v", sg, run.Addr, cur)
    57  		case run.Addr > cur:
    58  			if _, err := fmt.Fprintf(w, `{"Gap":%d},`, run.Addr-cur); err != nil {
    59  				return err
    60  			}
    61  			fallthrough
    62  		default:
    63  			if err := lowmemjson.NewEncoder(w).Encode(run); err != nil {
    64  				return err
    65  			}
    66  			cur = run.Addr + run.Size()
    67  		}
    68  	}
    69  	end := sg.Addr + sg.Size
    70  	switch {
    71  	case end < cur:
    72  		return fmt.Errorf("invalid %T: addr went backwards: %v < %v", sg, end, cur)
    73  	case end > cur:
    74  		if _, err := fmt.Fprintf(w, `,{"Gap":%d}`, end-cur); err != nil {
    75  			return err
    76  		}
    77  	}
    78  	if _, err := w.Write([]byte("]}")); err != nil {
    79  		return err
    80  	}
    81  	return nil
    82  }
    83  
    84  func (sg *SumRunWithGaps) DecodeJSON(r io.RuneScanner) error {
    85  	*sg = SumRunWithGaps{}
    86  	var name string
    87  	return lowmemjson.DecodeObject(r,
    88  		func(r io.RuneScanner) error {
    89  			return lowmemjson.NewDecoder(r).Decode(&name)
    90  		},
    91  		func(r io.RuneScanner) error {
    92  			switch name {
    93  			case "Addr":
    94  				return lowmemjson.NewDecoder(r).Decode(&sg.Addr)
    95  			case "Size":
    96  				return lowmemjson.NewDecoder(r).Decode(&sg.Size)
    97  			case "Runs":
    98  				return lowmemjson.DecodeArray(r, func(r io.RuneScanner) error {
    99  					var run SumRun
   100  					if err := lowmemjson.NewDecoder(r).Decode(&run); err != nil {
   101  						return err
   102  					}
   103  					if run.ChecksumSize > 0 {
   104  						sg.Runs = append(sg.Runs, run)
   105  					}
   106  					return nil
   107  				})
   108  			default:
   109  				return fmt.Errorf("unknown key %q", name)
   110  			}
   111  		})
   112  }
   113  
   114  func TestMethods(t *testing.T) {
   115  	t.Parallel()
   116  	in := SumRunWithGaps{
   117  		Addr: 13631488,
   118  		Size: 416033783808,
   119  		Runs: []SumRun{
   120  			{
   121  				ChecksumSize: 4,
   122  				Addr:         1095761920,
   123  				Sums:         "c160817cb5c72bbb",
   124  			},
   125  		},
   126  	}
   127  	var buf bytes.Buffer
   128  	assert.NoError(t, lowmemjson.NewEncoder(&buf).Encode(in))
   129  	assert.Equal(t,
   130  		`{"Addr":13631488,"Size":416033783808,"Runs":[{"Gap":1082130432},{"ChecksumSize":4,"Addr":1095761920,"Sums":"c160817cb5c72bbb"},{"Gap":414951645184}]}`,
   131  		buf.String())
   132  	var out SumRunWithGaps
   133  	assert.NoError(t, lowmemjson.NewDecoder(&buf).Decode(&out))
   134  	assert.Equal(t, in, out)
   135  }
   136  
   137  type strEncoder string
   138  
   139  func (s strEncoder) EncodeJSON(w io.Writer) error {
   140  	_, err := io.WriteString(w, string(s))
   141  	return err
   142  }
   143  
   144  type strMarshaler string
   145  
   146  func (s strMarshaler) MarshalJSON() ([]byte, error) {
   147  	return []byte(s), nil
   148  }
   149  
   150  type strTextMarshaler struct {
   151  	str string
   152  	err string
   153  }
   154  
   155  func (m strTextMarshaler) MarshalText() (txt []byte, err error) {
   156  	if len(m.str) > 0 {
   157  		txt = []byte(m.str)
   158  	}
   159  	if len(m.err) > 0 {
   160  		err = errors.New(m.err)
   161  	}
   162  	return
   163  }
   164  
   165  func TestMethodsEncode(t *testing.T) {
   166  	t.Parallel()
   167  	type testcase struct {
   168  		In          string
   169  		ExpectedErr string
   170  	}
   171  	testcases := map[string]testcase{
   172  		"basic": {In: `{}`},
   173  		"empty": {In: ``, ExpectedErr: `syntax error at input byte 0: EOF`},
   174  		"short": {In: `{`, ExpectedErr: `syntax error at input byte 1: unexpected EOF`},
   175  		"long":  {In: `{}{}`, ExpectedErr: `syntax error at input byte 2: invalid character '{' after top-level value`},
   176  	}
   177  	t.Run("encodable", func(t *testing.T) {
   178  		t.Parallel()
   179  		for tcName, tc := range testcases {
   180  			tc := tc
   181  			t.Run(tcName, func(t *testing.T) {
   182  				t.Parallel()
   183  				var buf strings.Builder
   184  				err := lowmemjson.NewEncoder(&buf).Encode([]any{strEncoder(tc.In)})
   185  				if tc.ExpectedErr == "" {
   186  					assert.NoError(t, err)
   187  					assert.Equal(t, "["+tc.In+"]", buf.String())
   188  				} else {
   189  					assert.EqualError(t, err,
   190  						`json: error calling EncodeJSON for type lowmemjson_test.strEncoder: `+
   191  							tc.ExpectedErr)
   192  				}
   193  			})
   194  		}
   195  	})
   196  	t.Run("marshaler", func(t *testing.T) {
   197  		t.Parallel()
   198  		for tcName, tc := range testcases {
   199  			tc := tc
   200  			t.Run(tcName, func(t *testing.T) {
   201  				t.Parallel()
   202  				var buf strings.Builder
   203  				err := lowmemjson.NewEncoder(&buf).Encode([]any{strMarshaler(tc.In)})
   204  				if tc.ExpectedErr == "" {
   205  					assert.NoError(t, err)
   206  					assert.Equal(t, "["+tc.In+"]", buf.String())
   207  				} else {
   208  					assert.EqualError(t, err,
   209  						`json: error calling MarshalJSON for type lowmemjson_test.strMarshaler: `+
   210  							tc.ExpectedErr)
   211  				}
   212  			})
   213  		}
   214  	})
   215  	t.Run("text", func(t *testing.T) {
   216  		t.Parallel()
   217  		type testcase struct {
   218  			Str string
   219  			Err string
   220  		}
   221  		testcases := map[string]testcase{
   222  			"basic": {Str: `a`},
   223  			"err":   {Err: `xxx`},
   224  			"both":  {Str: `a`, Err: `xxx`},
   225  		}
   226  		for tcName, tc := range testcases {
   227  			tc := tc
   228  			t.Run(tcName, func(t *testing.T) {
   229  				t.Parallel()
   230  				var buf strings.Builder
   231  				err := lowmemjson.NewEncoder(&buf).Encode([]any{strTextMarshaler{str: tc.Str, err: tc.Err}})
   232  				if tc.Err == "" {
   233  					assert.NoError(t, err)
   234  					assert.Equal(t, `["`+tc.Str+`"]`, buf.String())
   235  				} else {
   236  					assert.EqualError(t, err,
   237  						`json: error calling MarshalText for type lowmemjson_test.strTextMarshaler: `+
   238  							tc.Err)
   239  					assert.Equal(t, "[", buf.String())
   240  				}
   241  			})
   242  		}
   243  	})
   244  }
   245  
   246  type tstDecoder struct {
   247  	n   int
   248  	err string
   249  }
   250  
   251  func (d *tstDecoder) DecodeJSON(r io.RuneScanner) error {
   252  	for i := 0; i < d.n; i++ {
   253  		if _, _, err := r.ReadRune(); err != nil {
   254  			if err == io.EOF {
   255  				break
   256  			}
   257  			return err
   258  		}
   259  	}
   260  	if len(d.err) > 0 {
   261  		return errors.New(d.err)
   262  	}
   263  	return nil
   264  }
   265  
   266  type strUnmarshaler struct {
   267  	err string
   268  }
   269  
   270  func (u *strUnmarshaler) UnmarshalJSON([]byte) error {
   271  	if u.err == "" {
   272  		return nil
   273  	}
   274  	return errors.New(u.err)
   275  }
   276  
   277  type textUnmarshaler struct {
   278  	err string
   279  }
   280  
   281  func (u *textUnmarshaler) UnmarshalText([]byte) error {
   282  	if u.err == "" {
   283  		return nil
   284  	}
   285  	return errors.New(u.err)
   286  }
   287  
   288  type errTextUnmarshaler struct {
   289  	S string
   290  }
   291  
   292  func (u *errTextUnmarshaler) UnmarshalText(dat []byte) error {
   293  	u.S = string(dat)
   294  	return errors.New("eee")
   295  }
   296  
   297  func TestMethodsDecode(t *testing.T) {
   298  	t.Parallel()
   299  	type testcase struct {
   300  		In          string
   301  		Obj         any
   302  		ExpectedErr string
   303  	}
   304  	testcases := map[string]testcase{
   305  		"decode-basic":           {In: `{}`, Obj: &tstDecoder{n: 2}},
   306  		"decode-basic-eof":       {In: `{}`, Obj: &tstDecoder{n: 5}},
   307  		"decode-syntax-error":    {In: `{x}`, Obj: &tstDecoder{n: 5}, ExpectedErr: `json: v: syntax error at input byte 1: invalid character 'x' looking for beginning of object key string`},
   308  		"unmarshal-syntax-error": {In: `{x}`, Obj: &strUnmarshaler{}, ExpectedErr: `json: v: syntax error at input byte 1: invalid character 'x' looking for beginning of object key string`},
   309  		"decode-short":           {In: `{}`, Obj: &tstDecoder{n: 1}, ExpectedErr: `json: v: cannot decode JSON object at input byte 0 into Go *lowmemjson_test.tstDecoder: did not consume entire object`},
   310  		"decode-err":             {In: `{}`, Obj: &tstDecoder{err: "xxx"}, ExpectedErr: `json: v: cannot decode JSON object at input byte 0 into Go *lowmemjson_test.tstDecoder: xxx`},
   311  		"decode-err2":            {In: `{}`, Obj: &tstDecoder{n: 1, err: "yyy"}, ExpectedErr: `json: v: cannot decode JSON object at input byte 0 into Go *lowmemjson_test.tstDecoder: yyy`},
   312  		"unmarshal-err":          {In: `{}`, Obj: &strUnmarshaler{err: "zzz"}, ExpectedErr: `json: v: cannot decode JSON object at input byte 0 into Go *lowmemjson_test.strUnmarshaler: zzz`},
   313  		"unmarshaltext":          {In: `""`, Obj: &textUnmarshaler{}},
   314  		"unmarshaltext-nonstr":   {In: `{}`, Obj: &textUnmarshaler{}, ExpectedErr: `json: v: cannot decode JSON object at input byte 0 into Go *lowmemjson_test.textUnmarshaler`},
   315  		"unmarshaltext-err":      {In: `""`, Obj: &textUnmarshaler{err: "zzz"}, ExpectedErr: `json: v: cannot decode JSON string at input byte 0 into Go *lowmemjson_test.textUnmarshaler: zzz`},
   316  		"unmarshaltext-mapkey":   {In: `{"a":1}`, Obj: new(map[errTextUnmarshaler]int), ExpectedErr: `json: v: cannot decode JSON string at input byte 1 into Go *lowmemjson_test.errTextUnmarshaler: eee`},
   317  	}
   318  	for tcName, tc := range testcases {
   319  		tc := tc
   320  		t.Run(tcName, func(t *testing.T) {
   321  			t.Parallel()
   322  			obj := tc.Obj
   323  			err := lowmemjson.NewDecoder(strings.NewReader(tc.In)).Decode(&obj)
   324  			if tc.ExpectedErr == "" {
   325  				assert.NoError(t, err)
   326  			} else {
   327  				assert.EqualError(t, err, tc.ExpectedErr)
   328  			}
   329  		})
   330  	}
   331  }