|
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 |
| - |
67 |
| - for (const spec of suite.tests) { |
68 |
| - // Step 2: Run the test |
69 |
| - const mochaTest = it(spec.description, async () => await executeScenarioTest(spec, ctx)); |
70 |
| - |
71 |
| - // A pattern we don't need to repeat for unified tests |
72 |
| - // In order to give the beforeEach hook access to the |
73 |
| - // spec test so it can be responsible for skipping it |
74 |
| - // and executeScenarioSetup |
75 |
| - mochaTest.spec = spec; |
76 |
| - } |
77 |
| - }); |
78 |
| - } |
79 |
| -}); |
80 |
| - |
81 |
| -async function executeScenarioSetup(scenario, test, config, ctx) { |
82 |
| - const url = config.url(); |
83 |
| - const options = { |
84 |
| - ...test.clientOptions, |
85 |
| - heartbeatFrequencyMS: 100, |
86 |
| - monitorCommands: true, |
87 |
| - minPoolSize: 10 |
88 |
| - }; |
89 |
| - |
90 |
| - ctx.failPointName = test.failPoint && test.failPoint.configureFailPoint; |
91 |
| - |
92 |
| - const client = config.newClient(url, options); |
93 |
| - await client.connect(); |
94 |
| - |
95 |
| - ctx.client = client; |
96 |
| - ctx.db = client.db(config.db); |
97 |
| - ctx.collection = ctx.db.collection(`retryable_writes_test_${config.name}_${test.operation.name}`); |
98 |
| - |
99 |
| - try { |
100 |
| - await ctx.collection.drop(); |
101 |
| - } catch (error) { |
102 |
| - if (!error.message.match(/ns not found/)) { |
103 |
| - throw error; |
104 |
| - } |
105 |
| - } |
106 |
| - |
107 |
| - if (Array.isArray(scenario.data) && scenario.data.length) { |
108 |
| - await ctx.collection.insertMany(scenario.data); |
109 |
| - } |
110 |
| - |
111 |
| - if (test.failPoint) { |
112 |
| - await ctx.client.db('admin').command(test.failPoint); |
113 |
| - } |
114 |
| -} |
115 |
| - |
116 |
| -async function executeScenarioTest(test, ctx: RetryableWriteTestContext) { |
117 |
| - const args = generateArguments(test); |
118 |
| - |
119 |
| - // In case the spec files or our API changes |
120 |
| - expect(ctx.collection).to.have.property(test.operation.name).that.is.a('function'); |
121 |
| - |
122 |
| - // TODO(NODE-4033): Collect command started events and assert txn number existence or omission |
123 |
| - // have to add logic to handle bulkWrites which emit multiple command started events |
124 |
| - const resultOrError = await ctx.collection[test.operation.name](...args).catch(error => error); |
125 |
| - |
126 |
| - const outcome = test.outcome && test.outcome.result; |
127 |
| - const errorLabelsContain = outcome && outcome.errorLabelsContain; |
128 |
| - const errorLabelsOmit = outcome && outcome.errorLabelsOmit; |
129 |
| - const hasResult = outcome && !errorLabelsContain && !errorLabelsOmit; |
130 |
| - if (test.outcome.error) { |
131 |
| - const thrownError = resultOrError; |
132 |
| - expect(thrownError, `${test.operation.name} was supposed to fail but did not!`).to.exist; |
133 |
| - expect(thrownError).to.have.property('message'); |
134 |
| - |
135 |
| - if (hasResult) { |
136 |
| - expect(thrownError.result).to.matchMongoSpec(test.outcome.result); |
137 |
| - } |
138 |
| - |
139 |
| - if (errorLabelsContain) { |
140 |
| - expect(thrownError.errorLabels).to.include.members(errorLabelsContain); |
141 |
| - } |
142 |
| - |
143 |
| - if (errorLabelsOmit) { |
144 |
| - for (const label of errorLabelsOmit) { |
145 |
| - expect(thrownError.errorLabels).to.not.contain(label); |
146 |
| - } |
147 |
| - } |
148 |
| - } else if (test.outcome.result) { |
149 |
| - expect(resultOrError, resultOrError.stack).to.not.be.instanceOf(Error); |
150 |
| - const result = resultOrError; |
151 |
| - const expected = test.outcome.result; |
152 |
| - |
153 |
| - const actual = result.value ?? result; |
154 |
| - // Some of our result classes contain the optional 'acknowledged' property which is |
155 |
| - // not part of the test expectations. |
156 |
| - for (const property in expected) { |
157 |
| - expect(actual).to.have.deep.property(property, expected[property]); |
158 |
| - } |
159 |
| - } |
160 |
| - |
161 |
| - if (test.outcome.collection) { |
162 |
| - const collectionResults = await ctx.collection.find({}).toArray(); |
163 |
| - expect(collectionResults).to.deep.equal(test.outcome.collection.data); |
164 |
| - } |
165 |
| -} |
166 |
| - |
167 |
| -// Helper Functions |
168 |
| - |
169 |
| -/** Transforms the arguments from a test into actual arguments for our function calls */ |
170 |
| -function generateArguments(test) { |
171 |
| - const args = []; |
172 |
| - |
173 |
| - if (test.operation.arguments) { |
174 |
| - const options: Record<string, any> = {}; |
175 |
| - for (const arg of Object.keys(test.operation.arguments)) { |
176 |
| - if (arg === 'requests') { |
177 |
| - /** Transforms a request arg into a bulk write operation */ |
178 |
| - args.push( |
179 |
| - test.operation.arguments[arg].map(({ name, arguments: args }) => ({ [name]: args })) |
180 |
| - ); |
181 |
| - } else if (arg === 'upsert') { |
182 |
| - options.upsert = test.operation.arguments[arg]; |
183 |
| - } else if (arg === 'returnDocument') { |
184 |
| - options.returnDocument = test.operation.arguments[arg].toLowerCase(); |
185 |
| - } else { |
186 |
| - args.push(test.operation.arguments[arg]); |
187 |
| - } |
188 |
| - } |
189 |
| - |
190 |
| - if (Object.keys(options).length > 0) { |
191 |
| - args.push(options); |
192 |
| - } |
193 |
| - } |
194 |
| - |
195 |
| - return args; |
196 |
| -} |
197 |
| - |
198 |
| -/** Runs a command that turns off a fail point */ |
199 |
| -async function turnOffFailPoint(client, name) { |
200 |
| - return await client.db('admin').command({ |
201 |
| - configureFailPoint: name, |
202 |
| - mode: 'off' |
203 |
| - }); |
204 |
| -} |
205 |
| - |
206 |
| -describe('Retryable Writes (unified)', function () { |
| 6 | +describe.only('Retryable Writes (unified)', function () { |
207 | 7 | runUnifiedSuite(loadSpecTests(path.join('retryable-writes', 'unified')));
|
208 | 8 | });
|
0 commit comments