Skip to content

Commit 77a9a50

Browse files
authored
SWIFT-635 Implement parseError and decodeError BSON corpus tests (#344)
1 parent 11747d4 commit 77a9a50

File tree

5 files changed

+270
-128
lines changed

5 files changed

+270
-128
lines changed

Tests/LinuxMain.swift

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@ extension AuthTests {
1212
]
1313
}
1414

15+
extension BSONCorpusTests {
16+
static var allTests = [
17+
("testBSONCorpus", testBSONCorpus),
18+
]
19+
}
20+
1521
extension BSONValueTests {
1622
static var allTests = [
1723
("testInvalidDecimal128", testInvalidDecimal128),
@@ -110,7 +116,6 @@ extension DocumentTests {
110116
("testRawBSON", testRawBSON),
111117
("testValueBehavior", testValueBehavior),
112118
("testIntEncodesAsInt32OrInt64", testIntEncodesAsInt32OrInt64),
113-
("testBSONCorpus", testBSONCorpus),
114119
("testMerge", testMerge),
115120
("testNilInNestedArray", testNilInNestedArray),
116121
("testOverwritable", testOverwritable),
@@ -296,6 +301,7 @@ extension SDAMTests {
296301

297302
XCTMain([
298303
testCase(AuthTests.allTests),
304+
testCase(BSONCorpusTests.allTests),
299305
testCase(BSONValueTests.allTests),
300306
testCase(ChangeStreamSpecTests.allTests),
301307
testCase(ChangeStreamTests.allTests),
Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
1+
import Foundation
2+
import MongoSwift
3+
import Nimble
4+
import XCTest
5+
6+
final class BSONCorpusTests: MongoSwiftTestCase {
7+
/// Test case that includes 'canonical' forms of BSON and Extended JSON that are deemed equivalent and may provide
8+
/// additional cases or metadata for additional assertions.
9+
struct BSONCorpusValidityTest: Decodable {
10+
enum CodingKeys: String, CodingKey {
11+
case description, canonicalBSON = "canonical_bson", canonicalExtJSON = "canonical_extjson",
12+
relaxedExtJSON = "relaxed_extjson", degenerateBSON = "degenerate_bson",
13+
degenerateExtJSON = "degenerate_extjson", convertedBSON = "converted_bson",
14+
convertedExtJSON = "converted_extjson", lossy
15+
}
16+
17+
/// Human-readable test case label.
18+
let description: String
19+
20+
/// an (uppercase) big-endian hex representation of a BSON byte string.
21+
let canonicalBSON: String
22+
23+
/// a string containing a Canonical Extended JSON document. Because this is itself embedded as a string inside a
24+
/// JSON document, characters like quote and backslash are escaped.
25+
let canonicalExtJSON: String
26+
27+
/// A string containing a Relaxed Extended JSON document.
28+
/// Because this is itself embedded as a string inside a JSON document, characters like quote and backslash
29+
/// are escaped.
30+
let relaxedExtJSON: String?
31+
32+
/// An (uppercase) big-endian hex representation of a BSON byte string that is technically parseable, but
33+
/// not in compliance with the BSON spec.
34+
let degenerateBSON: String?
35+
36+
/// A string containing an invalid form of Canonical Extended JSON that is still parseable according to
37+
/// type-specific rules. (For example, "1e100" instead of "1E+100".)
38+
let degenerateExtJSON: String?
39+
40+
/// An (uppercase) big-endian hex representation of a BSON byte string. It may be present for deprecated types.
41+
/// It represents a possible conversion of the deprecated type to a non-deprecated type, e.g. symbol to string.
42+
let convertedBSON: String?
43+
44+
/// A string containing a Canonical Extended JSON document.
45+
/// Because this is itself embedded as a string inside a JSON document, characters like quote and backslash
46+
/// are escaped.
47+
/// It may be present for deprecated types and is the Canonical Extended JSON representation of `convertedBson`.
48+
let convertedExtJSON: String?
49+
50+
/// A bool that is present (and true) iff `canonicalBson` can't be represented exactly with extended
51+
/// JSON (e.g. NaN with a payload).
52+
let lossy: Bool?
53+
}
54+
55+
/// A test case that provides an invalid BSON document or field that should result in an error.
56+
struct BSONCorpusDecodeErrorTest: Decodable {
57+
/// Human-readable test case label.
58+
let description: String
59+
60+
/// An (uppercase) big-endian hex representation of an invalid BSON string that should fail to decode correctly.
61+
let bson: String
62+
}
63+
64+
/// Test case that is type-specific and represents some input that can not be encoded to the BSON type under test.
65+
struct BSONCorpusParseErrorTest: Decodable {
66+
/// Human-readable test case label.
67+
let description: String
68+
69+
/// A textual or numeric representation of an input that can't be parsed to a valid value of the given type.
70+
let string: String
71+
}
72+
73+
/// A BSON corpus test file for an individual BSON type.
74+
struct BSONCorpusTestFile: Decodable {
75+
enum CodingKeys: String, CodingKey {
76+
case description, bsonType = "bson_type", valid, parseErrors, decodeErrors, deprecated
77+
}
78+
79+
/// Human-readable rdescription of the file.
80+
let description: String
81+
82+
/// Hex string of the first byte of a BSON element (e.g. "0x01" for type "double").
83+
/// This will be the synthetic value "0x00" for "whole document" tests like top.json.
84+
let bsonType: String
85+
86+
/// An array of validity test cases.
87+
let valid: [BSONCorpusValidityTest]?
88+
89+
/// An array of decode error cases.
90+
let decodeErrors: [BSONCorpusDecodeErrorTest]?
91+
92+
/// An array of type-specific parse error cases.
93+
let parseErrors: [BSONCorpusParseErrorTest]?
94+
95+
/// This field will be present (and true) if the BSON type being tested has been deprecated (e.g. Symbol)
96+
let deprecated: Bool?
97+
}
98+
99+
// swiftlint:disable:next cyclomatic_complexity
100+
func testBSONCorpus() throws {
101+
let SKIPPED_CORPUS_TESTS = [
102+
/* CDRIVER-1879, can't make Code with embedded NIL */
103+
"Javascript Code": ["Embedded nulls"],
104+
"Javascript Code with Scope": ["Unicode and embedded null in code string, empty scope"],
105+
"Top-level document validity": [
106+
// CDRIVER-2223, legacy extended JSON $date syntax uses numbers
107+
"Bad $date (number, not string or hash)",
108+
// TODO: SWIFT-330: unskip these
109+
"Invalid BSON type low range",
110+
"Invalid BSON type high range"
111+
]
112+
]
113+
114+
let shouldSkip = { testFileDesc, testDesc in
115+
SKIPPED_CORPUS_TESTS[testFileDesc]?.contains { $0 == testDesc } == true
116+
}
117+
118+
for (_, testFile) in try retrieveSpecTestFiles(specName: "bson-corpus", asType: BSONCorpusTestFile.self) {
119+
if let validityTests = testFile.valid {
120+
for test in validityTests {
121+
guard !shouldSkip(testFile.description, test.description) else {
122+
continue
123+
}
124+
125+
guard let cBData = Data(hexString: test.canonicalBSON) else {
126+
XCTFail("Unable to interpret canonical_bson as Data")
127+
return
128+
}
129+
130+
guard let cEJData = test.canonicalExtJSON.data(using: .utf8) else {
131+
XCTFail("Unable to interpret canonical_extjson as Data")
132+
return
133+
}
134+
135+
let lossy = test.lossy ?? false
136+
137+
// for cB input:
138+
// native_to_bson( bson_to_native(cB) ) = cB
139+
let docFromCB = try Document(fromBSON: cBData)
140+
expect(docFromCB.rawBSON).to(equal(cBData))
141+
142+
// test round tripping through documents
143+
// We create an array by reading every element out of the document (and therefore out of the
144+
// bson_t). We then create a new document and append each element of the array to it. Once that
145+
// is done, every element in the original document will have gone from bson_t -> Swift data type
146+
// -> bson_t. At the end, the new bson_t should be identical to the original one. If not, our bson_t
147+
// translation layer is lossy and/or buggy.
148+
let nativeFromDoc = docFromCB.toArray()
149+
let docFromNative = Document(fromArray: nativeFromDoc)
150+
expect(docFromNative.rawBSON).to(equal(cBData))
151+
152+
// native_to_canonical_extended_json( bson_to_native(cB) ) = cEJ
153+
expect(docFromCB.canonicalExtendedJSON).to(cleanEqual(test.canonicalExtJSON))
154+
155+
// native_to_relaxed_extended_json( bson_to_native(cB) ) = rEJ (if rEJ exists)
156+
if let rEJ = test.relaxedExtJSON {
157+
expect(try Document(fromBSON: cBData).extendedJSON).to(cleanEqual(rEJ))
158+
}
159+
160+
// for cEJ input:
161+
// native_to_canonical_extended_json( json_to_native(cEJ) ) = cEJ
162+
expect(try Document(fromJSON: cEJData).canonicalExtendedJSON).to(cleanEqual(test.canonicalExtJSON))
163+
164+
// native_to_bson( json_to_native(cEJ) ) = cB (unless lossy)
165+
if !lossy {
166+
expect(try Document(fromJSON: cEJData).rawBSON).to(equal(cBData))
167+
}
168+
169+
// for dB input (if it exists):
170+
if let dB = test.degenerateBSON {
171+
guard let dBData = Data(hexString: dB) else {
172+
XCTFail("Unable to interpret degenerate_bson as Data")
173+
return
174+
}
175+
176+
// bson_to_canonical_extended_json(dB) = cEJ
177+
expect(try Document(fromBSON: dBData).canonicalExtendedJSON)
178+
.to(cleanEqual(test.canonicalExtJSON))
179+
180+
// bson_to_relaxed_extended_json(dB) = rEJ (if rEJ exists)
181+
if let rEJ = test.relaxedExtJSON {
182+
expect(try Document(fromBSON: dBData).extendedJSON).to(cleanEqual(rEJ))
183+
}
184+
}
185+
186+
// for dEJ input (if it exists):
187+
if let dEJ = test.degenerateExtJSON {
188+
// native_to_canonical_extended_json( json_to_native(dEJ) ) = cEJ
189+
expect(try Document(fromJSON: dEJ).canonicalExtendedJSON).to(cleanEqual(test.canonicalExtJSON))
190+
191+
// native_to_bson( json_to_native(dEJ) ) = cB (unless lossy)
192+
if !lossy {
193+
expect(try Document(fromJSON: dEJ).rawBSON).to(equal(cBData))
194+
}
195+
}
196+
197+
// for rEJ input (if it exists):
198+
if let rEJ = test.relaxedExtJSON {
199+
// native_to_relaxed_extended_json( json_to_native(rEJ) ) = rEJ
200+
expect(try Document(fromJSON: rEJ).extendedJSON).to(cleanEqual(rEJ))
201+
}
202+
}
203+
}
204+
205+
if let parseErrorTests = testFile.parseErrors {
206+
for test in parseErrorTests {
207+
guard !shouldSkip(testFile.description, test.description) else {
208+
continue
209+
}
210+
let description = "\(testFile.description)-\(test.description)"
211+
212+
switch BSONType(rawValue: UInt32(testFile.bsonType.dropFirst(2), radix: 16)!)! {
213+
case .invalid: // "top level document" uses 0x00 for the bson type
214+
expect(try Document(fromJSON: test.string))
215+
.to(throwError(), description: description)
216+
case .decimal128:
217+
expect(Decimal128(test.string))
218+
.to(beNil(), description: description)
219+
default:
220+
throw RuntimeError.internalError(
221+
message: "\(description): parse error tests not implemented"
222+
+ "for bson type \(testFile.bsonType)"
223+
)
224+
}
225+
}
226+
}
227+
228+
if let decodeErrors = testFile.decodeErrors {
229+
// TODO: SWIFT-330: unskip the other tests.
230+
guard testFile.description == "Top-level document validity" else {
231+
continue
232+
}
233+
234+
for test in decodeErrors {
235+
guard !shouldSkip(testFile.description, test.description) else {
236+
continue
237+
}
238+
let description = "\(testFile.description)-\(test.description)"
239+
240+
guard let data = Data(hexString: test.bson) else {
241+
XCTFail("\(description): Unable to interpret bson as Data")
242+
return
243+
}
244+
expect(try Document(fromBSON: data)).to(throwError(), description: description)
245+
}
246+
}
247+
}
248+
}
249+
}

0 commit comments

Comments
 (0)