Skip to content

Commit b5f19ef

Browse files
authored
test(NODE-2287): complete unified test runner (#2748)
Completes the unified runner by passing all the poc spec tests.
1 parent 2303b41 commit b5f19ef

File tree

8 files changed

+139
-102
lines changed

8 files changed

+139
-102
lines changed

src/apm.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export class Instrumentation extends EventEmitter {
3030
const instrumentation = this;
3131
mongoClientClass.prototype.connect = function (this: MongoClient, callback: Callback) {
3232
// override monitorCommands to be switched on
33-
this.s.options = { ...(this.s.options ?? {}), monitorCommands: true };
33+
this.monitorCommands = true;
3434

3535
this.on(Connection.COMMAND_STARTED, event =>
3636
instrumentation.emit(Instrumentation.STARTED, event)

src/mongo_client.ts

Lines changed: 34 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -230,14 +230,14 @@ export type WithSessionCallback = (session: ClientSession) => Promise<any> | voi
230230
/** @internal */
231231
export interface MongoClientPrivate {
232232
url: string;
233-
options?: MongoClientOptions;
234233
sessions: Set<ClientSession>;
235-
readConcern?: ReadConcern;
236-
writeConcern?: WriteConcern;
237-
readPreference: ReadPreference;
238234
bsonOptions: BSONSerializeOptions;
239235
namespace: MongoDBNamespace;
240-
logger: Logger;
236+
readonly options?: MongoOptions;
237+
readonly readConcern?: ReadConcern;
238+
readonly writeConcern?: WriteConcern;
239+
readonly readPreference: ReadPreference;
240+
readonly logger: Logger;
241241
}
242242

243243
const kOptions = Symbol('options');
@@ -293,29 +293,36 @@ export class MongoClient extends EventEmitter {
293293
*/
294294
[kOptions]: MongoOptions;
295295

296-
// debugging
297-
originalUri;
298-
originalOptions;
299-
300296
constructor(url: string, options?: MongoClientOptions) {
301297
super();
302298

303-
this.originalUri = url;
304-
this.originalOptions = options;
305-
306299
this[kOptions] = parseOptions(url, this, options);
307300

301+
// eslint-disable-next-line @typescript-eslint/no-this-alias
302+
const client = this;
303+
308304
// The internal state
309305
this.s = {
310306
url,
311-
options: this[kOptions],
312307
sessions: new Set(),
313-
readConcern: this[kOptions].readConcern,
314-
writeConcern: this[kOptions].writeConcern,
315-
readPreference: this[kOptions].readPreference,
316308
bsonOptions: resolveBSONOptions(this[kOptions]),
317309
namespace: ns('admin'),
318-
logger: this[kOptions].logger
310+
311+
get options() {
312+
return client[kOptions];
313+
},
314+
get readConcern() {
315+
return client[kOptions].readConcern;
316+
},
317+
get writeConcern() {
318+
return client[kOptions].writeConcern;
319+
},
320+
get readPreference() {
321+
return client[kOptions].readPreference;
322+
},
323+
get logger() {
324+
return client[kOptions].logger;
325+
}
319326
};
320327
}
321328

@@ -326,6 +333,16 @@ export class MongoClient extends EventEmitter {
326333
get serverApi(): Readonly<ServerApi | undefined> {
327334
return this[kOptions].serverApi && Object.freeze({ ...this[kOptions].serverApi });
328335
}
336+
/**
337+
* Intended for APM use only
338+
* @internal
339+
*/
340+
get monitorCommands(): boolean {
341+
return this[kOptions].monitorCommands;
342+
}
343+
set monitorCommands(value: boolean) {
344+
this[kOptions].monitorCommands = value;
345+
}
329346

330347
get autoEncrypter(): AutoEncrypter | undefined {
331348
return this[kOptions].autoEncrypter;

src/sdam/topology.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ import type { MongoCredentials } from '../cmap/auth/mongo_credentials';
5050
import type { Transaction } from '../transactions';
5151
import type { CloseOptions } from '../cmap/connection_pool';
5252
import { DestroyOptions, Connection } from '../cmap/connection';
53-
import type { MongoClientOptions, ServerApi } from '../mongo_client';
53+
import type { MongoOptions, ServerApi } from '../mongo_client';
5454
import { DEFAULT_OPTIONS } from '../connection_string';
5555
import { serialize, deserialize } from '../bson';
5656

@@ -566,7 +566,7 @@ export class Topology extends EventEmitter {
566566
}
567567

568568
/** Start a logical session */
569-
startSession(options: ClientSessionOptions, clientOptions?: MongoClientOptions): ClientSession {
569+
startSession(options: ClientSessionOptions, clientOptions?: MongoOptions): ClientSession {
570570
const session = new ClientSession(this, this.s.sessionPool, options, clientOptions);
571571
session.once('ended', () => {
572572
this.s.sessions.delete(session);

src/sessions.ts

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import {
2222
maybePromise
2323
} from './utils';
2424
import type { Topology } from './sdam/topology';
25-
import type { MongoClientOptions } from './mongo_client';
25+
import type { MongoOptions } from './mongo_client';
2626
import { executeOperation } from './operations/execute_operation';
2727
import { RunAdminCommandOperation } from './operations/run_command';
2828
import type { AbstractCursor } from './cursor/abstract_cursor';
@@ -76,7 +76,7 @@ class ClientSession extends EventEmitter {
7676
/** @internal */
7777
sessionPool: ServerSessionPool;
7878
hasEnded: boolean;
79-
clientOptions?: MongoClientOptions;
79+
clientOptions?: MongoOptions;
8080
supports: { causalConsistency: boolean };
8181
clusterTime?: ClusterTime;
8282
operationTime?: Timestamp;
@@ -98,7 +98,7 @@ class ClientSession extends EventEmitter {
9898
topology: Topology,
9999
sessionPool: ServerSessionPool,
100100
options: ClientSessionOptions,
101-
clientOptions?: MongoClientOptions
101+
clientOptions?: MongoOptions
102102
) {
103103
super();
104104

@@ -111,7 +111,6 @@ class ClientSession extends EventEmitter {
111111
}
112112

113113
options = options ?? {};
114-
clientOptions = clientOptions || {};
115114

116115
this.topology = topology;
117116
this.sessionPool = sessionPool;
@@ -263,11 +262,22 @@ class ClientSession extends EventEmitter {
263262

264263
// increment txnNumber
265264
this.incrementTransactionNumber();
266-
267265
// create transaction state
268-
this.transaction = new Transaction(
269-
Object.assign({}, this.clientOptions, options || this.defaultTransactionOptions)
270-
);
266+
this.transaction = new Transaction({
267+
readConcern:
268+
options?.readConcern ??
269+
this.defaultTransactionOptions.readConcern ??
270+
this.clientOptions?.readConcern,
271+
writeConcern:
272+
options?.writeConcern ??
273+
this.defaultTransactionOptions.writeConcern ??
274+
this.clientOptions?.writeConcern,
275+
readPreference:
276+
options?.readPreference ??
277+
this.defaultTransactionOptions.readPreference ??
278+
this.clientOptions?.readPreference,
279+
maxCommitTimeMS: options?.maxCommitTimeMS ?? this.defaultTransactionOptions.maxCommitTimeMS
280+
});
271281

272282
this.transaction.transition(TxnState.STARTING_TRANSACTION);
273283
}
@@ -503,8 +513,8 @@ function endTransaction(session: ClientSession, commandName: string, callback: C
503513
let writeConcern;
504514
if (session.transaction.options.writeConcern) {
505515
writeConcern = Object.assign({}, session.transaction.options.writeConcern);
506-
} else if (session.clientOptions && session.clientOptions.w) {
507-
writeConcern = { w: session.clientOptions.w };
516+
} else if (session.clientOptions && session.clientOptions.writeConcern) {
517+
writeConcern = { w: session.clientOptions.writeConcern.w };
508518
}
509519

510520
if (txnState === TxnState.TRANSACTION_COMMITTED) {

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

Lines changed: 60 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
1-
import { MongoClient, Db, Collection, GridFSBucket, Document } from '../../../src/index';
1+
import {
2+
MongoClient,
3+
Db,
4+
Collection,
5+
GridFSBucket,
6+
Document,
7+
HostAddress
8+
} from '../../../src/index';
29
import { ReadConcern } from '../../../src/read_concern';
310
import { WriteConcern } from '../../../src/write_concern';
411
import { ReadPreference } from '../../../src/read_preference';
@@ -18,10 +25,6 @@ interface UnifiedChangeStream extends ChangeStream {
1825
eventCollector: InstanceType<typeof import('../../tools/utils')['EventCollector']>;
1926
}
2027

21-
interface UnifiedClientSession extends ClientSession {
22-
client: UnifiedMongoClient;
23-
}
24-
2528
export type CommandEvent = CommandStartedEvent | CommandSucceededEvent | CommandFailedEvent;
2629

2730
export class UnifiedMongoClient extends MongoClient {
@@ -75,23 +78,49 @@ export class UnifiedMongoClient extends MongoClient {
7578
}
7679
return this.events;
7780
}
81+
}
7882

79-
async enableFailPoint(failPoint: Document): Promise<Document> {
80-
const admin = this.db().admin();
83+
export class FailPointMap extends Map<string, Document> {
84+
async enableFailPoint(
85+
addressOrClient: HostAddress | UnifiedMongoClient,
86+
failPoint: Document
87+
): Promise<Document> {
88+
let client: MongoClient;
89+
let address: string;
90+
if (addressOrClient instanceof MongoClient) {
91+
client = addressOrClient;
92+
address = client.topology.s.seedlist.join(',');
93+
} else {
94+
// create a new client
95+
address = addressOrClient.toString();
96+
client = new MongoClient(`mongodb://${address}`);
97+
await client.connect();
98+
}
99+
100+
const admin = client.db('admin');
81101
const result = await admin.command(failPoint);
102+
103+
if (!(addressOrClient instanceof MongoClient)) {
104+
// we created this client
105+
await client.close();
106+
}
107+
82108
expect(result).to.have.property('ok', 1);
83-
this.failPoints.push(failPoint.configureFailPoint);
109+
this.set(address, failPoint.configureFailPoint);
84110
return result;
85111
}
86112

87-
async disableFailPoints(): Promise<Document[]> {
88-
return Promise.all(
89-
this.failPoints.map(configureFailPoint =>
90-
this.db().admin().command({
91-
configureFailPoint,
92-
mode: 'off'
93-
})
94-
)
113+
async disableFailPoints(): Promise<void> {
114+
const entries = Array.from(this.entries());
115+
await Promise.all(
116+
entries.map(async ([hostAddress, configureFailPoint]) => {
117+
const client = new MongoClient(`mongodb://${hostAddress}`);
118+
await client.connect();
119+
const admin = client.db('admin');
120+
const result = await admin.command({ configureFailPoint, mode: 'off' });
121+
expect(result).to.have.property('ok', 1);
122+
await client.close();
123+
})
95124
);
96125
}
97126
}
@@ -100,7 +129,7 @@ export type Entity =
100129
| UnifiedMongoClient
101130
| Db
102131
| Collection
103-
| UnifiedClientSession
132+
| ClientSession
104133
| UnifiedChangeStream
105134
| GridFSBucket
106135
| Document; // Results from operations
@@ -124,10 +153,17 @@ ENTITY_CTORS.set('bucket', GridFSBucket);
124153
ENTITY_CTORS.set('stream', ChangeStream);
125154

126155
export class EntitiesMap<E = Entity> extends Map<string, E> {
156+
failPoints: FailPointMap;
157+
158+
constructor(entries?: readonly (readonly [string, E])[] | null) {
159+
super(entries);
160+
this.failPoints = new FailPointMap();
161+
}
162+
127163
mapOf(type: 'client'): EntitiesMap<UnifiedMongoClient>;
128164
mapOf(type: 'db'): EntitiesMap<Db>;
129165
mapOf(type: 'collection'): EntitiesMap<Collection>;
130-
mapOf(type: 'session'): EntitiesMap<UnifiedClientSession>;
166+
mapOf(type: 'session'): EntitiesMap<ClientSession>;
131167
mapOf(type: 'bucket'): EntitiesMap<GridFSBucket>;
132168
mapOf(type: 'stream'): EntitiesMap<UnifiedChangeStream>;
133169
mapOf(type: EntityTypeId): EntitiesMap<Entity> {
@@ -141,7 +177,7 @@ export class EntitiesMap<E = Entity> extends Map<string, E> {
141177
getEntity(type: 'client', key: string, assertExists?: boolean): UnifiedMongoClient;
142178
getEntity(type: 'db', key: string, assertExists?: boolean): Db;
143179
getEntity(type: 'collection', key: string, assertExists?: boolean): Collection;
144-
getEntity(type: 'session', key: string, assertExists?: boolean): UnifiedClientSession;
180+
getEntity(type: 'session', key: string, assertExists?: boolean): ClientSession;
145181
getEntity(type: 'bucket', key: string, assertExists?: boolean): GridFSBucket;
146182
getEntity(type: 'stream', key: string, assertExists?: boolean): UnifiedChangeStream;
147183
getEntity(type: EntityTypeId, key: string, assertExists = true): Entity {
@@ -161,8 +197,8 @@ export class EntitiesMap<E = Entity> extends Map<string, E> {
161197
}
162198

163199
async cleanup(): Promise<void> {
200+
await this.failPoints.disableFailPoints();
164201
for (const [, client] of this.mapOf('client')) {
165-
await client.disableFailPoints();
166202
await client.close();
167203
}
168204
for (const [, session] of this.mapOf('session')) {
@@ -178,7 +214,9 @@ export class EntitiesMap<E = Entity> extends Map<string, E> {
178214
const map = new EntitiesMap();
179215
for (const entity of entities ?? []) {
180216
if ('client' in entity) {
181-
const uri = config.url({ useMultipleMongoses: entity.client.useMultipleMongoses });
217+
const useMultipleMongoses =
218+
config.topologyType === 'Sharded' && entity.client.useMultipleMongoses;
219+
const uri = config.url({ useMultipleMongoses });
182220
const client = new UnifiedMongoClient(uri, entity.client);
183221
await client.connect();
184222
map.set(entity.client.id, client);
@@ -228,10 +266,7 @@ export class EntitiesMap<E = Entity> extends Map<string, E> {
228266
}
229267
}
230268

231-
const session = client.startSession(options) as UnifiedClientSession;
232-
// targetedFailPoint operations need to access the client the session came from
233-
session.client = client;
234-
269+
const session = client.startSession(options);
235270
map.set(entity.session.id, session);
236271
} else if ('bucket' in entity) {
237272
const db = map.getEntity('db', entity.bucket.database);

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

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ operations.set('assertSessionNotDirty', async ({ entities, operation }) => {
136136

137137
operations.set('assertSessionPinned', async ({ entities, operation }) => {
138138
const session = entities.getEntity('session', operation.arguments.session);
139-
expect(session.transaction.isPinned).to.be.false;
139+
expect(session.transaction.isPinned).to.be.true;
140140
});
141141

142142
operations.set('assertSessionUnpinned', async ({ entities, operation }) => {
@@ -249,7 +249,7 @@ operations.set('findOneAndUpdate', async ({ entities, operation }) => {
249249

250250
operations.set('failPoint', async ({ entities, operation }) => {
251251
const client = entities.getEntity('client', operation.arguments.client);
252-
return client.enableFailPoint(operation.arguments.failPoint);
252+
return entities.failPoints.enableFailPoint(client, operation.arguments.failPoint);
253253
});
254254

255255
operations.set('insertOne', async ({ entities, operation }) => {
@@ -309,8 +309,10 @@ operations.set('startTransaction', async ({ entities, operation }) => {
309309
operations.set('targetedFailPoint', async ({ entities, operation }) => {
310310
const session = entities.getEntity('session', operation.arguments.session);
311311
expect(session.transaction.isPinned, 'Session must be pinned for a targetedFailPoint').to.be.true;
312-
const client = session.client;
313-
client.enableFailPoint(operation.arguments.failPoint);
312+
await entities.failPoints.enableFailPoint(
313+
session.transaction._pinnedServer.s.description.hostAddress,
314+
operation.arguments.failPoint
315+
);
314316
});
315317

316318
operations.set('delete', async ({ entities, operation }) => {

0 commit comments

Comments
 (0)