@@ -3,309 +3,24 @@ import Nimble
33import TestsCommon
44import 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 ------------ \n Executing 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