-
Notifications
You must be signed in to change notification settings - Fork 67
SWIFT-1326 Run load balancer spec tests #669
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
041f9d4
3948900
e6b9374
9ad67b2
9403e2e
a4b16a0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,65 @@ | ||
| import MongoSwiftSync | ||
| import Nimble | ||
| import TestsCommon | ||
|
|
||
| final class LoadBalancerTests: MongoSwiftTestCase { | ||
| let skipFiles: [String] = [ | ||
| // We don't support this option. | ||
| "wait-queue-timeouts.json" | ||
| ] | ||
|
|
||
| func testLoadBalancers() throws { | ||
| let tests = try retrieveSpecTestFiles( | ||
| specName: "load-balancers", | ||
| excludeFiles: skipFiles, | ||
| asType: UnifiedTestFile.self | ||
| ).map { $0.1 } | ||
|
|
||
| let skipTests = [ | ||
| // The sessions spec requires that sessions can only be used with the MongoClient that created them. | ||
| // Consequently, libmongoc enforces that a `mongoc_client_session_t` may only be used with the | ||
| // `mongoc_client_t` that created it. In Swift, this translates to a requirement that we always pin | ||
| // `Connection`s to `ClientSession`s from the time the session is first used until it is closed/ | ||
| // deinitialized. Since all of these tests use a session entity that is created before the tests are run | ||
| // and closed after they complete, the session always has the connection pinned to it, and we never release | ||
| // the connection as these tests expect. | ||
| "transactions are correctly pinned to connections for load-balanced clusters": [ | ||
| "pinned connection is released after a non-transient abort error", | ||
| "pinned connection is released after a transient non-network CRUD error", | ||
| "pinned connection is released after a transient network CRUD error", | ||
| "pinned connection is released after a transient non-network commit error", | ||
| "pinned connection is released after a transient network commit error", | ||
| "pinned connection is released after a transient non-network abort error", | ||
| "pinned connection is released after a transient network abort error", | ||
| "pinned connection is released on successful abort", | ||
| "pinned connection is returned when a new transaction is started", | ||
| "pinned connection is returned when a non-transaction operation uses the session", | ||
| "a connection can be shared by a transaction and a cursor" | ||
| ], | ||
| "cursors are correctly pinned to connections for load-balanced clusters": [ | ||
| // This test assumes that we release a cursor's pinned connection as soon as the cursor is exhausted | ||
| // server-side, regardless of if it has been fully iterated. However, we do not release connections | ||
| // until the first iteration attempt after the last document in the cursor. | ||
| "no connection is pinned if all documents are returned in the initial batch", | ||
| // This test assumes that we release a cursor's pinned connection after the last document is returned. | ||
| // However, currently there is no way for us to reliably tell a libmongoc cursor is at its end without | ||
| // attempting to iterate it first. To avoid having to implement some sort of caching mechanism we do | ||
| // not close cursors until we attempt to iterate and get nil back, so this cursor is not closed/its | ||
| // connection is not released after 3 iterations, as the test expects. | ||
| "pinned connections are returned when the cursor is drained", | ||
| // These tests assume we do not automatically close the cursor when we encounter such errors, however | ||
| // we close cursors on all errors besides decoding errors, so the connections do get returned. | ||
| "pinned connections are not returned after an network error during getMore", | ||
| "pinned connections are not returned to the pool after a non-network error on getMore", | ||
| // TODO: SWIFT-1322 Unskip. | ||
| "listCollections pins the cursor to a connection", | ||
| // Skipping as we do not support a batchSize for listIndexes. We closed SWIFT-1325 as "won't fix", but | ||
| // should we ever decide to do it we could unskip this test then. | ||
| "listIndexes pins the cursor to a connection" | ||
| ] | ||
| ] | ||
|
|
||
| let runner = try UnifiedTestRunner() | ||
| try runner.runFiles(tests, skipTests: skipTests) | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,10 +6,35 @@ struct ExpectedEventsForClient: Decodable { | |
| /// Client entity on which the events are expected to be observed. | ||
| let client: String | ||
|
|
||
| enum EventType: String, Decodable { | ||
| case command, cmap | ||
| } | ||
|
|
||
| /// The type of events to be observed. | ||
| let eventType: EventType | ||
|
|
||
| /// List of events, which are expected to be observed (in this order) on the corresponding client while executing | ||
| /// operations. If the array is empty, the test runner MUST assert that no events were observed on the client | ||
| /// (excluding ignored events). | ||
| let events: [ExpectedEvent] | ||
|
|
||
| enum CodingKeys: String, CodingKey { | ||
| case client, eventType, events | ||
| } | ||
|
|
||
| init(from decoder: Decoder) throws { | ||
| let container = try decoder.container(keyedBy: CodingKeys.self) | ||
| self.client = try container.decode(String.self, forKey: .client) | ||
| // Defaults to command if omitted. | ||
| self.eventType = try container.decodeIfPresent(EventType.self, forKey: .eventType) ?? .command | ||
| switch self.eventType { | ||
| case .command: | ||
| self.events = try container.decode([ExpectedEvent].self, forKey: .events) | ||
| case .cmap: | ||
| // TODO: SWIFT-1321 actually parse these out. | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. for now, there is just always an empty array, and in the runner below if |
||
| self.events = [] | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /// Describes expected events. | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -75,6 +75,44 @@ struct UnifiedCreateIndex: UnifiedOperationProtocol { | |
| } | ||
| } | ||
|
|
||
| struct UnifiedListIndexes: UnifiedOperationProtocol { | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we don't actually end up using this operation since we skip the listIndexes cursor test, but I figured we would need it eventually and adding it seemed like a reasonable way to allow us to decode |
||
| /// Optional identifier for a session entity to use. | ||
| let session: String? | ||
|
|
||
| /// We consider this a known argument and decode it even though we don't support it, because a load balancer test | ||
| /// file uses this option and we could not decode/run the entire file otherwise. | ||
| let batchSize: Int? | ||
|
|
||
| private enum CodingKeys: String, CodingKey, CaseIterable { | ||
| case session, batchSize | ||
| } | ||
|
|
||
| static var knownArguments: Set<String> { | ||
| Set( | ||
| CodingKeys.allCases.map { $0.rawValue } | ||
| ) | ||
| } | ||
|
|
||
| init(from decoder: Decoder) throws { | ||
| let container = try decoder.container(keyedBy: CodingKeys.self) | ||
| self.session = try container.decodeIfPresent(String.self, forKey: .session) | ||
| self.batchSize = try container.decodeIfPresent(Int.self, forKey: .batchSize) | ||
| } | ||
|
|
||
| func execute(on object: UnifiedOperation.Object, context: Context) throws -> UnifiedOperationResult { | ||
| guard self.batchSize == nil else { | ||
| throw TestError( | ||
| message: "listIndexes operation specifies a batchSize, but we do not support the option -- you may " + | ||
| "need to skip this test. Path: \(context.path)" | ||
| ) | ||
| } | ||
| let collection = try context.entities.getEntity(from: object).asCollection() | ||
| let session = try context.entities.resolveSession(id: self.session) | ||
| let results = try collection.listIndexes(session: session) | ||
| return .rootDocumentArray(try results.map { try $0.get() }.map { try BSONEncoder().encode($0) }) | ||
| } | ||
| } | ||
|
|
||
| struct UnifiedBulkWrite: UnifiedOperationProtocol { | ||
| /// Writes to perform. | ||
| let requests: [WriteModel<BSONDocument>] | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,4 @@ | ||
| import MongoSwiftSync | ||
| import SwiftBSON | ||
| struct UnifiedCreateCollection: UnifiedOperationProtocol { | ||
| /// The collection to create. | ||
|
|
@@ -63,3 +64,38 @@ struct UnifiedRunCommand: UnifiedOperationProtocol { | |
| return .none | ||
| } | ||
| } | ||
|
|
||
| struct UnifiedListCollections: UnifiedOperationProtocol { | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. similarly, this operation ends up going unused but I added it for completeness. |
||
| /// Filter to use for the command. | ||
| let filter: BSONDocument | ||
|
|
||
| /// Optional identifier for a session entity to use. | ||
| let session: String? | ||
|
|
||
| let options: ListCollectionsOptions | ||
|
|
||
| enum CodingKeys: String, CodingKey, CaseIterable { | ||
| case filter, session | ||
| } | ||
|
|
||
| init(from decoder: Decoder) throws { | ||
| self.options = try decoder.singleValueContainer().decode(ListCollectionsOptions.self) | ||
| let container = try decoder.container(keyedBy: CodingKeys.self) | ||
| self.session = try container.decodeIfPresent(String.self, forKey: .session) | ||
| self.filter = try container.decode(BSONDocument.self, forKey: .filter) | ||
| } | ||
|
|
||
| static var knownArguments: Set<String> { | ||
| Set( | ||
| CodingKeys.allCases.map { $0.rawValue } + | ||
| ListCollectionsOptions().propertyNames | ||
| ) | ||
| } | ||
|
|
||
| func execute(on object: UnifiedOperation.Object, context: Context) throws -> UnifiedOperationResult { | ||
| let db = try context.entities.getEntity(from: object).asDatabase() | ||
| let session = try context.entities.resolveSession(id: self.session) | ||
| let results = try db.listCollections(self.filter, options: self.options, session: session) | ||
| return .rootDocumentArray(try results.map { try $0.get() }.map { try BSONEncoder().encode($0) }) | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
unfortunately a lot of caveats here, but we still do get to run a decent number of the tests, so it seems better than nothing. let me know if all the reasoning below makes sense.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All the skip reasons seem clear and detailed, LGTM.