bson/raw_test.go
2025-03-17 20:58:26 +01:00

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)
}