Skip to content

Commit 296cf6b

Browse files
committed
test(NODE-3049): drivers atlas testing
1 parent a17b0af commit 296cf6b

File tree

11 files changed

+440
-14
lines changed

11 files changed

+440
-14
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@
143143
"check:unit": "mocha test/unit",
144144
"check:ts": "node ./node_modules/typescript/bin/tsc -v && node ./node_modules/typescript/bin/tsc --noEmit",
145145
"check:atlas": "mocha --config test/manual/mocharc.json test/manual/atlas_connectivity.test.js",
146+
"check:drivers-atlas-testing": "mocha --config test/mocha_mongodb.json test/atlas/drivers_atlas_testing.test.ts",
146147
"check:adl": "mocha --config test/mocha_mongodb.json test/manual/atlas-data-lake-testing",
147148
"check:aws": "nyc mocha --config test/mocha_mongodb.json test/integration/auth/mongodb_aws.test.ts",
148149
"check:oidc": "mocha --config test/mocha_mongodb.json test/manual/mongodb_oidc.prose.test.ts",
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { runUnifiedSuite } from '../tools/unified-spec-runner/runner';
2+
3+
describe('Node Driver Atlas Testing', async function () {
4+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
5+
const spec = JSON.parse(process.env.WORKLOAD_SPECIFICATION!);
6+
runUnifiedSuite([spec]);
7+
});

test/tools/reporter/mongodb_reporter.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ class MongoDBMochaReporter extends mocha.reporters.Spec {
103103
catchErr(test => this.testEnd(test))
104104
);
105105

106-
process.on('SIGINT', () => this.end(true));
106+
process.prependListener('SIGINT', () => this.end(true));
107107
}
108108

109109
start() {}
@@ -183,7 +183,11 @@ class MongoDBMochaReporter extends mocha.reporters.Spec {
183183
} catch (error) {
184184
console.error(chalk.red(`Failed to output xunit report! ${error}`));
185185
} finally {
186-
if (ctrlC) process.exit(1);
186+
// Dont exit the process on Astrolabe testing, let it interrupt and
187+
// finish naturally.
188+
if (!process.env.WORKLOAD_SPECIFICATION) {
189+
process.exit(1);
190+
}
187191
}
188192
}
189193

test/tools/runner/config.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,11 @@ export class TestConfiguration {
159159
}
160160

161161
newClient(dbOptions?: string | Record<string, any>, serverOptions?: Record<string, any>) {
162+
if (process.env.DRIVERS_ATLAS_TESTING_URI) {
163+
console.log('Using drivers Atlas testing URI', process.env.DRIVERS_ATLAS_TESTING_URI);
164+
return new MongoClient(process.env.DRIVERS_ATLAS_TESTING_URI);
165+
}
166+
162167
serverOptions = Object.assign({}, getEnvironmentalOptions(), serverOptions);
163168

164169
// support MongoClient constructor form (url, options) for `newClient`
@@ -258,6 +263,11 @@ export class TestConfiguration {
258263
...options
259264
};
260265

266+
if (process.env.DRIVERS_ATLAS_TESTING_URI) {
267+
console.log('Using drivers Atlas testing URI', process.env.DRIVERS_ATLAS_TESTING_URI);
268+
return process.env.DRIVERS_ATLAS_TESTING_URI;
269+
}
270+
261271
const FILLER_HOST = 'fillerHost';
262272

263273
const protocol = this.isServerless ? 'mongodb+srv' : 'mongodb';
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { writeFile } from 'node:fs/promises';
2+
3+
import * as path from 'path';
4+
5+
import type { EntitiesMap } from './entities';
6+
import { trace } from './runner';
7+
8+
/**
9+
* Writes the entities saved from the loop operations run in the
10+
* Astrolabe workload executor to the required files.
11+
*/
12+
export class AstrolabeResultsWriter {
13+
constructor(private entities: EntitiesMap) {
14+
this.entities = entities;
15+
}
16+
17+
async write(): Promise<void> {
18+
// Write the events.json to the execution directory.
19+
const errors = this.entities.getEntity('errors', 'errors', false);
20+
const failures = this.entities.getEntity('failures', 'failures', false);
21+
const events = this.entities.getEntity('events', 'events', false);
22+
const iterations = this.entities.getEntity('iterations', 'iterations', false);
23+
const successes = this.entities.getEntity('successes', 'successes', false);
24+
25+
// Write the events.json to the execution directory.
26+
trace('writing events.json');
27+
await writeFile(
28+
path.join(process.env.OUTPUT_DIRECTORY ?? '', 'events.json'),
29+
JSON.stringify({ events: events ?? [], errors: errors ?? [], failures: failures ?? [] })
30+
);
31+
32+
// Write the results.json to the execution directory.
33+
trace('writing results.json');
34+
await writeFile(
35+
path.join(process.env.OUTPUT_DIRECTORY ?? '', 'results.json'),
36+
JSON.stringify({
37+
numErrors: errors?.length ?? 0,
38+
numFailures: failures?.length ?? 0,
39+
numSuccesses: successes ?? 0,
40+
numIterations: iterations ?? 0
41+
})
42+
);
43+
}
44+
}

test/tools/unified-spec-runner/entities.ts

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { EventEmitter } from 'events';
55
import {
66
AbstractCursor,
77
ChangeStream,
8+
ClientEncryption,
89
ClientSession,
910
Collection,
1011
type CommandFailedEvent,
@@ -37,13 +38,9 @@ import {
3738
} from '../../mongodb';
3839
import { ejson, getEnvironmentalOptions } from '../../tools/utils';
3940
import type { TestConfiguration } from '../runner/config';
41+
import { EntityEventRegistry } from './entity_event_registry';
4042
import { trace } from './runner';
41-
import type {
42-
ClientEncryption,
43-
ClientEntity,
44-
EntityDescription,
45-
ExpectedLogMessage
46-
} from './schema';
43+
import type { ClientEntity, EntityDescription, ExpectedLogMessage } from './schema';
4744
import {
4845
createClientEncryption,
4946
makeConnectionString,
@@ -357,9 +354,10 @@ export type Entity =
357354
| AbstractCursor
358355
| UnifiedChangeStream
359356
| GridFSBucket
357+
| Document
360358
| ClientEncryption
361359
| TopologyDescription // From recordTopologyDescription operation
362-
| Document; // Results from operations
360+
| number;
363361

364362
export type EntityCtor =
365363
| typeof UnifiedMongoClient
@@ -370,7 +368,7 @@ export type EntityCtor =
370368
| typeof AbstractCursor
371369
| typeof GridFSBucket
372370
| typeof UnifiedThread
373-
| ClientEncryption;
371+
| typeof ClientEncryption;
374372

375373
export type EntityTypeId =
376374
| 'client'
@@ -381,18 +379,26 @@ export type EntityTypeId =
381379
| 'thread'
382380
| 'cursor'
383381
| 'stream'
384-
| 'clientEncryption';
382+
| 'clientEncryption'
383+
| 'errors'
384+
| 'failures'
385+
| 'events'
386+
| 'iterations'
387+
| 'successes';
385388

386389
const ENTITY_CTORS = new Map<EntityTypeId, EntityCtor>();
387390
ENTITY_CTORS.set('client', UnifiedMongoClient);
388391
ENTITY_CTORS.set('db', Db);
392+
ENTITY_CTORS.set('clientEncryption', ClientEncryption);
389393
ENTITY_CTORS.set('collection', Collection);
390394
ENTITY_CTORS.set('session', ClientSession);
391395
ENTITY_CTORS.set('bucket', GridFSBucket);
392396
ENTITY_CTORS.set('thread', UnifiedThread);
393397
ENTITY_CTORS.set('cursor', AbstractCursor);
394398
ENTITY_CTORS.set('stream', ChangeStream);
395399

400+
const NO_INSTANCE_CHECK = ['errors', 'failures', 'events', 'successes', 'iterations'];
401+
396402
export class EntitiesMap<E = Entity> extends Map<string, E> {
397403
failPoints: FailPointMap;
398404

@@ -435,19 +441,25 @@ export class EntitiesMap<E = Entity> extends Map<string, E> {
435441
getEntity(type: 'thread', key: string, assertExists?: boolean): UnifiedThread;
436442
getEntity(type: 'cursor', key: string, assertExists?: boolean): AbstractCursor;
437443
getEntity(type: 'stream', key: string, assertExists?: boolean): UnifiedChangeStream;
444+
getEntity(type: 'iterations', key: string, assertExists?: boolean): number;
445+
getEntity(type: 'successes', key: string, assertExists?: boolean): number;
446+
getEntity(type: 'errors', key: string, assertExists?: boolean): Document[];
447+
getEntity(type: 'failures', key: string, assertExists?: boolean): Document[];
448+
getEntity(type: 'events', key: string, assertExists?: boolean): Document[];
438449
getEntity(type: 'clientEncryption', key: string, assertExists?: boolean): ClientEncryption;
439450
getEntity(type: EntityTypeId, key: string, assertExists = true): Entity | undefined {
440451
const entity = this.get(key);
441452
if (!entity) {
442453
if (assertExists) throw new Error(`Entity '${key}' does not exist`);
443454
return;
444455
}
445-
if (type === 'clientEncryption') {
446-
// we do not have instanceof checking here since csfle might not be installed
456+
if (NO_INSTANCE_CHECK.includes(type)) {
457+
// Skip constructor checks for interfaces.
447458
return entity;
448459
}
449460
const ctor = ENTITY_CTORS.get(type);
450461
if (!ctor) {
462+
console.log('CTORS', type, ENTITY_CTORS);
451463
throw new Error(`Unknown type ${type}`);
452464
}
453465
if (!(entity instanceof ctor)) {
@@ -499,6 +511,7 @@ export class EntitiesMap<E = Entity> extends Map<string, E> {
499511
entity.client.uriOptions
500512
);
501513
const client = new UnifiedMongoClient(uri, entity.client);
514+
new EntityEventRegistry(client, entity.client, map).register();
502515
try {
503516
await client.connect();
504517
} catch (error) {
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import {
2+
COMMAND_FAILED,
3+
COMMAND_STARTED,
4+
COMMAND_SUCCEEDED,
5+
CONNECTION_CHECK_OUT_FAILED,
6+
CONNECTION_CHECK_OUT_STARTED,
7+
CONNECTION_CHECKED_IN,
8+
CONNECTION_CHECKED_OUT,
9+
CONNECTION_CLOSED,
10+
CONNECTION_CREATED,
11+
CONNECTION_POOL_CLEARED,
12+
CONNECTION_POOL_CLOSED,
13+
CONNECTION_POOL_CREATED,
14+
CONNECTION_POOL_READY,
15+
CONNECTION_READY
16+
} from '../../mongodb';
17+
import { type EntitiesMap, type UnifiedMongoClient } from './entities';
18+
import { type ClientEntity } from './schema';
19+
20+
/**
21+
* Maps the names of the events the unified runner passes and maps
22+
* them to the names of the events emitted in the driver.
23+
*/
24+
const MAPPINGS = {
25+
PoolCreatedEvent: CONNECTION_POOL_CREATED,
26+
PoolReadyEvent: CONNECTION_POOL_READY,
27+
PoolClearedEvent: CONNECTION_POOL_CLEARED,
28+
PoolClosedEvent: CONNECTION_POOL_CLOSED,
29+
ConnectionCreatedEvent: CONNECTION_CREATED,
30+
ConnectionReadyEvent: CONNECTION_READY,
31+
ConnectionClosedEvent: CONNECTION_CLOSED,
32+
ConnectionCheckOutStartedEvent: CONNECTION_CHECK_OUT_STARTED,
33+
ConnectionCheckOutFailedEvent: CONNECTION_CHECK_OUT_FAILED,
34+
ConnectionCheckedOutEvent: CONNECTION_CHECKED_OUT,
35+
ConnectionCheckedInEvent: CONNECTION_CHECKED_IN,
36+
CommandStartedEvent: COMMAND_STARTED,
37+
CommandSucceededEvent: COMMAND_SUCCEEDED,
38+
CommandFailedEvent: COMMAND_FAILED
39+
};
40+
41+
/**
42+
* Registers events that need to be stored in the entities map, since
43+
* the UnifiedMongoClient does not contain a ciclical dependency on the
44+
* entities map itself.
45+
*/
46+
export class EntityEventRegistry {
47+
constructor(
48+
private client: UnifiedMongoClient,
49+
private clientEntity: ClientEntity,
50+
private entitiesMap: EntitiesMap
51+
) {
52+
this.client = client;
53+
this.clientEntity = clientEntity;
54+
this.entitiesMap = entitiesMap;
55+
}
56+
57+
/**
58+
* Connect the event listeners on the client and the entities map.
59+
*/
60+
register(): void {
61+
if (this.clientEntity.storeEventsAsEntities) {
62+
for (const { id, events } of this.clientEntity.storeEventsAsEntities) {
63+
this.entitiesMap.set(id, []);
64+
for (const eventName of events) {
65+
// Need to map the event names to the Node event names.
66+
this.client.on(MAPPINGS[eventName], () => {
67+
this.entitiesMap.getEntity('events', id).push({
68+
name: eventName,
69+
observedAt: Date.now()
70+
});
71+
});
72+
}
73+
}
74+
}
75+
}
76+
}

test/tools/unified-spec-runner/operations.ts

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
/* eslint-disable @typescript-eslint/no-non-null-assertion */
33
import { once, Writable } from 'node:stream';
44

5-
import { expect } from 'chai';
5+
import { AssertionError, expect } from 'chai';
66

77
import {
88
AbstractCursor,
@@ -377,6 +377,79 @@ operations.set('listIndexes', async ({ entities, operation }) => {
377377
return collection.listIndexes(operation.arguments!).toArray();
378378
});
379379

380+
operations.set('loop', async ({ entities, operation, client, testConfig }) => {
381+
const controller = new AbortController();
382+
// We always want the process to exit on SIGINT last, so all other
383+
// SIGINT events listeners must be prepended.
384+
process.prependListener('SIGINT', () => {
385+
console.log('Receiving SIGINT to process.');
386+
controller.abort('Process received SIGINT, aborting operation loop.');
387+
});
388+
const args = operation.arguments!;
389+
const storeIterationsAsEntity = args.storeIterationsAsEntity;
390+
const storeSuccessesAsEntity = args.storeSuccessesAsEntity;
391+
const storeErrorsAsEntity = args.storeErrorsAsEntity;
392+
const storeFailuresAsEntity = args.storeFailuresAsEntity;
393+
394+
if (storeErrorsAsEntity) {
395+
entities.set(storeErrorsAsEntity, []);
396+
}
397+
if (storeFailuresAsEntity) {
398+
entities.set(storeFailuresAsEntity, []);
399+
}
400+
401+
let iterations = 0;
402+
let successes = 0;
403+
while (!controller.signal.aborted) {
404+
if (storeIterationsAsEntity) {
405+
entities.set(storeIterationsAsEntity, iterations++);
406+
}
407+
for (const op of args.operations) {
408+
console.log('op', op);
409+
try {
410+
await executeOperationAndCheck(op, entities, client, testConfig);
411+
if (storeSuccessesAsEntity) {
412+
entities.set(storeSuccessesAsEntity, successes++);
413+
}
414+
} catch (error) {
415+
console.log('error', error);
416+
// From the unified spec:
417+
// If neither storeErrorsAsEntity nor storeFailuresAsEntity are specified,
418+
// the loop MUST terminate and raise the error/failure (i.e. the error/failure
419+
// will interrupt the test).
420+
if (!storeErrorsAsEntity && !storeFailuresAsEntity) {
421+
entities.set('errors', [
422+
{
423+
error: 'Neither storeErrorsAsEntity or storeFailuresAsEntity specified',
424+
time: Date.now()
425+
}
426+
]);
427+
controller.abort('Neither storeErrorsAsEntity or storeFailuresAsEntity specified');
428+
return;
429+
}
430+
431+
// From the unified spec format specification for the loop operation:
432+
// A failure is when the result or outcome of an operation executed by the test
433+
// runner differs from its expected outcome. For example, an expectResult assertion
434+
// failing to match a BSON document or an expectError assertion failing to match
435+
// an error message would be considered a failure.
436+
// An error is any other type of error raised by the test runner. For example, an
437+
// unsupported operation or inability to resolve an entity name would be considered
438+
// an error.
439+
if (storeFailuresAsEntity && error instanceof AssertionError) {
440+
entities
441+
.getEntity('failures', storeFailuresAsEntity)
442+
.push({ error: error.message, time: Date.now() });
443+
} else if (storeErrorsAsEntity) {
444+
entities
445+
.getEntity('errors', storeErrorsAsEntity)
446+
.push({ error: error.message, time: Date.now() });
447+
}
448+
}
449+
}
450+
}
451+
});
452+
380453
operations.set('replaceOne', async ({ entities, operation }) => {
381454
const collection = entities.getEntity('collection', operation.object);
382455
const { filter, replacement, ...opts } = operation.arguments!;

0 commit comments

Comments
 (0)