From 4d17e8bce4d0dbc58de850db72d4541bfaaf5562 Mon Sep 17 00:00:00 2001 From: Qingyang Hu Date: Wed, 16 Jul 2025 10:59:43 -0400 Subject: [PATCH 1/4] Refactor StringN --- bson/raw_test.go | 4 +- internal/bsoncoreutil/bsoncoreutil.go | 2 +- x/bsonx/bsoncore/array.go | 71 +++++++++++------- x/bsonx/bsoncore/array_test.go | 4 +- x/bsonx/bsoncore/document.go | 72 +++++++++++-------- x/bsonx/bsoncore/document_test.go | 2 +- x/bsonx/bsoncore/element.go | 69 ++++++++++++++---- x/bsonx/bsoncore/value.go | 100 ++++++++++++++------------ x/bsonx/bsoncore/value_test.go | 2 +- 9 files changed, 203 insertions(+), 123 deletions(-) diff --git a/bson/raw_test.go b/bson/raw_test.go index 4cb2f5a077..d09a4d2d71 100644 --- a/bson/raw_test.go +++ b/bson/raw_test.go @@ -518,7 +518,7 @@ func createMassiveArraysDocument(arraySize int) D { func createUniqueVoluminousDocument(t *testing.T, size int) bsoncore.Document { t.Helper() - docs := make(D, size) + docs := make(D, 0, size) for i := 0; i < size; i++ { docs = append(docs, E{ @@ -561,7 +561,7 @@ func createLargeSingleDoc(t *testing.T) bsoncore.Document { func createVoluminousArrayDocuments(t *testing.T, size int) bsoncore.Document { t.Helper() - docs := make(D, size) + docs := make(D, 0, size) for i := 0; i < size; i++ { docs = append(docs, E{ diff --git a/internal/bsoncoreutil/bsoncoreutil.go b/internal/bsoncoreutil/bsoncoreutil.go index ff0f64931f..7acdb2f1bb 100644 --- a/internal/bsoncoreutil/bsoncoreutil.go +++ b/internal/bsoncoreutil/bsoncoreutil.go @@ -8,7 +8,7 @@ package bsoncoreutil // Truncate truncates a given string for a certain width func Truncate(str string, width int) string { - if width == 0 { + if width <= 0 { return "" } diff --git a/x/bsonx/bsoncore/array.go b/x/bsonx/bsoncore/array.go index 0efbdc0fd9..c977037db2 100644 --- a/x/bsonx/bsoncore/array.go +++ b/x/bsonx/bsoncore/array.go @@ -9,7 +9,6 @@ package bsoncore import ( "fmt" "io" - "math" "strconv" "strings" ) @@ -83,55 +82,73 @@ func (a Array) DebugString() string { // String outputs an ExtendedJSON version of Array. If the Array is not valid, this method // returns an empty string. func (a Array) String() string { - return a.StringN(math.MaxInt) + str, _ := a.stringN(0) + return str } // StringN stringifies an array upto N bytes func (a Array) StringN(n int) string { - if lens, _, _ := ReadLength(a); lens < 5 || n <= 0 { + if n <= 0 { return "" } + str, _ := a.stringN(n) + return str +} + +// stringN stringify an array. If N is larger than 0, it will truncate the string to N bytes. +func (a Array) stringN(n int) (string, bool) { + if lens, _, _ := ReadLength(a); lens < 5 { + return "", false + } var buf strings.Builder buf.WriteByte('[') length, rem, _ := ReadLength(a) // We know we have enough bytes to read the length - length -= 4 + length -= (4 /* length bytes */ + 1 /* final null byte */) + var truncated bool var elem Element var ok bool - - if n > 0 { - for length > 1 { - elem, rem, ok = ReadElement(rem) - - length -= int32(len(elem)) - if !ok { - return "" - } - - str := elem.Value().StringN(n - buf.Len()) - - buf.WriteString(str) - - if buf.Len() == n { - return buf.String() + var str string + first := true + for length > 0 && !truncated { + l := 0 + if n > 0 { + if buf.Len() >= n { + truncated = true + break } - - if length > 1 { - buf.WriteByte(',') + l = n - buf.Len() + } + if !first { + buf.WriteByte(',') + if l > 0 { + l-- + if l == 0 { + truncated = true + break + } } } - if length != 1 { // Missing final null byte or inaccurate length - return "" + + elem, rem, ok = ReadElement(rem) + length -= int32(len(elem)) + if !ok || length < 0 { + return "", true } + + str, truncated = elem.Value().stringN(l) + buf.WriteString(str) + + first = false } - if buf.Len()+1 <= n { + if n <= 0 || (buf.Len() < n && !truncated) { buf.WriteByte(']') } - return buf.String() + return buf.String(), truncated } // Values returns this array as a slice of values. The returned slice will contain valid values. diff --git a/x/bsonx/bsoncore/array_test.go b/x/bsonx/bsoncore/array_test.go index 81685f793c..ebc8e0a0b2 100644 --- a/x/bsonx/bsoncore/array_test.go +++ b/x/bsonx/bsoncore/array_test.go @@ -506,7 +506,7 @@ func TestArray_StringN(t *testing.T) { }, { description: "n>0, array EQ n", - n: 22, + n: 20, values: []Value{ { Type: TypeInt32, @@ -576,7 +576,7 @@ func TestArray_StringN(t *testing.T) { got := Array(BuildArray(nil, tc.values...)).StringN(tc.n) assert.Equal(t, tc.want, got) if tc.n >= 0 { - assert.LessOrEqual(t, len(got), tc.n) + assert.LessOrEqual(t, len(got), tc.n, "got %v, want %v", got, tc.want) } }) } diff --git a/x/bsonx/bsoncore/document.go b/x/bsonx/bsoncore/document.go index 6a59908d5a..5f2b03d70b 100644 --- a/x/bsonx/bsoncore/document.go +++ b/x/bsonx/bsoncore/document.go @@ -10,11 +10,8 @@ import ( "errors" "fmt" "io" - "math" "strconv" "strings" - - "go.mongodb.org/mongo-driver/v2/internal/bsoncoreutil" ) // ValidationError is an error type returned when attempting to validate a document or array. @@ -264,58 +261,73 @@ func (d Document) DebugString() string { // String outputs an ExtendedJSON version of Document. If the document is not valid, this method // returns an empty string. func (d Document) String() string { - return d.StringN(math.MaxInt) + str, _ := d.stringN(0) + return str } // StringN stringifies a document upto N bytes func (d Document) StringN(n int) string { - if len(d) < 5 || n <= 0 { + if n <= 0 { return "" } + str, _ := d.stringN(n) + return str +} - var buf strings.Builder +// stringN stringify a document. If N is larger than 0, it will truncate the string to N bytes. +func (d Document) stringN(n int) (string, bool) { + if len(d) < 5 { + return "", false + } + var buf strings.Builder buf.WriteByte('{') length, rem, _ := ReadLength(d) - length -= 4 + length -= (4 /* length bytes */ + 1 /* final null byte */) + var truncated bool var elem Element var ok bool - + var str string first := true - truncated := false - - if n > 0 { - for length > 1 { - if !first { - buf.WriteByte(',') - } - elem, rem, ok = ReadElement(rem) - length -= int32(len(elem)) - if !ok { - return "" - } - - str := elem.StringN(n) - if buf.Len()+len(str) > n { - truncatedStr := bsoncoreutil.Truncate(str, n-buf.Len()) - buf.WriteString(truncatedStr) - + for length > 0 && !truncated { + l := 0 + if n > 0 { + if buf.Len() >= n { truncated = true break } + l = n - buf.Len() + } + if !first { + buf.WriteByte(',') + if l > 0 { + l-- + if l == 0 { + truncated = true + break + } + } + } - buf.WriteString(str) - first = false + elem, rem, ok = ReadElement(rem) + length -= int32(len(elem)) + if !ok || length < 0 { + return "", true } + + str, truncated = elem.stringN(l) + buf.WriteString(str) + + first = false } - if !truncated { + if n <= 0 || (buf.Len() < n && !truncated) { buf.WriteByte('}') } - return buf.String() + return buf.String(), truncated } // Elements returns this document as a slice of elements. The returned slice will contain valid diff --git a/x/bsonx/bsoncore/document_test.go b/x/bsonx/bsoncore/document_test.go index 5d5a2306d9..638420eefd 100644 --- a/x/bsonx/bsoncore/document_test.go +++ b/x/bsonx/bsoncore/document_test.go @@ -532,7 +532,7 @@ func TestDocument_StringN(t *testing.T) { got := bs.StringN(tc.n) assert.Equal(t, tc.want, got) if tc.n >= 0 { - assert.LessOrEqual(t, len(got), tc.n) + assert.LessOrEqual(t, len(got), tc.n, "got %v, want %v", got, tc.want) } }) } diff --git a/x/bsonx/bsoncore/element.go b/x/bsonx/bsoncore/element.go index dcb5e86e71..37025489b9 100644 --- a/x/bsonx/bsoncore/element.go +++ b/x/bsonx/bsoncore/element.go @@ -9,7 +9,9 @@ package bsoncore import ( "bytes" "fmt" - "math" + "strings" + + "go.mongodb.org/mongo-driver/v2/internal/bsoncoreutil" ) // MalformedElementError represents a class of errors that RawElement methods return. @@ -115,35 +117,78 @@ func (e Element) ValueErr() (Value, error) { // String implements the fmt.String interface. The output will be in extended JSON format. func (e Element) String() string { - return e.StringN(math.MaxInt) + str, _ := e.stringN(0) + return str } // StringN implements the fmt.String interface for upto N bytes. The output will be in extended JSON format. func (e Element) StringN(n int) string { - if len(e) == 0 { + if n <= 0 { return "" } + str, _ := e.stringN(n) + return str +} + +// stringN stringify an element. If N is larger than 0, it will truncate the string to N bytes. +func (e Element) stringN(n int) (string, bool) { + if len(e) == 0 { + return "", false + } + if n == 1 { + return `"`, true + } + t := Type(e[0]) idx := bytes.IndexByte(e[1:], 0x00) - if idx == -1 { - return "" - } - key, valBytes := []byte(e[1:idx+1]), []byte(e[idx+2:]) - val, _, valid := ReadValue(valBytes, t) + if idx <= 0 { + return "", false + } + key := e[1 : idx+1] + + var buf strings.Builder + buf.WriteByte('"') + const postfix = `": ` + switch { + case n <= 0 || idx <= n-4: + buf.Write(key) + buf.WriteString(postfix) + case idx < n: + buf.Write(key) + buf.WriteString(postfix[:n-idx]) + default: + buf.WriteString(bsoncoreutil.Truncate(string(key), n-1)) + } + + l := 0 + if n > 0 { + if buf.Len() >= n { + return buf.String(), true + } + l = n - buf.Len() + } + + val, _, valid := ReadValue(e[idx+2:], t) if !valid { - return "" + return "", false } var str string + var truncated bool if _, ok := val.StringValueOK(); ok { - str = val.StringN(n) + str, truncated = val.stringN(l) } else if arr, ok := val.ArrayOK(); ok { - str = arr.StringN(n) + str, truncated = arr.stringN(l) } else { str = val.String() + if l > 0 && len(str) > l { + truncated = true + str = bsoncoreutil.Truncate(str, l) + } } - return "\"" + string(key) + "\": " + str + buf.WriteString(str) + return buf.String(), truncated } // DebugString outputs a human readable version of RawElement. It will attempt to stringify the diff --git a/x/bsonx/bsoncore/value.go b/x/bsonx/bsoncore/value.go index b0fd907436..fa509411e1 100644 --- a/x/bsonx/bsoncore/value.go +++ b/x/bsonx/bsoncore/value.go @@ -218,7 +218,8 @@ func idHex(id [12]byte) string { // String implements the fmt.String interface. This method will return values in extended JSON // format. If the value is not valid, this returns an empty string func (v Value) String() string { - return v.StringN(math.MaxInt) + str, _ := v.stringN(0) + return str } // StringN implements the fmt.String interface. This method will return values in extended JSON @@ -227,129 +228,134 @@ func (v Value) StringN(n int) string { if n <= 0 { return "" } + str, _ := v.stringN(n) + return str +} +// stringN stringify a value. If N is larger than 0, it will truncate the string to N bytes. +func (v Value) stringN(n int) (string, bool) { + var str string switch v.Type { case TypeString: - str, ok := v.StringValueOK() + s, ok := v.StringValueOK() if !ok { - return "" - } - str = escapeString(str) - if len(str) > n { - truncatedStr := bsoncoreutil.Truncate(str, n) - return truncatedStr + return "", false } - return str + str = escapeString(s) case TypeEmbeddedDocument: doc, ok := v.DocumentOK() if !ok { - return "" + return "", false } - return doc.StringN(n) + return doc.stringN(n) case TypeArray: arr, ok := v.ArrayOK() if !ok { - return "" + return "", false } - return arr.StringN(n) + return arr.stringN(n) case TypeDouble: f64, ok := v.DoubleOK() if !ok { - return "" + return "", false } - return bsoncoreutil.Truncate(fmt.Sprintf(`{"$numberDouble":"%s"}`, formatDouble(f64)), n) + str = fmt.Sprintf(`{"$numberDouble":"%s"}`, formatDouble(f64)) case TypeBinary: subtype, data, ok := v.BinaryOK() if !ok { - return "" + return "", false } - return bsoncoreutil.Truncate(fmt.Sprintf(`{"$binary":{"base64":"%s","subType":"%02x"}}`, base64.StdEncoding.EncodeToString(data), subtype), n) + str = fmt.Sprintf(`{"$binary":{"base64":"%s","subType":"%02x"}}`, base64.StdEncoding.EncodeToString(data), subtype) case TypeUndefined: - return bsoncoreutil.Truncate(`{"$undefined":true}`, n) + str = `{"$undefined":true}` case TypeObjectID: oid, ok := v.ObjectIDOK() if !ok { - return "" + return "", false } - return bsoncoreutil.Truncate(fmt.Sprintf(`{"$oid":"%s"}`, idHex(oid)), n) + str = fmt.Sprintf(`{"$oid":"%s"}`, idHex(oid)) case TypeBoolean: b, ok := v.BooleanOK() if !ok { - return "" + return "", false } - return bsoncoreutil.Truncate(strconv.FormatBool(b), n) + str = strconv.FormatBool(b) case TypeDateTime: dt, ok := v.DateTimeOK() if !ok { - return "" + return "", false } - return bsoncoreutil.Truncate(fmt.Sprintf(`{"$date":{"$numberLong":"%d"}}`, dt), n) + str = fmt.Sprintf(`{"$date":{"$numberLong":"%d"}}`, dt) case TypeNull: - return bsoncoreutil.Truncate("null", n) + str = "null" case TypeRegex: pattern, options, ok := v.RegexOK() if !ok { - return "" + return "", false } - return bsoncoreutil.Truncate(fmt.Sprintf( + str = fmt.Sprintf( `{"$regularExpression":{"pattern":%s,"options":"%s"}}`, escapeString(pattern), sortStringAlphebeticAscending(options), - ), n) + ) case TypeDBPointer: ns, pointer, ok := v.DBPointerOK() if !ok { - return "" + return "", false } - return bsoncoreutil.Truncate(fmt.Sprintf(`{"$dbPointer":{"$ref":%s,"$id":{"$oid":"%s"}}}`, escapeString(ns), idHex(pointer)), n) + str = fmt.Sprintf(`{"$dbPointer":{"$ref":%s,"$id":{"$oid":"%s"}}}`, escapeString(ns), idHex(pointer)) case TypeJavaScript: js, ok := v.JavaScriptOK() if !ok { - return "" + return "", false } - return bsoncoreutil.Truncate(fmt.Sprintf(`{"$code":%s}`, escapeString(js)), n) + str = fmt.Sprintf(`{"$code":%s}`, escapeString(js)) case TypeSymbol: symbol, ok := v.SymbolOK() if !ok { - return "" + return "", false } - return bsoncoreutil.Truncate(fmt.Sprintf(`{"$symbol":%s}`, escapeString(symbol)), n) + str = fmt.Sprintf(`{"$symbol":%s}`, escapeString(symbol)) case TypeCodeWithScope: code, scope, ok := v.CodeWithScopeOK() if !ok { - return "" + return "", false } - return bsoncoreutil.Truncate(fmt.Sprintf(`{"$code":%s,"$scope":%s}`, code, scope), n) + str = fmt.Sprintf(`{"$code":%s,"$scope":%s}`, code, scope) case TypeInt32: i32, ok := v.Int32OK() if !ok { - return "" + return "", false } - return bsoncoreutil.Truncate(fmt.Sprintf(`{"$numberInt":"%d"}`, i32), n) + str = fmt.Sprintf(`{"$numberInt":"%d"}`, i32) case TypeTimestamp: t, i, ok := v.TimestampOK() if !ok { - return "" + return "", false } - return bsoncoreutil.Truncate(fmt.Sprintf(`{"$timestamp":{"t":%v,"i":%v}}`, t, i), n) + str = fmt.Sprintf(`{"$timestamp":{"t":%v,"i":%v}}`, t, i) case TypeInt64: i64, ok := v.Int64OK() if !ok { - return "" + return "", false } - return bsoncoreutil.Truncate(fmt.Sprintf(`{"$numberLong":"%d"}`, i64), n) + str = fmt.Sprintf(`{"$numberLong":"%d"}`, i64) case TypeDecimal128: h, l, ok := v.Decimal128OK() if !ok { - return "" + return "", false } - return bsoncoreutil.Truncate(fmt.Sprintf(`{"$numberDecimal":"%s"}`, decimal128.String(h, l)), n) + str = fmt.Sprintf(`{"$numberDecimal":"%s"}`, decimal128.String(h, l)) case TypeMinKey: - return bsoncoreutil.Truncate(`{"$minKey":1}`, n) + str = `{"$minKey":1}` case TypeMaxKey: - return bsoncoreutil.Truncate(`{"$maxKey":1}`, n) + str = `{"$maxKey":1}` default: - return "" + str = "" + } + if n > 0 && len(str) > n { + return bsoncoreutil.Truncate(str, n), true } + return str, false } // DebugString outputs a human readable version of Document. It will attempt to stringify the diff --git a/x/bsonx/bsoncore/value_test.go b/x/bsonx/bsoncore/value_test.go index ca92adf39e..7627c5dab4 100644 --- a/x/bsonx/bsoncore/value_test.go +++ b/x/bsonx/bsoncore/value_test.go @@ -832,7 +832,7 @@ func TestValue_StringN(t *testing.T) { got := tc.val.StringN(tc.n) assert.Equal(t, tc.want, got) if tc.n >= 0 { - assert.LessOrEqual(t, len(got), tc.n) + assert.LessOrEqual(t, len(got), tc.n, "got %v, want %v", got, tc.want) } }) } From 0563a2b0a2b8f820e753882cdcc6ce14b861cf03 Mon Sep 17 00:00:00 2001 From: Qingyang Hu Date: Wed, 16 Jul 2025 19:58:09 -0400 Subject: [PATCH 2/4] fixes --- bson/raw_test.go | 4 ++-- internal/logger/logger.go | 5 ++--- x/bsonx/bsoncore/array.go | 20 ++++++++++---------- x/bsonx/bsoncore/array_test.go | 2 +- x/bsonx/bsoncore/document.go | 20 ++++++++++---------- x/bsonx/bsoncore/document_test.go | 2 +- x/bsonx/bsoncore/element.go | 9 ++++----- x/bsonx/bsoncore/value.go | 8 ++++---- x/bsonx/bsoncore/value_test.go | 2 +- 9 files changed, 35 insertions(+), 37 deletions(-) diff --git a/bson/raw_test.go b/bson/raw_test.go index d09a4d2d71..55bb5dfc3b 100644 --- a/bson/raw_test.go +++ b/bson/raw_test.go @@ -450,7 +450,7 @@ func BenchmarkRawString(b *testing.B) { 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 + _, _ = bsoncore.Document(bs).StringN(1024) // Assuming you want to limit to 1024 bytes for this benchmark } }) } @@ -473,7 +473,7 @@ func TestComplexDocuments_StringN(t *testing.T) { bson, _ := Marshal(tc.doc) bsonDoc := bsoncore.Document(bson) - got := bsonDoc.StringN(tc.n) + got, _ := bsonDoc.StringN(tc.n) assert.Equal(t, tc.n, len(got)) }) } diff --git a/internal/logger/logger.go b/internal/logger/logger.go index c43e37c277..6c3136d8d8 100644 --- a/internal/logger/logger.go +++ b/internal/logger/logger.go @@ -241,10 +241,9 @@ func FormatDocument(msg bson.Raw, width uint) string { return "{}" } - str := bsoncore.Document(msg).StringN(int(width)) + str, truncated := bsoncore.Document(msg).StringN(int(width)) - // If the last byte is not a closing bracket, then the document was truncated - if len(str) > 0 && str[len(str)-1] != '}' { + if truncated { str += TruncationSuffix } diff --git a/x/bsonx/bsoncore/array.go b/x/bsonx/bsoncore/array.go index c977037db2..7fbab12aa6 100644 --- a/x/bsonx/bsoncore/array.go +++ b/x/bsonx/bsoncore/array.go @@ -87,29 +87,29 @@ func (a Array) String() string { } // StringN stringifies an array upto N bytes -func (a Array) StringN(n int) string { +func (a Array) StringN(n int) (string, bool) { if n <= 0 { - return "" + if l, _, ok := ReadLength(a); !ok || l < 5 { + return "", false + } + return "", true } - str, _ := a.stringN(n) - return str + return a.stringN(n) } // stringN stringify an array. If N is larger than 0, it will truncate the string to N bytes. func (a Array) stringN(n int) (string, bool) { - if lens, _, _ := ReadLength(a); lens < 5 { + length, rem, ok := ReadLength(a) // We know we have enough bytes to read the length + if !ok || length < 5 { return "", false } + length -= (4 /* length bytes */ + 1 /* final null byte */) var buf strings.Builder buf.WriteByte('[') - length, rem, _ := ReadLength(a) // We know we have enough bytes to read the length - length -= (4 /* length bytes */ + 1 /* final null byte */) - var truncated bool var elem Element - var ok bool var str string first := true for length > 0 && !truncated { @@ -135,7 +135,7 @@ func (a Array) stringN(n int) (string, bool) { elem, rem, ok = ReadElement(rem) length -= int32(len(elem)) if !ok || length < 0 { - return "", true + return "", false } str, truncated = elem.Value().stringN(l) diff --git a/x/bsonx/bsoncore/array_test.go b/x/bsonx/bsoncore/array_test.go index ebc8e0a0b2..58ca292192 100644 --- a/x/bsonx/bsoncore/array_test.go +++ b/x/bsonx/bsoncore/array_test.go @@ -573,7 +573,7 @@ func TestArray_StringN(t *testing.T) { for _, tc := range testCases { t.Run(tc.description, func(t *testing.T) { - got := Array(BuildArray(nil, tc.values...)).StringN(tc.n) + got, _ := Array(BuildArray(nil, tc.values...)).StringN(tc.n) assert.Equal(t, tc.want, got) if tc.n >= 0 { assert.LessOrEqual(t, len(got), tc.n, "got %v, want %v", got, tc.want) diff --git a/x/bsonx/bsoncore/document.go b/x/bsonx/bsoncore/document.go index 5f2b03d70b..753709657b 100644 --- a/x/bsonx/bsoncore/document.go +++ b/x/bsonx/bsoncore/document.go @@ -266,29 +266,29 @@ func (d Document) String() string { } // StringN stringifies a document upto N bytes -func (d Document) StringN(n int) string { +func (d Document) StringN(n int) (string, bool) { if n <= 0 { - return "" + if l, _, ok := ReadLength(d); !ok || l < 5 { + return "", false + } + return "", true } - str, _ := d.stringN(n) - return str + return d.stringN(n) } // stringN stringify a document. If N is larger than 0, it will truncate the string to N bytes. func (d Document) stringN(n int) (string, bool) { - if len(d) < 5 { + length, rem, ok := ReadLength(d) + if !ok || length < 5 { return "", false } + length -= (4 /* length bytes */ + 1 /* final null byte */) var buf strings.Builder buf.WriteByte('{') - length, rem, _ := ReadLength(d) - length -= (4 /* length bytes */ + 1 /* final null byte */) - var truncated bool var elem Element - var ok bool var str string first := true for length > 0 && !truncated { @@ -314,7 +314,7 @@ func (d Document) stringN(n int) (string, bool) { elem, rem, ok = ReadElement(rem) length -= int32(len(elem)) if !ok || length < 0 { - return "", true + return "", false } str, truncated = elem.stringN(l) diff --git a/x/bsonx/bsoncore/document_test.go b/x/bsonx/bsoncore/document_test.go index 638420eefd..0d4d168ad2 100644 --- a/x/bsonx/bsoncore/document_test.go +++ b/x/bsonx/bsoncore/document_test.go @@ -529,7 +529,7 @@ func TestDocument_StringN(t *testing.T) { for _, tc := range testCases { t.Run(tc.description, func(t *testing.T) { bs := tc.doc - got := bs.StringN(tc.n) + got, _ := bs.StringN(tc.n) assert.Equal(t, tc.want, got) if tc.n >= 0 { assert.LessOrEqual(t, len(got), tc.n, "got %v, want %v", got, tc.want) diff --git a/x/bsonx/bsoncore/element.go b/x/bsonx/bsoncore/element.go index 37025489b9..e6867f5fd1 100644 --- a/x/bsonx/bsoncore/element.go +++ b/x/bsonx/bsoncore/element.go @@ -122,12 +122,11 @@ func (e Element) String() string { } // StringN implements the fmt.String interface for upto N bytes. The output will be in extended JSON format. -func (e Element) StringN(n int) string { +func (e Element) StringN(n int) (string, bool) { if n <= 0 { - return "" + return "", len(e) > 0 } - str, _ := e.stringN(n) - return str + return e.stringN(n) } // stringN stringify an element. If N is larger than 0, it will truncate the string to N bytes. @@ -155,7 +154,7 @@ func (e Element) stringN(n int) (string, bool) { buf.WriteString(postfix) case idx < n: buf.Write(key) - buf.WriteString(postfix[:n-idx]) + buf.WriteString(postfix[:n-idx-1]) default: buf.WriteString(bsoncoreutil.Truncate(string(key), n-1)) } diff --git a/x/bsonx/bsoncore/value.go b/x/bsonx/bsoncore/value.go index fa509411e1..968f57e126 100644 --- a/x/bsonx/bsoncore/value.go +++ b/x/bsonx/bsoncore/value.go @@ -224,12 +224,12 @@ func (v Value) String() string { // StringN implements the fmt.String interface. This method will return values in extended JSON // format that will stringify a value upto N bytes. If the value is not valid, this returns an empty string -func (v Value) StringN(n int) string { +func (v Value) StringN(n int) (string, bool) { + str, truncated := v.stringN(n) if n <= 0 { - return "" + return "", len(str) > 0 } - str, _ := v.stringN(n) - return str + return str, truncated } // stringN stringify a value. If N is larger than 0, it will truncate the string to N bytes. diff --git a/x/bsonx/bsoncore/value_test.go b/x/bsonx/bsoncore/value_test.go index 7627c5dab4..794a18ec1b 100644 --- a/x/bsonx/bsoncore/value_test.go +++ b/x/bsonx/bsoncore/value_test.go @@ -829,7 +829,7 @@ func TestValue_StringN(t *testing.T) { for _, tc := range testCases { t.Run(tc.description, func(t *testing.T) { - got := tc.val.StringN(tc.n) + got, _ := tc.val.StringN(tc.n) assert.Equal(t, tc.want, got) if tc.n >= 0 { assert.LessOrEqual(t, len(got), tc.n, "got %v, want %v", got, tc.want) From 3d68a750c48eebfd84be6b5971e74f5dbaf6311c Mon Sep 17 00:00:00 2001 From: Qingyang Hu Date: Wed, 23 Jul 2025 18:53:05 -0400 Subject: [PATCH 3/4] update unit tests --- x/bsonx/bsoncore/array.go | 2 +- x/bsonx/bsoncore/array_test.go | 246 ++++----------------- x/bsonx/bsoncore/document_test.go | 161 +++++--------- x/bsonx/bsoncore/element.go | 6 +- x/bsonx/bsoncore/value_test.go | 349 +++++++++++++++++++----------- 5 files changed, 322 insertions(+), 442 deletions(-) diff --git a/x/bsonx/bsoncore/array.go b/x/bsonx/bsoncore/array.go index 7fbab12aa6..874e93193f 100644 --- a/x/bsonx/bsoncore/array.go +++ b/x/bsonx/bsoncore/array.go @@ -99,7 +99,7 @@ func (a Array) StringN(n int) (string, bool) { // stringN stringify an array. If N is larger than 0, it will truncate the string to N bytes. func (a Array) stringN(n int) (string, bool) { - length, rem, ok := ReadLength(a) // We know we have enough bytes to read the length + length, rem, ok := ReadLength(a) if !ok || length < 5 { return "", false } diff --git a/x/bsonx/bsoncore/array_test.go b/x/bsonx/bsoncore/array_test.go index 58ca292192..ad648c28db 100644 --- a/x/bsonx/bsoncore/array_test.go +++ b/x/bsonx/bsoncore/array_test.go @@ -10,6 +10,7 @@ import ( "bytes" "encoding/binary" "errors" + "fmt" "io" "testing" @@ -348,236 +349,77 @@ func TestArray(t *testing.T) { }) } -func TestArray_StringN(t *testing.T) { +func TestArray_Stringer(t *testing.T) { testCases := []struct { description string - n int - values []Value + array Array want string }{ - // n = 0 cases { - description: "n=0, array with 1 element", - n: 0, - values: []Value{ - { - Type: TypeString, - Data: AppendString(nil, "abc"), - }, - }, - want: "", - }, - { - description: "n=0, empty array", - n: 0, - values: []Value{}, - want: "", + description: "empty array", + array: BuildArray(nil), + want: `[]`, }, { - description: "n=0, nested array", - n: 0, - values: []Value{ - { - Type: TypeArray, - Data: BuildArray(nil, Value{ - Type: TypeString, - Data: AppendString(nil, "abc"), - }), - }, - }, - want: "", - }, - { - description: "n=0, array with mixed types", - n: 0, - values: []Value{ - { - Type: TypeString, - Data: AppendString(nil, "abc"), - }, - { - Type: TypeInt32, - Data: AppendInt32(nil, 123), - }, - { - Type: TypeBoolean, - Data: AppendBoolean(nil, true), - }, - }, - want: "", + description: "array with 1 element", + array: BuildArray(nil, Value{ + Type: TypeInt32, + Data: AppendInt32(nil, 123), + }), + want: `[{"$numberInt":"123"}]`, }, - - // n < 0 cases { - description: "n<0, array with 1 element", - n: -1, - values: []Value{ - { + description: "nested array", + array: BuildArray(nil, Value{ + Type: TypeArray, + Data: BuildArray(nil, Value{ Type: TypeString, Data: AppendString(nil, "abc"), - }, - }, - want: "", - }, - { - description: "n<0, empty array", - n: -1, - values: []Value{}, - want: "", - }, - { - description: "n<0, nested array", - n: -1, - values: []Value{ - { - Type: TypeArray, - Data: BuildArray(nil, Value{ - Type: TypeString, - Data: AppendString(nil, "abc"), - }), - }, - }, - want: "", - }, - { - description: "n<0, array with mixed types", - n: -1, - values: []Value{ - { - Type: TypeString, - Data: AppendString(nil, "abc"), - }, - { - Type: TypeInt32, - Data: AppendInt32(nil, 123), - }, - { - Type: TypeBoolean, - Data: AppendBoolean(nil, true), - }, - }, - want: "", - }, - - // n > 0 cases - { - description: "n>0, array LT n", - n: 1, - values: []Value{ - { - Type: TypeInt32, - Data: AppendInt32(nil, 2), - }, - }, - want: "[", - }, - { - description: "n>0, array LT n", - n: 2, - values: []Value{ - { - Type: TypeInt32, - Data: AppendInt32(nil, 2), - }, - }, - want: "[{", - }, - { - description: "n>0, array LT n", - n: 14, - values: []Value{ - { - Type: TypeInt32, - Data: AppendInt32(nil, 2), - }, - }, - want: `[{"$numberInt"`, - }, - { - description: "n>0, array GT n", - n: 30, - values: []Value{ - { - Type: TypeInt32, - Data: AppendInt32(nil, 2), - }, - }, - want: `[{"$numberInt":"2"}]`, - }, - { - description: "n>0, array EQ n", - n: 20, - values: []Value{ - { - Type: TypeInt32, - Data: AppendInt32(nil, 2), - }, - }, - want: `[{"$numberInt":"2"}]`, - }, - { - description: "n>0, mixed array", - n: 24, - values: []Value{ - { - Type: TypeInt32, - Data: AppendInt32(nil, 1), - }, - { - Type: TypeString, - Data: AppendString(nil, "foo"), - }, - }, - want: `[{"$numberInt":"1"},"foo`, - }, - { - description: "n>0, empty array", - n: 10, - values: []Value{}, - want: "[]", - }, - { - description: "n>0, nested array", - n: 10, - values: []Value{ - { - Type: TypeArray, - Data: BuildArray(nil, Value{ - Type: TypeString, - Data: AppendString(nil, "abc"), - }), - }, - }, + }), + }), want: `[["abc"]]`, }, { - description: "n>0, array with mixed types", - n: 32, - values: []Value{ - { + description: "array with mixed types", + array: BuildArray(nil, + Value{ Type: TypeString, Data: AppendString(nil, "abc"), }, - { + Value{ Type: TypeInt32, Data: AppendInt32(nil, 123), }, - { + Value{ Type: TypeBoolean, Data: AppendBoolean(nil, true), }, - }, - want: `["abc",{"$numberInt":"123"},true`, + ), + want: `["abc",{"$numberInt":"123"},true]`, }, } for _, tc := range testCases { - t.Run(tc.description, func(t *testing.T) { - got, _ := Array(BuildArray(nil, tc.values...)).StringN(tc.n) + t.Run(fmt.Sprintf("String %s", tc.description), func(t *testing.T) { + got := tc.array.String() assert.Equal(t, tc.want, got) - if tc.n >= 0 { - assert.LessOrEqual(t, len(got), tc.n, "got %v, want %v", got, tc.want) - } }) } + + for _, tc := range testCases { + for n := -1; n <= len(tc.want)+1; n++ { + t.Run(fmt.Sprintf("StringN %s n==%d", tc.description, n), func(t *testing.T) { + got, _ := tc.array.StringN(n) + l := n + if l < 0 { + l = 0 + } + if l > len(tc.want) { + l = len(tc.want) + } + want := tc.want[:l] + assert.Equal(t, want, got, "got %v, want %v", got, want) + }) + } + } } diff --git a/x/bsonx/bsoncore/document_test.go b/x/bsonx/bsoncore/document_test.go index 0d4d168ad2..02b5796b8c 100644 --- a/x/bsonx/bsoncore/document_test.go +++ b/x/bsonx/bsoncore/document_test.go @@ -12,7 +12,6 @@ import ( "errors" "fmt" "io" - "strings" "testing" "github.com/google/go-cmp/cmp" @@ -413,127 +412,67 @@ func TestDocument(t *testing.T) { }) } -func TestDocument_StringN(t *testing.T) { - var buf strings.Builder - for i := 0; i < 16000000; i++ { - buf.WriteString("abcdefgh") - } - str1k := buf.String() - str128 := str1k[:128] - +func TestDocument_Stringer(t *testing.T) { testCases := []struct { description string n int doc Document want string }{ - // n = 0 cases - {"n=0, document with 1 field", 0, BuildDocument(nil, - AppendStringElement(nil, "key", str128), - ), ""}, - - {"n=0, empty document", 0, Document{}, ""}, - - {"n=0, document with nested documents", 0, BuildDocument(nil, - AppendDocumentElement(nil, "key", BuildDocument(nil, - AppendStringElement(nil, "nestedKey", str128), - )), - ), ""}, - - {"n=0, document with mixed types", 0, BuildDocument(nil, - AppendStringElement(nil, "key", str128), - AppendInt32Element(nil, "number", 123), - ), ""}, - - {"n=0, deeply nested document", 0, BuildDocument(nil, - AppendDocumentElement(nil, "a", BuildDocument(nil, - AppendDocumentElement(nil, "b", BuildDocument(nil, - AppendStringElement(nil, "c", str128), - )), - )), - ), ""}, - - {"n=0, complex value", 0, BuildDocument(nil, - AppendDocumentElement(nil, "key", BuildDocument(nil, - AppendStringElement(nil, "nestedKey", str128), - )), - ), ""}, - - // n < 0 cases - {"n<0, document with 1 field", -1, BuildDocument(nil, - AppendStringElement(nil, "key", str128), - ), ""}, - - {"n<0, empty document", -1, Document{}, ""}, - - {"n<0, document with nested documents", -1, BuildDocument(nil, - AppendDocumentElement(nil, "key", BuildDocument(nil, - AppendStringElement(nil, "nestedKey", str128), - )), - ), ""}, - - {"n<0, document with mixed types", -1, BuildDocument(nil, - AppendStringElement(nil, "key", str128), - AppendInt32Element(nil, "number", 123), - ), ""}, - - {"n<0, deeply nested document", -1, BuildDocument(nil, - AppendDocumentElement(nil, "a", BuildDocument(nil, - AppendDocumentElement(nil, "b", BuildDocument(nil, - AppendStringElement(nil, "c", str128), - )), - )), - ), ""}, - - {"n<0, complex value", -1, BuildDocument(nil, - AppendDocumentElement(nil, "key", BuildDocument(nil, - AppendStringElement(nil, "nestedKey", str128), - )), - ), ""}, - - // n > 0 cases - {"n>0, document LT n", 3, BuildDocument(nil, - AppendStringElement(nil, "key", "value"), - ), `{"k`}, - - {"n>0, document GT n", 25, BuildDocument(nil, - AppendStringElement(nil, "key", "value"), - ), `{"key": "value"}`}, - - {"n>0, document EQ n", 16, BuildDocument(nil, - AppendStringElement(nil, "key", "value"), - ), `{"key": "value"}`}, - - {"n>0, document with nested documents", 15, BuildDocument(nil, - AppendDocumentElement(nil, "key", BuildDocument(nil, - AppendStringElement(nil, "nestedKey", str128), - )), - ), `{"key": {"neste`}, - - {"n>0, document with mixed types", 11, BuildDocument(nil, - AppendStringElement(nil, "key", str128), - AppendInt32Element(nil, "number", 123), - ), `{"key": "ab`}, - - {"n>0, deeply nested document", 17, BuildDocument(nil, - AppendDocumentElement(nil, "a", BuildDocument(nil, - AppendDocumentElement(nil, "b", BuildDocument(nil, - AppendStringElement(nil, "c", str128), + { + description: "empty document", + doc: BuildDocument(nil), + want: `{}`, + }, + { + description: "document with 1 field", + doc: BuildDocument(nil, + AppendInt32Element(nil, "number", 123), + ), + want: `{"number": {"$numberInt":"123"}}`, + }, + { + description: "nested documents", + doc: BuildDocument(nil, + AppendDocumentElement(nil, "key", BuildDocument(nil, + AppendStringElement(nil, "nestedKey", "abc"), )), - )), - ), `{"a": {"b": {"c":`}, - - {"n>0, empty document", 10, Document{}, ""}, + ), + want: `{"key": {"nestedKey": "abc"}}`, + }, + + { + description: "document with mixed types", + doc: BuildDocument(nil, + AppendStringElement(nil, "key", "abc"), + AppendInt32Element(nil, "number", 123), + AppendBooleanElement(nil, "flag", true), + ), + want: `{"key": "abc","number": {"$numberInt":"123"},"flag": true}`, + }, } for _, tc := range testCases { - t.Run(tc.description, func(t *testing.T) { - bs := tc.doc - got, _ := bs.StringN(tc.n) + t.Run(fmt.Sprintf("String %s", tc.description), func(t *testing.T) { + got := tc.doc.String() assert.Equal(t, tc.want, got) - if tc.n >= 0 { - assert.LessOrEqual(t, len(got), tc.n, "got %v, want %v", got, tc.want) - } }) } + + for _, tc := range testCases { + for n := -1; n <= len(tc.want)+1; n++ { + t.Run(fmt.Sprintf("StringN %s n==%d", tc.description, n), func(t *testing.T) { + got, _ := tc.doc.StringN(n) + l := n + if l < 0 { + l = 0 + } + if l > len(tc.want) { + l = len(tc.want) + } + want := tc.want[:l] + assert.Equal(t, want, got, "got %v, want %v", got, want) + }) + } + } } diff --git a/x/bsonx/bsoncore/element.go b/x/bsonx/bsoncore/element.go index e6867f5fd1..08363ff07c 100644 --- a/x/bsonx/bsoncore/element.go +++ b/x/bsonx/bsoncore/element.go @@ -147,14 +147,14 @@ func (e Element) stringN(n int) (string, bool) { var buf strings.Builder buf.WriteByte('"') - const postfix = `": ` + const suffix = `": ` switch { case n <= 0 || idx <= n-4: buf.Write(key) - buf.WriteString(postfix) + buf.WriteString(suffix) case idx < n: buf.Write(key) - buf.WriteString(postfix[:n-idx-1]) + buf.WriteString(suffix[:n-idx-1]) default: buf.WriteString(bsoncoreutil.Truncate(string(key), n-1)) } diff --git a/x/bsonx/bsoncore/value_test.go b/x/bsonx/bsoncore/value_test.go index 794a18ec1b..cf59d9800d 100644 --- a/x/bsonx/bsoncore/value_test.go +++ b/x/bsonx/bsoncore/value_test.go @@ -7,8 +7,8 @@ package bsoncore import ( + "fmt" "reflect" - "strings" "testing" "time" @@ -677,163 +677,262 @@ func TestValue(t *testing.T) { } } -func TestValue_StringN(t *testing.T) { - var buf strings.Builder - for i := 0; i < 16000000; i++ { - buf.WriteString("abcdefgh") - } - str1k := buf.String() - str128 := str1k[:128] +func TestValue_Stringer(t *testing.T) { testObjectID := [12]byte{0x60, 0xd4, 0xc2, 0x1f, 0x4e, 0x60, 0x4a, 0x0c, 0x8b, 0x2e, 0x9c, 0x3f} testCases := []struct { description string - n int val Value want string }{ - // n = 0 cases - {"n=0, single value", 0, Value{ - Type: TypeString, Data: AppendString(nil, "abcdefgh")}, ""}, - - {"n=0, large string value", 0, Value{ - Type: TypeString, Data: AppendString(nil, "abcdefgh")}, ""}, - - {"n=0, value with special characters", 0, Value{ - Type: TypeString, Data: AppendString(nil, "!@#$%^&*()")}, ""}, - - // n < 0 cases - {"n<0, single value", -1, Value{ - Type: TypeString, Data: AppendString(nil, "abcdefgh")}, ""}, - - {"n<0, large string value", -1, Value{ - Type: TypeString, Data: AppendString(nil, "abcdefgh")}, ""}, - - {"n<0, value with special characters", -1, Value{ - Type: TypeString, Data: AppendString(nil, "!@#$%^&*()")}, ""}, - - // n > 0 cases - {"n>0, string LT n", 4, Value{ - Type: TypeString, Data: AppendString(nil, "foo")}, `"foo`}, - - {"n>0, string GT n", 10, Value{ - Type: TypeString, Data: AppendString(nil, "foo")}, `"foo"`}, - - {"n>0, string EQ n", 5, Value{ - Type: TypeString, Data: AppendString(nil, "foo")}, `"foo"`}, - - {"n>0, multi-byte string LT n", 10, Value{ - Type: TypeString, Data: AppendString(nil, "𨉟呐㗂越")}, `"𨉟呐`}, - - {"n>0, multi-byte string GT n", 21, Value{ - Type: TypeString, Data: AppendString(nil, "𨉟呐㗂越")}, `"𨉟呐㗂越"`}, - - {"n>0, multi-byte string EQ n", 15, Value{ - Type: TypeString, Data: AppendString(nil, "𨉟呐㗂越")}, `"𨉟呐㗂越"`}, - - {"n>0, multi-byte string exact character boundary", 6, Value{ - Type: TypeString, Data: AppendString(nil, "𨉟呐㗂越")}, `"𨉟`}, - - {"n>0, multi-byte string mid character", 8, Value{ - Type: TypeString, Data: AppendString(nil, "𨉟呐㗂越")}, `"𨉟`}, - - {"n>0, multi-byte string edge case", 10, Value{ - Type: TypeString, Data: AppendString(nil, "𨉟呐㗂越")}, `"𨉟呐`}, - - {"n>0, single value", 10, Value{ - Type: TypeString, Data: AppendString(nil, str128)}, `"abcdefgha`}, + { + description: "string value", + val: Value{ + Type: TypeString, Data: AppendString(nil, "abcdefgh")}, + want: `"abcdefgh"`, + }, - {"n>0, large string value", 10, Value{ - Type: TypeString, Data: AppendString(nil, str1k)}, `"abcdefgha`}, + { + description: "value with special characters", + val: Value{ + Type: TypeString, + Data: AppendString(nil, "!@#$%^&*()")}, + want: `"!@#$%^\u0026*()"`, + }, - {"n>0, value with special characters", 5, Value{ - Type: TypeString, Data: AppendString(nil, "!@#$%^&*()")}, `"!@#$`}, + { + description: "TypeEmbeddedDocument", + val: Value{ + Type: TypeEmbeddedDocument, + Data: BuildDocument(nil, + AppendInt32Element(nil, "number", 123), + ), + }, + want: `{"number": {"$numberInt":"123"}}`, + }, - // Extended cases for each type - {"n>0, TypeEmbeddedDocument", 10, Value{ - Type: TypeEmbeddedDocument, Data: BuildDocument(nil, - AppendStringElement(nil, "key", "value"))}, `{"key": "v`}, + { + description: "TypeArray", + val: Value{ + Type: TypeArray, + Data: BuildArray(nil, + Value{ + Type: TypeString, + Data: AppendString(nil, "abc"), + }, + Value{ + Type: TypeInt32, + Data: AppendInt32(nil, 123), + }, + Value{ + Type: TypeBoolean, + Data: AppendBoolean(nil, true), + }, + )}, + want: `["abc",{"$numberInt":"123"},true]`, + }, - {"n>0, TypeArray", 10, Value{ - Type: TypeArray, - Data: BuildArray(nil, - Value{ - Type: TypeString, - Data: AppendString(nil, "abc"), - }, - Value{ - Type: TypeInt32, - Data: AppendInt32(nil, 123), - }, - Value{ - Type: TypeBoolean, - Data: AppendBoolean(nil, true), - }, - )}, `["abc",{"$`}, + { + description: "TypeDouble", + val: Value{ + Type: TypeDouble, + Data: AppendDouble(nil, 123.456), + }, + want: `{"$numberDouble":"123.456"}`, + }, - {"n>0, TypeDouble", 10, Value{ - Type: TypeDouble, Data: AppendDouble(nil, 123.456)}, `{"$numberD`}, + { + description: "TypeBinary", + val: Value{ + Type: TypeBinary, + Data: AppendBinary(nil, 0x00, []byte{0x01, 0x02, 0x03})}, + want: `{"$binary":{"base64":"AQID","subType":"00"}}`, + }, - {"n>0, TypeBinary", 10, Value{ - Type: TypeBinary, Data: AppendBinary(nil, 0x00, []byte{0x01, 0x02, 0x03})}, `{"$binary"`}, + { + description: "TypeUndefined", + val: Value{ + Type: TypeUndefined, + }, + want: `{"$undefined":true}`, + }, - {"n>0, TypeUndefined", 10, Value{ - Type: TypeUndefined}, `{"$undefin`}, + { + description: "TypeObjectID", + val: Value{ + Type: TypeObjectID, + Data: AppendObjectID(nil, testObjectID), + }, + want: `{"$oid":"60d4c21f4e604a0c8b2e9c3f"}`, + }, - {"n>0, TypeObjectID", 10, Value{ - Type: TypeObjectID, Data: AppendObjectID(nil, testObjectID)}, `{"$oid":"6`}, + { + description: "TypeBoolean", + val: Value{ + Type: TypeBoolean, + Data: AppendBoolean(nil, true), + }, + want: `true`, + }, - {"n>0, TypeBoolean", 3, Value{ - Type: TypeBoolean, Data: AppendBoolean(nil, true)}, `tru`}, + { + description: "TypeDateTime", + val: Value{ + Type: TypeDateTime, + Data: AppendDateTime(nil, 1234567890), + }, + want: `{"$date":{"$numberLong":"1234567890"}}`, + }, - {"n>0, TypeDateTime", 10, Value{ - Type: TypeDateTime, Data: AppendDateTime(nil, 1234567890)}, `{"$date":{`}, + { + description: "TypeNull", + val: Value{ + Type: TypeNull, + }, + want: `null`, + }, - {"n>0, TypeNull", 3, Value{ - Type: TypeNull}, `nul`}, + { + description: "TypeRegex", + val: Value{ + Type: TypeRegex, + Data: AppendRegex(nil, "pattern", "i"), + }, + want: `{"$regularExpression":{"pattern":"pattern","options":"i"}}`, + }, - {"n>0, TypeRegex", 10, Value{ - Type: TypeRegex, Data: AppendRegex(nil, "pattern", "options")}, `{"$regular`}, + { + description: "TypeDBPointer", + val: Value{ + Type: TypeDBPointer, + Data: AppendDBPointer(nil, "namespace", testObjectID), + }, + want: `{"$dbPointer":{"$ref":"namespace","$id":{"$oid":"60d4c21f4e604a0c8b2e9c3f"}}}`, + }, - {"n>0, TypeDBPointer", 15, Value{ - Type: TypeDBPointer, Data: AppendDBPointer(nil, "namespace", testObjectID)}, `{"$dbPointer":{`}, + { + description: "TypeJavaScript", + val: Value{ + Type: TypeJavaScript, + Data: AppendJavaScript(nil, "code"), + }, + want: `{"$code":"code"}`, + }, - {"n>0, TypeJavaScript", 15, Value{ - Type: TypeJavaScript, Data: AppendJavaScript(nil, "code")}, `{"$code":"code"`}, + { + description: "TypeSymbol", + val: Value{ + Type: TypeSymbol, + Data: AppendSymbol(nil, "symbol"), + }, + want: `{"$symbol":"symbol"}`, + }, - {"n>0, TypeSymbol", 10, Value{ - Type: TypeSymbol, Data: AppendSymbol(nil, "symbol")}, `{"$symbol"`}, + { + description: "TypeCodeWithScope", + val: Value{ + Type: TypeCodeWithScope, + Data: AppendCodeWithScope(nil, "code", + BuildDocument(nil, AppendStringElement(nil, "key", "value")), + ), + }, + want: `{"$code":code,"$scope":{"key": "value"}}`, + }, - {"n>0, TypeCodeWithScope", 10, Value{ - Type: TypeCodeWithScope, Data: AppendCodeWithScope(nil, "code", BuildDocument(nil, - AppendStringElement(nil, "key", "value")))}, `{"$code":c`}, + { + description: "TypeInt32", + val: Value{ + Type: TypeInt32, + Data: AppendInt32(nil, 123), + }, + want: `{"$numberInt":"123"}`, + }, - {"n>0, TypeInt32", 10, Value{ - Type: TypeInt32, Data: AppendInt32(nil, 123)}, `{"$numberI`}, + { + description: "TypeTimestamp", + val: Value{ + Type: TypeTimestamp, + Data: AppendTimestamp(nil, 123, 456), + }, + want: `{"$timestamp":{"t":123,"i":456}}`, + }, - {"n>0, TypeTimestamp", 10, Value{ - Type: TypeTimestamp, Data: AppendTimestamp(nil, 123, 456)}, `{"$timesta`}, + { + description: "TypeInt64", + val: Value{ + Type: TypeInt64, + Data: AppendInt64(nil, 1234567890), + }, + want: `{"$numberLong":"1234567890"}`, + }, - {"n>0, TypeInt64", 10, Value{ - Type: TypeInt64, Data: AppendInt64(nil, 1234567890)}, `{"$numberL`}, + { + description: "TypeDecimal128", + val: Value{ + Type: TypeDecimal128, + Data: AppendDecimal128(nil, 0x3040000000000000, 0x0000000000000000), + }, + want: `{"$numberDecimal":"0"}`, + }, - {"n>0, TypeDecimal128", 10, Value{ - Type: TypeDecimal128, Data: AppendDecimal128(nil, 0x3040000000000000, 0x0000000000000000)}, `{"$numberD`}, + { + description: "TypeMinKey", + val: Value{ + Type: TypeMinKey, + }, + want: `{"$minKey":1}`, + }, - {"n>0, TypeMinKey", 10, Value{ - Type: TypeMinKey}, `{"$minKey"`}, + { + description: "TypeMaxKey", + val: Value{ + Type: TypeMaxKey, + }, + want: `{"$maxKey":1}`, + }, + } - {"n>0, TypeMaxKey", 10, Value{ - Type: TypeMaxKey}, `{"$maxKey"`}, + for _, tc := range testCases { + t.Run(fmt.Sprintf("String %s", tc.description), func(t *testing.T) { + got := tc.val.String() + assert.Equal(t, tc.want, got) + }) } for _, tc := range testCases { - t.Run(tc.description, func(t *testing.T) { - got, _ := tc.val.StringN(tc.n) + for n := -1; n <= len(tc.want)+1; n++ { + t.Run(fmt.Sprintf("StringN %s n==%d", tc.description, n), func(t *testing.T) { + got, _ := tc.val.StringN(n) + l := n + if l < 0 { + l = 0 + } + if l > len(tc.want) { + l = len(tc.want) + } + want := tc.want[:l] + assert.Equal(t, want, got, "got %v, want %v", got, want) + }) + } + } + + multiByteString := Value{ + Type: TypeString, + Data: AppendString(nil, "𨉟呐㗂越"), + } + for _, tc := range []struct { + n int + want string + }{ + {6, `"𨉟`}, + {8, `"𨉟`}, + {10, `"𨉟呐`}, + {15, `"𨉟呐㗂越"`}, + {21, `"𨉟呐㗂越"`}, + } { + t.Run(fmt.Sprintf("StringN multi-byte string n==%d", tc.n), func(t *testing.T) { + got, _ := multiByteString.StringN(tc.n) assert.Equal(t, tc.want, got) - if tc.n >= 0 { - assert.LessOrEqual(t, len(got), tc.n, "got %v, want %v", got, tc.want) - } }) } } From 555951fdee5ffda49b527f749fce4191e714b9e5 Mon Sep 17 00:00:00 2001 From: Qingyang Hu Date: Fri, 1 Aug 2025 11:06:42 -0400 Subject: [PATCH 4/4] Update StringN() behavior. --- x/bsonx/bsoncore/array.go | 46 +-- x/bsonx/bsoncore/array_test.go | 118 ++++---- x/bsonx/bsoncore/document.go | 46 +-- x/bsonx/bsoncore/document_test.go | 136 +++++---- x/bsonx/bsoncore/element.go | 37 +-- x/bsonx/bsoncore/value.go | 23 +- x/bsonx/bsoncore/value_test.go | 476 +++++++++++++++--------------- 7 files changed, 466 insertions(+), 416 deletions(-) diff --git a/x/bsonx/bsoncore/array.go b/x/bsonx/bsoncore/array.go index 874e93193f..bfedbc8661 100644 --- a/x/bsonx/bsoncore/array.go +++ b/x/bsonx/bsoncore/array.go @@ -82,28 +82,24 @@ func (a Array) DebugString() string { // String outputs an ExtendedJSON version of Array. If the Array is not valid, this method // returns an empty string. func (a Array) String() string { - str, _ := a.stringN(0) + str, _ := a.StringN(-1) return str } -// StringN stringifies an array upto N bytes +// StringN stringifies an array. If N is non-negative, it will truncate the string to N bytes. +// Otherwise, it will return the full string representation. The second return value indicates +// whether the string was truncated or not. func (a Array) StringN(n int) (string, bool) { - if n <= 0 { - if l, _, ok := ReadLength(a); !ok || l < 5 { - return "", false - } - return "", true - } - return a.stringN(n) -} - -// stringN stringify an array. If N is larger than 0, it will truncate the string to N bytes. -func (a Array) stringN(n int) (string, bool) { length, rem, ok := ReadLength(a) if !ok || length < 5 { return "", false } - length -= (4 /* length bytes */ + 1 /* final null byte */) + length -= 4 // length bytes + length-- // final null byte + + if n == 0 { + return "", true + } var buf strings.Builder buf.WriteByte('[') @@ -113,19 +109,25 @@ func (a Array) stringN(n int) (string, bool) { var str string first := true for length > 0 && !truncated { - l := 0 + needStrLen := -1 + // Set needStrLen if n is positive, meaning we want to limit the string length. if n > 0 { + // Stop stringifying if we reach the limit, that also ensures needStrLen is + // greater than 0 if we need to limit the length. if buf.Len() >= n { truncated = true break } - l = n - buf.Len() + needStrLen = n - buf.Len() } + + // Append a comma if this is not the first element. if !first { buf.WriteByte(',') - if l > 0 { - l-- - if l == 0 { + // If we are truncating, we need to account for the comma in the length. + if needStrLen > 0 { + needStrLen-- + if needStrLen == 0 { truncated = true break } @@ -134,11 +136,13 @@ func (a Array) stringN(n int) (string, bool) { elem, rem, ok = ReadElement(rem) length -= int32(len(elem)) + // Exit on malformed element. if !ok || length < 0 { return "", false } - str, truncated = elem.Value().stringN(l) + // Delegate to StringN() on the element. + str, truncated = elem.Value().StringN(needStrLen) buf.WriteString(str) first = false @@ -146,6 +150,8 @@ func (a Array) stringN(n int) (string, bool) { if n <= 0 || (buf.Len() < n && !truncated) { buf.WriteByte(']') + } else { + truncated = true } return buf.String(), truncated diff --git a/x/bsonx/bsoncore/array_test.go b/x/bsonx/bsoncore/array_test.go index ad648c28db..eeb07b6c3d 100644 --- a/x/bsonx/bsoncore/array_test.go +++ b/x/bsonx/bsoncore/array_test.go @@ -349,76 +349,78 @@ func TestArray(t *testing.T) { }) } -func TestArray_Stringer(t *testing.T) { - testCases := []struct { - description string - array Array - want string - }{ - { - description: "empty array", - array: BuildArray(nil), - want: `[]`, - }, - { - description: "array with 1 element", - array: BuildArray(nil, Value{ +var arrayStringTestCases = []struct { + description string + array Array + want string +}{ + { + description: "empty array", + array: BuildArray(nil), + want: `[]`, + }, + { + description: "array with 1 element", + array: BuildArray(nil, Value{ + Type: TypeInt32, + Data: AppendInt32(nil, 123), + }), + want: `[{"$numberInt":"123"}]`, + }, + { + description: "nested array", + array: BuildArray(nil, Value{ + Type: TypeArray, + Data: BuildArray(nil, Value{ + Type: TypeString, + Data: AppendString(nil, "abc"), + }), + }), + want: `[["abc"]]`, + }, + { + description: "array with mixed types", + array: BuildArray(nil, + Value{ + Type: TypeString, + Data: AppendString(nil, "abc"), + }, + Value{ Type: TypeInt32, Data: AppendInt32(nil, 123), - }), - want: `[{"$numberInt":"123"}]`, - }, - { - description: "nested array", - array: BuildArray(nil, Value{ - Type: TypeArray, - Data: BuildArray(nil, Value{ - Type: TypeString, - Data: AppendString(nil, "abc"), - }), - }), - want: `[["abc"]]`, - }, - { - description: "array with mixed types", - array: BuildArray(nil, - Value{ - Type: TypeString, - Data: AppendString(nil, "abc"), - }, - Value{ - Type: TypeInt32, - Data: AppendInt32(nil, 123), - }, - Value{ - Type: TypeBoolean, - Data: AppendBoolean(nil, true), - }, - ), - want: `["abc",{"$numberInt":"123"},true]`, - }, - } + }, + Value{ + Type: TypeBoolean, + Data: AppendBoolean(nil, true), + }, + ), + want: `["abc",{"$numberInt":"123"},true]`, + }, +} - for _, tc := range testCases { - t.Run(fmt.Sprintf("String %s", tc.description), func(t *testing.T) { +func TestArray_String(t *testing.T) { + for _, tc := range arrayStringTestCases { + t.Run(tc.description, func(t *testing.T) { got := tc.array.String() - assert.Equal(t, tc.want, got) + assert.Equal(t, tc.want, got, "expected string %s, got %s", tc.want, got) }) } +} - for _, tc := range testCases { +func TestArray_StringN(t *testing.T) { + for _, tc := range arrayStringTestCases { for n := -1; n <= len(tc.want)+1; n++ { - t.Run(fmt.Sprintf("StringN %s n==%d", tc.description, n), func(t *testing.T) { - got, _ := tc.array.StringN(n) + t.Run(fmt.Sprintf("%s n==%d", tc.description, n), func(t *testing.T) { + got, truncated := tc.array.StringN(n) l := n - if l < 0 { - l = 0 - } - if l > len(tc.want) { + toBeTruncated := true + if l >= len(tc.want) || l < 0 { l = len(tc.want) + toBeTruncated = false } want := tc.want[:l] - assert.Equal(t, want, got, "got %v, want %v", got, want) + assert.Equal(t, want, got, "expected truncated string %s, got %s", want, got) + assert.Equal(t, toBeTruncated, truncated, "expected truncated to be %t, got %t", toBeTruncated, truncated) }) } } diff --git a/x/bsonx/bsoncore/document.go b/x/bsonx/bsoncore/document.go index 753709657b..03e78e5997 100644 --- a/x/bsonx/bsoncore/document.go +++ b/x/bsonx/bsoncore/document.go @@ -261,28 +261,24 @@ func (d Document) DebugString() string { // String outputs an ExtendedJSON version of Document. If the document is not valid, this method // returns an empty string. func (d Document) String() string { - str, _ := d.stringN(0) + str, _ := d.StringN(-1) return str } -// StringN stringifies a document upto N bytes +// StringN stringifies a document. If N is non-negative, it will truncate the string to N bytes. +// Otherwise, it will return the full string representation. The second return value indicates +// whether the string was truncated or not. func (d Document) StringN(n int) (string, bool) { - if n <= 0 { - if l, _, ok := ReadLength(d); !ok || l < 5 { - return "", false - } - return "", true - } - return d.stringN(n) -} - -// stringN stringify a document. If N is larger than 0, it will truncate the string to N bytes. -func (d Document) stringN(n int) (string, bool) { length, rem, ok := ReadLength(d) if !ok || length < 5 { return "", false } - length -= (4 /* length bytes */ + 1 /* final null byte */) + length -= 4 // length bytes + length-- // final null byte + + if n == 0 { + return "", true + } var buf strings.Builder buf.WriteByte('{') @@ -292,19 +288,25 @@ func (d Document) stringN(n int) (string, bool) { var str string first := true for length > 0 && !truncated { - l := 0 + needStrLen := -1 + // Set needStrLen if n is positive, meaning we want to limit the string length. if n > 0 { + // Stop stringifying if we reach the limit, that also ensures needStrLen is + // greater than 0 if we need to limit the length. if buf.Len() >= n { truncated = true break } - l = n - buf.Len() + needStrLen = n - buf.Len() } + + // Append a comma if this is not the first element. if !first { buf.WriteByte(',') - if l > 0 { - l-- - if l == 0 { + // If we are truncating, we need to account for the comma in the length. + if needStrLen > 0 { + needStrLen-- + if needStrLen == 0 { truncated = true break } @@ -313,11 +315,13 @@ func (d Document) stringN(n int) (string, bool) { elem, rem, ok = ReadElement(rem) length -= int32(len(elem)) + // Exit on malformed element. if !ok || length < 0 { return "", false } - str, truncated = elem.stringN(l) + // Delegate to StringN() on the element. + str, truncated = elem.StringN(needStrLen) buf.WriteString(str) first = false @@ -325,6 +329,8 @@ func (d Document) stringN(n int) (string, bool) { if n <= 0 || (buf.Len() < n && !truncated) { buf.WriteByte('}') + } else { + truncated = true } return buf.String(), truncated diff --git a/x/bsonx/bsoncore/document_test.go b/x/bsonx/bsoncore/document_test.go index 02b5796b8c..121ddacf03 100644 --- a/x/bsonx/bsoncore/document_test.go +++ b/x/bsonx/bsoncore/document_test.go @@ -412,67 +412,103 @@ func TestDocument(t *testing.T) { }) } -func TestDocument_Stringer(t *testing.T) { - testCases := []struct { - description string - n int - doc Document - want string - }{ - { - description: "empty document", - doc: BuildDocument(nil), - want: `{}`, - }, - { - description: "document with 1 field", - doc: BuildDocument(nil, - AppendInt32Element(nil, "number", 123), - ), - want: `{"number": {"$numberInt":"123"}}`, - }, - { - description: "nested documents", - doc: BuildDocument(nil, - AppendDocumentElement(nil, "key", BuildDocument(nil, - AppendStringElement(nil, "nestedKey", "abc"), - )), - ), - want: `{"key": {"nestedKey": "abc"}}`, - }, - - { - description: "document with mixed types", - doc: BuildDocument(nil, - AppendStringElement(nil, "key", "abc"), - AppendInt32Element(nil, "number", 123), - AppendBooleanElement(nil, "flag", true), - ), - want: `{"key": "abc","number": {"$numberInt":"123"},"flag": true}`, - }, - } +var documentStringTestCases = []struct { + description string + doc Document + want string +}{ + { + description: "empty document", + doc: BuildDocument(nil), + want: `{}`, + }, + { + description: "document with 1 field", + doc: BuildDocument(nil, + AppendInt32Element(nil, "number", 123), + ), + want: `{"number": {"$numberInt":"123"}}`, + }, + { + description: "nested documents", + doc: BuildDocument(nil, + AppendDocumentElement(nil, "key", BuildDocument(nil, + AppendStringElement(nil, "nestedKey", "abc"), + )), + ), + want: `{"key": {"nestedKey": "abc"}}`, + }, + { + description: "document with mixed types", + doc: BuildDocument(nil, + AppendStringElement(nil, "key", "abc"), + AppendInt32Element(nil, "number", 123), + AppendBooleanElement(nil, "flag", true), + ), + want: `{"key": "abc","number": {"$numberInt":"123"},"flag": true}`, + }, +} - for _, tc := range testCases { - t.Run(fmt.Sprintf("String %s", tc.description), func(t *testing.T) { +func TestDocument_String(t *testing.T) { + for _, tc := range documentStringTestCases { + t.Run(tc.description, func(t *testing.T) { got := tc.doc.String() - assert.Equal(t, tc.want, got) + assert.Equal(t, tc.want, got, "expected string %s, got %s", tc.want, got) }) } +} - for _, tc := range testCases { +func TestDocument_StringN(t *testing.T) { + for _, tc := range documentStringTestCases { for n := -1; n <= len(tc.want)+1; n++ { - t.Run(fmt.Sprintf("StringN %s n==%d", tc.description, n), func(t *testing.T) { - got, _ := tc.doc.StringN(n) + t.Run(fmt.Sprintf("%s n==%d", tc.description, n), func(t *testing.T) { + got, truncated := tc.doc.StringN(n) l := n - if l < 0 { - l = 0 - } - if l > len(tc.want) { + toBeTruncated := true + if l >= len(tc.want) || l < 0 { l = len(tc.want) + toBeTruncated = false } want := tc.want[:l] - assert.Equal(t, want, got, "got %v, want %v", got, want) + assert.Equal(t, want, got, "expected truncated string %s, got %s", want, got) + assert.Equal(t, toBeTruncated, truncated, "expected truncated to be %t, got %t", toBeTruncated, truncated) }) } } } + +func TestDocument_StringN_Multibyte(t *testing.T) { + multiByteString := Document(BuildDocument(nil, + AppendStringElement(nil, "𨉟呐㗂越", "abc"), + )) + for i, tc := range []struct { + n int + want string + }{ + {-1, `{"𨉟呐㗂越": "abc"}`}, + {0, ``}, + {1, `{`}, + {2, `{"`}, + {3, `{"`}, + {4, `{"`}, + {5, `{"`}, + {6, `{"`}, + {7, `{"𨉟`}, + {8, `{"𨉟`}, + {9, `{"𨉟`}, + {10, `{"𨉟呐`}, + {14, `{"𨉟呐㗂`}, + {15, `{"𨉟呐㗂越`}, + {16, `{"𨉟呐㗂越"`}, + {17, `{"𨉟呐㗂越":`}, + {18, `{"𨉟呐㗂越": `}, + {19, `{"𨉟呐㗂越": "`}, + {20, `{"𨉟呐㗂越": "a`}, + } { + t.Run(fmt.Sprintf("case %d", i), func(t *testing.T) { + got, truncated := multiByteString.StringN(tc.n) + assert.Equal(t, tc.want, got, "expected truncated string %s, got %s", tc.want, got) + assert.Equal(t, tc.n != -1, truncated, "expected truncated to be %t, got %t", tc.n != -1, truncated) + }) + } +} diff --git a/x/bsonx/bsoncore/element.go b/x/bsonx/bsoncore/element.go index 08363ff07c..4214e5a7c3 100644 --- a/x/bsonx/bsoncore/element.go +++ b/x/bsonx/bsoncore/element.go @@ -117,23 +117,21 @@ func (e Element) ValueErr() (Value, error) { // String implements the fmt.String interface. The output will be in extended JSON format. func (e Element) String() string { - str, _ := e.stringN(0) + str, _ := e.StringN(-1) return str } -// StringN implements the fmt.String interface for upto N bytes. The output will be in extended JSON format. +// StringN will return values in extended JSON format that will stringify an element upto N bytes. +// If N is non-negative, it will truncate the string to N bytes. Otherwise, it will return the full +// string representation. The second return value indicates whether the string was truncated or not. +// If the element is not valid, this returns an empty string func (e Element) StringN(n int) (string, bool) { - if n <= 0 { - return "", len(e) > 0 - } - return e.stringN(n) -} - -// stringN stringify an element. If N is larger than 0, it will truncate the string to N bytes. -func (e Element) stringN(n int) (string, bool) { if len(e) == 0 { return "", false } + if n == 0 { + return "", true + } if n == 1 { return `"`, true } @@ -149,22 +147,27 @@ func (e Element) stringN(n int) (string, bool) { buf.WriteByte('"') const suffix = `": ` switch { - case n <= 0 || idx <= n-4: + case n < 0 || idx <= n-buf.Len()-len(suffix): buf.Write(key) buf.WriteString(suffix) case idx < n: buf.Write(key) buf.WriteString(suffix[:n-idx-1]) + return buf.String(), true default: buf.WriteString(bsoncoreutil.Truncate(string(key), n-1)) + return buf.String(), true } - l := 0 + needStrLen := -1 + // Set needStrLen if n is positive, meaning we want to limit the string length. if n > 0 { + // Stop stringifying if we reach the limit, that also ensures needStrLen is + // greater than 0 if we need to limit the length. if buf.Len() >= n { return buf.String(), true } - l = n - buf.Len() + needStrLen = n - buf.Len() } val, _, valid := ReadValue(e[idx+2:], t) @@ -175,14 +178,14 @@ func (e Element) stringN(n int) (string, bool) { var str string var truncated bool if _, ok := val.StringValueOK(); ok { - str, truncated = val.stringN(l) + str, truncated = val.StringN(needStrLen) } else if arr, ok := val.ArrayOK(); ok { - str, truncated = arr.stringN(l) + str, truncated = arr.StringN(needStrLen) } else { str = val.String() - if l > 0 && len(str) > l { + if needStrLen > 0 && len(str) > needStrLen { truncated = true - str = bsoncoreutil.Truncate(str, l) + str = bsoncoreutil.Truncate(str, needStrLen) } } diff --git a/x/bsonx/bsoncore/value.go b/x/bsonx/bsoncore/value.go index 968f57e126..fec33029e0 100644 --- a/x/bsonx/bsoncore/value.go +++ b/x/bsonx/bsoncore/value.go @@ -218,22 +218,15 @@ func idHex(id [12]byte) string { // String implements the fmt.String interface. This method will return values in extended JSON // format. If the value is not valid, this returns an empty string func (v Value) String() string { - str, _ := v.stringN(0) + str, _ := v.StringN(-1) return str } -// StringN implements the fmt.String interface. This method will return values in extended JSON -// format that will stringify a value upto N bytes. If the value is not valid, this returns an empty string +// StringN will return values in extended JSON format that will stringify a value upto N bytes. +// If N is non-negative, it will truncate the string to N bytes. Otherwise, it will return the full +// string representation. The second return value indicates whether the string was truncated or not. +// If the value is not valid, this returns an empty string func (v Value) StringN(n int) (string, bool) { - str, truncated := v.stringN(n) - if n <= 0 { - return "", len(str) > 0 - } - return str, truncated -} - -// stringN stringify a value. If N is larger than 0, it will truncate the string to N bytes. -func (v Value) stringN(n int) (string, bool) { var str string switch v.Type { case TypeString: @@ -247,13 +240,13 @@ func (v Value) stringN(n int) (string, bool) { if !ok { return "", false } - return doc.stringN(n) + return doc.StringN(n) case TypeArray: arr, ok := v.ArrayOK() if !ok { return "", false } - return arr.stringN(n) + return arr.StringN(n) case TypeDouble: f64, ok := v.DoubleOK() if !ok { @@ -352,7 +345,7 @@ func (v Value) stringN(n int) (string, bool) { default: str = "" } - if n > 0 && len(str) > n { + if n >= 0 && len(str) > n { return bsoncoreutil.Truncate(str, n), true } return str, false diff --git a/x/bsonx/bsoncore/value_test.go b/x/bsonx/bsoncore/value_test.go index cf59d9800d..82fdab35ae 100644 --- a/x/bsonx/bsoncore/value_test.go +++ b/x/bsonx/bsoncore/value_test.go @@ -677,262 +677,266 @@ func TestValue(t *testing.T) { } } -func TestValue_Stringer(t *testing.T) { - testObjectID := [12]byte{0x60, 0xd4, 0xc2, 0x1f, 0x4e, 0x60, 0x4a, 0x0c, 0x8b, 0x2e, 0x9c, 0x3f} - - testCases := []struct { - description string - val Value - want string - }{ - { - description: "string value", - val: Value{ - Type: TypeString, Data: AppendString(nil, "abcdefgh")}, - want: `"abcdefgh"`, - }, - - { - description: "value with special characters", - val: Value{ - Type: TypeString, - Data: AppendString(nil, "!@#$%^&*()")}, - want: `"!@#$%^\u0026*()"`, - }, - - { - description: "TypeEmbeddedDocument", - val: Value{ - Type: TypeEmbeddedDocument, - Data: BuildDocument(nil, - AppendInt32Element(nil, "number", 123), - ), - }, - want: `{"number": {"$numberInt":"123"}}`, - }, - - { - description: "TypeArray", - val: Value{ - Type: TypeArray, - Data: BuildArray(nil, - Value{ - Type: TypeString, - Data: AppendString(nil, "abc"), - }, - Value{ - Type: TypeInt32, - Data: AppendInt32(nil, 123), - }, - Value{ - Type: TypeBoolean, - Data: AppendBoolean(nil, true), - }, - )}, - want: `["abc",{"$numberInt":"123"},true]`, - }, - - { - description: "TypeDouble", - val: Value{ - Type: TypeDouble, - Data: AppendDouble(nil, 123.456), - }, - want: `{"$numberDouble":"123.456"}`, - }, - - { - description: "TypeBinary", - val: Value{ - Type: TypeBinary, - Data: AppendBinary(nil, 0x00, []byte{0x01, 0x02, 0x03})}, - want: `{"$binary":{"base64":"AQID","subType":"00"}}`, - }, - - { - description: "TypeUndefined", - val: Value{ - Type: TypeUndefined, - }, - want: `{"$undefined":true}`, - }, - - { - description: "TypeObjectID", - val: Value{ - Type: TypeObjectID, - Data: AppendObjectID(nil, testObjectID), - }, - want: `{"$oid":"60d4c21f4e604a0c8b2e9c3f"}`, - }, - - { - description: "TypeBoolean", - val: Value{ - Type: TypeBoolean, - Data: AppendBoolean(nil, true), - }, - want: `true`, - }, - - { - description: "TypeDateTime", - val: Value{ - Type: TypeDateTime, - Data: AppendDateTime(nil, 1234567890), - }, - want: `{"$date":{"$numberLong":"1234567890"}}`, - }, - - { - description: "TypeNull", - val: Value{ - Type: TypeNull, - }, - want: `null`, - }, - - { - description: "TypeRegex", - val: Value{ - Type: TypeRegex, - Data: AppendRegex(nil, "pattern", "i"), - }, - want: `{"$regularExpression":{"pattern":"pattern","options":"i"}}`, - }, - - { - description: "TypeDBPointer", - val: Value{ - Type: TypeDBPointer, - Data: AppendDBPointer(nil, "namespace", testObjectID), - }, - want: `{"$dbPointer":{"$ref":"namespace","$id":{"$oid":"60d4c21f4e604a0c8b2e9c3f"}}}`, - }, - - { - description: "TypeJavaScript", - val: Value{ - Type: TypeJavaScript, - Data: AppendJavaScript(nil, "code"), - }, - want: `{"$code":"code"}`, - }, - - { - description: "TypeSymbol", - val: Value{ - Type: TypeSymbol, - Data: AppendSymbol(nil, "symbol"), - }, - want: `{"$symbol":"symbol"}`, - }, - - { - description: "TypeCodeWithScope", - val: Value{ - Type: TypeCodeWithScope, - Data: AppendCodeWithScope(nil, "code", - BuildDocument(nil, AppendStringElement(nil, "key", "value")), - ), - }, - want: `{"$code":code,"$scope":{"key": "value"}}`, - }, - - { - description: "TypeInt32", - val: Value{ - Type: TypeInt32, - Data: AppendInt32(nil, 123), - }, - want: `{"$numberInt":"123"}`, - }, - - { - description: "TypeTimestamp", - val: Value{ - Type: TypeTimestamp, - Data: AppendTimestamp(nil, 123, 456), - }, - want: `{"$timestamp":{"t":123,"i":456}}`, - }, - - { - description: "TypeInt64", - val: Value{ - Type: TypeInt64, - Data: AppendInt64(nil, 1234567890), - }, - want: `{"$numberLong":"1234567890"}`, - }, - - { - description: "TypeDecimal128", - val: Value{ - Type: TypeDecimal128, - Data: AppendDecimal128(nil, 0x3040000000000000, 0x0000000000000000), - }, - want: `{"$numberDecimal":"0"}`, - }, - - { - description: "TypeMinKey", - val: Value{ - Type: TypeMinKey, - }, - want: `{"$minKey":1}`, - }, - - { - description: "TypeMaxKey", - val: Value{ - Type: TypeMaxKey, - }, - want: `{"$maxKey":1}`, - }, - } +var valueStringTestCases = []struct { + description string + val Value + want string +}{ + { + description: "string value", + val: Value{ + Type: TypeString, Data: AppendString(nil, "abcdefgh")}, + want: `"abcdefgh"`, + }, + + { + description: "value with special characters", + val: Value{ + Type: TypeString, + Data: AppendString(nil, "!@#$%^&*()")}, + want: `"!@#$%^\u0026*()"`, + }, + + { + description: "TypeEmbeddedDocument", + val: Value{ + Type: TypeEmbeddedDocument, + Data: BuildDocument(nil, + AppendInt32Element(nil, "number", 123), + ), + }, + want: `{"number": {"$numberInt":"123"}}`, + }, + + { + description: "TypeArray", + val: Value{ + Type: TypeArray, + Data: BuildArray(nil, + Value{ + Type: TypeString, + Data: AppendString(nil, "abc"), + }, + Value{ + Type: TypeInt32, + Data: AppendInt32(nil, 123), + }, + Value{ + Type: TypeBoolean, + Data: AppendBoolean(nil, true), + }, + )}, + want: `["abc",{"$numberInt":"123"},true]`, + }, + + { + description: "TypeDouble", + val: Value{ + Type: TypeDouble, + Data: AppendDouble(nil, 123.456), + }, + want: `{"$numberDouble":"123.456"}`, + }, + + { + description: "TypeBinary", + val: Value{ + Type: TypeBinary, + Data: AppendBinary(nil, 0x00, []byte{0x01, 0x02, 0x03})}, + want: `{"$binary":{"base64":"AQID","subType":"00"}}`, + }, + + { + description: "TypeUndefined", + val: Value{ + Type: TypeUndefined, + }, + want: `{"$undefined":true}`, + }, + + { + description: "TypeObjectID", + val: Value{ + Type: TypeObjectID, + Data: AppendObjectID(nil, [12]byte{0x60, 0xd4, 0xc2, 0x1f, 0x4e, 0x60, 0x4a, 0x0c, 0x8b, 0x2e, 0x9c, 0x3f}), + }, + want: `{"$oid":"60d4c21f4e604a0c8b2e9c3f"}`, + }, + + { + description: "TypeBoolean", + val: Value{ + Type: TypeBoolean, + Data: AppendBoolean(nil, true), + }, + want: `true`, + }, + + { + description: "TypeDateTime", + val: Value{ + Type: TypeDateTime, + Data: AppendDateTime(nil, 1234567890), + }, + want: `{"$date":{"$numberLong":"1234567890"}}`, + }, + + { + description: "TypeNull", + val: Value{ + Type: TypeNull, + }, + want: `null`, + }, + + { + description: "TypeRegex", + val: Value{ + Type: TypeRegex, + Data: AppendRegex(nil, "pattern", "i"), + }, + want: `{"$regularExpression":{"pattern":"pattern","options":"i"}}`, + }, + + { + description: "TypeDBPointer", + val: Value{ + Type: TypeDBPointer, + Data: AppendDBPointer(nil, "namespace", [12]byte{0x60, 0xd4, 0xc2, 0x1f, 0x4e, 0x60, 0x4a, 0x0c, 0x8b, 0x2e, 0x9c, 0x3f}), + }, + want: `{"$dbPointer":{"$ref":"namespace","$id":{"$oid":"60d4c21f4e604a0c8b2e9c3f"}}}`, + }, + + { + description: "TypeJavaScript", + val: Value{ + Type: TypeJavaScript, + Data: AppendJavaScript(nil, "code"), + }, + want: `{"$code":"code"}`, + }, + + { + description: "TypeSymbol", + val: Value{ + Type: TypeSymbol, + Data: AppendSymbol(nil, "symbol"), + }, + want: `{"$symbol":"symbol"}`, + }, + + { + description: "TypeCodeWithScope", + val: Value{ + Type: TypeCodeWithScope, + Data: AppendCodeWithScope(nil, "code", + BuildDocument(nil, AppendStringElement(nil, "key", "value")), + ), + }, + want: `{"$code":code,"$scope":{"key": "value"}}`, + }, + + { + description: "TypeInt32", + val: Value{ + Type: TypeInt32, + Data: AppendInt32(nil, 123), + }, + want: `{"$numberInt":"123"}`, + }, + + { + description: "TypeTimestamp", + val: Value{ + Type: TypeTimestamp, + Data: AppendTimestamp(nil, 123, 456), + }, + want: `{"$timestamp":{"t":123,"i":456}}`, + }, + + { + description: "TypeInt64", + val: Value{ + Type: TypeInt64, + Data: AppendInt64(nil, 1234567890), + }, + want: `{"$numberLong":"1234567890"}`, + }, + + { + description: "TypeDecimal128", + val: Value{ + Type: TypeDecimal128, + Data: AppendDecimal128(nil, 0x3040000000000000, 0x0000000000000000), + }, + want: `{"$numberDecimal":"0"}`, + }, + + { + description: "TypeMinKey", + val: Value{ + Type: TypeMinKey, + }, + want: `{"$minKey":1}`, + }, + + { + description: "TypeMaxKey", + val: Value{ + Type: TypeMaxKey, + }, + want: `{"$maxKey":1}`, + }, +} - for _, tc := range testCases { - t.Run(fmt.Sprintf("String %s", tc.description), func(t *testing.T) { +func TestValue_String(t *testing.T) { + for _, tc := range valueStringTestCases { + t.Run(tc.description, func(t *testing.T) { got := tc.val.String() - assert.Equal(t, tc.want, got) + assert.Equal(t, tc.want, got, "expected string %s, got %s", tc.want, got) }) } +} - for _, tc := range testCases { +func TestValue_StringN(t *testing.T) { + for _, tc := range valueStringTestCases { for n := -1; n <= len(tc.want)+1; n++ { - t.Run(fmt.Sprintf("StringN %s n==%d", tc.description, n), func(t *testing.T) { - got, _ := tc.val.StringN(n) + t.Run(fmt.Sprintf("%s n==%d", tc.description, n), func(t *testing.T) { + got, truncated := tc.val.StringN(n) l := n - if l < 0 { - l = 0 - } - if l > len(tc.want) { + toBeTruncated := true + if l >= len(tc.want) || l < 0 { l = len(tc.want) + toBeTruncated = false } want := tc.want[:l] - assert.Equal(t, want, got, "got %v, want %v", got, want) + assert.Equal(t, want, got, "expected string %s, got %s", tc.want, got) + assert.Equal(t, toBeTruncated, truncated, "expected truncated to be %t, got %t", toBeTruncated, truncated) }) } } +} +func TestArray_StringN_Multibyte(t *testing.T) { multiByteString := Value{ Type: TypeString, Data: AppendString(nil, "𨉟呐㗂越"), } - for _, tc := range []struct { - n int - want string + for i, tc := range []struct { + n int + want string + truncated bool }{ - {6, `"𨉟`}, - {8, `"𨉟`}, - {10, `"𨉟呐`}, - {15, `"𨉟呐㗂越"`}, - {21, `"𨉟呐㗂越"`}, + {6, `"𨉟`, true}, + {8, `"𨉟`, true}, + {10, `"𨉟呐`, true}, + {15, `"𨉟呐㗂越"`, false}, + {21, `"𨉟呐㗂越"`, false}, } { - t.Run(fmt.Sprintf("StringN multi-byte string n==%d", tc.n), func(t *testing.T) { - got, _ := multiByteString.StringN(tc.n) - assert.Equal(t, tc.want, got) + t.Run(fmt.Sprintf("case %d", i), func(t *testing.T) { + got, truncated := multiByteString.StringN(tc.n) + assert.Equal(t, tc.want, got, "expected string %s, got %s", tc.want, got) + assert.Equal(t, tc.truncated, truncated, "expected truncated to be %t, got %t", tc.truncated, truncated) }) } }