Skip to content

Commit c834c91

Browse files
authored
SWIFT-926 Enable Extended JSON Corpus Tests (#33)
Also: SWIFT-939 Update spec tests to include new timestamp test
1 parent d070d9c commit c834c91

File tree

8 files changed

+281
-102
lines changed

8 files changed

+281
-102
lines changed

Sources/BSON/BSONBinary.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,7 @@ extension BSONBinary: BSONValue {
208208
[
209209
"$binary": [
210210
"base64": .string(Data(self.data.readableBytesView).base64EncodedString()),
211-
"subType": .string(String(self.subtype.rawValue, radix: 16))
211+
"subType": .string(String(format: "%02x", self.subtype.rawValue))
212212
]
213213
]
214214
}

Sources/BSON/Date+BSONValue.swift

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,12 @@ extension Date: BSONValue {
3333
self = Date(msSinceEpoch: int)
3434
case let .string(s):
3535
// relaxed extended JSON
36-
guard let date = ExtendedJSONDecoder.extJSONDateFormatter.date(from: s) else {
36+
// If fractional seconds are omitted in the input (no decimal point "."),
37+
// formatter should only account for seconds, otherwise formatter should take milliseconds into account
38+
let formatter = s.contains(".")
39+
? ExtendedJSONDecoder.extJSONDateFormatterMilliseconds
40+
: ExtendedJSONDecoder.extJSONDateFormatterSeconds
41+
guard let date = formatter.date(from: s) else {
3742
throw DecodingError._extendedJSONError(
3843
keyPath: keyPath,
3944
debugDescription: "Expected \(s) to be an ISO-8601 Internet Date/Time Format" +
@@ -42,7 +47,13 @@ extension Date: BSONValue {
4247
}
4348
self = date
4449
default:
45-
return nil
50+
throw DecodingError._extendedJSONError(
51+
keyPath: keyPath,
52+
debugDescription: "Expected \(value) to be canonical extended JSON representing a " +
53+
"64-bit signed integer giving millisecs relative to the epoch, as a string OR " +
54+
"relaxed extended JSON representing a ISO-8601 Internet Date/Time Format with " +
55+
"maximum time precision of milliseconds as a string."
56+
)
4657
}
4758
}
4859

@@ -52,7 +63,12 @@ extension Date: BSONValue {
5263
// relaxed extended json depending on if the date is between 1970 and 9999
5364
// 1970 is 0 milliseconds since epoch, and 10,000 is 253,402,300,800,000.
5465
if self.msSinceEpoch >= 0 && self.msSinceEpoch < 253_402_300_800_000 {
55-
let date = ExtendedJSONDecoder.extJSONDateFormatter.string(from: self)
66+
// Fractional seconds SHOULD have exactly 3 decimal places if the fractional part is non-zero.
67+
// Otherwise, fractional seconds SHOULD be omitted if zero.
68+
let formatter = self.timeIntervalSince1970.truncatingRemainder(dividingBy: 1) == 0
69+
? ExtendedJSONDecoder.extJSONDateFormatterSeconds
70+
: ExtendedJSONDecoder.extJSONDateFormatterMilliseconds
71+
let date = formatter.string(from: self)
5672
return ["$date": .string(date)]
5773
} else {
5874
return self.toCanonicalExtendedJSON()

Sources/BSON/Double+BSONValue.swift

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,14 +41,31 @@ extension Double: BSONValue {
4141
}
4242
}
4343

44+
/// Helper function to make sure ExtendedJSON formatting matches Corpus Tests
45+
internal func formatForExtendedJSON() -> String {
46+
if self.isNaN {
47+
return "NaN"
48+
} else if self == Double.infinity {
49+
return "Infinity"
50+
} else if self == -Double.infinity {
51+
return "-Infinity"
52+
} else {
53+
return String(describing: self).uppercased()
54+
}
55+
}
56+
4457
/// Converts this `Double` to a corresponding `JSON` in relaxed extendedJSON format.
45-
func toRelaxedExtendedJSON() -> JSON {
46-
.number(self)
58+
internal func toRelaxedExtendedJSON() -> JSON {
59+
if self.isInfinite || self.isNaN {
60+
return self.toCanonicalExtendedJSON()
61+
} else {
62+
return .number(self)
63+
}
4764
}
4865

4966
/// Converts this `Double` to a corresponding `JSON` in canonical extendedJSON format.
50-
func toCanonicalExtendedJSON() -> JSON {
51-
["$numberDouble": .string(String(describing: self))]
67+
internal func toCanonicalExtendedJSON() -> JSON {
68+
["$numberDouble": .string(self.formatForExtendedJSON())]
5269
}
5370

5471
internal static var bsonType: BSONType { .double }

Sources/BSON/ExtendedJSONDecoder.swift

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
import Foundation
22
/// `ExtendedJSONDecoder` facilitates the decoding of ExtendedJSON into `Decodable` values.
33
public class ExtendedJSONDecoder {
4-
internal static var extJSONDateFormatter: ISO8601DateFormatter = {
4+
internal static var extJSONDateFormatterSeconds: ISO8601DateFormatter = {
5+
let formatter = ISO8601DateFormatter()
6+
formatter.formatOptions = [.withInternetDateTime]
7+
return formatter
8+
}()
9+
10+
internal static var extJSONDateFormatterMilliseconds: ISO8601DateFormatter = {
511
let formatter = ISO8601DateFormatter()
612
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
713
return formatter

Tests/BSONTests/BSONCorpusTests.swift

Lines changed: 109 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -98,19 +98,53 @@ final class BSONCorpusTests: BSONTestCase {
9898

9999
// swiftlint:disable:next cyclomatic_complexity
100100
func testBSONCorpus() throws {
101+
let SKIPPED_CORPUS_TESTS = [
102+
"Decimal128":
103+
[
104+
// TODO: SWIFT-962
105+
"Exact rounding",
106+
"[dqbsr531] negatives (Rounded)",
107+
"[dqbsr431] check rounding modes heeded (Rounded)",
108+
"OK2",
109+
// TODO: SWIFT-965
110+
"[decq438] clamped zeros... (Clamped)",
111+
"[decq418] clamped zeros... (Clamped)"
112+
],
113+
"Array":
114+
[
115+
// TODO: SWIFT-963
116+
"Multi Element Array with duplicate indexes",
117+
"Single Element Array with index set incorrectly to empty string",
118+
"Single Element Array with index set incorrectly to ab"
119+
],
120+
"Top-level document validity": [
121+
"Bad DBRef (ref is number, not string)",
122+
"Bad DBRef (db is number, not string)"
123+
]
124+
]
125+
126+
let shouldSkip = { testFileDesc, testDesc in
127+
SKIPPED_CORPUS_TESTS[testFileDesc]?.contains { $0 == testDesc } == true
128+
}
129+
130+
let decoder = ExtendedJSONDecoder()
131+
101132
for (_, testFile) in try retrieveSpecTestFiles(specName: "bson-corpus", asType: BSONCorpusTestFile.self) {
102133
if let validityTests = testFile.valid {
103134
for test in validityTests {
135+
guard !shouldSkip(testFile.description, test.description) else {
136+
continue
137+
}
104138
guard let cBData = Data(hexString: test.canonicalBSON) else {
105139
XCTFail("Unable to interpret canonical_bson as Data")
106140
return
107141
}
108-
// guard let cEJData = test.canonicalExtJSON.data(using: .utf8) else {
109-
// XCTFail("Unable to interpret canonical_extjson as Data")
110-
// return
111-
// }
142+
guard let cEJData = test.canonicalExtJSON.data(using: .utf8) else {
143+
XCTFail("Unable to interpret canonical_extjson as Data")
144+
return
145+
}
112146

113-
// let lossy = test.lossy ?? false
147+
let lossy = test.lossy ?? false
114148

115149
// for cB input:
116150
// native_to_bson( bson_to_native(cB) ) = cB
@@ -129,97 +163,93 @@ final class BSONCorpusTests: BSONTestCase {
129163
let docFromNative = BSONDocument(fromArray: nativeFromDoc)
130164
expect(docFromNative.toByteString()).to(equal(cBData.toByteString()))
131165

132-
if testFile.description == "Decimal128" {
133-
// TODO: This should be tested by EXTJSON
134-
struct Decimal128CanonicalExtJSON: Codable {
135-
struct Value: Codable {
136-
enum CodingKeys: String, CodingKey {
137-
case numberDecimal = "$numberDecimal"
138-
}
139-
140-
var numberDecimal: String
141-
}
166+
// native_to_canonical_extended_json( bson_to_native(cB) ) = cEJ
167+
let canonicalEncoder = ExtendedJSONEncoder()
168+
canonicalEncoder.mode = .canonical
169+
expect(try canonicalEncoder.encode(docFromCB))
170+
.to(cleanEqual(test.canonicalExtJSON), description: test.description)
142171

143-
var d: Value
144-
}
145-
let extjson = test.canonicalExtJSON.data(using: .ascii)!
146-
let jsonResult = try JSONDecoder().decode(Decimal128CanonicalExtJSON.self, from: extjson)
147-
let decimal128CorpusString = jsonResult.d.numberDecimal
172+
// native_to_relaxed_extended_json( bson_to_native(cB) ) = rEJ (if rEJ exists)
173+
let relaxedEncoder = ExtendedJSONEncoder() // default mode is .relaxed
174+
if let rEJ = test.relaxedExtJSON {
175+
expect(try relaxedEncoder.encode(docFromCB))
176+
.to(cleanEqual(rEJ), description: test.description)
177+
}
148178

149-
let decimal128FromString = try BSONDecimal128(decimal128CorpusString)
150-
let decimal128FromBinary = docFromCB.d!.decimal128Value!
179+
// for cEJ input:
180+
// native_to_canonical_extended_json( json_to_native(cEJ) ) = cEJ
181+
expect(try canonicalEncoder.encode(try decoder.decode(BSONDocument.self, from: cEJData)))
182+
.to(cleanEqual(test.canonicalExtJSON), description: test.description)
151183

152-
expect(decimal128FromString.description).to(equal(decimal128CorpusString))
153-
expect(decimal128FromBinary.description).to(equal(decimal128CorpusString))
184+
// native_to_bson( json_to_native(cEJ) ) = cB (unless lossy)
185+
if !lossy {
186+
expect(try decoder.decode(BSONDocument.self, from: cEJData))
187+
.to(sortedEqual(docFromCB), description: test.description)
154188
}
155189

156-
// native_to_canonical_extended_json( bson_to_native(cB) ) = cEJ
157-
// expect(docFromCB.canonicalExtendedJSON).to(cleanEqual(test.canonicalExtJSON))
190+
// for dB input (if it exists): (change to language native part)
191+
if let dB = test.degenerateBSON {
192+
guard let dBData = Data(hexString: dB) else {
193+
XCTFail("Unable to interpret degenerate_bson as Data")
194+
return
195+
}
158196

159-
// native_to_relaxed_extended_json( bson_to_native(cB) ) = rEJ (if rEJ exists)
160-
// if let rEJ = test.relaxedExtJSON {
161-
// expect(try Document(fromBSON: cBData).extendedJSON).to(cleanEqual(rEJ))
162-
// }
197+
let docFromDB = try BSONDocument(fromBSON: dBData)
163198

164-
// for cEJ input:
165-
// native_to_canonical_extended_json( json_to_native(cEJ) ) = cEJ
166-
// expect(try Document(fromJSON: cEJData).canonicalExtendedJSON)
167-
// .to(cleanEqual(test.canonicalExtJSON))
168-
169-
// // native_to_bson( json_to_native(cEJ) ) = cB (unless lossy)
170-
// if !lossy {
171-
// expect(try Document(fromJSON: cEJData).rawBSON).to(equal(cBData))
172-
// }
173-
174-
// for dB input (if it exists):
175-
// if let dB = test.degenerateBSON {
176-
// guard let dBData = Data(hexString: dB) else {
177-
// XCTFail("Unable to interpret degenerate_bson as Data")
178-
// return
179-
// }
180-
181-
// // bson_to_canonical_extended_json(dB) = cEJ
182-
// expect(try Document(fromBSON: dBData).canonicalExtendedJSON)
183-
// .to(cleanEqual(test.canonicalExtJSON))
184-
185-
// // bson_to_relaxed_extended_json(dB) = rEJ (if rEJ exists)
186-
// if let rEJ = test.relaxedExtJSON {
187-
// expect(try Document(fromBSON: dBData).extendedJSON).to(cleanEqual(rEJ))
188-
// }
189-
// }
199+
// SKIPPING: native_to_bson( bson_to_native(dB) ) = cB
200+
// We only validate the BSON bytes, we do not clean them up, so can't do this assertion
201+
// Degenerate BSON round trip tests will be added in SWIFT-964
190202

191-
// for dEJ input (if it exists):
192-
// if let dEJ = test.degenerateExtJSON {
193-
// // native_to_canonical_extended_json( json_to_native(dEJ) ) = cEJ
194-
// expect(try Document(fromJSON: dEJ).canonicalExtendedJSON)
195-
// .to(cleanEqual(test.canonicalExtJSON))
203+
// native_to_canonical_extended_json( bson_to_native(dB) ) = cEJ
204+
// (Not in spec yet, might be added in DRIVERS-1355)
205+
expect(try canonicalEncoder.encode(docFromDB))
206+
.to(cleanEqual(test.canonicalExtJSON))
196207

197-
// // native_to_bson( json_to_native(dEJ) ) = cB (unless lossy)
198-
// if !lossy {
199-
// expect(try Document(fromJSON: dEJ).rawBSON).to(equal(cBData))
200-
// }
201-
// }
208+
// native_to_relaxed_extended_json( bson_to_native(dB) ) = rEJ (if rEJ exists)
209+
// (Not in spec yet, might be added in DRIVERS-1355)
210+
if let rEJ = test.relaxedExtJSON {
211+
expect(try relaxedEncoder.encode(docFromDB))
212+
.to(cleanEqual(rEJ), description: test.description)
213+
}
214+
}
215+
216+
// for dEJ input (if it exists):
217+
if let dEJ = test.degenerateExtJSON, let dEJData = dEJ.data(using: .utf8) {
218+
// native_to_canonical_extended_json( json_to_native(dEJ) ) = cEJ
219+
expect(try canonicalEncoder.encode(try decoder.decode(BSONDocument.self, from: dEJData)))
220+
.to(cleanEqual(test.canonicalExtJSON), description: test.description)
221+
// native_to_bson( json_to_native(dEJ) ) = cB (unless lossy)
222+
if !lossy {
223+
try expect(try decoder.decode(BSONDocument.self, from: dEJData))
224+
.to(sortedEqual(BSONDocument(fromBSON: cBData)), description: test.description)
225+
}
226+
}
202227

203228
// for rEJ input (if it exists):
204-
// if let rEJ = test.relaxedExtJSON {
205-
// // native_to_relaxed_extended_json( json_to_native(rEJ) ) = rEJ
206-
// expect(try Document(fromJSON: rEJ).extendedJSON).to(cleanEqual(rEJ))
207-
// }
229+
if let rEJ = test.relaxedExtJSON, let rEJData = rEJ.data(using: .utf8) {
230+
// native_to_relaxed_extended_json( json_to_native(rEJ) ) = rEJ
231+
expect(try relaxedEncoder.encode(try decoder.decode(BSONDocument.self, from: rEJData)))
232+
.to(cleanEqual(rEJ), description: test.description)
233+
}
208234
}
209235
}
210236

211237
if let parseErrorTests = testFile.parseErrors {
212-
continue // TODO: EXT JSON support required
213238
for test in parseErrorTests {
239+
guard !shouldSkip(testFile.description, test.description) else {
240+
continue
241+
}
214242
let description = "\(testFile.description)-\(test.description)"
215-
216243
switch BSONType(rawValue: UInt8(testFile.bsonType.dropFirst(2), radix: 16)!)! {
217244
case .invalid: // "top level document" uses 0x00 for the bson type
218-
_ = ()
219-
// expect(try BSONDocument(fromJSON: test.string)).to(throwError(), description: description)
245+
guard let testData = test.string.data(using: .utf8) else {
246+
XCTFail("Unable to interpret canonical_bson as Data")
247+
return
248+
}
249+
expect(try decoder.decode(BSONDocument.self, from: testData))
250+
.to(throwError(errorType: DecodingError.self), description: description)
220251
case .decimal128:
221-
_ = ()
222-
// expect(BSONDecimal128(test.string)).to(beNil(), description: description)
252+
continue // TODO: SWIFT-968
223253
default:
224254
throw TestError(
225255
message: "\(description): parse error tests not implemented"
@@ -231,6 +261,9 @@ final class BSONCorpusTests: BSONTestCase {
231261

232262
if let decodeErrors = testFile.decodeErrors {
233263
for test in decodeErrors {
264+
guard !shouldSkip(testFile.description, test.description) else {
265+
continue
266+
}
234267
let description = "\(testFile.description)-\(test.description)"
235268
guard let data = Data(hexString: test.bson) else {
236269
XCTFail("\(description): Unable to interpret bson as Data")

0 commit comments

Comments
 (0)