summaryrefslogtreecommitdiffstats
path: root/dependencies/pkg/mod/go.uber.org/zap@v1.23.0/zapcore/json_encoder_impl_test.go
diff options
context:
space:
mode:
Diffstat (limited to 'dependencies/pkg/mod/go.uber.org/zap@v1.23.0/zapcore/json_encoder_impl_test.go')
-rw-r--r--dependencies/pkg/mod/go.uber.org/zap@v1.23.0/zapcore/json_encoder_impl_test.go659
1 files changed, 659 insertions, 0 deletions
diff --git a/dependencies/pkg/mod/go.uber.org/zap@v1.23.0/zapcore/json_encoder_impl_test.go b/dependencies/pkg/mod/go.uber.org/zap@v1.23.0/zapcore/json_encoder_impl_test.go
new file mode 100644
index 0000000..fde241f
--- /dev/null
+++ b/dependencies/pkg/mod/go.uber.org/zap@v1.23.0/zapcore/json_encoder_impl_test.go
@@ -0,0 +1,659 @@
+// Copyright (c) 2016 Uber Technologies, Inc.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+package zapcore
+
+import (
+ "encoding/json"
+ "errors"
+ "math"
+ "math/rand"
+ "reflect"
+ "testing"
+ "testing/quick"
+ "time"
+
+ "go.uber.org/zap/internal/bufferpool"
+
+ "github.com/stretchr/testify/assert"
+ "go.uber.org/multierr"
+)
+
+var _defaultEncoderConfig = EncoderConfig{
+ EncodeTime: EpochTimeEncoder,
+ EncodeDuration: SecondsDurationEncoder,
+}
+
+func TestJSONClone(t *testing.T) {
+ // The parent encoder is created with plenty of excess capacity.
+ parent := &jsonEncoder{buf: bufferpool.Get()}
+ clone := parent.Clone()
+
+ // Adding to the parent shouldn't affect the clone, and vice versa.
+ parent.AddString("foo", "bar")
+ clone.AddString("baz", "bing")
+
+ assertJSON(t, `"foo":"bar"`, parent)
+ assertJSON(t, `"baz":"bing"`, clone.(*jsonEncoder))
+}
+
+func TestJSONEscaping(t *testing.T) {
+ enc := &jsonEncoder{buf: bufferpool.Get()}
+ // Test all the edge cases of JSON escaping directly.
+ cases := map[string]string{
+ // ASCII.
+ `foo`: `foo`,
+ // Special-cased characters.
+ `"`: `\"`,
+ `\`: `\\`,
+ // Special-cased characters within everyday ASCII.
+ `foo"foo`: `foo\"foo`,
+ "foo\n": `foo\n`,
+ // Special-cased control characters.
+ "\n": `\n`,
+ "\r": `\r`,
+ "\t": `\t`,
+ // \b and \f are sometimes backslash-escaped, but this representation is also
+ // conformant.
+ "\b": `\u0008`,
+ "\f": `\u000c`,
+ // The standard lib special-cases angle brackets and ampersands by default,
+ // because it wants to protect users from browser exploits. In a logging
+ // context, we shouldn't special-case these characters.
+ "<": "<",
+ ">": ">",
+ "&": "&",
+ // ASCII bell - not special-cased.
+ string(byte(0x07)): `\u0007`,
+ // Astral-plane unicode.
+ `☃`: `☃`,
+ // Decodes to (RuneError, 1)
+ "\xed\xa0\x80": `\ufffd\ufffd\ufffd`,
+ "foo\xed\xa0\x80": `foo\ufffd\ufffd\ufffd`,
+ }
+
+ t.Run("String", func(t *testing.T) {
+ for input, output := range cases {
+ enc.truncate()
+ enc.safeAddString(input)
+ assertJSON(t, output, enc)
+ }
+ })
+
+ t.Run("ByteString", func(t *testing.T) {
+ for input, output := range cases {
+ enc.truncate()
+ enc.safeAddByteString([]byte(input))
+ assertJSON(t, output, enc)
+ }
+ })
+}
+
+func TestJSONEncoderObjectFields(t *testing.T) {
+ tests := []struct {
+ desc string
+ expected string
+ f func(Encoder)
+ }{
+ {"binary", `"k":"YWIxMg=="`, func(e Encoder) { e.AddBinary("k", []byte("ab12")) }},
+ {"bool", `"k\\":true`, func(e Encoder) { e.AddBool(`k\`, true) }}, // test key escaping once
+ {"bool", `"k":true`, func(e Encoder) { e.AddBool("k", true) }},
+ {"bool", `"k":false`, func(e Encoder) { e.AddBool("k", false) }},
+ {"byteString", `"k":"v\\"`, func(e Encoder) { e.AddByteString(`k`, []byte(`v\`)) }},
+ {"byteString", `"k":"v"`, func(e Encoder) { e.AddByteString("k", []byte("v")) }},
+ {"byteString", `"k":""`, func(e Encoder) { e.AddByteString("k", []byte{}) }},
+ {"byteString", `"k":""`, func(e Encoder) { e.AddByteString("k", nil) }},
+ {"complex128", `"k":"1+2i"`, func(e Encoder) { e.AddComplex128("k", 1+2i) }},
+ {"complex128/negative_i", `"k":"1-2i"`, func(e Encoder) { e.AddComplex128("k", 1-2i) }},
+ {"complex64", `"k":"1+2i"`, func(e Encoder) { e.AddComplex64("k", 1+2i) }},
+ {"complex64/negative_i", `"k":"1-2i"`, func(e Encoder) { e.AddComplex64("k", 1-2i) }},
+ {"complex64", `"k":"2.71+3.14i"`, func(e Encoder) { e.AddComplex64("k", 2.71+3.14i) }},
+ {"duration", `"k":0.000000001`, func(e Encoder) { e.AddDuration("k", 1) }},
+ {"duration/negative", `"k":-0.000000001`, func(e Encoder) { e.AddDuration("k", -1) }},
+ {"float64", `"k":1`, func(e Encoder) { e.AddFloat64("k", 1.0) }},
+ {"float64", `"k":10000000000`, func(e Encoder) { e.AddFloat64("k", 1e10) }},
+ {"float64", `"k":"NaN"`, func(e Encoder) { e.AddFloat64("k", math.NaN()) }},
+ {"float64", `"k":"+Inf"`, func(e Encoder) { e.AddFloat64("k", math.Inf(1)) }},
+ {"float64", `"k":"-Inf"`, func(e Encoder) { e.AddFloat64("k", math.Inf(-1)) }},
+ {"float64/pi", `"k":3.141592653589793`, func(e Encoder) { e.AddFloat64("k", math.Pi) }},
+ {"float32", `"k":1`, func(e Encoder) { e.AddFloat32("k", 1.0) }},
+ {"float32", `"k":2.71`, func(e Encoder) { e.AddFloat32("k", 2.71) }},
+ {"float32", `"k":0.1`, func(e Encoder) { e.AddFloat32("k", 0.1) }},
+ {"float32", `"k":10000000000`, func(e Encoder) { e.AddFloat32("k", 1e10) }},
+ {"float32", `"k":"NaN"`, func(e Encoder) { e.AddFloat32("k", float32(math.NaN())) }},
+ {"float32", `"k":"+Inf"`, func(e Encoder) { e.AddFloat32("k", float32(math.Inf(1))) }},
+ {"float32", `"k":"-Inf"`, func(e Encoder) { e.AddFloat32("k", float32(math.Inf(-1))) }},
+ {"float32/pi", `"k":3.1415927`, func(e Encoder) { e.AddFloat32("k", math.Pi) }},
+ {"int", `"k":42`, func(e Encoder) { e.AddInt("k", 42) }},
+ {"int64", `"k":42`, func(e Encoder) { e.AddInt64("k", 42) }},
+ {"int64/min", `"k":-9223372036854775808`, func(e Encoder) { e.AddInt64("k", math.MinInt64) }},
+ {"int64/max", `"k":9223372036854775807`, func(e Encoder) { e.AddInt64("k", math.MaxInt64) }},
+ {"int32", `"k":42`, func(e Encoder) { e.AddInt32("k", 42) }},
+ {"int32/min", `"k":-2147483648`, func(e Encoder) { e.AddInt32("k", math.MinInt32) }},
+ {"int32/max", `"k":2147483647`, func(e Encoder) { e.AddInt32("k", math.MaxInt32) }},
+ {"int16", `"k":42`, func(e Encoder) { e.AddInt16("k", 42) }},
+ {"int16/min", `"k":-32768`, func(e Encoder) { e.AddInt16("k", math.MinInt16) }},
+ {"int16/max", `"k":32767`, func(e Encoder) { e.AddInt16("k", math.MaxInt16) }},
+ {"int8", `"k":42`, func(e Encoder) { e.AddInt8("k", 42) }},
+ {"int8/min", `"k":-128`, func(e Encoder) { e.AddInt8("k", math.MinInt8) }},
+ {"int8/max", `"k":127`, func(e Encoder) { e.AddInt8("k", math.MaxInt8) }},
+ {"string", `"k":"v\\"`, func(e Encoder) { e.AddString(`k`, `v\`) }},
+ {"string", `"k":"v"`, func(e Encoder) { e.AddString("k", "v") }},
+ {"string", `"k":""`, func(e Encoder) { e.AddString("k", "") }},
+ {"time", `"k":1`, func(e Encoder) { e.AddTime("k", time.Unix(1, 0)) }},
+ {"uint", `"k":42`, func(e Encoder) { e.AddUint("k", 42) }},
+ {"uint64", `"k":42`, func(e Encoder) { e.AddUint64("k", 42) }},
+ {"uint64/max", `"k":18446744073709551615`, func(e Encoder) { e.AddUint64("k", math.MaxUint64) }},
+ {"uint32", `"k":42`, func(e Encoder) { e.AddUint32("k", 42) }},
+ {"uint32/max", `"k":4294967295`, func(e Encoder) { e.AddUint32("k", math.MaxUint32) }},
+ {"uint16", `"k":42`, func(e Encoder) { e.AddUint16("k", 42) }},
+ {"uint16/max", `"k":65535`, func(e Encoder) { e.AddUint16("k", math.MaxUint16) }},
+ {"uint8", `"k":42`, func(e Encoder) { e.AddUint8("k", 42) }},
+ {"uint8/max", `"k":255`, func(e Encoder) { e.AddUint8("k", math.MaxUint8) }},
+ {"uintptr", `"k":42`, func(e Encoder) { e.AddUintptr("k", 42) }},
+ {
+ desc: "object (success)",
+ expected: `"k":{"loggable":"yes"}`,
+ f: func(e Encoder) {
+ assert.NoError(t, e.AddObject("k", loggable{true}), "Unexpected error calling MarshalLogObject.")
+ },
+ },
+ {
+ desc: "object (error)",
+ expected: `"k":{}`,
+ f: func(e Encoder) {
+ assert.Error(t, e.AddObject("k", loggable{false}), "Expected an error calling MarshalLogObject.")
+ },
+ },
+ {
+ desc: "object (with nested array)",
+ expected: `"turducken":{"ducks":[{"in":"chicken"},{"in":"chicken"}]}`,
+ f: func(e Encoder) {
+ assert.NoError(
+ t,
+ e.AddObject("turducken", turducken{}),
+ "Unexpected error calling MarshalLogObject with nested ObjectMarshalers and ArrayMarshalers.",
+ )
+ },
+ },
+ {
+ desc: "array (with nested object)",
+ expected: `"turduckens":[{"ducks":[{"in":"chicken"},{"in":"chicken"}]},{"ducks":[{"in":"chicken"},{"in":"chicken"}]}]`,
+ f: func(e Encoder) {
+ assert.NoError(
+ t,
+ e.AddArray("turduckens", turduckens(2)),
+ "Unexpected error calling MarshalLogObject with nested ObjectMarshalers and ArrayMarshalers.",
+ )
+ },
+ },
+ {
+ desc: "array (success)",
+ expected: `"k":[true]`,
+ f: func(e Encoder) {
+ assert.NoError(t, e.AddArray(`k`, loggable{true}), "Unexpected error calling MarshalLogArray.")
+ },
+ },
+ {
+ desc: "array (error)",
+ expected: `"k":[]`,
+ f: func(e Encoder) {
+ assert.Error(t, e.AddArray("k", loggable{false}), "Expected an error calling MarshalLogArray.")
+ },
+ },
+ {
+ desc: "reflect (success)",
+ expected: `"k":{"escape":"<&>","loggable":"yes"}`,
+ f: func(e Encoder) {
+ assert.NoError(t, e.AddReflected("k", map[string]string{"escape": "<&>", "loggable": "yes"}), "Unexpected error JSON-serializing a map.")
+ },
+ },
+ {
+ desc: "reflect (failure)",
+ expected: "",
+ f: func(e Encoder) {
+ assert.Error(t, e.AddReflected("k", noJSON{}), "Unexpected success JSON-serializing a noJSON.")
+ },
+ },
+ {
+ desc: "namespace",
+ // EncodeEntry is responsible for closing all open namespaces.
+ expected: `"outermost":{"outer":{"foo":1,"inner":{"foo":2,"innermost":{`,
+ f: func(e Encoder) {
+ e.OpenNamespace("outermost")
+ e.OpenNamespace("outer")
+ e.AddInt("foo", 1)
+ e.OpenNamespace("inner")
+ e.AddInt("foo", 2)
+ e.OpenNamespace("innermost")
+ },
+ },
+ {
+ desc: "object (no nested namespace)",
+ expected: `"obj":{"obj-out":"obj-outside-namespace"},"not-obj":"should-be-outside-obj"`,
+ f: func(e Encoder) {
+ e.AddObject("obj", maybeNamespace{false})
+ e.AddString("not-obj", "should-be-outside-obj")
+ },
+ },
+ {
+ desc: "object (with nested namespace)",
+ expected: `"obj":{"obj-out":"obj-outside-namespace","obj-namespace":{"obj-in":"obj-inside-namespace"}},"not-obj":"should-be-outside-obj"`,
+ f: func(e Encoder) {
+ e.AddObject("obj", maybeNamespace{true})
+ e.AddString("not-obj", "should-be-outside-obj")
+ },
+ },
+ {
+ desc: "multiple open namespaces",
+ expected: `"k":{"foo":1,"middle":{"foo":2,"inner":{"foo":3}}}`,
+ f: func(e Encoder) {
+ e.AddObject("k", ObjectMarshalerFunc(func(enc ObjectEncoder) error {
+ e.AddInt("foo", 1)
+ e.OpenNamespace("middle")
+ e.AddInt("foo", 2)
+ e.OpenNamespace("inner")
+ e.AddInt("foo", 3)
+ return nil
+ }))
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.desc, func(t *testing.T) {
+ assertOutput(t, _defaultEncoderConfig, tt.expected, tt.f)
+ })
+ }
+}
+
+func TestJSONEncoderTimeFormats(t *testing.T) {
+ date := time.Date(2000, time.January, 2, 3, 4, 5, 6, time.UTC)
+
+ f := func(e Encoder) {
+ e.AddTime("k", date)
+ e.AddArray("a", ArrayMarshalerFunc(func(enc ArrayEncoder) error {
+ enc.AppendTime(date)
+ return nil
+ }))
+ }
+ tests := []struct {
+ desc string
+ cfg EncoderConfig
+ expected string
+ }{
+ {
+ desc: "time.Time ISO8601",
+ cfg: EncoderConfig{
+ EncodeDuration: NanosDurationEncoder,
+ EncodeTime: ISO8601TimeEncoder,
+ },
+ expected: `"k":"2000-01-02T03:04:05.000Z","a":["2000-01-02T03:04:05.000Z"]`,
+ },
+ {
+ desc: "time.Time RFC3339",
+ cfg: EncoderConfig{
+ EncodeDuration: NanosDurationEncoder,
+ EncodeTime: RFC3339TimeEncoder,
+ },
+ expected: `"k":"2000-01-02T03:04:05Z","a":["2000-01-02T03:04:05Z"]`,
+ },
+ {
+ desc: "time.Time RFC3339Nano",
+ cfg: EncoderConfig{
+ EncodeDuration: NanosDurationEncoder,
+ EncodeTime: RFC3339NanoTimeEncoder,
+ },
+ expected: `"k":"2000-01-02T03:04:05.000000006Z","a":["2000-01-02T03:04:05.000000006Z"]`,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.desc, func(t *testing.T) {
+ assertOutput(t, tt.cfg, tt.expected, f)
+ })
+ }
+}
+
+func TestJSONEncoderArrays(t *testing.T) {
+ tests := []struct {
+ desc string
+ expected string // expect f to be called twice
+ f func(ArrayEncoder)
+ }{
+ {"bool", `[true,true]`, func(e ArrayEncoder) { e.AppendBool(true) }},
+ {"byteString", `["k","k"]`, func(e ArrayEncoder) { e.AppendByteString([]byte("k")) }},
+ {"byteString", `["k\\","k\\"]`, func(e ArrayEncoder) { e.AppendByteString([]byte(`k\`)) }},
+ {"complex128", `["1+2i","1+2i"]`, func(e ArrayEncoder) { e.AppendComplex128(1 + 2i) }},
+ {"complex64", `["1+2i","1+2i"]`, func(e ArrayEncoder) { e.AppendComplex64(1 + 2i) }},
+ {"durations", `[0.000000002,0.000000002]`, func(e ArrayEncoder) { e.AppendDuration(2) }},
+ {"float64", `[3.14,3.14]`, func(e ArrayEncoder) { e.AppendFloat64(3.14) }},
+ {"float32", `[3.14,3.14]`, func(e ArrayEncoder) { e.AppendFloat32(3.14) }},
+ {"int", `[42,42]`, func(e ArrayEncoder) { e.AppendInt(42) }},
+ {"int64", `[42,42]`, func(e ArrayEncoder) { e.AppendInt64(42) }},
+ {"int32", `[42,42]`, func(e ArrayEncoder) { e.AppendInt32(42) }},
+ {"int16", `[42,42]`, func(e ArrayEncoder) { e.AppendInt16(42) }},
+ {"int8", `[42,42]`, func(e ArrayEncoder) { e.AppendInt8(42) }},
+ {"string", `["k","k"]`, func(e ArrayEncoder) { e.AppendString("k") }},
+ {"string", `["k\\","k\\"]`, func(e ArrayEncoder) { e.AppendString(`k\`) }},
+ {"times", `[1,1]`, func(e ArrayEncoder) { e.AppendTime(time.Unix(1, 0)) }},
+ {"uint", `[42,42]`, func(e ArrayEncoder) { e.AppendUint(42) }},
+ {"uint64", `[42,42]`, func(e ArrayEncoder) { e.AppendUint64(42) }},
+ {"uint32", `[42,42]`, func(e ArrayEncoder) { e.AppendUint32(42) }},
+ {"uint16", `[42,42]`, func(e ArrayEncoder) { e.AppendUint16(42) }},
+ {"uint8", `[42,42]`, func(e ArrayEncoder) { e.AppendUint8(42) }},
+ {"uintptr", `[42,42]`, func(e ArrayEncoder) { e.AppendUintptr(42) }},
+ {
+ desc: "arrays (success)",
+ expected: `[[true],[true]]`,
+ f: func(arr ArrayEncoder) {
+ assert.NoError(t, arr.AppendArray(ArrayMarshalerFunc(func(inner ArrayEncoder) error {
+ inner.AppendBool(true)
+ return nil
+ })), "Unexpected error appending an array.")
+ },
+ },
+ {
+ desc: "arrays (error)",
+ expected: `[[true],[true]]`,
+ f: func(arr ArrayEncoder) {
+ assert.Error(t, arr.AppendArray(ArrayMarshalerFunc(func(inner ArrayEncoder) error {
+ inner.AppendBool(true)
+ return errors.New("fail")
+ })), "Expected an error appending an array.")
+ },
+ },
+ {
+ desc: "objects (success)",
+ expected: `[{"loggable":"yes"},{"loggable":"yes"}]`,
+ f: func(arr ArrayEncoder) {
+ assert.NoError(t, arr.AppendObject(loggable{true}), "Unexpected error appending an object.")
+ },
+ },
+ {
+ desc: "objects (error)",
+ expected: `[{},{}]`,
+ f: func(arr ArrayEncoder) {
+ assert.Error(t, arr.AppendObject(loggable{false}), "Expected an error appending an object.")
+ },
+ },
+ {
+ desc: "reflect (success)",
+ expected: `[{"foo":5},{"foo":5}]`,
+ f: func(arr ArrayEncoder) {
+ assert.NoError(
+ t,
+ arr.AppendReflected(map[string]int{"foo": 5}),
+ "Unexpected an error appending an object with reflection.",
+ )
+ },
+ },
+ {
+ desc: "reflect (error)",
+ expected: `[]`,
+ f: func(arr ArrayEncoder) {
+ assert.Error(
+ t,
+ arr.AppendReflected(noJSON{}),
+ "Unexpected an error appending an object with reflection.",
+ )
+ },
+ },
+ {
+ desc: "object (no nested namespace) then string",
+ expected: `[{"obj-out":"obj-outside-namespace"},"should-be-outside-obj",{"obj-out":"obj-outside-namespace"},"should-be-outside-obj"]`,
+ f: func(arr ArrayEncoder) {
+ arr.AppendObject(maybeNamespace{false})
+ arr.AppendString("should-be-outside-obj")
+ },
+ },
+ {
+ desc: "object (with nested namespace) then string",
+ expected: `[{"obj-out":"obj-outside-namespace","obj-namespace":{"obj-in":"obj-inside-namespace"}},"should-be-outside-obj",{"obj-out":"obj-outside-namespace","obj-namespace":{"obj-in":"obj-inside-namespace"}},"should-be-outside-obj"]`,
+ f: func(arr ArrayEncoder) {
+ arr.AppendObject(maybeNamespace{true})
+ arr.AppendString("should-be-outside-obj")
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.desc, func(t *testing.T) {
+ f := func(enc Encoder) error {
+ return enc.AddArray("array", ArrayMarshalerFunc(func(arr ArrayEncoder) error {
+ tt.f(arr)
+ tt.f(arr)
+ return nil
+ }))
+ }
+ assertOutput(t, _defaultEncoderConfig, `"array":`+tt.expected, func(enc Encoder) {
+ err := f(enc)
+ assert.NoError(t, err, "Unexpected error adding array to JSON encoder.")
+ })
+ })
+ }
+}
+
+func TestJSONEncoderTimeArrays(t *testing.T) {
+ times := []time.Time{
+ time.Unix(1008720000, 0).UTC(), // 2001-12-19
+ time.Unix(1040169600, 0).UTC(), // 2002-12-18
+ time.Unix(1071619200, 0).UTC(), // 2003-12-17
+ }
+
+ tests := []struct {
+ desc string
+ encoder TimeEncoder
+ want string
+ }{
+ {
+ desc: "epoch",
+ encoder: EpochTimeEncoder,
+ want: `[1008720000,1040169600,1071619200]`,
+ },
+ {
+ desc: "epoch millis",
+ encoder: EpochMillisTimeEncoder,
+ want: `[1008720000000,1040169600000,1071619200000]`,
+ },
+ {
+ desc: "iso8601",
+ encoder: ISO8601TimeEncoder,
+ want: `["2001-12-19T00:00:00.000Z","2002-12-18T00:00:00.000Z","2003-12-17T00:00:00.000Z"]`,
+ },
+ {
+ desc: "rfc3339",
+ encoder: RFC3339TimeEncoder,
+ want: `["2001-12-19T00:00:00Z","2002-12-18T00:00:00Z","2003-12-17T00:00:00Z"]`,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.desc, func(t *testing.T) {
+ cfg := _defaultEncoderConfig
+ cfg.EncodeTime = tt.encoder
+
+ enc := &jsonEncoder{buf: bufferpool.Get(), EncoderConfig: &cfg}
+ err := enc.AddArray("array", ArrayMarshalerFunc(func(arr ArrayEncoder) error {
+ for _, time := range times {
+ arr.AppendTime(time)
+ }
+ return nil
+ }))
+ assert.NoError(t, err)
+ assert.Equal(t, `"array":`+tt.want, enc.buf.String())
+ })
+ }
+}
+
+func assertJSON(t *testing.T, expected string, enc *jsonEncoder) {
+ assert.Equal(t, expected, enc.buf.String(), "Encoded JSON didn't match expectations.")
+}
+
+func assertOutput(t testing.TB, cfg EncoderConfig, expected string, f func(Encoder)) {
+ enc := NewJSONEncoder(cfg).(*jsonEncoder)
+ f(enc)
+ assert.Equal(t, expected, enc.buf.String(), "Unexpected encoder output after adding.")
+
+ enc.truncate()
+ enc.AddString("foo", "bar")
+ f(enc)
+ expectedPrefix := `"foo":"bar"`
+ if expected != "" {
+ // If we expect output, it should be comma-separated from the previous
+ // field.
+ expectedPrefix += ","
+ }
+ assert.Equal(t, expectedPrefix+expected, enc.buf.String(), "Unexpected encoder output after adding as a second field.")
+}
+
+// Nested Array- and ObjectMarshalers.
+type turducken struct{}
+
+func (t turducken) MarshalLogObject(enc ObjectEncoder) error {
+ return enc.AddArray("ducks", ArrayMarshalerFunc(func(arr ArrayEncoder) error {
+ for i := 0; i < 2; i++ {
+ arr.AppendObject(ObjectMarshalerFunc(func(inner ObjectEncoder) error {
+ inner.AddString("in", "chicken")
+ return nil
+ }))
+ }
+ return nil
+ }))
+}
+
+type turduckens int
+
+func (t turduckens) MarshalLogArray(enc ArrayEncoder) error {
+ var err error
+ tur := turducken{}
+ for i := 0; i < int(t); i++ {
+ err = multierr.Append(err, enc.AppendObject(tur))
+ }
+ return err
+}
+
+type loggable struct{ bool }
+
+func (l loggable) MarshalLogObject(enc ObjectEncoder) error {
+ if !l.bool {
+ return errors.New("can't marshal")
+ }
+ enc.AddString("loggable", "yes")
+ return nil
+}
+
+func (l loggable) MarshalLogArray(enc ArrayEncoder) error {
+ if !l.bool {
+ return errors.New("can't marshal")
+ }
+ enc.AppendBool(true)
+ return nil
+}
+
+// maybeNamespace is an ObjectMarshaler that sometimes opens a namespace
+type maybeNamespace struct{ bool }
+
+func (m maybeNamespace) MarshalLogObject(enc ObjectEncoder) error {
+ enc.AddString("obj-out", "obj-outside-namespace")
+ if m.bool {
+ enc.OpenNamespace("obj-namespace")
+ enc.AddString("obj-in", "obj-inside-namespace")
+ }
+ return nil
+}
+
+type noJSON struct{}
+
+func (nj noJSON) MarshalJSON() ([]byte, error) {
+ return nil, errors.New("no")
+}
+
+func zapEncode(encode func(*jsonEncoder, string)) func(s string) []byte {
+ return func(s string) []byte {
+ enc := &jsonEncoder{buf: bufferpool.Get()}
+ // Escape and quote a string using our encoder.
+ var ret []byte
+ encode(enc, s)
+ ret = make([]byte, 0, enc.buf.Len()+2)
+ ret = append(ret, '"')
+ ret = append(ret, enc.buf.Bytes()...)
+ ret = append(ret, '"')
+ return ret
+ }
+}
+
+func roundTripsCorrectly(encode func(string) []byte, original string) bool {
+ // Encode using our encoder, decode using the standard library, and assert
+ // that we haven't lost any information.
+ encoded := encode(original)
+
+ var decoded string
+ err := json.Unmarshal(encoded, &decoded)
+ if err != nil {
+ return false
+ }
+ return original == decoded
+}
+
+func roundTripsCorrectlyString(original string) bool {
+ return roundTripsCorrectly(zapEncode((*jsonEncoder).safeAddString), original)
+}
+
+func roundTripsCorrectlyByteString(original string) bool {
+ return roundTripsCorrectly(
+ zapEncode(func(enc *jsonEncoder, s string) {
+ enc.safeAddByteString([]byte(s))
+ }),
+ original)
+}
+
+type ASCII string
+
+func (s ASCII) Generate(r *rand.Rand, size int) reflect.Value {
+ bs := make([]byte, size)
+ for i := range bs {
+ bs[i] = byte(r.Intn(128))
+ }
+ a := ASCII(bs)
+ return reflect.ValueOf(a)
+}
+
+func asciiRoundTripsCorrectlyString(s ASCII) bool {
+ return roundTripsCorrectlyString(string(s))
+}
+
+func asciiRoundTripsCorrectlyByteString(s ASCII) bool {
+ return roundTripsCorrectlyByteString(string(s))
+}
+
+func TestJSONQuick(t *testing.T) {
+ check := func(f interface{}) {
+ err := quick.Check(f, &quick.Config{MaxCountScale: 100.0})
+ assert.NoError(t, err)
+ }
+ // Test the full range of UTF-8 strings.
+ check(roundTripsCorrectlyString)
+ check(roundTripsCorrectlyByteString)
+
+ // Focus on ASCII strings.
+ check(asciiRoundTripsCorrectlyString)
+ check(asciiRoundTripsCorrectlyByteString)
+}