|
1 |
| -import { expect } from 'chai'; |
2 | 1 | import * as path from 'path';
|
3 | 2 |
|
4 |
| -import type { Collection, Db, MongoClient } from '../../mongodb'; |
5 | 3 | import { loadSpecTests } from '../../spec';
|
6 |
| -import { legacyRunOnToRunOnRequirement } from '../../tools/spec-runner'; |
7 | 4 | import { runUnifiedSuite } from '../../tools/unified-spec-runner/runner';
|
8 |
| -import { isAnyRequirementSatisfied } from '../../tools/unified-spec-runner/unified-utils'; |
9 | 5 |
|
10 |
| -interface RetryableWriteTestContext { |
11 |
| - client?: MongoClient; |
12 |
| - db?: Db; |
13 |
| - collection?: Collection; |
14 |
| - failPointName?: any; |
15 |
| -} |
16 |
| - |
17 |
| -describe('Legacy Retryable Writes Specs', function () { |
18 |
| - let ctx: RetryableWriteTestContext = {}; |
19 |
| - |
20 |
| - const retryableWrites = loadSpecTests('retryable-writes', 'legacy'); |
21 |
| - |
22 |
| - for (const suite of retryableWrites) { |
23 |
| - describe(suite.name, function () { |
24 |
| - beforeEach(async function () { |
25 |
| - let utilClient: MongoClient; |
26 |
| - if (this.configuration.isLoadBalanced) { |
27 |
| - // The util client can always point at the single mongos LB frontend. |
28 |
| - utilClient = this.configuration.newClient(this.configuration.singleMongosLoadBalancerUri); |
29 |
| - } else { |
30 |
| - utilClient = this.configuration.newClient(); |
31 |
| - } |
32 |
| - |
33 |
| - await utilClient.connect(); |
34 |
| - |
35 |
| - const allRequirements = suite.runOn.map(legacyRunOnToRunOnRequirement); |
36 |
| - |
37 |
| - const someRequirementMet = |
38 |
| - !allRequirements.length || |
39 |
| - (await isAnyRequirementSatisfied(this.currentTest.ctx, allRequirements, utilClient)); |
40 |
| - |
41 |
| - await utilClient.close(); |
42 |
| - |
43 |
| - if (!someRequirementMet) this.skip(); |
44 |
| - }); |
45 |
| - |
46 |
| - beforeEach(async function () { |
47 |
| - // Step 1: Test Setup. Includes a lot of boilerplate stuff |
48 |
| - // like creating a client, dropping and refilling data collections, |
49 |
| - // and enabling failpoints |
50 |
| - const { spec } = this.currentTest; |
51 |
| - await executeScenarioSetup(suite, spec, this.configuration, ctx); |
52 |
| - }); |
53 |
| - |
54 |
| - afterEach(async function () { |
55 |
| - // Step 3: Test Teardown. Turn off failpoints, and close client |
56 |
| - if (!ctx.db || !ctx.client) { |
57 |
| - return; |
58 |
| - } |
59 |
| - |
60 |
| - if (ctx.failPointName) { |
61 |
| - await turnOffFailPoint(ctx.client, ctx.failPointName); |
62 |
| - } |
63 |
| - await ctx.client.close(); |
64 |
| - ctx = {}; // reset context |
65 |
| - }); |
66 |
| - for (const spec of suite.tests) { |
67 |
| - // Step 2: Run the test |
68 |
| - const mochaTest = it(spec.description, async () => await executeScenarioTest(spec, ctx)); |
69 |
| - |
70 |
| - // A pattern we don't need to repeat for unified tests |
71 |
| - // In order to give the beforeEach hook access to the |
72 |
| - // spec test so it can be responsible for skipping it |
73 |
| - // and executeScenarioSetup |
74 |
| - mochaTest.spec = spec; |
75 |
| - } |
76 |
| - }); |
77 |
| - } |
78 |
| -}); |
79 |
| - |
80 |
| -async function executeScenarioSetup(scenario, test, config, ctx) { |
81 |
| - const url = config.url(); |
82 |
| - const options = { |
83 |
| - ...test.clientOptions, |
84 |
| - heartbeatFrequencyMS: 100, |
85 |
| - monitorCommands: true, |
86 |
| - minPoolSize: 10 |
87 |
| - }; |
88 |
| - |
89 |
| - ctx.failPointName = test.failPoint && test.failPoint.configureFailPoint; |
90 |
| - |
91 |
| - const client = config.newClient(url, options); |
92 |
| - await client.connect(); |
93 |
| - |
94 |
| - ctx.client = client; |
95 |
| - ctx.db = client.db(config.db); |
96 |
| - ctx.collection = ctx.db.collection(`retryable_writes_test_${config.name}_${test.operation.name}`); |
97 |
| - |
98 |
| - try { |
99 |
| - await ctx.collection.drop(); |
100 |
| - } catch (error) { |
101 |
| - if (!error.message.match(/ns not found/)) { |
102 |
| - throw error; |
103 |
| - } |
104 |
| - } |
105 |
| - |
106 |
| - if (Array.isArray(scenario.data) && scenario.data.length) { |
107 |
| - await ctx.collection.insertMany(scenario.data); |
108 |
| - } |
109 |
| - |
110 |
| - if (test.failPoint) { |
111 |
| - await ctx.client.db('admin').command(test.failPoint); |
112 |
| - } |
113 |
| -} |
114 |
| - |
115 |
| -async function executeScenarioTest(test, ctx: RetryableWriteTestContext) { |
116 |
| - const args = generateArguments(test); |
117 |
| - |
118 |
| - // In case the spec files or our API changes |
119 |
| - expect(ctx.collection).to.have.property(test.operation.name).that.is.a('function'); |
120 |
| - |
121 |
| - // TODO(NODE-4033): Collect command started events and assert txn number existence or omission |
122 |
| - // have to add logic to handle bulkWrites which emit multiple command started events |
123 |
| - const resultOrError = await ctx.collection[test.operation.name](...args).catch(error => error); |
124 |
| - |
125 |
| - const outcome = test.outcome && test.outcome.result; |
126 |
| - const errorLabelsContain = outcome && outcome.errorLabelsContain; |
127 |
| - const errorLabelsOmit = outcome && outcome.errorLabelsOmit; |
128 |
| - const hasResult = outcome && !errorLabelsContain && !errorLabelsOmit; |
129 |
| - if (test.outcome.error) { |
130 |
| - const thrownError = resultOrError; |
131 |
| - expect(thrownError, `${test.operation.name} was supposed to fail but did not!`).to.exist; |
132 |
| - expect(thrownError).to.have.property('message'); |
133 |
| - |
134 |
| - if (hasResult) { |
135 |
| - expect(thrownError.result).to.matchMongoSpec(test.outcome.result); |
136 |
| - } |
137 |
| - |
138 |
| - if (errorLabelsContain) { |
139 |
| - expect(thrownError.errorLabels).to.include.members(errorLabelsContain); |
140 |
| - } |
141 |
| - |
142 |
| - if (errorLabelsOmit) { |
143 |
| - for (const label of errorLabelsOmit) { |
144 |
| - expect(thrownError.errorLabels).to.not.contain(label); |
145 |
| - } |
146 |
| - } |
147 |
| - } else if (test.outcome.result) { |
148 |
| - expect(resultOrError, resultOrError.stack).to.not.be.instanceOf(Error); |
149 |
| - const result = resultOrError; |
150 |
| - const expected = test.outcome.result; |
151 |
| - |
152 |
| - const actual = result.value ?? result; |
153 |
| - // Some of our result classes contain the optional 'acknowledged' property which is |
154 |
| - // not part of the test expectations. |
155 |
| - for (const property in expected) { |
156 |
| - expect(actual).to.have.deep.property(property, expected[property]); |
157 |
| - } |
158 |
| - } |
159 |
| - |
160 |
| - if (test.outcome.collection) { |
161 |
| - const collectionResults = await ctx.collection.find({}).toArray(); |
162 |
| - expect(collectionResults).to.deep.equal(test.outcome.collection.data); |
163 |
| - } |
164 |
| -} |
165 |
| - |
166 |
| -// Helper Functions |
167 |
| - |
168 |
| -/** Transforms the arguments from a test into actual arguments for our function calls */ |
169 |
| -function generateArguments(test) { |
170 |
| - const args = []; |
171 |
| - |
172 |
| - if (test.operation.arguments) { |
173 |
| - const options: Record<string, any> = {}; |
174 |
| - for (const arg of Object.keys(test.operation.arguments)) { |
175 |
| - if (arg === 'requests') { |
176 |
| - /** Transforms a request arg into a bulk write operation */ |
177 |
| - args.push( |
178 |
| - test.operation.arguments[arg].map(({ name, arguments: args }) => ({ [name]: args })) |
179 |
| - ); |
180 |
| - } else if (arg === 'upsert') { |
181 |
| - options.upsert = test.operation.arguments[arg]; |
182 |
| - } else if (arg === 'returnDocument') { |
183 |
| - options.returnDocument = test.operation.arguments[arg].toLowerCase(); |
184 |
| - } else { |
185 |
| - args.push(test.operation.arguments[arg]); |
186 |
| - } |
187 |
| - } |
188 |
| - |
189 |
| - if (Object.keys(options).length > 0) { |
190 |
| - args.push(options); |
191 |
| - } |
192 |
| - } |
193 |
| - |
194 |
| - return args; |
195 |
| -} |
196 |
| - |
197 |
| -/** Runs a command that turns off a fail point */ |
198 |
| -async function turnOffFailPoint(client, name) { |
199 |
| - return await client.db('admin').command({ |
200 |
| - configureFailPoint: name, |
201 |
| - mode: 'off' |
202 |
| - }); |
203 |
| -} |
| 6 | +const clientBulkWriteTests = [ |
| 7 | + 'client bulkWrite with one network error succeeds after retry', |
| 8 | + 'client bulkWrite with two network errors fails after retry', |
| 9 | + 'client bulkWrite with no multi: true operations succeeds after retryable top-level error', |
| 10 | + 'client bulkWrite with multi: true operations fails after retryable top-level error', |
| 11 | + 'client bulkWrite with no multi: true operations succeeds after retryable writeConcernError', |
| 12 | + 'client bulkWrite with multi: true operations fails after retryable writeConcernError', |
| 13 | + 'client bulkWrite with retryWrites: false does not retry', |
| 14 | + 'client.clientBulkWrite succeeds after retryable handshake network error', |
| 15 | + 'client.clientBulkWrite succeeds after retryable handshake server error (ShutdownInProgress)' |
| 16 | +]; |
204 | 17 |
|
205 | 18 | describe('Retryable Writes (unified)', function () {
|
206 |
| - runUnifiedSuite(loadSpecTests(path.join('retryable-writes', 'unified'))); |
| 19 | + runUnifiedSuite(loadSpecTests(path.join('retryable-writes', 'unified')), ({ description }) => { |
| 20 | + return clientBulkWriteTests.includes(description) |
| 21 | + ? `TODO(NODE-xxxx): implement client-level bulk write.` |
| 22 | + : false; |
| 23 | + }); |
207 | 24 | });
|
0 commit comments