580 lines
15 KiB
Go
580 lines
15 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"
|
|
"encoding/binary"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"strings"
|
|
"testing"
|
|
|
|
"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 ExampleRaw_Validate() {
|
|
rdr := make(Raw, 500)
|
|
rdr[250], rdr[251], rdr[252], rdr[253], rdr[254] = '\x05', '\x00', '\x00', '\x00', '\x00'
|
|
err := rdr[250:].Validate()
|
|
fmt.Println(err)
|
|
|
|
// Output: <nil>
|
|
}
|
|
|
|
func BenchmarkRawValidate(b *testing.B) {
|
|
for i := 0; i < b.N; i++ {
|
|
rdr := make(Raw, 500)
|
|
rdr[250], rdr[251], rdr[252], rdr[253], rdr[254] = '\x05', '\x00', '\x00', '\x00', '\x00'
|
|
_ = rdr[250:].Validate()
|
|
}
|
|
|
|
}
|
|
|
|
func TestRaw(t *testing.T) {
|
|
t.Run("Validate", func(t *testing.T) {
|
|
t.Run("TooShort", func(t *testing.T) {
|
|
want := bsoncore.NewInsufficientBytesError(nil, nil)
|
|
got := Raw{'\x00', '\x00'}.Validate()
|
|
if !assert.CompareErrors(got, want) {
|
|
t.Errorf("Did not get expected error. got %v; want %v", got, want)
|
|
}
|
|
})
|
|
t.Run("InvalidLength", func(t *testing.T) {
|
|
want := bsoncore.ValidationError("document length exceeds available bytes. length=200 remainingBytes=5")
|
|
r := make(Raw, 5)
|
|
binary.LittleEndian.PutUint32(r[0:4], 200)
|
|
got := r.Validate()
|
|
if !errors.Is(got, want) {
|
|
t.Errorf("Did not get expected error. got %v; want %v", got, want)
|
|
}
|
|
})
|
|
t.Run("keyLength-error", func(t *testing.T) {
|
|
want := bsoncore.ErrMissingNull
|
|
r := make(Raw, 8)
|
|
binary.LittleEndian.PutUint32(r[0:4], 8)
|
|
r[4], r[5], r[6], r[7] = '\x02', 'f', 'o', 'o'
|
|
got := r.Validate()
|
|
if !errors.Is(got, want) {
|
|
t.Errorf("Did not get expected error. got %v; want %v", got, want)
|
|
}
|
|
})
|
|
t.Run("Missing-Null-Terminator", func(t *testing.T) {
|
|
want := bsoncore.ErrMissingNull
|
|
r := make(Raw, 9)
|
|
binary.LittleEndian.PutUint32(r[0:4], 9)
|
|
r[4], r[5], r[6], r[7], r[8] = '\x0A', 'f', 'o', 'o', '\x00'
|
|
got := r.Validate()
|
|
if !errors.Is(got, want) {
|
|
t.Errorf("Did not get expected error. got %v; want %v", got, want)
|
|
}
|
|
})
|
|
t.Run("validateValue-error", func(t *testing.T) {
|
|
want := bsoncore.ErrMissingNull
|
|
r := make(Raw, 11)
|
|
binary.LittleEndian.PutUint32(r[0:4], 11)
|
|
r[4], r[5], r[6], r[7], r[8], r[9], r[10] = '\x01', 'f', 'o', 'o', '\x00', '\x01', '\x02'
|
|
got := r.Validate()
|
|
if !assert.CompareErrors(got, want) {
|
|
t.Errorf("Did not get expected error. got %v; want %v", got, want)
|
|
}
|
|
})
|
|
testCases := []struct {
|
|
name string
|
|
r Raw
|
|
err error
|
|
}{
|
|
{"null", Raw{'\x08', '\x00', '\x00', '\x00', '\x0A', 'x', '\x00', '\x00'}, nil},
|
|
{"subdocument",
|
|
Raw{
|
|
'\x15', '\x00', '\x00', '\x00',
|
|
'\x03',
|
|
'f', 'o', 'o', '\x00',
|
|
'\x0B', '\x00', '\x00', '\x00', '\x0A', 'a', '\x00',
|
|
'\x0A', 'b', '\x00', '\x00', '\x00',
|
|
},
|
|
nil,
|
|
},
|
|
{"array",
|
|
Raw{
|
|
'\x15', '\x00', '\x00', '\x00',
|
|
'\x04',
|
|
'f', 'o', 'o', '\x00',
|
|
'\x0B', '\x00', '\x00', '\x00', '\x0A', '1', '\x00',
|
|
'\x0A', '2', '\x00', '\x00', '\x00',
|
|
},
|
|
nil,
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
err := tc.r.Validate()
|
|
if !errors.Is(err, tc.err) {
|
|
t.Errorf("Returned error does not match. got %v; want %v", err, tc.err)
|
|
}
|
|
})
|
|
}
|
|
})
|
|
t.Run("Lookup", func(t *testing.T) {
|
|
t.Run("empty-key", func(t *testing.T) {
|
|
rdr := Raw{'\x05', '\x00', '\x00', '\x00', '\x00'}
|
|
_, err := rdr.LookupErr()
|
|
if !errors.Is(err, bsoncore.ErrEmptyKey) {
|
|
t.Errorf("Empty key lookup did not return expected result. got %v; want %v", err, bsoncore.ErrEmptyKey)
|
|
}
|
|
})
|
|
t.Run("corrupted-subdocument", func(t *testing.T) {
|
|
rdr := Raw{
|
|
'\x0D', '\x00', '\x00', '\x00',
|
|
'\x03', 'x', '\x00',
|
|
'\x06', '\x00', '\x00', '\x00',
|
|
'\x01',
|
|
'\x00',
|
|
'\x00',
|
|
}
|
|
_, err := rdr.LookupErr("x", "y")
|
|
want := bsoncore.NewInsufficientBytesError(nil, nil)
|
|
if !assert.CompareErrors(err, want) {
|
|
t.Errorf("Empty key lookup did not return expected result. got %v; want %v", err, want)
|
|
}
|
|
})
|
|
t.Run("corrupted-array", func(t *testing.T) {
|
|
rdr := Raw{
|
|
'\x0D', '\x00', '\x00', '\x00',
|
|
'\x04', 'x', '\x00',
|
|
'\x06', '\x00', '\x00', '\x00',
|
|
'\x01',
|
|
'\x00',
|
|
'\x00',
|
|
}
|
|
_, err := rdr.LookupErr("x", "y")
|
|
want := bsoncore.NewInsufficientBytesError(nil, nil)
|
|
if !assert.CompareErrors(err, want) {
|
|
t.Errorf("Empty key lookup did not return expected result. got %v; want %v", err, want)
|
|
}
|
|
})
|
|
t.Run("invalid-traversal", func(t *testing.T) {
|
|
rdr := Raw{'\x08', '\x00', '\x00', '\x00', '\x0A', 'x', '\x00', '\x00'}
|
|
_, err := rdr.LookupErr("x", "y")
|
|
want := bsoncore.InvalidDepthTraversalError{Key: "x", Type: bsoncore.TypeNull}
|
|
if !assert.CompareErrors(err, want) {
|
|
t.Errorf("Empty key lookup did not return expected result. got %v; want %v", err, want)
|
|
}
|
|
})
|
|
testCases := []struct {
|
|
name string
|
|
r Raw
|
|
key []string
|
|
want RawValue
|
|
err error
|
|
}{
|
|
{"first",
|
|
Raw{
|
|
'\x08', '\x00', '\x00', '\x00', '\x0A', 'x', '\x00', '\x00',
|
|
},
|
|
[]string{"x"},
|
|
RawValue{Type: TypeNull}, nil,
|
|
},
|
|
{"first-second",
|
|
Raw{
|
|
'\x15', '\x00', '\x00', '\x00',
|
|
'\x03',
|
|
'f', 'o', 'o', '\x00',
|
|
'\x0B', '\x00', '\x00', '\x00', '\x0A', 'a', '\x00',
|
|
'\x0A', 'b', '\x00', '\x00', '\x00',
|
|
},
|
|
[]string{"foo", "b"},
|
|
RawValue{Type: TypeNull}, nil,
|
|
},
|
|
{"first-second-array",
|
|
Raw{
|
|
'\x15', '\x00', '\x00', '\x00',
|
|
'\x04',
|
|
'f', 'o', 'o', '\x00',
|
|
'\x0B', '\x00', '\x00', '\x00', '\x0A', '1', '\x00',
|
|
'\x0A', '2', '\x00', '\x00', '\x00',
|
|
},
|
|
[]string{"foo", "2"},
|
|
RawValue{Type: TypeNull}, nil,
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
got, err := tc.r.LookupErr(tc.key...)
|
|
if !errors.Is(err, tc.err) {
|
|
t.Errorf("Returned error does not match. got %v; want %v", err, tc.err)
|
|
}
|
|
if !cmp.Equal(got, tc.want) {
|
|
t.Errorf("Returned element does not match expected element. got %v; want %v", got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
})
|
|
t.Run("ElementAt", func(t *testing.T) {
|
|
t.Run("Out of bounds", func(t *testing.T) {
|
|
rdr := Raw{0xe, 0x0, 0x0, 0x0, 0xa, 0x78, 0x0, 0xa, 0x79, 0x0, 0xa, 0x7a, 0x0, 0x0}
|
|
_, err := rdr.IndexErr(3)
|
|
if !errors.Is(err, bsoncore.ErrOutOfBounds) {
|
|
t.Errorf("Out of bounds should be returned when accessing element beyond end of document. got %v; want %v", err, bsoncore.ErrOutOfBounds)
|
|
}
|
|
})
|
|
t.Run("Validation Error", func(t *testing.T) {
|
|
rdr := Raw{0x07, 0x00, 0x00, 0x00, 0x00}
|
|
_, err := rdr.IndexErr(1)
|
|
want := bsoncore.NewInsufficientBytesError(nil, nil)
|
|
if !assert.CompareErrors(err, want) {
|
|
t.Errorf("Did not receive expected error. got %v; want %v", err, want)
|
|
}
|
|
})
|
|
testCases := []struct {
|
|
name string
|
|
rdr Raw
|
|
index uint
|
|
want RawElement
|
|
}{
|
|
{"first",
|
|
Raw{0xe, 0x0, 0x0, 0x0, 0xa, 0x78, 0x0, 0xa, 0x79, 0x0, 0xa, 0x7a, 0x0, 0x0},
|
|
0, bsoncore.AppendNullElement(nil, "x")},
|
|
{"second",
|
|
Raw{0xe, 0x0, 0x0, 0x0, 0xa, 0x78, 0x0, 0xa, 0x79, 0x0, 0xa, 0x7a, 0x0, 0x0},
|
|
1, bsoncore.AppendNullElement(nil, "y")},
|
|
{"third",
|
|
Raw{0xe, 0x0, 0x0, 0x0, 0xa, 0x78, 0x0, 0xa, 0x79, 0x0, 0xa, 0x7a, 0x0, 0x0},
|
|
2, bsoncore.AppendNullElement(nil, "z")},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
got, err := tc.rdr.IndexErr(tc.index)
|
|
if err != nil {
|
|
t.Errorf("Unexpected error from ElementAt: %s", err)
|
|
}
|
|
if diff := cmp.Diff(got, tc.want); diff != "" {
|
|
t.Errorf("Documents differ: (-got +want)\n%s", diff)
|
|
}
|
|
})
|
|
}
|
|
})
|
|
t.Run("ReadDocument", func(t *testing.T) {
|
|
t.Parallel()
|
|
testCases := []struct {
|
|
name string
|
|
ioReader io.Reader
|
|
bsonReader Raw
|
|
err error
|
|
}{
|
|
{
|
|
"nil reader",
|
|
nil,
|
|
nil,
|
|
ErrNilReader,
|
|
},
|
|
{
|
|
"premature end of reader",
|
|
bytes.NewBuffer([]byte{}),
|
|
nil,
|
|
io.EOF,
|
|
},
|
|
{
|
|
"empty document",
|
|
bytes.NewBuffer([]byte{5, 0, 0, 0, 0}),
|
|
[]byte{5, 0, 0, 0, 0},
|
|
nil,
|
|
},
|
|
{
|
|
"non-empty document",
|
|
bytes.NewBuffer([]byte{
|
|
// length
|
|
0x17, 0x0, 0x0, 0x0,
|
|
|
|
// type - string
|
|
0x2,
|
|
// key - "foo"
|
|
0x66, 0x6f, 0x6f, 0x0,
|
|
// value - string length
|
|
0x4, 0x0, 0x0, 0x0,
|
|
// value - string "bar"
|
|
0x62, 0x61, 0x72, 0x0,
|
|
|
|
// type - null
|
|
0xa,
|
|
// key - "baz"
|
|
0x62, 0x61, 0x7a, 0x0,
|
|
|
|
// null terminator
|
|
0x0,
|
|
}),
|
|
[]byte{
|
|
// length
|
|
0x17, 0x0, 0x0, 0x0,
|
|
|
|
// type - string
|
|
0x2,
|
|
// key - "foo"
|
|
0x66, 0x6f, 0x6f, 0x0,
|
|
// value - string length
|
|
0x4, 0x0, 0x0, 0x0,
|
|
// value - string "bar"
|
|
0x62, 0x61, 0x72, 0x0,
|
|
|
|
// type - null
|
|
0xa,
|
|
// key - "baz"
|
|
0x62, 0x61, 0x7a, 0x0,
|
|
|
|
// null terminator
|
|
0x0,
|
|
},
|
|
nil,
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
tc := tc // Capture range variable.
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
reader, err := ReadDocument(tc.ioReader)
|
|
require.Equal(t, err, tc.err)
|
|
require.True(t, bytes.Equal(tc.bsonReader, reader))
|
|
})
|
|
}
|
|
})
|
|
}
|
|
|
|
func BenchmarkRawString(b *testing.B) {
|
|
// Create 1KiB and 128B strings to exercise the string-heavy call paths in
|
|
// the "Raw.String" method.
|
|
var buf strings.Builder
|
|
for i := 0; i < 16000000; i++ {
|
|
buf.WriteString("abcdefgh")
|
|
}
|
|
str1k := buf.String()
|
|
str128 := str1k[:128]
|
|
|
|
cases := []struct {
|
|
description string
|
|
value interface{}
|
|
}{
|
|
{
|
|
description: "string",
|
|
value: D{{Key: "key", Value: str128}},
|
|
},
|
|
{
|
|
description: "integer",
|
|
value: D{{Key: "key", Value: int64(1234567890)}},
|
|
},
|
|
{
|
|
description: "float",
|
|
value: D{{Key: "key", Value: float64(1234567890.123456789)}},
|
|
},
|
|
{
|
|
description: "nested document",
|
|
value: D{{
|
|
Key: "key",
|
|
Value: D{{
|
|
Key: "key",
|
|
Value: D{{
|
|
Key: "key",
|
|
Value: str128,
|
|
}},
|
|
}},
|
|
}},
|
|
},
|
|
{
|
|
description: "array of strings",
|
|
value: D{{
|
|
Key: "key",
|
|
Value: []string{str128, str128, str128, str128},
|
|
}},
|
|
},
|
|
{
|
|
description: "mixed struct",
|
|
value: struct {
|
|
Key1 struct {
|
|
Nested string
|
|
}
|
|
Key2 string
|
|
Key3 []string
|
|
Key4 float64
|
|
}{
|
|
Key1: struct{ Nested string }{Nested: str1k},
|
|
Key2: str1k,
|
|
Key3: []string{str1k, str1k, str1k, str1k},
|
|
Key4: 1234567890.123456789,
|
|
},
|
|
},
|
|
// Very voluminous document with hundreds of thousands of keys
|
|
{
|
|
description: "very_voluminous_document",
|
|
value: createVoluminousDocument(100000),
|
|
},
|
|
// Document containing large strings and values
|
|
{
|
|
description: "large_strings_and_values",
|
|
value: createLargeStringsDocument(10),
|
|
},
|
|
// Document with massive arrays that are large
|
|
{
|
|
description: "massive_arrays",
|
|
value: createMassiveArraysDocument(100000),
|
|
},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
b.Run(tc.description, func(b *testing.B) {
|
|
bs, err := Marshal(tc.value)
|
|
require.NoError(b, err)
|
|
|
|
b.ReportAllocs()
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
_ = Raw(bs).String()
|
|
}
|
|
})
|
|
|
|
b.Run(tc.description+"_StringN", func(b *testing.B) {
|
|
bs, err := Marshal(tc.value)
|
|
require.NoError(b, err)
|
|
|
|
b.ReportAllocs()
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
_ = bsoncore.Document(bs).StringN(1024) // Assuming you want to limit to 1024 bytes for this benchmark
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestComplexDocuments_StringN(t *testing.T) {
|
|
testCases := []struct {
|
|
description string
|
|
n int
|
|
doc any
|
|
}{
|
|
{"n>0, massive array documents", 1000, createMassiveArraysDocument(1000)},
|
|
{"n>0, voluminous document with unique values", 1000, createUniqueVoluminousDocument(t, 1000)},
|
|
{"n>0, large single document", 1000, createLargeSingleDoc(t)},
|
|
{"n>0, voluminous document with arrays containing documents", 1000, createVoluminousArrayDocuments(t, 1000)},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.description, func(t *testing.T) {
|
|
bson, _ := Marshal(tc.doc)
|
|
bsonDoc := bsoncore.Document(bson)
|
|
|
|
got := bsonDoc.StringN(tc.n)
|
|
assert.Equal(t, tc.n, len(got))
|
|
})
|
|
}
|
|
}
|
|
|
|
// createVoluminousDocument generates a document with a specified number of keys, simulating a very large document in terms of the number of keys.
|
|
func createVoluminousDocument(numKeys int) D {
|
|
d := make(D, numKeys)
|
|
for i := 0; i < numKeys; i++ {
|
|
d = append(d, E{Key: fmt.Sprintf("key%d", i), Value: "value"})
|
|
}
|
|
return d
|
|
}
|
|
|
|
// createLargeStringsDocument generates a document with large string values, simulating a document with very large data values.
|
|
func createLargeStringsDocument(sizeMB int) D {
|
|
largeString := strings.Repeat("a", sizeMB*1024*1024)
|
|
return D{
|
|
{Key: "largeString1", Value: largeString},
|
|
{Key: "largeString2", Value: largeString},
|
|
{Key: "largeString3", Value: largeString},
|
|
{Key: "largeString4", Value: largeString},
|
|
}
|
|
}
|
|
|
|
// createMassiveArraysDocument generates a document with massive arrays, simulating a document that contains large arrays of data.
|
|
func createMassiveArraysDocument(arraySize int) D {
|
|
massiveArray := make([]string, arraySize)
|
|
for i := 0; i < arraySize; i++ {
|
|
massiveArray[i] = "value"
|
|
}
|
|
|
|
return D{
|
|
{Key: "massiveArray1", Value: massiveArray},
|
|
{Key: "massiveArray2", Value: massiveArray},
|
|
{Key: "massiveArray3", Value: massiveArray},
|
|
{Key: "massiveArray4", Value: massiveArray},
|
|
}
|
|
}
|
|
|
|
// createUniqueVoluminousDocument creates a BSON document with multiple key value pairs and unique value types.
|
|
func createUniqueVoluminousDocument(t *testing.T, size int) bsoncore.Document {
|
|
t.Helper()
|
|
|
|
docs := make(D, size)
|
|
|
|
for i := 0; i < size; i++ {
|
|
docs = append(docs, E{
|
|
Key: "x", Value: NewObjectID(),
|
|
})
|
|
docs = append(docs, E{
|
|
Key: "z", Value: "y",
|
|
})
|
|
}
|
|
|
|
bsonData, err := Marshal(docs)
|
|
assert.NoError(t, err)
|
|
|
|
return bsoncore.Document(bsonData)
|
|
}
|
|
|
|
// createLargeSingleDoc creates a large single BSON document.
|
|
func createLargeSingleDoc(t *testing.T) bsoncore.Document {
|
|
t.Helper()
|
|
|
|
var b strings.Builder
|
|
b.Grow(1048577)
|
|
|
|
for i := 0; i < 1048577; i++ {
|
|
b.WriteByte(0)
|
|
}
|
|
s := b.String()
|
|
|
|
doc := D{
|
|
{Key: "x", Value: s},
|
|
}
|
|
|
|
bsonData, err := Marshal(doc)
|
|
assert.NoError(t, err)
|
|
|
|
return bsoncore.Document(bsonData)
|
|
}
|
|
|
|
// createVoluminousArrayDocuments creates a volumninous BSON document with arrays containing documents.
|
|
func createVoluminousArrayDocuments(t *testing.T, size int) bsoncore.Document {
|
|
t.Helper()
|
|
|
|
docs := make(D, size)
|
|
|
|
for i := 0; i < size; i++ {
|
|
docs = append(docs, E{
|
|
Key: "x", Value: NewObjectID(),
|
|
})
|
|
docs = append(docs, E{
|
|
Key: "z", Value: A{D{{Key: "x", Value: "y"}}},
|
|
})
|
|
}
|
|
|
|
bsonData, err := Marshal(docs)
|
|
assert.NoError(t, err)
|
|
|
|
return bsoncore.Document(bsonData)
|
|
}
|