Skip to content

Commit

Permalink
fix(NODE-4533): session support error message and unified test runner (
Browse files Browse the repository at this point in the history
  • Loading branch information
dariakp committed Aug 16, 2022
1 parent 6425c7a commit 6a0e502
Show file tree
Hide file tree
Showing 7 changed files with 167 additions and 141 deletions.
2 changes: 1 addition & 1 deletion src/sessions.ts
Expand Up @@ -383,7 +383,7 @@ export class ClientSession extends TypedEventEmitter<ClientSessionEvents> {
*/
startTransaction(options?: TransactionOptions): void {
if (this[kSnapshotEnabled]) {
throw new MongoCompatibilityError('Transactions are not allowed with snapshot sessions');
throw new MongoCompatibilityError('Transactions are not supported in snapshot sessions');
}

if (this.inTransaction()) {
Expand Down
40 changes: 21 additions & 19 deletions test/tools/unified-spec-runner/entities.ts
@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import { expect } from 'chai';

import { ChangeStream } from '../../../src/change_stream';
Expand Down Expand Up @@ -64,8 +65,6 @@ function getClient(address) {
return new MongoClient(`mongodb://${address}`, getEnvironmentalOptions());
}

type PushFunction = (e: any) => void;

export class UnifiedMongoClient extends MongoClient {
commandEvents: CommandEvent[];
cmapEvents: CmapEvent[];
Expand Down Expand Up @@ -139,6 +138,17 @@ export class UnifiedMongoClient extends MongoClient {
return this.ignoredEvents.includes(e.commandName);
}

getCapturedEvents(eventType: string): CommandEvent[] | CmapEvent[] {
switch (eventType) {
case 'command':
return this.commandEvents;
case 'cmap':
return this.cmapEvents;
default:
throw new Error(`Unknown eventType: ${eventType}`);
}
}

// NOTE: pushCommandEvent must be an arrow function
pushCommandEvent: (e: CommandEvent) => void = e => {
if (!this.isIgnored(e)) {
Expand All @@ -151,22 +161,14 @@ export class UnifiedMongoClient extends MongoClient {
this.cmapEvents.push(e);
};

stopCapturingEvents(pushFn: PushFunction): void {
const observedEvents = [...this.observedCommandEvents, ...this.observedCmapEvents];
for (const eventName of observedEvents) {
this.off(eventName, pushFn);
}
}

/** Disables command monitoring for the client and returns a list of the captured events. */
stopCapturingCommandEvents(): CommandEvent[] {
this.stopCapturingEvents(this.pushCommandEvent);
return this.commandEvents;
}

stopCapturingCmapEvents(): CmapEvent[] {
this.stopCapturingEvents(this.pushCmapEvent);
return this.cmapEvents;
stopCapturingEvents(): void {
for (const eventName of this.observedCommandEvents) {
this.off(eventName, this.pushCommandEvent);
}
for (const eventName of this.observedCmapEvents) {
this.off(eventName, this.pushCmapEvent);
}
}
}

Expand All @@ -179,7 +181,7 @@ export class FailPointMap extends Map<string, Document> {
let address: string;
if (addressOrClient instanceof MongoClient) {
client = addressOrClient;
address = client.topology.s.seedlist.join(',');
address = client.topology!.s.seedlist.join(',');
} else {
// create a new client
address = addressOrClient.toString();
Expand Down Expand Up @@ -300,7 +302,7 @@ export class EntitiesMap<E = Entity> extends Map<string, E> {
getEntity(type: 'cursor', key: string, assertExists?: boolean): AbstractCursor;
getEntity(type: 'stream', key: string, assertExists?: boolean): UnifiedChangeStream;
getEntity(type: 'clientEncryption', key: string, assertExists?: boolean): ClientEncryption;
getEntity(type: EntityTypeId, key: string, assertExists = true): Entity {
getEntity(type: EntityTypeId, key: string, assertExists = true): Entity | undefined {
const entity = this.get(key);
if (!entity) {
if (assertExists) throw new Error(`Entity '${key}' does not exist`);
Expand Down
90 changes: 54 additions & 36 deletions test/tools/unified-spec-runner/match.ts
@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import { expect } from 'chai';
import { inspect } from 'util';

Expand All @@ -7,6 +8,7 @@ import {
Document,
Long,
MongoError,
MongoServerError,
ObjectId,
OneOrMore
} from '../../../src';
Expand Down Expand Up @@ -264,11 +266,11 @@ export function specialCheck(
entities: EntitiesMap,
path: string[] = [],
checkExtraKeys: boolean
): boolean {
): void {
if (isUnsetOrMatchesOperator(expected)) {
if (actual === null || actual === undefined) return;

resultCheck(actual, expected.$$unsetOrMatches, entities, path, checkExtraKeys);
resultCheck(actual, expected.$$unsetOrMatches as any, entities, path, checkExtraKeys);
} else if (isMatchesEntityOperator(expected)) {
// $$matchesEntity
const entity = entities.get(expected.$$matchesEntity);
Expand All @@ -290,15 +292,15 @@ export function specialCheck(
// $$sessionLsid
const session = entities.getEntity('session', expected.$$sessionLsid, false);
expect(session, `Session ${expected.$$sessionLsid} does not exist in entities`).to.exist;
const entitySessionHex = session.id.id.buffer.toString('hex').toUpperCase();
const entitySessionHex = session.id!.id.buffer.toString('hex').toUpperCase();
const actualSessionHex = actual.id.buffer.toString('hex').toUpperCase();
expect(
entitySessionHex,
`Session entity ${expected.$$sessionLsid} does not match lsid`
).to.equal(actualSessionHex);
} else if (isTypeOperator(expected)) {
// $$type
let ok: boolean;
let ok = false;
const types = Array.isArray(expected.$$type) ? expected.$$type : [expected.$$type];
for (const type of types) {
ok ||= TYPE_MAP.get(type)(actual);
Expand Down Expand Up @@ -364,19 +366,23 @@ function compareCommandStartedEvents(
entities: EntitiesMap,
prefix: string
) {
if (expected.command) {
resultCheck(actual.command, expected.command, entities, [`${prefix}.command`]);
if (expected!.command) {
resultCheck(actual.command, expected!.command, entities, [`${prefix}.command`]);
}
if (expected.commandName) {
if (expected!.commandName) {
expect(
expected.commandName,
`expected ${prefix}.commandName to equal ${expected.commandName} but received ${actual.commandName}`
expected!.commandName,
`expected ${prefix}.commandName to equal ${expected!.commandName} but received ${
actual.commandName
}`
).to.equal(actual.commandName);
}
if (expected.databaseName) {
if (expected!.databaseName) {
expect(
expected.databaseName,
`expected ${prefix}.databaseName to equal ${expected.databaseName} but received ${actual.databaseName}`
expected!.databaseName,
`expected ${prefix}.databaseName to equal ${expected!.databaseName} but received ${
actual.databaseName
}`
).to.equal(actual.databaseName);
}
}
Expand All @@ -387,13 +393,15 @@ function compareCommandSucceededEvents(
entities: EntitiesMap,
prefix: string
) {
if (expected.reply) {
resultCheck(actual.reply, expected.reply, entities, [prefix]);
if (expected!.reply) {
resultCheck(actual.reply as Document, expected!.reply, entities, [prefix]);
}
if (expected.commandName) {
if (expected!.commandName) {
expect(
expected.commandName,
`expected ${prefix}.commandName to equal ${expected.commandName} but received ${actual.commandName}`
expected!.commandName,
`expected ${prefix}.commandName to equal ${expected!.commandName} but received ${
actual.commandName
}`
).to.equal(actual.commandName);
}
}
Expand All @@ -404,10 +412,12 @@ function compareCommandFailedEvents(
entities: EntitiesMap,
prefix: string
) {
if (expected.commandName) {
if (expected!.commandName) {
expect(
expected.commandName,
`expected ${prefix}.commandName to equal ${expected.commandName} but received ${actual.commandName}`
expected!.commandName,
`expected ${prefix}.commandName to equal ${expected!.commandName} but received ${
actual.commandName
}`
).to.equal(actual.commandName);
}
}
Expand Down Expand Up @@ -489,28 +499,34 @@ export function matchesEvents(
}
}

function isMongoCryptError(err): boolean {
if (err.constructor.name === 'MongoCryptError') {
return true;
}
return err.stack.includes('at ClientEncryption');
}

export function expectErrorCheck(
error: Error | MongoError,
expected: ExpectedError,
entities: EntitiesMap
): boolean {
if (Object.keys(expected)[0] === 'isClientError' || Object.keys(expected)[0] === 'isError') {
// FIXME: We cannot tell if Error arose from driver and not from server
return;
): void {
const expectMessage = `\n\nOriginal Error Stack:\n${error.stack}\n\n`;

if (!isMongoCryptError(error)) {
expect(error, expectMessage).to.be.instanceOf(MongoError);
}

const expectMessage = `\n\nOriginal Error Stack:\n${error.stack}\n\n`;
if (expected.isClientError === false) {
expect(error).to.be.instanceOf(MongoServerError);
} else if (expected.isClientError === true) {
expect(error).not.to.be.instanceOf(MongoServerError);
}

if (expected.errorContains != null) {
expect(error.message, expectMessage).to.include(expected.errorContains);
}

if (!(error instanceof MongoError)) {
// if statement asserts type for TS, expect will always fail
expect(error, expectMessage).to.be.instanceOf(MongoError);
return;
}

if (expected.errorCode != null) {
expect(error, expectMessage).to.have.property('code', expected.errorCode);
}
Expand All @@ -520,24 +536,26 @@ export function expectErrorCheck(
}

if (expected.errorLabelsContain != null) {
const mongoError = error as MongoError;
for (const errorLabel of expected.errorLabelsContain) {
expect(
error.hasErrorLabel(errorLabel),
`Error was supposed to have label ${errorLabel}, has [${error.errorLabels}] -- ${expectMessage}`
mongoError.hasErrorLabel(errorLabel),
`Error was supposed to have label ${errorLabel}, has [${mongoError.errorLabels}] -- ${expectMessage}`
).to.be.true;
}
}

if (expected.errorLabelsOmit != null) {
const mongoError = error as MongoError;
for (const errorLabel of expected.errorLabelsOmit) {
expect(
error.hasErrorLabel(errorLabel),
`Error was supposed to have label ${errorLabel}, has [${error.errorLabels}] -- ${expectMessage}`
mongoError.hasErrorLabel(errorLabel),
`Error was not supposed to have label ${errorLabel}, has [${mongoError.errorLabels}] -- ${expectMessage}`
).to.be.false;
}
}

if (expected.expectResult != null) {
resultCheck(error, expected.expectResult, entities);
resultCheck(error, expected.expectResult as any, entities);
}
}

0 comments on commit 6a0e502

Please sign in to comment.