-
Notifications
You must be signed in to change notification settings - Fork 1.8k
/
Copy pathrunner.ts
347 lines (312 loc) · 12 KB
/
runner.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import { AssertionError, expect } from 'chai';
import { gte as semverGte, satisfies as semverSatisfies } from 'semver';
import {
type MongoClient,
MONGODB_ERROR_CODES,
MongoParseError,
MongoServerError,
ns,
ReadPreference,
TopologyType
} from '../../mongodb';
import { ejson } from '../utils';
import { AstrolabeResultsWriter } from './astrolabe_results_writer';
import { EntitiesMap, type UnifiedMongoClient } from './entities';
import { compareLogs, filterIgnoredMessages, matchesEvents } from './match';
import { executeOperationAndCheck } from './operations';
import * as uni from './schema';
import { isAnyRequirementSatisfied, patchVersion, zip } from './unified-utils';
export function trace(message: string): void {
if (process.env.UTR_TRACE) {
console.error(` > ${message}`);
}
}
async function isAtlasDataLake(client: MongoClient): Promise<boolean> {
const buildInfo = await client.db('admin').admin().buildInfo();
return 'dataLake' in buildInfo;
}
async function terminateOpenTransactions(client: MongoClient) {
// Note: killAllSession is not supported on serverless, see CLOUDP-84298
// killAllSession is not allowed in ADL either.
if (process.env.SERVERLESS || (await isAtlasDataLake(client))) {
return;
}
// TODO(NODE-3491): on sharded clusters this has to be run on each mongos
try {
await client.db().admin().command({ killAllSessions: [] });
} catch (err) {
if (err.code === 11601 || err.code === 13 || err.code === 59) {
return;
}
throw err;
}
}
/*
* @param skipFilter - a function that returns null if the test should be run,
* or a skip reason if the test should be skipped
*/
async function runUnifiedTest(
ctx: Mocha.Context,
unifiedSuite: uni.UnifiedSuite,
test: uni.Test,
skipFilter: uni.TestFilter = () => false
): Promise<void> {
// Some basic expectations we can catch early
expect(test).to.exist;
expect(unifiedSuite).to.exist;
expect(ctx).to.exist;
expect(ctx.configuration).to.exist;
expect(ctx.test, 'encountered a unified test where the test is undefined').to.exist;
expect(ctx.currentTest, '`runUnifiedTest` can only be used inside of it blocks').to.be.undefined;
const schemaVersion = patchVersion(unifiedSuite.schemaVersion);
expect(semverSatisfies(schemaVersion, uni.SupportedVersion)).to.be.true;
const skipReason = test.skipReason ?? skipFilter(test, ctx.configuration);
if (typeof skipReason === 'string') {
if (skipReason.length === 0) {
expect.fail(`Test was skipped with an empty skip reason: ${test.description}`);
}
ctx.test!.skipReason = skipReason;
ctx.skip();
}
let utilClient;
if (ctx.configuration.isLoadBalanced) {
// The util client can always point at the single mongos LB frontend.
utilClient = ctx.configuration.newClient(ctx.configuration.singleMongosLoadBalancerUri);
} else if (process.env.UTIL_CLIENT_USER && process.env.UTIL_CLIENT_PASSWORD) {
// For OIDC tests the MONGODB_URI is the base admin URI that the util client will use.
utilClient = ctx.configuration.newClient(process.env.MONGODB_URI, {
auth: {
username: process.env.UTIL_CLIENT_USER,
password: process.env.UTIL_CLIENT_PASSWORD
}
});
} else {
utilClient = ctx.configuration.newClient();
}
let entities: EntitiesMap | undefined;
try {
trace('\n starting test:');
try {
await utilClient.connect();
} catch (error) {
console.error(
ejson`failed to connect utilClient ${utilClient.s.url} - ${utilClient.options}`
);
throw error;
}
// terminate all sessions before each test suite
await terminateOpenTransactions(utilClient);
// Must fetch parameters before checking runOnRequirements
ctx.configuration.parameters = (await isAtlasDataLake(utilClient))
? {}
: await utilClient.db().admin().command({ getParameter: '*' });
// If test.runOnRequirements is specified, the test runner MUST skip the test unless one or more
// runOnRequirement objects are satisfied.
const suiteRequirements = unifiedSuite.runOnRequirements ?? [];
const testRequirements = test.runOnRequirements ?? [];
trace('satisfiesRequirements');
const isSomeSuiteRequirementMet =
!suiteRequirements.length ||
(await isAnyRequirementSatisfied(ctx, suiteRequirements, utilClient));
const isSomeTestRequirementMet =
isSomeSuiteRequirementMet &&
(!testRequirements.length ||
(await isAnyRequirementSatisfied(ctx, testRequirements, utilClient)));
if (!isSomeTestRequirementMet) {
return ctx.skip();
}
// If initialData is specified, for each collectionData therein the test runner MUST drop the
// collection and insert the specified documents (if any) using a "majority" write concern. If no
// documents are specified, the test runner MUST create the collection with a "majority" write concern.
// The test runner MUST use the internal MongoClient for these operations.
if (unifiedSuite.initialData) {
trace('initialData');
for (const { databaseName, collectionName } of unifiedSuite.initialData) {
const db = utilClient.db(databaseName);
const collection = db.collection(collectionName, {
writeConcern: { w: 'majority' }
});
trace('listCollections');
const collectionList = await db.listCollections({ name: collectionName }).toArray();
if (collectionList.length !== 0) {
trace('drop');
expect(await collection.drop()).to.be.true;
}
}
for (const {
databaseName,
collectionName,
createOptions,
documents = []
} of unifiedSuite.initialData) {
const db = utilClient.db(databaseName);
const collection = db.collection(collectionName, {
writeConcern: { w: 'majority' }
});
if (createOptions || !documents.length) {
trace('createCollection');
const options = createOptions ?? {};
await db.createCollection(collectionName, {
...options,
writeConcern: { w: 'majority' }
});
}
if (documents.length > 0) {
trace('insertMany');
await collection.insertMany(documents);
}
}
}
const ping = await utilClient.db().admin().command({ ping: 1 });
const clusterTime = ping.$clusterTime;
trace('createEntities');
entities = await EntitiesMap.createEntities(
ctx.configuration,
clusterTime,
unifiedSuite.createEntities
);
// Workaround for SERVER-39704:
// test runners MUST execute a non-transactional distinct command on
// each mongos server before running any test that might execute distinct within a transaction.
// To ease the implementation, test runners MAY execute distinct before every test.
const topologyType = ctx.configuration.topologyType;
if (topologyType === TopologyType.Sharded || topologyType === TopologyType.LoadBalanced) {
for (const [, collection] of entities.mapOf('collection')) {
try {
await utilClient.db(ns(collection.namespace).db).command({
distinct: collection.collectionName,
key: '_id'
});
} catch (err) {
// https://jira.mongodb.org/browse/SERVER-60533
// distinct throws namespace not found errors on servers 5.2.2 and under.
// For now, we skip these errors to be addressed in NODE-4238.
if (err.code !== MONGODB_ERROR_CODES.NamespaceNotFound) {
throw err;
}
const serverVersion = ctx.configuration.version;
if (semverGte(serverVersion, '5.2.2')) {
throw err;
}
}
}
}
for (const operation of test.operations) {
trace(operation.name);
try {
await executeOperationAndCheck(operation, entities, utilClient, ctx.configuration);
} catch (e) {
// clean up all sessions on failed test, and rethrow
await terminateOpenTransactions(utilClient);
throw e;
}
}
const clientList = new Map<string, UnifiedMongoClient>();
// If any event listeners were enabled on any client entities,
// the test runner MUST now disable those event listeners.
for (const [id, client] of entities.mapOf('client')) {
client.stopCapturingEvents();
clientList.set(id, client);
}
if (test.expectEvents) {
for (const expectedEventsForClient of test.expectEvents) {
const clientId = expectedEventsForClient.client;
const eventType = expectedEventsForClient.eventType;
// If no event type is provided it defaults to 'command', so just
// check for 'cmap' here for now.
const testClient = clientList.get(clientId);
expect(testClient, `No client entity found with id ${clientId}`).to.exist;
matchesEvents(
expectedEventsForClient,
testClient!.getCapturedEvents(eventType ?? 'command'),
entities
);
}
}
if (test.expectLogMessages) {
for (const expectedLogsForClient of test.expectLogMessages) {
const clientId = expectedLogsForClient.client;
const testClient = clientList.get(clientId);
expect(testClient, `No client entity found with id ${clientId}`).to.exist;
const filteredTestClientLogs = expectedLogsForClient.ignoreMessages
? filterIgnoredMessages(
expectedLogsForClient.ignoreMessages,
testClient!.collectedLogs,
entities
)
: testClient!.collectedLogs;
compareLogs(
expectedLogsForClient.messages,
filteredTestClientLogs,
entities,
expectedLogsForClient.ignoreExtraMessages
);
}
}
if (test.outcome) {
for (const collectionData of test.outcome) {
const collection = utilClient
.db(collectionData.databaseName)
.collection(collectionData.collectionName);
const findOpts = {
readConcern: 'local' as const,
readPreference: ReadPreference.primary,
sort: { _id: 'asc' as const }
};
const documents = await collection.find({}, findOpts).toArray();
expect(documents).to.have.lengthOf(collectionData.documents.length);
for (const [expected, actual] of zip(collectionData.documents, documents)) {
expect(actual).to.deep.include(expected);
}
}
}
} finally {
await utilClient.close();
// For astrolabe testing we need to write the entities to files.
if (process.env.WORKLOAD_SPECIFICATION) {
const writer = new AstrolabeResultsWriter(entities!);
await writer.write();
}
await entities?.cleanup();
}
}
/**
*
* @param skipFilter - a function that returns null if the test should be run,
* or a skip reason if the test should be skipped
*/
export function runUnifiedSuite(
specTests: uni.UnifiedSuite[],
skipFilter: uni.TestFilter = () => false,
expectRuntimeError = false
): void {
for (const unifiedSuite of specTests) {
context(String(unifiedSuite.description), function () {
for (const [index, test] of unifiedSuite.tests.entries()) {
for (let i = 0; i < 1000; ++i) {
it(
String(test.description === '' ? `Test ${index}` : test.description) + i,
async function () {
if (expectRuntimeError) {
const error = await runUnifiedTest(this, unifiedSuite, test, skipFilter).catch(
error => error
);
expect(error).to.satisfy(value => {
return (
value instanceof AssertionError ||
value instanceof MongoServerError ||
value instanceof TypeError ||
value instanceof MongoParseError
);
});
} else {
await runUnifiedTest(this, unifiedSuite, test, skipFilter);
}
}
);
}
}
});
}
}