blob: c896a2ead85de5e5df4e7bacd5489a0002e50138 [file] [log] [blame]
// Copyright 2019 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package json_test
import (
"strings"
"testing"
"unicode/utf8"
"google.golang.org/protobuf/internal/encoding/json"
)
type R struct {
// T is expected Type returned from calling Decoder.Read.
T json.Type
// E is expected error substring from calling Decoder.Read if set.
E string
// V is expected value from calling
// Value.{Bool()|Float()|Int()|Uint()|String()} depending on type.
V interface{}
// VE is expected error substring from calling
// Value.{Bool()|Float()|Int()|Uint()|String()} depending on type if set.
VE string
}
func TestDecoder(t *testing.T) {
const space = " \n\r\t"
tests := []struct {
input string
// want is a list of expected values returned from calling
// Decoder.Read. An item makes the test code invoke
// Decoder.Read and compare against R.T and R.E. For Bool,
// Number and String tokens, it invokes the corresponding getter method
// and compares the returned value against R.V or R.VE if it returned an
// error.
want []R
}{
{
input: ``,
want: []R{{T: json.EOF}},
},
{
input: space,
want: []R{{T: json.EOF}},
},
{
// Calling Read after EOF will keep returning EOF for
// succeeding Read calls.
input: space,
want: []R{
{T: json.EOF},
{T: json.EOF},
{T: json.EOF},
},
},
// JSON literals.
{
input: space + `null` + space,
want: []R{
{T: json.Null},
{T: json.EOF},
},
},
{
input: space + `true` + space,
want: []R{
{T: json.Bool, V: true},
{T: json.EOF},
},
},
{
input: space + `false` + space,
want: []R{
{T: json.Bool, V: false},
{T: json.EOF},
},
},
{
// Error returned will produce the same error again.
input: space + `foo` + space,
want: []R{
{E: `invalid value foo`},
{E: `invalid value foo`},
},
},
// JSON strings.
{
input: space + `""` + space,
want: []R{
{T: json.String, V: ""},
{T: json.EOF},
},
},
{
input: space + `"hello"` + space,
want: []R{
{T: json.String, V: "hello"},
{T: json.EOF},
},
},
{
input: `"hello`,
want: []R{{E: `unexpected EOF`}},
},
{
input: "\"\x00\"",
want: []R{{E: `invalid character '\x00' in string`}},
},
{
input: "\"\u0031\u0032\"",
want: []R{
{T: json.String, V: "12"},
{T: json.EOF},
},
},
{
// Invalid UTF-8 error is returned in ReadString instead of Read.
input: "\"\xff\"",
want: []R{{E: `syntax error (line 1:1): invalid UTF-8 in string`}},
},
{
input: `"` + string(utf8.RuneError) + `"`,
want: []R{
{T: json.String, V: string(utf8.RuneError)},
{T: json.EOF},
},
},
{
input: `"\uFFFD"`,
want: []R{
{T: json.String, V: string(utf8.RuneError)},
{T: json.EOF},
},
},
{
input: `"\x"`,
want: []R{{E: `invalid escape code "\\x" in string`}},
},
{
input: `"\uXXXX"`,
want: []R{{E: `invalid escape code "\\uXXXX" in string`}},
},
{
input: `"\uDEAD"`, // unmatched surrogate pair
want: []R{{E: `unexpected EOF`}},
},
{
input: `"\uDEAD\uBEEF"`, // invalid surrogate half
want: []R{{E: `invalid escape code "\\uBEEF" in string`}},
},
{
input: `"\uD800\udead"`, // valid surrogate pair
want: []R{
{T: json.String, V: `𐊭`},
{T: json.EOF},
},
},
{
input: `"\u0000\"\\\/\b\f\n\r\t"`,
want: []R{
{T: json.String, V: "\u0000\"\\/\b\f\n\r\t"},
{T: json.EOF},
},
},
// Invalid JSON numbers.
{
input: `-`,
want: []R{{E: `invalid number -`}},
},
{
input: `+0`,
want: []R{{E: `invalid value +0`}},
},
{
input: `-+`,
want: []R{{E: `invalid number -+`}},
},
{
input: `0.`,
want: []R{{E: `invalid number 0.`}},
},
{
input: `.1`,
want: []R{{E: `invalid value .1`}},
},
{
input: `1.0.1`,
want: []R{{E: `invalid number 1.0.1`}},
},
{
input: `1..1`,
want: []R{{E: `invalid number 1..1`}},
},
{
input: `-1-2`,
want: []R{{E: `invalid number -1-2`}},
},
{
input: `01`,
want: []R{{E: `invalid number 01`}},
},
{
input: `1e`,
want: []R{{E: `invalid number 1e`}},
},
{
input: `1e1.2`,
want: []R{{E: `invalid number 1e1.2`}},
},
{
input: `1Ee`,
want: []R{{E: `invalid number 1Ee`}},
},
{
input: `1.e1`,
want: []R{{E: `invalid number 1.e1`}},
},
{
input: `1.e+`,
want: []R{{E: `invalid number 1.e+`}},
},
{
input: `1e+-2`,
want: []R{{E: `invalid number 1e+-2`}},
},
{
input: `1e--2`,
want: []R{{E: `invalid number 1e--2`}},
},
{
input: `1.0true`,
want: []R{{E: `invalid number 1.0true`}},
},
// JSON numbers as floating point.
{
input: space + `0.0` + space,
want: []R{
{T: json.Number, V: float32(0)},
{T: json.EOF},
},
},
{
input: space + `0` + space,
want: []R{
{T: json.Number, V: float32(0)},
{T: json.EOF},
},
},
{
input: space + `-0` + space,
want: []R{
{T: json.Number, V: float32(0)},
{T: json.EOF},
},
},
{
input: `-1.02`,
want: []R{
{T: json.Number, V: float32(-1.02)},
{T: json.EOF},
},
},
{
input: `1.020000`,
want: []R{
{T: json.Number, V: float32(1.02)},
{T: json.EOF},
},
},
{
input: `-1.0e0`,
want: []R{
{T: json.Number, V: float32(-1)},
{T: json.EOF},
},
},
{
input: `1.0e-000`,
want: []R{
{T: json.Number, V: float32(1)},
{T: json.EOF},
},
},
{
input: `1e+00`,
want: []R{
{T: json.Number, V: float32(1)},
{T: json.EOF},
},
},
{
input: `1.02e3`,
want: []R{
{T: json.Number, V: float32(1.02e3)},
{T: json.EOF},
},
},
{
input: `-1.02E03`,
want: []R{
{T: json.Number, V: float32(-1.02e3)},
{T: json.EOF},
},
},
{
input: `1.0200e+3`,
want: []R{
{T: json.Number, V: float32(1.02e3)},
{T: json.EOF},
},
},
{
input: `-1.0200E+03`,
want: []R{
{T: json.Number, V: float32(-1.02e3)},
{T: json.EOF},
},
},
{
input: `1.0200e-3`,
want: []R{
{T: json.Number, V: float32(1.02e-3)},
{T: json.EOF},
},
},
{
input: `-1.0200E-03`,
want: []R{
{T: json.Number, V: float32(-1.02e-3)},
{T: json.EOF},
},
},
{
// Exceeds max float32 limit, but should be ok for float64.
input: `3.4e39`,
want: []R{
{T: json.Number, V: float64(3.4e39)},
{T: json.EOF},
},
},
{
// Exceeds max float32 limit.
input: `3.4e39`,
want: []R{
{T: json.Number, V: float32(0), VE: `value out of range`},
{T: json.EOF},
},
},
{
// Less than negative max float32 limit.
input: `-3.4e39`,
want: []R{
{T: json.Number, V: float32(0), VE: `value out of range`},
{T: json.EOF},
},
},
{
// Exceeds max float64 limit.
input: `1.79e+309`,
want: []R{
{T: json.Number, V: float64(0), VE: `value out of range`},
{T: json.EOF},
},
},
{
// Less than negative max float64 limit.
input: `-1.79e+309`,
want: []R{
{T: json.Number, V: float64(0), VE: `value out of range`},
{T: json.EOF},
},
},
// JSON numbers as signed integers.
{
input: space + `0` + space,
want: []R{
{T: json.Number, V: int32(0)},
{T: json.EOF},
},
},
{
input: space + `-0` + space,
want: []R{
{T: json.Number, V: int32(0)},
{T: json.EOF},
},
},
{
// Fractional part equals 0 is ok.
input: `1.00000`,
want: []R{
{T: json.Number, V: int32(1)},
{T: json.EOF},
},
},
{
// Fractional part not equals 0 returns error.
input: `1.0000000001`,
want: []R{
{T: json.Number, V: int32(0), VE: `cannot convert 1.0000000001 to integer`},
{T: json.EOF},
},
},
{
input: `0e0`,
want: []R{
{T: json.Number, V: int32(0)},
{T: json.EOF},
},
},
{
input: `0.0E0`,
want: []R{
{T: json.Number, V: int32(0)},
{T: json.EOF},
},
},
{
input: `0.0E10`,
want: []R{
{T: json.Number, V: int32(0)},
{T: json.EOF},
},
},
{
input: `-1`,
want: []R{
{T: json.Number, V: int32(-1)},
{T: json.EOF},
},
},
{
input: `1.0e+0`,
want: []R{
{T: json.Number, V: int32(1)},
{T: json.EOF},
},
},
{
input: `-1E-0`,
want: []R{
{T: json.Number, V: int32(-1)},
{T: json.EOF},
},
},
{
input: `1E1`,
want: []R{
{T: json.Number, V: int32(10)},
{T: json.EOF},
},
},
{
input: `-100.00e-02`,
want: []R{
{T: json.Number, V: int32(-1)},
{T: json.EOF},
},
},
{
input: `0.1200E+02`,
want: []R{
{T: json.Number, V: int64(12)},
{T: json.EOF},
},
},
{
input: `0.012e2`,
want: []R{
{T: json.Number, V: int32(0), VE: `cannot convert 0.012e2 to integer`},
{T: json.EOF},
},
},
{
input: `12e-2`,
want: []R{
{T: json.Number, V: int32(0), VE: `cannot convert 12e-2 to integer`},
{T: json.EOF},
},
},
{
// Exceeds math.MaxInt32.
input: `2147483648`,
want: []R{
{T: json.Number, V: int32(0), VE: `value out of range`},
{T: json.EOF},
},
},
{
// Exceeds math.MinInt32.
input: `-2147483649`,
want: []R{
{T: json.Number, V: int32(0), VE: `value out of range`},
{T: json.EOF},
},
},
{
// Exceeds math.MaxInt32, but ok for int64.
input: `2147483648`,
want: []R{
{T: json.Number, V: int64(2147483648)},
{T: json.EOF},
},
},
{
// Exceeds math.MinInt32, but ok for int64.
input: `-2147483649`,
want: []R{
{T: json.Number, V: int64(-2147483649)},
{T: json.EOF},
},
},
{
// Exceeds math.MaxInt64.
input: `9223372036854775808`,
want: []R{
{T: json.Number, V: int64(0), VE: `value out of range`},
{T: json.EOF},
},
},
{
// Exceeds math.MinInt64.
input: `-9223372036854775809`,
want: []R{
{T: json.Number, V: int64(0), VE: `value out of range`},
{T: json.EOF},
},
},
// JSON numbers as unsigned integers.
{
input: space + `0` + space,
want: []R{
{T: json.Number, V: uint32(0)},
{T: json.EOF},
},
},
{
input: space + `-0` + space,
want: []R{
{T: json.Number, V: uint32(0)},
{T: json.EOF},
},
},
{
input: `-1`,
want: []R{
{T: json.Number, V: uint32(0), VE: `invalid syntax`},
{T: json.EOF},
},
},
{
// Exceeds math.MaxUint32.
input: `4294967296`,
want: []R{
{T: json.Number, V: uint32(0), VE: `value out of range`},
{T: json.EOF},
},
},
{
// Exceeds math.MaxUint64.
input: `18446744073709551616`,
want: []R{
{T: json.Number, V: uint64(0), VE: `value out of range`},
{T: json.EOF},
},
},
// JSON sequence of values.
{
input: `true null`,
want: []R{
{T: json.Bool, V: true},
{E: `unexpected value null`},
},
},
{
input: "null false",
want: []R{
{T: json.Null},
{E: `unexpected value false`},
},
},
{
input: `true,false`,
want: []R{
{T: json.Bool, V: true},
{E: `unexpected character ,`},
},
},
{
input: `47"hello"`,
want: []R{
{T: json.Number, V: int32(47)},
{E: `unexpected value "hello"`},
},
},
{
input: `47 "hello"`,
want: []R{
{T: json.Number, V: int32(47)},
{E: `unexpected value "hello"`},
},
},
{
input: `true 42`,
want: []R{
{T: json.Bool, V: true},
{E: `unexpected value 42`},
},
},
// JSON arrays.
{
input: space + `[]` + space,
want: []R{
{T: json.StartArray},
{T: json.EndArray},
{T: json.EOF},
},
},
{
input: space + `[` + space + `]` + space,
want: []R{
{T: json.StartArray},
{T: json.EndArray},
{T: json.EOF},
},
},
{
input: space + `[` + space,
want: []R{
{T: json.StartArray},
{E: `unexpected EOF`},
},
},
{
input: space + `]` + space,
want: []R{{E: `unexpected character ]`}},
},
{
input: `[null,true,false, 1e1, "hello" ]`,
want: []R{
{T: json.StartArray},
{T: json.Null},
{T: json.Bool, V: true},
{T: json.Bool, V: false},
{T: json.Number, V: int32(10)},
{T: json.String, V: "hello"},
{T: json.EndArray},
{T: json.EOF},
},
},
{
input: `[` + space + `true` + space + `,` + space + `"hello"` + space + `]`,
want: []R{
{T: json.StartArray},
{T: json.Bool, V: true},
{T: json.String, V: "hello"},
{T: json.EndArray},
{T: json.EOF},
},
},
{
input: `[` + space + `true` + space + `,` + space + `]`,
want: []R{
{T: json.StartArray},
{T: json.Bool, V: true},
{E: `unexpected character ]`},
},
},
{
input: `[` + space + `false` + space + `]`,
want: []R{
{T: json.StartArray},
{T: json.Bool, V: false},
{T: json.EndArray},
{T: json.EOF},
},
},
{
input: `[` + space + `1` + space + `0` + space + `]`,
want: []R{
{T: json.StartArray},
{T: json.Number, V: int64(1)},
{E: `unexpected value 0`},
},
},
{
input: `[null`,
want: []R{
{T: json.StartArray},
{T: json.Null},
{E: `unexpected EOF`},
},
},
{
input: `[foo]`,
want: []R{
{T: json.StartArray},
{E: `invalid value foo`},
},
},
{
input: `[{}, "hello", [true, false], null]`,
want: []R{
{T: json.StartArray},
{T: json.StartObject},
{T: json.EndObject},
{T: json.String, V: "hello"},
{T: json.StartArray},
{T: json.Bool, V: true},
{T: json.Bool, V: false},
{T: json.EndArray},
{T: json.Null},
{T: json.EndArray},
{T: json.EOF},
},
},
{
input: `[{ ]`,
want: []R{
{T: json.StartArray},
{T: json.StartObject},
{E: `unexpected character ]`},
},
},
{
input: `[[ ]`,
want: []R{
{T: json.StartArray},
{T: json.StartArray},
{T: json.EndArray},
{E: `unexpected EOF`},
},
},
{
input: `[,]`,
want: []R{
{T: json.StartArray},
{E: `unexpected character ,`},
},
},
{
input: `[true "hello"]`,
want: []R{
{T: json.StartArray},
{T: json.Bool, V: true},
{E: `unexpected value "hello"`},
},
},
{
input: `[] null`,
want: []R{
{T: json.StartArray},
{T: json.EndArray},
{E: `unexpected value null`},
},
},
{
input: `true []`,
want: []R{
{T: json.Bool, V: true},
{E: `unexpected character [`},
},
},
// JSON objects.
{
input: space + `{}` + space,
want: []R{
{T: json.StartObject},
{T: json.EndObject},
{T: json.EOF},
},
},
{
input: space + `{` + space + `}` + space,
want: []R{
{T: json.StartObject},
{T: json.EndObject},
{T: json.EOF},
},
},
{
input: space + `{` + space,
want: []R{
{T: json.StartObject},
{E: `unexpected EOF`},
},
},
{
input: space + `}` + space,
want: []R{{E: `unexpected character }`}},
},
{
input: `{` + space + `null` + space + `}`,
want: []R{
{T: json.StartObject},
{E: `unexpected value null`},
},
},
{
input: `{[]}`,
want: []R{
{T: json.StartObject},
{E: `unexpected character [`},
},
},
{
input: `{,}`,
want: []R{
{T: json.StartObject},
{E: `unexpected character ,`},
},
},
{
input: `{"345678"}`,
want: []R{
{T: json.StartObject},
{E: `unexpected character }, missing ":" after object name`},
},
},
{
input: `{` + space + `"hello"` + space + `:` + space + `"world"` + space + `}`,
want: []R{
{T: json.StartObject},
{T: json.Name, V: "hello"},
{T: json.String, V: "world"},
{T: json.EndObject},
{T: json.EOF},
},
},
{
input: `{"hello" "world"}`,
want: []R{
{T: json.StartObject},
{E: `unexpected character ", missing ":" after object name`},
},
},
{
input: `{"hello":`,
want: []R{
{T: json.StartObject},
{T: json.Name, V: "hello"},
{E: `unexpected EOF`},
},
},
{
input: `{"hello":"world"`,
want: []R{
{T: json.StartObject},
{T: json.Name, V: "hello"},
{T: json.String, V: "world"},
{E: `unexpected EOF`},
},
},
{
input: `{"hello":"world",`,
want: []R{
{T: json.StartObject},
{T: json.Name, V: "hello"},
{T: json.String, V: "world"},
{E: `unexpected EOF`},
},
},
{
input: `{""`,
want: []R{
{T: json.StartObject},
{E: `syntax error (line 1:4): unexpected EOF`},
},
},
{
input: `{"34":"89",}`,
want: []R{
{T: json.StartObject},
{T: json.Name, V: "34"},
{T: json.String, V: "89"},
{E: `syntax error (line 1:12): unexpected character }`},
},
},
{
input: `{
"number": 123e2,
"bool" : false,
"object": {"string": "world"},
"null" : null,
"array" : [1.01, "hello", true],
"string": "hello"
}`,
want: []R{
{T: json.StartObject},
{T: json.Name, V: "number"},
{T: json.Number, V: int32(12300)},
{T: json.Name, V: "bool"},
{T: json.Bool, V: false},
{T: json.Name, V: "object"},
{T: json.StartObject},
{T: json.Name, V: "string"},
{T: json.String, V: "world"},
{T: json.EndObject},
{T: json.Name, V: "null"},
{T: json.Null},
{T: json.Name, V: "array"},
{T: json.StartArray},
{T: json.Number, V: float32(1.01)},
{T: json.String, V: "hello"},
{T: json.Bool, V: true},
{T: json.EndArray},
{T: json.Name, V: "string"},
{T: json.String, V: "hello"},
{T: json.EndObject},
{T: json.EOF},
},
},
{
input: `[
{"object": {"number": 47}},
["list"],
null
]`,
want: []R{
{T: json.StartArray},
{T: json.StartObject},
{T: json.Name, V: "object"},
{T: json.StartObject},
{T: json.Name, V: "number"},
{T: json.Number, V: uint32(47)},
{T: json.EndObject},
{T: json.EndObject},
{T: json.StartArray},
{T: json.String, V: "list"},
{T: json.EndArray},
{T: json.Null},
{T: json.EndArray},
{T: json.EOF},
},
},
// Tests for line and column info.
{
input: `12345678 x`,
want: []R{
{T: json.Number, V: int64(12345678)},
{E: `syntax error (line 1:10): invalid value x`},
},
},
{
input: "\ntrue\n x",
want: []R{
{T: json.Bool, V: true},
{E: `syntax error (line 3:4): invalid value x`},
},
},
{
input: `"💩"x`,
want: []R{
{T: json.String, V: "💩"},
{E: `syntax error (line 1:4): invalid value x`},
},
},
{
input: "\n\n[\"🔥🔥🔥\"x",
want: []R{
{T: json.StartArray},
{T: json.String, V: "🔥🔥🔥"},
{E: `syntax error (line 3:7): invalid value x`},
},
},
{
// Multi-rune emojis.
input: `["👍🏻👍🏿"x`,
want: []R{
{T: json.StartArray},
{T: json.String, V: "👍🏻👍🏿"},
{E: `syntax error (line 1:8): invalid value x`},
},
},
{
input: `{
"45678":-1
}`,
want: []R{
{T: json.StartObject},
{T: json.Name, V: "45678"},
{T: json.Number, V: uint64(1), VE: "error (line 2:11)"},
},
},
}
for _, tc := range tests {
tc := tc
t.Run("", func(t *testing.T) {
dec := json.NewDecoder([]byte(tc.input))
for i, want := range tc.want {
typ := dec.Peek()
if typ != want.T {
t.Errorf("input: %v\nPeek() got %v want %v", tc.input, typ, want.T)
}
value, err := dec.Read()
if err != nil {
if want.E == "" {
t.Errorf("input: %v\nRead() got unexpected error: %v", tc.input, err)
} else if !strings.Contains(err.Error(), want.E) {
t.Errorf("input: %v\nRead() got %q, want %q", tc.input, err, want.E)
}
} else {
if want.E != "" {
t.Errorf("input: %v\nRead() got nil error, want %q", tc.input, want.E)
}
}
token := value.Type()
if token != want.T {
t.Errorf("input: %v\nRead() got %v, want %v", tc.input, token, want.T)
break
}
checkValue(t, value, i, want)
}
})
}
}
func checkValue(t *testing.T, value json.Value, wantIdx int, want R) {
var got interface{}
var err error
switch value.Type() {
case json.Bool:
got, err = value.Bool()
case json.String:
got = value.String()
case json.Name:
got, err = value.Name()
case json.Number:
switch want.V.(type) {
case float32:
got, err = value.Float(32)
got = float32(got.(float64))
case float64:
got, err = value.Float(64)
case int32:
got, err = value.Int(32)
got = int32(got.(int64))
case int64:
got, err = value.Int(64)
case uint32:
got, err = value.Uint(32)
got = uint32(got.(uint64))
case uint64:
got, err = value.Uint(64)
}
default:
return
}
if err != nil {
if want.VE == "" {
t.Errorf("want%d: %v got unexpected error: %v", wantIdx, value, err)
} else if !strings.Contains(err.Error(), want.VE) {
t.Errorf("want#%d: %v got %q, want %q", wantIdx, value, err, want.VE)
}
return
} else {
if want.VE != "" {
t.Errorf("want#%d: %v got nil error, want %q", wantIdx, value, want.VE)
return
}
}
if got != want.V {
t.Errorf("want#%d: %v got %v, want %v", wantIdx, value, got, want.V)
}
}
func TestClone(t *testing.T) {
input := `{"outer":{"str":"hello", "number": 123}}`
dec := json.NewDecoder([]byte(input))
// Clone at the start should produce the same reads as the original.
clone := dec.Clone()
compareDecoders(t, dec, clone)
// Advance to inner object, clone and compare again.
dec.Read() // Read StartObject.
dec.Read() // Read Name.
clone = dec.Clone()
compareDecoders(t, dec, clone)
}
func compareDecoders(t *testing.T, d1 *json.Decoder, d2 *json.Decoder) {
for {
v1, err1 := d1.Read()
v2, err2 := d2.Read()
if v1.Type() != v2.Type() {
t.Errorf("cloned decoder: got Type %v, want %v", v2.Type(), v1.Type())
}
if v1.Raw() != v2.Raw() {
t.Errorf("cloned decoder: got Raw %v, want %v", v2.Raw(), v1.Raw())
}
if err1 != err2 {
t.Errorf("cloned decoder: got error %v, want %v", err2, err1)
}
if v1.Type() == json.EOF {
break
}
}
}