329 lines
8.3 KiB
Go
329 lines
8.3 KiB
Go
// Copyright (C) MongoDB, Inc. 2017-present.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
// not use this file except in compliance with the License. You may obtain
|
|
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
package bson
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"reflect"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"gitea.psichedelico.com/go/bson/internal/assert"
|
|
"gitea.psichedelico.com/go/bson/internal/require"
|
|
"gitea.psichedelico.com/go/bson/x/bsonx/bsoncore"
|
|
"github.com/google/go-cmp/cmp"
|
|
)
|
|
|
|
func TestMarshalWithRegistry(t *testing.T) {
|
|
for _, tc := range marshalingTestCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
var reg *Registry
|
|
if tc.reg != nil {
|
|
reg = tc.reg
|
|
} else {
|
|
reg = defaultRegistry
|
|
}
|
|
buf := new(bytes.Buffer)
|
|
vw := NewDocumentWriter(buf)
|
|
enc := NewEncoder(vw)
|
|
enc.SetRegistry(reg)
|
|
err := enc.Encode(tc.val)
|
|
noerr(t, err)
|
|
|
|
if got := buf.Bytes(); !bytes.Equal(got, tc.want) {
|
|
t.Errorf("Bytes are not equal. got %v; want %v", got, tc.want)
|
|
t.Errorf("Bytes:\n%v\n%v", got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMarshalWithContext(t *testing.T) {
|
|
for _, tc := range marshalingTestCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
var reg *Registry
|
|
if tc.reg != nil {
|
|
reg = tc.reg
|
|
} else {
|
|
reg = defaultRegistry
|
|
}
|
|
buf := new(bytes.Buffer)
|
|
vw := NewDocumentWriter(buf)
|
|
enc := NewEncoder(vw)
|
|
enc.IntMinSize()
|
|
enc.SetRegistry(reg)
|
|
err := enc.Encode(tc.val)
|
|
noerr(t, err)
|
|
|
|
if got := buf.Bytes(); !bytes.Equal(got, tc.want) {
|
|
t.Errorf("Bytes are not equal. got %v; want %v", got, tc.want)
|
|
t.Errorf("Bytes:\n%v\n%v", got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMarshalExtJSON(t *testing.T) {
|
|
t.Run("MarshalExtJSON", func(t *testing.T) {
|
|
type teststruct struct{ Foo int }
|
|
val := teststruct{1}
|
|
got, err := MarshalExtJSON(val, true, false)
|
|
noerr(t, err)
|
|
want := []byte(`{"foo":{"$numberInt":"1"}}`)
|
|
if !bytes.Equal(got, want) {
|
|
t.Errorf("Bytes are not equal. got %v; want %v", got, want)
|
|
t.Errorf("Bytes:\n%s\n%s", got, want)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestMarshal_roundtripFromBytes(t *testing.T) {
|
|
before := []byte{
|
|
// length
|
|
0x1c, 0x0, 0x0, 0x0,
|
|
|
|
// --- begin array ---
|
|
|
|
// type - document
|
|
0x3,
|
|
// key - "foo"
|
|
0x66, 0x6f, 0x6f, 0x0,
|
|
|
|
// length
|
|
0x12, 0x0, 0x0, 0x0,
|
|
// type - string
|
|
0x2,
|
|
// key - "bar"
|
|
0x62, 0x61, 0x72, 0x0,
|
|
// value - string length
|
|
0x4, 0x0, 0x0, 0x0,
|
|
// value - "baz"
|
|
0x62, 0x61, 0x7a, 0x0,
|
|
|
|
// null terminator
|
|
0x0,
|
|
|
|
// --- end array ---
|
|
|
|
// null terminator
|
|
0x0,
|
|
}
|
|
|
|
var doc D
|
|
require.NoError(t, Unmarshal(before, &doc))
|
|
|
|
after, err := Marshal(doc)
|
|
require.NoError(t, err)
|
|
|
|
require.True(t, bytes.Equal(before, after))
|
|
}
|
|
|
|
func TestMarshal_roundtripFromDoc(t *testing.T) {
|
|
before := D{
|
|
{"foo", "bar"},
|
|
{"baz", int64(-27)},
|
|
{"bing", A{nil, Regex{Pattern: "word", Options: "i"}}},
|
|
}
|
|
|
|
b, err := Marshal(before)
|
|
require.NoError(t, err)
|
|
|
|
var after D
|
|
require.NoError(t, Unmarshal(b, &after))
|
|
|
|
if !cmp.Equal(after, before) {
|
|
t.Errorf("Documents to not match. got %v; want %v", after, before)
|
|
}
|
|
}
|
|
|
|
func TestCachingEncodersNotSharedAcrossRegistries(t *testing.T) {
|
|
// Encoders that have caches for recursive encoder lookup should not be shared across Registry instances. Otherwise,
|
|
// the first EncodeValue call would cache an encoder and a subsequent call would see that encoder even if a
|
|
// different Registry is used.
|
|
|
|
// Create a custom Registry that negates int32 values when encoding.
|
|
var encodeInt32 ValueEncoderFunc = func(_ EncodeContext, vw ValueWriter, val reflect.Value) error {
|
|
if val.Kind() != reflect.Int32 {
|
|
return fmt.Errorf("expected kind to be int32, got %v", val.Kind())
|
|
}
|
|
|
|
return vw.WriteInt32(int32(val.Int()) * -1)
|
|
}
|
|
customReg := NewRegistry()
|
|
customReg.RegisterTypeEncoder(tInt32, encodeInt32)
|
|
|
|
// Helper function to run the test and make assertions. The provided original value should result in the document
|
|
// {"x": {$numberInt: 1}} when marshalled with the default registry.
|
|
verifyResults := func(t *testing.T, original interface{}) {
|
|
// Marshal using the default and custom registries. Assert that the result is {x: 1} and {x: -1}, respectively.
|
|
|
|
first, err := Marshal(original)
|
|
assert.Nil(t, err, "Marshal error: %v", err)
|
|
expectedFirst := Raw(bsoncore.BuildDocumentFromElements(
|
|
nil,
|
|
bsoncore.AppendInt32Element(nil, "x", 1),
|
|
))
|
|
assert.Equal(t, expectedFirst, Raw(first), "expected document %v, got %v", expectedFirst, Raw(first))
|
|
|
|
buf := new(bytes.Buffer)
|
|
vw := NewDocumentWriter(buf)
|
|
enc := NewEncoder(vw)
|
|
enc.SetRegistry(customReg)
|
|
err = enc.Encode(original)
|
|
assert.Nil(t, err, "Encode error: %v", err)
|
|
second := buf.Bytes()
|
|
expectedSecond := Raw(bsoncore.BuildDocumentFromElements(
|
|
nil,
|
|
bsoncore.AppendInt32Element(nil, "x", -1),
|
|
))
|
|
assert.Equal(t, expectedSecond, Raw(second), "expected document %v, got %v", expectedSecond, Raw(second))
|
|
}
|
|
|
|
t.Run("struct", func(t *testing.T) {
|
|
type Struct struct {
|
|
X int32
|
|
}
|
|
verifyResults(t, Struct{
|
|
X: 1,
|
|
})
|
|
})
|
|
t.Run("pointer", func(t *testing.T) {
|
|
i32 := int32(1)
|
|
verifyResults(t, M{
|
|
"x": &i32,
|
|
})
|
|
})
|
|
}
|
|
|
|
func TestNullBytes(t *testing.T) {
|
|
t.Run("element keys", func(t *testing.T) {
|
|
doc := D{{"a\x00", "foobar"}}
|
|
res, err := Marshal(doc)
|
|
want := errors.New("BSON element key cannot contain null bytes")
|
|
assert.Equal(t, want, err, "expected Marshal error %v, got error %v with result %q", want, err, Raw(res))
|
|
})
|
|
|
|
t.Run("regex values", func(t *testing.T) {
|
|
wantErr := errors.New("BSON regex values cannot contain null bytes")
|
|
|
|
testCases := []struct {
|
|
name string
|
|
pattern string
|
|
options string
|
|
}{
|
|
{"null bytes in pattern", "a\x00", "i"},
|
|
{"null bytes in options", "pattern", "i\x00"},
|
|
}
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
regex := Regex{
|
|
Pattern: tc.pattern,
|
|
Options: tc.options,
|
|
}
|
|
res, err := Marshal(D{{"foo", regex}})
|
|
assert.Equal(t, wantErr, err, "expected Marshal error %v, got error %v with result %q", wantErr, err, Raw(res))
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("sub document field name", func(t *testing.T) {
|
|
doc := D{{"foo", D{{"foobar", D{{"a\x00", "foobar"}}}}}}
|
|
res, err := Marshal(doc)
|
|
wantErr := errors.New("BSON element key cannot contain null bytes")
|
|
assert.Equal(t, wantErr, err, "expected Marshal error %v, got error %v with result %q", wantErr, err, Raw(res))
|
|
})
|
|
}
|
|
|
|
func TestMarshalExtJSONIndent(t *testing.T) {
|
|
type indentTestCase struct {
|
|
name string
|
|
val interface{}
|
|
expectedExtJSON string
|
|
}
|
|
|
|
// expectedExtJSON must be written as below because single-quoted
|
|
// literal strings capture undesired code formatting tabs
|
|
testCases := []indentTestCase{
|
|
{
|
|
"empty val",
|
|
struct{}{},
|
|
`{}`,
|
|
},
|
|
{
|
|
"embedded struct",
|
|
struct {
|
|
Embedded interface{} `json:"embedded"`
|
|
Foo string `json:"foo"`
|
|
}{
|
|
Embedded: struct {
|
|
Name string `json:"name"`
|
|
Word string `json:"word"`
|
|
}{
|
|
Name: "test",
|
|
Word: "word",
|
|
},
|
|
Foo: "bar",
|
|
},
|
|
"{\n\t\"embedded\": {\n\t\t\"name\": \"test\",\n\t\t\"word\": \"word\"\n\t},\n\t\"foo\": \"bar\"\n}",
|
|
},
|
|
{
|
|
"date struct",
|
|
struct {
|
|
Foo string `json:"foo"`
|
|
Date time.Time `json:"date"`
|
|
}{
|
|
Foo: "bar",
|
|
Date: time.Date(2000, time.January, 1, 12, 0, 0, 0, time.UTC),
|
|
},
|
|
"{\n\t\"foo\": \"bar\",\n\t\"date\": {\n\t\t\"$date\": {\n\t\t\t\"$numberLong\": \"946728000000\"\n\t\t}\n\t}\n}",
|
|
},
|
|
{
|
|
"float struct",
|
|
struct {
|
|
Foo string `json:"foo"`
|
|
Float float32 `json:"float"`
|
|
}{
|
|
Foo: "bar",
|
|
Float: 3.14,
|
|
},
|
|
"{\n\t\"foo\": \"bar\",\n\t\"float\": {\n\t\t\"$numberDouble\": \"3.140000104904175\"\n\t}\n}",
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
tc := tc
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
extJSONBytes, err := MarshalExtJSONIndent(tc.val, true, false, "", "\t")
|
|
assert.Nil(t, err, "Marshal indent error: %v", err)
|
|
|
|
expectedExtJSONBytes := []byte(tc.expectedExtJSON)
|
|
|
|
assert.Equal(t, expectedExtJSONBytes, extJSONBytes, "expected:\n%s\ngot:\n%s", expectedExtJSONBytes, extJSONBytes)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMarshalConcurrently(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
const size = 10_000
|
|
|
|
wg := sync.WaitGroup{}
|
|
wg.Add(size)
|
|
for i := 0; i < size; i++ {
|
|
go func() {
|
|
defer wg.Done()
|
|
_, _ = Marshal(struct{ LastError error }{})
|
|
}()
|
|
}
|
|
wg.Wait()
|
|
}
|