Skip to content

Commit d070d9c

Browse files
authored
Hodgepodge of test fixes (#28)
1 parent 2218e1b commit d070d9c

File tree

9 files changed

+466
-688
lines changed

9 files changed

+466
-688
lines changed

Sources/BSON/BSON.swift

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ public enum BSON {
4444
case regex(BSONRegularExpression)
4545

4646
/// A BSON ObjectID
47-
case objectID(BSONObjectID)
47+
case objectID(BSONObjectID = BSONObjectID())
4848

4949
/// A BSON DBPointer
5050
case dbPointer(BSONDBPointer)
@@ -266,7 +266,7 @@ extension BSON {
266266
}
267267
}
268268

269-
/// toInt* helper functions
269+
/// Helper functions for converting to numbers.
270270
extension BSON {
271271
/// Return this BSON as an `Int` if possible.
272272
/// This will coerce non-integer numeric cases (e.g. `.double`) into an `Int` if such coercion would be lossless.
@@ -312,6 +312,37 @@ extension BSON {
312312
return nil
313313
}
314314
}
315+
316+
/// Return this BSON as a `Double` if possible.
317+
/// This will coerce numeric cases (e.g. `.int32`) into a `Double` if such coercion would be lossless.
318+
public func toDouble() -> Double? {
319+
switch self {
320+
case let .double(d):
321+
return d
322+
default:
323+
guard let intValue = self.toInt() else {
324+
return nil
325+
}
326+
return Double(intValue)
327+
}
328+
}
329+
330+
/// Return this BSON as a `BSONDecimal128` if possible.
331+
/// This will coerce numeric cases (e.g. `.double`) into a `BSONDecimal128` if such coercion would be lossless.
332+
public func toDecimal128() -> BSONDecimal128? {
333+
switch self {
334+
case let .decimal128(d):
335+
return d
336+
case let .int64(i):
337+
return try? BSONDecimal128(String(i))
338+
case let .int32(i):
339+
return try? BSONDecimal128(String(i))
340+
case let .double(d):
341+
return try? BSONDecimal128(String(d))
342+
default:
343+
return nil
344+
}
345+
}
315346
}
316347

317348
/// Extension providing the internal API of `BSON`

Sources/BSON/BSONCode.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ extension BSONCodeWithScope: BSONValue {
114114
*/
115115
internal init?(fromExtJSON json: JSON, keyPath: [String]) throws {
116116
switch json {
117-
case let .object(obj):
117+
case .object:
118118
// canonical and relaxed extended JSON
119119
guard let (code, scope) = try json.unwrapObject(withKeys: "$code", "$scope", keyPath: keyPath) else {
120120
return nil

Sources/BSON/BSONDecimal128.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ public struct BSONDecimal128: Equatable, Hashable, CustomStringConvertible {
128128
private static let nanRegex = #"NaN"#
129129
private static let exponentRegex = "\(indicatorRegex)(\(signRegex))?(\(digitsRegex))"
130130
private static let numericValueRegex = "(\(signRegex))?(?:(\(decimalRegex))(?:\(exponentRegex))?|(\(infinityRegex)))"
131-
public static let decimal128Regex = "\(numericValueRegex)|(\(nanRegex))"
131+
private static let decimal128Regex = "^\(numericValueRegex)$|^(\(nanRegex))$"
132132
// swiftlint:enable line_length
133133

134134
/// The precision of the Decimal128 format
@@ -190,7 +190,7 @@ public struct BSONDecimal128: Equatable, Hashable, CustomStringConvertible {
190190
)
191191
let wholeRepr = NSRange(data.startIndex..<data.endIndex, in: data)
192192
guard let match: NSTextCheckingResult = regex.firstMatch(in: data, range: wholeRepr) else {
193-
throw BSONError.InvalidArgumentError(message: "Syntax Error: \(data) does not match \(regex)")
193+
throw BSONError.InvalidArgumentError(message: "Syntax Error: Invalid Decimal128 string \(data)")
194194
}
195195

196196
var sign = 1

Sources/BSON/BSONDocument.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -291,7 +291,7 @@ public struct BSONDocument {
291291

292292
guard byteLength == self.buffer.readableBytes else {
293293
throw BSONError.InvalidArgumentError(
294-
message: "BSONDocument's encoded byte length is \(byteLength) however the" +
294+
message: "BSONDocument's encoded byte length is \(byteLength), however the" +
295295
"buffer has \(self.buffer.readableBytes) readable bytes"
296296
)
297297
}
@@ -313,6 +313,10 @@ public struct BSONDocument {
313313
continue
314314
}
315315
}
316+
} catch let error as BSONError.InternalError {
317+
throw BSONError.InvalidArgumentError(
318+
message: "Validation Failed: \(error.message)"
319+
)
316320
}
317321
}
318322
}

Tests/BSONTests/BSONDocumentTests.swift

Lines changed: 0 additions & 57 deletions
This file was deleted.

Tests/BSONTests/BSONObjectIDTests.swift

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,4 +69,30 @@ final class BSONObjectIDTests: BSONTestCase {
6969

7070
expect(format.string(from: dateFromID)).to(equal(format.string(from: date)))
7171
}
72+
73+
/// Test object for testObjectIdJSONCodable
74+
private struct TestObject: Codable, Equatable {
75+
private let _id: BSONObjectID
76+
77+
init(id: BSONObjectID) {
78+
self._id = id
79+
}
80+
}
81+
82+
func testObjectIdJSONCodable() throws {
83+
let id = BSONObjectID()
84+
let obj = TestObject(id: id)
85+
let output = try JSONEncoder().encode(obj)
86+
let outputStr = String(decoding: output, as: UTF8.self)
87+
expect(outputStr).to(equal("{\"_id\":\"\(id.hex)\"}"))
88+
89+
let decoded = try JSONDecoder().decode(TestObject.self, from: output)
90+
expect(decoded).to(equal(obj))
91+
92+
// expect a decoding error when the hex string is invalid
93+
let invalidHex = id.hex.dropFirst()
94+
let invalidJSON = "{\"_id\":\"\(invalidHex)\"}".data(using: .utf8)!
95+
expect(try JSONDecoder().decode(TestObject.self, from: invalidJSON))
96+
.to(throwError(errorType: DecodingError.self))
97+
}
7298
}

Tests/BSONTests/BSONValueTests.swift

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
@testable import BSON
2+
import Foundation
3+
import Nimble
4+
import XCTest
5+
6+
final class BSONValueTests: BSONTestCase {
7+
func testInvalidDecimal128() throws {
8+
expect(try BSONDecimal128("hi")).to(throwError())
9+
expect(try BSONDecimal128("123.4.5")).to(throwError())
10+
expect(try BSONDecimal128("10")).toNot(throwError())
11+
12+
expect(try BSONDecimal128("nan")).toNot(throwError())
13+
expect(try BSONDecimal128("nana")).to(throwError())
14+
}
15+
16+
func testUUIDBytes() throws {
17+
let twoBytes = Data(base64Encoded: "//8=")!
18+
let sixteenBytes = Data(base64Encoded: "c//SZESzTGmQ6OfR38A11A==")!
19+
20+
// UUIDs must have 16 bytes
21+
expect(try BSONBinary(data: twoBytes, subtype: .uuidDeprecated))
22+
.to(throwError(errorType: BSONError.InvalidArgumentError.self))
23+
expect(try BSONBinary(data: twoBytes, subtype: .uuid))
24+
.to(throwError(errorType: BSONError.InvalidArgumentError.self))
25+
expect(try BSONBinary(data: sixteenBytes, subtype: .uuidDeprecated)).toNot(throwError())
26+
expect(try BSONBinary(data: sixteenBytes, subtype: .uuid)).toNot(throwError())
27+
}
28+
29+
fileprivate func checkTrueAndFalse(val: BSON, alternate: BSON) {
30+
expect(val).to(equal(val))
31+
expect(val).toNot(equal(alternate))
32+
}
33+
34+
func testBSONEquatable() throws {
35+
// Int
36+
self.checkTrueAndFalse(val: 1, alternate: 2)
37+
// Int32
38+
self.checkTrueAndFalse(val: .int32(32), alternate: .int32(33))
39+
// Int64
40+
self.checkTrueAndFalse(val: .int64(64), alternate: .int64(65))
41+
// Double
42+
self.checkTrueAndFalse(val: 1.618, alternate: 2.718)
43+
// Decimal128
44+
self.checkTrueAndFalse(
45+
val: .decimal128(try BSONDecimal128("1.618")),
46+
alternate: .decimal128(try BSONDecimal128("2.718"))
47+
)
48+
// Bool
49+
self.checkTrueAndFalse(val: true, alternate: false)
50+
// String
51+
self.checkTrueAndFalse(val: "some", alternate: "not some")
52+
// RegularExpression
53+
self.checkTrueAndFalse(
54+
val: .regex(BSONRegularExpression(pattern: ".*", options: "")),
55+
alternate: .regex(BSONRegularExpression(pattern: ".+", options: ""))
56+
)
57+
// Timestamp
58+
self.checkTrueAndFalse(
59+
val: .timestamp(BSONTimestamp(timestamp: 1, inc: 2)),
60+
alternate: .timestamp(BSONTimestamp(timestamp: 5, inc: 10))
61+
)
62+
// Date
63+
self.checkTrueAndFalse(
64+
val: .datetime(Date(timeIntervalSinceReferenceDate: 5000)),
65+
alternate: .datetime(Date(timeIntervalSinceReferenceDate: 5001))
66+
)
67+
// MinKey & MaxKey
68+
expect(BSON.minKey).to(equal(.minKey))
69+
expect(BSON.maxKey).to(equal(.maxKey))
70+
// ObjectId
71+
self.checkTrueAndFalse(val: .objectID(), alternate: .objectID())
72+
// CodeWithScope
73+
self.checkTrueAndFalse(
74+
val: .codeWithScope(BSONCodeWithScope(code: "console.log('foo');", scope: [:])),
75+
alternate: .codeWithScope(BSONCodeWithScope(code: "console.log(x);", scope: ["x": 2]))
76+
)
77+
// Binary
78+
self.checkTrueAndFalse(
79+
val: .binary(try BSONBinary(data: Data(base64Encoded: "c//SZESzTGmQ6OfR38A11A==")!, subtype: .uuid)),
80+
alternate: .binary(try BSONBinary(data: Data(base64Encoded: "c//88KLnfdfefOfR33ddFA==")!, subtype: .uuid))
81+
)
82+
// Document
83+
self.checkTrueAndFalse(
84+
val: [
85+
"foo": 1.414,
86+
"bar": "swift",
87+
"nested": ["a": 1, "b": "2"]
88+
],
89+
alternate: [
90+
"foo": 1.414,
91+
"bar": "swift",
92+
"nested": ["a": 1, "b": "different"]
93+
]
94+
)
95+
96+
// Different types
97+
expect(BSON.int32(4)).toNot(equal("swift"))
98+
99+
// Arrays of different sizes should not be equal
100+
let b0: BSON = [1, 2]
101+
let b1: BSON = [1, 2, 3]
102+
expect(b0).toNot(equal(b1))
103+
}
104+
105+
struct BSONNumberTestCase {
106+
let int: Int?
107+
let double: Double?
108+
let int32: Int32?
109+
let int64: Int64?
110+
let decimal: BSONDecimal128?
111+
112+
static func compare<T: Equatable>(computed: T?, expected: T?) {
113+
guard computed != nil else {
114+
expect(expected).to(beNil())
115+
return
116+
}
117+
expect(computed).to(equal(expected))
118+
}
119+
120+
func run() {
121+
let candidates: [BSON?] = [
122+
self.int.map { BSON(integerLiteral: $0) },
123+
self.double.map { .double($0) },
124+
self.int32.map { .int32($0) },
125+
self.int64.map { .int64($0) },
126+
self.decimal.map { .decimal128($0) }
127+
]
128+
129+
candidates.compactMap { $0 }.forEach { l in
130+
// Skip the Decimal128 conversions until they're implemented
131+
// TODO: don't skip these (SWIFT-367)
132+
guard l.decimal128Value == nil else {
133+
return
134+
}
135+
136+
BSONNumberTestCase.compare(computed: l.toInt(), expected: self.int)
137+
BSONNumberTestCase.compare(computed: l.toInt32(), expected: self.int32)
138+
BSONNumberTestCase.compare(computed: l.toInt64(), expected: self.int64)
139+
BSONNumberTestCase.compare(computed: l.toDouble(), expected: self.double)
140+
141+
// Skip double for this conversion since it generates a Decimal128(5.0) =/= Decimal128(5)
142+
if l.doubleValue == nil {
143+
BSONNumberTestCase.compare(computed: l.toDecimal128(), expected: self.decimal)
144+
}
145+
}
146+
}
147+
}
148+
149+
func testBSONNumber() throws {
150+
let decimal128 = try BSONDecimal128("5.5")
151+
let double: BSON = 5.5
152+
153+
expect(double.toDouble()).to(equal(5.5))
154+
expect(double.toDecimal128()).to(equal(decimal128))
155+
156+
let cases = [
157+
BSONNumberTestCase(int: 5, double: 5.0, int32: Int32(5), int64: Int64(5), decimal: try BSONDecimal128("5")),
158+
BSONNumberTestCase(
159+
int: -5,
160+
double: -5.0,
161+
int32: Int32(-5),
162+
int64: Int64(-5),
163+
decimal: try BSONDecimal128("-5")
164+
),
165+
BSONNumberTestCase(int: 0, double: 0.0, int32: Int32(0), int64: Int64(0), decimal: try BSONDecimal128("0")),
166+
BSONNumberTestCase(int: nil, double: 1.234, int32: nil, int64: nil, decimal: try BSONDecimal128("1.234")),
167+
BSONNumberTestCase(
168+
int: nil,
169+
double: -31.234,
170+
int32: nil,
171+
int64: nil,
172+
decimal: try BSONDecimal128("-31.234")
173+
)
174+
]
175+
176+
cases.forEach { $0.run() }
177+
}
178+
}

0 commit comments

Comments
 (0)