Skip to content

Commit 55a2d42

Browse files
authored
SWIFT-1191, SWIFT-1604: Switch to unified format change streams tests (#772)
1 parent 2d579e2 commit 55a2d42

15 files changed

+9074
-4643
lines changed

Tests/MongoSwiftSyncTests/SyncChangeStreamTests.swift

Lines changed: 8 additions & 293 deletions
Original file line numberDiff line numberDiff line change
@@ -3,309 +3,24 @@ import Nimble
33
import TestsCommon
44
import XCTest
55

6-
/// The entity on which to start a change stream.
7-
internal enum ChangeStreamTarget: String, Decodable {
8-
/// Indicates the change stream will be opened to watch a client.
9-
case client
10-
11-
/// Indicates the change stream will be opened to watch a database.
12-
case database
13-
14-
/// Indicates the change stream will be opened to watch a collection.
15-
case collection
16-
17-
/// Open a change stream against this target. An error will be thrown if the necessary namespace information is not
18-
/// provided.
19-
internal func watch(
20-
_ client: MongoClient,
21-
_ database: String?,
22-
_ collection: String?,
23-
_ pipeline: [BSONDocument],
24-
_ options: ChangeStreamOptions
25-
) throws -> ChangeStream<BSONDocument> {
26-
switch self {
27-
case .client:
28-
return try client.watch(pipeline, options: options, withEventType: BSONDocument.self)
29-
case .database:
30-
guard let database = database else {
31-
throw TestError(message: "missing db in watch")
32-
}
33-
return try client.db(database).watch(pipeline, options: options, withEventType: BSONDocument.self)
34-
case .collection:
35-
guard let collection = collection, let database = database else {
36-
throw TestError(message: "missing db or collection in watch")
37-
}
38-
return try client.db(database)
39-
.collection(collection)
40-
.watch(pipeline, options: options, withEventType: BSONDocument.self)
41-
}
42-
}
43-
}
44-
45-
/// An operation performed as part of a `ChangeStreamTest` (e.g. a CRUD operation, an drop, etc.)
46-
/// This struct includes the namespace against which it should be run.
47-
internal struct ChangeStreamTestOperation: Decodable {
48-
/// The operation itself to run.
49-
private let operation: AnyTestOperation
50-
51-
/// The database to run the operation against.
52-
private let database: String
53-
54-
/// The collection to run the operation against.
55-
private let collection: String
56-
57-
private enum CodingKeys: String, CodingKey {
58-
case database, collection
59-
}
60-
61-
public init(from decoder: Decoder) throws {
62-
let container = try decoder.container(keyedBy: CodingKeys.self)
63-
self.database = try container.decode(String.self, forKey: .database)
64-
self.collection = try container.decode(String.self, forKey: .collection)
65-
self.operation = try AnyTestOperation(from: decoder)
66-
}
67-
68-
/// Run the operation against the namespace associated with this operation.
69-
internal func execute(using client: MongoClient) throws -> TestOperationResult? {
70-
let db = client.db(self.database)
71-
let coll = db.collection(self.collection)
72-
return try self.operation.op.execute(on: coll, sessions: [:])
73-
}
74-
}
75-
76-
/// The outcome of a given `ChangeStreamTest`.
77-
internal enum ChangeStreamTestResult: Decodable {
78-
/// Describes an error received during the test
79-
case error(code: Int, labels: [String]?)
80-
81-
/// An array of event documents expected to be received from the change stream without error during the test.
82-
case success([BSONDocument])
83-
84-
/// Top-level coding keys. Used for determining whether this result is a success or failure.
85-
internal enum CodingKeys: CodingKey {
86-
case error, success
87-
}
88-
89-
/// Coding keys used specifically for decoding the `.error` case.
90-
internal enum ErrorCodingKeys: CodingKey {
91-
case code, errorLabels
92-
}
93-
94-
/// Asserts that the given error matches the one expected by this result.
95-
internal func assertMatchesError(error: Error, description: String) {
96-
guard case let .error(code, labels) = self else {
97-
fail("\(description) failed: got error but result success")
98-
return
99-
}
100-
guard let seenError = error as? MongoError.CommandError else {
101-
fail("\(description) failed: didn't get command error")
102-
return
103-
}
104-
105-
expect(seenError.code).to(equal(code), description: description)
106-
if let labels = labels {
107-
expect(seenError.errorLabels).toNot(beNil(), description: description)
108-
expect(seenError.errorLabels).to(equal(labels), description: description)
109-
}
110-
}
111-
112-
public init(from decoder: Decoder) throws {
113-
let container = try decoder.container(keyedBy: CodingKeys.self)
114-
if container.contains(.success) {
115-
self = .success(try container.decode([BSONDocument].self, forKey: .success))
116-
} else {
117-
let nested = try container.nestedContainer(keyedBy: ErrorCodingKeys.self, forKey: .error)
118-
let code = try nested.decode(Int.self, forKey: .code)
119-
let labels = try nested.decodeIfPresent([String].self, forKey: .errorLabels)
120-
self = .error(code: code, labels: labels)
121-
}
122-
}
123-
}
124-
125-
/// Struct representing a single test within a spec test JSON file.
126-
internal struct ChangeStreamTest: Decodable, FailPointConfigured {
127-
/// The title of this test.
128-
let description: String
129-
130-
/// The minimum server version that this test can be run against.
131-
let minServerVersion: ServerVersion
132-
133-
/// The maximum server version that this test can be run against.
134-
let maxServerVersion: ServerVersion?
135-
136-
/// The fail point that should be set prior to running this test.
137-
let failPoint: FailPoint?
138-
139-
/// The entity on which to run the change stream.
140-
let target: ChangeStreamTarget
141-
142-
/// An array of server topologies against which to run the test.
143-
let topology: [TestTopologyConfiguration]
144-
145-
/// An array of additional aggregation pipeline stages to pass to the `watch` used to create the change stream for
146-
/// this test.
147-
let changeStreamPipeline: [BSONDocument]
148-
149-
/// Additional options to pass to the `watch` used to create the change stream for this test.
150-
let changeStreamOptions: ChangeStreamOptions
151-
152-
/// An array of documents, each describing an operation that should be run as part of this test.
153-
let operations: [ChangeStreamTestOperation]
154-
155-
/// A list of command-started events that are expected to have been emitted by the client that starts the change
156-
/// stream for this test.
157-
let expectations: [TestCommandStartedEvent]?
158-
159-
// The expected result of running this test.
160-
let result: ChangeStreamTestResult
161-
162-
var activeFailPoint: FailPoint?
163-
var targetedHost: ServerAddress?
164-
165-
internal mutating func run(globalClient: MongoClient, database: String, collection: String) throws {
166-
let client = try MongoClient.makeTestClient()
167-
let monitor = client.addCommandMonitor()
168-
169-
if let failPoint = self.failPoint {
170-
try failPoint.enable(using: globalClient)
171-
}
172-
defer { self.failPoint?.disable(using: globalClient) }
173-
174-
monitor.captureEvents {
175-
do {
176-
let changeStream = try self.target.watch(
177-
client,
178-
database,
179-
collection,
180-
self.changeStreamPipeline,
181-
self.changeStreamOptions
182-
)
183-
for operation in self.operations {
184-
_ = try operation.execute(using: globalClient)
185-
}
186-
187-
switch self.result {
188-
case .error:
189-
_ = try changeStream.nextWithTimeout()
190-
fail("\(self.description) failed: expected error but got none while iterating")
191-
case let .success(events):
192-
var seenEvents: [BSONDocument] = []
193-
for _ in 0..<events.count {
194-
guard let event = try changeStream.nextWithTimeout() else {
195-
XCTFail("Unexpectedly got no event from change stream in test: \(self.description)")
196-
return
197-
}
198-
seenEvents.append(event)
199-
}
200-
expect(seenEvents).to(match(events), description: self.description)
201-
}
202-
} catch {
203-
self.result.assertMatchesError(error: error, description: self.description)
204-
}
205-
}
206-
207-
if let expectations = self.expectations {
208-
let commandEvents = monitor.commandStartedEvents()
209-
.filter { ![LEGACY_HELLO, "hello", "killCursors"].contains($0.commandName) }
210-
.map { TestCommandStartedEvent(from: $0) }
211-
expect(commandEvents).to(match(expectations), description: self.description)
212-
}
213-
}
214-
}
215-
216-
/// Struct representing a single change-streams spec test JSON file.
217-
private struct ChangeStreamTestFile: Decodable {
218-
private enum CodingKeys: String, CodingKey {
219-
case databaseName = "database_name",
220-
collectionName = "collection_name",
221-
database2Name = "database2_name",
222-
collection2Name = "collection2_name",
223-
tests
224-
}
225-
226-
/// The default database.
227-
let databaseName: String
228-
229-
/// The default collection.
230-
let collectionName: String
231-
232-
/// Secondary database.
233-
let database2Name: String?
234-
235-
// Secondary collection.
236-
let collection2Name: String?
237-
238-
/// An array of tests that are to be run independently of each other.
239-
let tests: [ChangeStreamTest]
240-
}
241-
242-
/// Class covering the JSON spec tests associated with change streams.
243-
final class ChangeStreamSpecTests: MongoSwiftTestCase {
244-
func testChangeStreamSpec() throws {
245-
let tests = try retrieveSpecTestFiles(
246-
specName: "change-streams",
247-
subdirectory: "legacy",
248-
asType: ChangeStreamTestFile.self
249-
)
250-
251-
let globalClient = try MongoClient.makeTestClient()
252-
253-
for (testName, testFile) in tests {
254-
let db1 = globalClient.db(testFile.databaseName)
255-
// only some test files use a second database.
256-
let db2: MongoDatabase?
257-
if let db2Name = testFile.database2Name {
258-
db2 = globalClient.db(db2Name)
259-
} else {
260-
db2 = nil
261-
}
262-
defer {
263-
try? db1.drop()
264-
try? db2?.drop()
265-
}
266-
print("\n------------\nExecuting tests from file \(testName)...\n")
267-
for var test in testFile.tests {
268-
let testRequirements = TestRequirement(
269-
minServerVersion: test.minServerVersion,
270-
maxServerVersion: test.maxServerVersion,
271-
acceptableTopologies: test.topology
272-
)
273-
274-
let unmetRequirement = try globalClient.getUnmetRequirement(testRequirements)
275-
guard unmetRequirement == nil else {
276-
printSkipMessage(testName: test.description, unmetRequirement: unmetRequirement!)
277-
continue
278-
}
279-
280-
print("Executing test: \(test.description)")
281-
282-
try db1.drop()
283-
try db2?.drop()
284-
_ = try db1.createCollection(testFile.collectionName)
285-
_ = try db2?.createCollection(testFile.collection2Name ?? "foo")
286-
287-
try test.run(
288-
globalClient: globalClient,
289-
database: testFile.databaseName,
290-
collection: testFile.collectionName
291-
)
292-
}
293-
}
294-
}
295-
6+
final class SyncChangeStreamTests: MongoSwiftTestCase {
7+
let excludeFiles = [
8+
// TODO: SWIFT-1458 Unskip.
9+
"change-streams-showExpandedEvents.json",
10+
// TODO: SWIFT-1472 Unskip.
11+
"change-streams-pre_and_post_images.json"
12+
]
29613
func testChangeStreamSpecUnified() throws {
29714
let tests = try retrieveSpecTestFiles(
29815
specName: "change-streams",
29916
subdirectory: "unified",
17+
excludeFiles: excludeFiles,
30018
asType: UnifiedTestFile.self
30119
).map { $0.1 }
30220
let testRunner = try UnifiedTestRunner()
30321
try testRunner.runFiles(tests)
30422
}
305-
}
30623

307-
/// Class for spec prose tests and other integration tests associated with change streams.
308-
final class SyncChangeStreamTests: MongoSwiftTestCase {
30924
/// How long in total a change stream should poll for an event or error before returning.
31025
/// Used as a default value for `ChangeStream.nextWithTimeout`
31126
public static let TIMEOUT: TimeInterval = 15

0 commit comments

Comments
 (0)