Skip to content

Commit 177a4fc

Browse files
authored
test(NODE-3049): add astrolabe support to the UnifiedTestRunner (#3805)
1 parent 3c4feae commit 177a4fc

File tree

12 files changed

+464
-17
lines changed

12 files changed

+464
-17
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: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { runUnifiedSuite } from '../tools/unified-spec-runner/runner';
2+
3+
describe('Node Driver Atlas Testing', async function () {
4+
// Astrolabe can, well, take some time. In some cases up to 800s to
5+
// reconfigure clusters.
6+
this.timeout(0);
7+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
8+
const spec = JSON.parse(process.env.WORKLOAD_SPECIFICATION!);
9+
runUnifiedSuite([spec]);
10+
});

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+
if (ctrlC) process.exit(1);
190+
}
187191
}
188192
}
189193

test/tools/runner/config.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -376,3 +376,18 @@ export class AtlasTestConfiguration extends TestConfiguration {
376376
return process.env.MONGODB_URI!;
377377
}
378378
}
379+
380+
/**
381+
* Test configuration specific to Astrolabe testing.
382+
*/
383+
export class AstrolabeTestConfiguration extends TestConfiguration {
384+
override newClient(): MongoClient {
385+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
386+
return new MongoClient(process.env.DRIVERS_ATLAS_TESTING_URI!);
387+
}
388+
389+
override url(): string {
390+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
391+
return process.env.DRIVERS_ATLAS_TESTING_URI!;
392+
}
393+
}

test/tools/runner/hooks/configuration.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ require('source-map-support').install({
77
const path = require('path');
88
const fs = require('fs');
99
const { MongoClient } = require('../../../mongodb');
10-
const { TestConfiguration } = require('../config');
10+
const { AstrolabeTestConfiguration, TestConfiguration } = require('../config');
1111
const { getEnvironmentalOptions } = require('../../utils');
1212
const mock = require('../../mongodb-mock/index');
1313
const { inspect } = require('util');
@@ -105,6 +105,10 @@ const skipBrokenAuthTestBeforeEachHook = function ({ skippedTests } = { skippedT
105105
};
106106

107107
const testConfigBeforeHook = async function () {
108+
if (process.env.DRIVERS_ATLAS_TESTING_URI) {
109+
this.configuration = new AstrolabeTestConfiguration(process.env.DRIVERS_ATLAS_TESTING_URI, {});
110+
return;
111+
}
108112
// TODO(NODE-5035): Implement OIDC support. Creating the MongoClient will fail
109113
// with "MongoInvalidArgumentError: AuthMechanism 'MONGODB-OIDC' not supported"
110114
// as is expected until that ticket goes in. Then this condition gets removed.
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: 23 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,15 +441,20 @@ 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);
@@ -499,6 +510,7 @@ export class EntitiesMap<E = Entity> extends Map<string, E> {
499510
entity.client.uriOptions
500511
);
501512
const client = new UnifiedMongoClient(uri, entity.client);
513+
new EntityEventRegistry(client, entity.client, map).register();
502514
try {
503515
await client.connect();
504516
} 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 cyclical 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+
}

0 commit comments

Comments
 (0)