From 83049ade4ad27a49075524d93fc48924ee22f895 Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Wed, 13 Jan 2021 16:30:57 -0500 Subject: [PATCH 1/8] refactor: reduce EntityMap helpers verbosity --- test/functional/unified-spec-runner/entities.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/test/functional/unified-spec-runner/entities.ts b/test/functional/unified-spec-runner/entities.ts index 9acf9064fca..64bdc4214fe 100644 --- a/test/functional/unified-spec-runner/entities.ts +++ b/test/functional/unified-spec-runner/entities.ts @@ -9,6 +9,7 @@ import type { } from '../../../src/cmap/events'; import { patchCollectionOptions, patchDbOptions } from './unified-utils'; import { TestConfiguration } from './unified.test'; +import { EntryType } from 'perf_hooks'; export type CommandEvent = CommandStartedEvent | CommandSucceededEvent | CommandFailedEvent; From 651cfbc425ee59c29dc34ec396cb53d29fff40dc Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Wed, 13 Jan 2021 16:40:06 -0500 Subject: [PATCH 2/8] fix: lint --- test/functional/unified-spec-runner/entities.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/test/functional/unified-spec-runner/entities.ts b/test/functional/unified-spec-runner/entities.ts index 64bdc4214fe..9acf9064fca 100644 --- a/test/functional/unified-spec-runner/entities.ts +++ b/test/functional/unified-spec-runner/entities.ts @@ -9,7 +9,6 @@ import type { } from '../../../src/cmap/events'; import { patchCollectionOptions, patchDbOptions } from './unified-utils'; import { TestConfiguration } from './unified.test'; -import { EntryType } from 'perf_hooks'; export type CommandEvent = CommandStartedEvent | CommandSucceededEvent | CommandFailedEvent; From 57d82f12d565b2c11a4c6dc6e3f02770a7e5a80b Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Fri, 8 Jan 2021 17:47:35 -0500 Subject: [PATCH 3/8] test: Add unified spec runner scaffolding Using the unfied spec test schema this scaffolding outlines the structure of a unified runner. Most tests are skipped by the runOn requirements or not implemented errors thrown by empty operation functions. NODE-2287 --- package-lock.json | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/package-lock.json b/package-lock.json index 8147f2678ce..4d3c18660f6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -542,6 +542,21 @@ "@types/chai": "*" } }, + "@types/chai": { + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.2.14.tgz", + "integrity": "sha512-G+ITQPXkwTrslfG5L/BksmbLUA0M1iybEsmCWPqzSxsRRhJZimBKJkoMi8fr/CPygPTj4zO5pJH7I2/cm9M7SQ==", + "dev": true + }, + "@types/chai-subset": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@types/chai-subset/-/chai-subset-1.3.3.tgz", + "integrity": "sha512-frBecisrNGz+F4T6bcc+NLeolfiojh5FxW2klu669+8BARtyQv2C/GkNW6FUodVe4BroGMP/wER/YDGc7rEllw==", + "dev": true, + "requires": { + "@types/chai": "*" + } + }, "@types/color-name": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", From a874e5c30d795ab73e67e68755027dc8df6c44c6 Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Thu, 21 Jan 2021 18:35:18 -0500 Subject: [PATCH 4/8] test: Complete all matching operations Adds special operations support i.e. $$unsetOrMatches for testing nested schema equality of results and events. Adds Recursive equality test that tracks the path into the object it is testing for ease of error tracing. Enable Change Stream tests with the repurposed EventsCollector class to iterate change events. Adds find, insertMany, iterateUntilDocumentOrError, and failPoint operations. NODE-2287 --- package-lock.json | 15 - test/functional/change_stream.test.js | 51 +-- .../unified-spec-runner/entities.ts | 44 ++- test/functional/unified-spec-runner/match.ts | 296 ++++++++++++++++++ .../unified-spec-runner/operations.ts | 150 +++++---- test/functional/unified-spec-runner/schema.ts | 2 +- .../unified-spec-runner/unified-utils.ts | 110 +------ .../unified-spec-runner/unified.test.ts | 13 +- test/tools/runner/config.js | 1 + test/tools/utils.js | 70 +++++ 10 files changed, 497 insertions(+), 255 deletions(-) create mode 100644 test/functional/unified-spec-runner/match.ts diff --git a/package-lock.json b/package-lock.json index 4d3c18660f6..8147f2678ce 100644 --- a/package-lock.json +++ b/package-lock.json @@ -542,21 +542,6 @@ "@types/chai": "*" } }, - "@types/chai": { - "version": "4.2.14", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.2.14.tgz", - "integrity": "sha512-G+ITQPXkwTrslfG5L/BksmbLUA0M1iybEsmCWPqzSxsRRhJZimBKJkoMi8fr/CPygPTj4zO5pJH7I2/cm9M7SQ==", - "dev": true - }, - "@types/chai-subset": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@types/chai-subset/-/chai-subset-1.3.3.tgz", - "integrity": "sha512-frBecisrNGz+F4T6bcc+NLeolfiojh5FxW2klu669+8BARtyQv2C/GkNW6FUodVe4BroGMP/wER/YDGc7rEllw==", - "dev": true, - "requires": { - "@types/chai": "*" - } - }, "@types/color-name": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", diff --git a/test/functional/change_stream.test.js b/test/functional/change_stream.test.js index 470a077742e..37cfbde8080 100644 --- a/test/functional/change_stream.test.js +++ b/test/functional/change_stream.test.js @@ -5,6 +5,7 @@ const { MongoNetworkError } = require('../../src/error'); const { delay, setupDatabase, withClient, withCursor } = require('./shared'); const co = require('co'); const mock = require('../tools/mock'); +const { EventCollector } = require('../tools/utils'); const chai = require('chai'); const expect = chai.expect; const sinon = require('sinon'); @@ -233,56 +234,6 @@ describe('Change Streams', function () { } }); - class EventCollector { - constructor(obj, events, options) { - this._events = []; - this._timeout = options ? options.timeout : 5000; - - events.forEach(eventName => { - this._events[eventName] = []; - obj.on(eventName, event => this._events[eventName].push(event)); - }); - } - - waitForEvent(eventName, count, callback) { - if (typeof count === 'function') { - callback = count; - count = 1; - } - - waitForEventImpl(this, Date.now(), eventName, count, callback); - } - - reset(eventName) { - if (eventName == null) { - Object.keys(this._events).forEach(eventName => { - this._events[eventName] = []; - }); - - return; - } - - if (this._events[eventName] == null) { - throw new TypeError(`invalid event name "${eventName}" specified for reset`); - } - - this._events[eventName] = []; - } - } - - function waitForEventImpl(collector, start, eventName, count, callback) { - const events = collector._events[eventName]; - if (events.length >= count) { - return callback(undefined, events); - } - - if (Date.now() - start >= collector._timeout) { - return callback(new Error(`timed out waiting for event "${eventName}"`)); - } - - setTimeout(() => waitForEventImpl(collector, start, eventName, count, callback), 10); - } - it('should create a ChangeStream on a collection and emit `change` events', { metadata: { requires: { topology: 'replicaset', mongodb: '>=3.6' } }, diff --git a/test/functional/unified-spec-runner/entities.ts b/test/functional/unified-spec-runner/entities.ts index 9acf9064fca..32ee805b44a 100644 --- a/test/functional/unified-spec-runner/entities.ts +++ b/test/functional/unified-spec-runner/entities.ts @@ -9,11 +9,18 @@ import type { } from '../../../src/cmap/events'; import { patchCollectionOptions, patchDbOptions } from './unified-utils'; import { TestConfiguration } from './unified.test'; +import { expect } from 'chai'; + +interface UnifiedChangeStream extends ChangeStream { + eventCollector: InstanceType; +} export type CommandEvent = CommandStartedEvent | CommandSucceededEvent | CommandFailedEvent; export class UnifiedMongoClient extends MongoClient { events: CommandEvent[]; + failPoints: Document[]; + ignoredEvents: string[]; observedEvents: ('commandStarted' | 'commandSucceeded' | 'commandFailed')[]; static EVENT_NAME_LOOKUP = { @@ -25,6 +32,11 @@ export class UnifiedMongoClient extends MongoClient { constructor(url: string, description: ClientEntity) { super(url, { monitorCommands: true, ...description.uriOptions }); this.events = []; + this.failPoints = []; + this.ignoredEvents = [ + ...(description.ignoreCommandMonitoringEvents ?? []), + 'configureFailPoint' + ]; // apm this.observedEvents = (description.observeEvents ?? []).map( e => UnifiedMongoClient.EVENT_NAME_LOOKUP[e] @@ -34,9 +46,11 @@ export class UnifiedMongoClient extends MongoClient { } } - // NOTE: this must be an arrow function for `this` to work. + // NOTE: pushEvent must be an arrow function pushEvent: (e: CommandEvent) => void = e => { - this.events.push(e); + if (!this.ignoredEvents.includes(e.commandName)) { + this.events.push(e); + } }; /** Disables command monitoring for the client and returns a list of the captured events. */ @@ -46,6 +60,25 @@ export class UnifiedMongoClient extends MongoClient { } return this.events; } + + async enableFailPoint(failPoint: Document): Promise { + const admin = this.db().admin(); + const result = await admin.command(failPoint); + expect(result).to.have.property('ok', 1); + this.failPoints.push(failPoint.configureFailPoint); + return result; + } + + async disableFailPoints(): Promise { + return Promise.all( + this.failPoints.map(configureFailPoint => + this.db().admin().command({ + configureFailPoint, + mode: 'off' + }) + ) + ); + } } export type Entity = @@ -53,7 +86,7 @@ export type Entity = | Db | Collection | ClientSession - | ChangeStream + | UnifiedChangeStream | GridFSBucket | Document; // Results from operations @@ -81,7 +114,7 @@ export class EntitiesMap extends Map { mapOf(type: 'collection'): EntitiesMap; mapOf(type: 'session'): EntitiesMap; mapOf(type: 'bucket'): EntitiesMap; - mapOf(type: 'stream'): EntitiesMap; + mapOf(type: 'stream'): EntitiesMap; mapOf(type: EntityTypeId): EntitiesMap { const ctor = ENTITY_CTORS.get(type); if (!ctor) { @@ -95,7 +128,7 @@ export class EntitiesMap extends Map { getEntity(type: 'collection', key: string, assertExists?: boolean): Collection; getEntity(type: 'session', key: string, assertExists?: boolean): ClientSession; getEntity(type: 'bucket', key: string, assertExists?: boolean): GridFSBucket; - getEntity(type: 'stream', key: string, assertExists?: boolean): ChangeStream; + getEntity(type: 'stream', key: string, assertExists?: boolean): UnifiedChangeStream; getEntity(type: EntityTypeId, key: string, assertExists = true): Entity { const entity = this.get(key); if (!entity) { @@ -114,6 +147,7 @@ export class EntitiesMap extends Map { async cleanup(): Promise { for (const [, client] of this.mapOf('client')) { + await client.disableFailPoints(); await client.close(); } for (const [, session] of this.mapOf('session')) { diff --git a/test/functional/unified-spec-runner/match.ts b/test/functional/unified-spec-runner/match.ts new file mode 100644 index 00000000000..1bb632f3ead --- /dev/null +++ b/test/functional/unified-spec-runner/match.ts @@ -0,0 +1,296 @@ +import { expect } from 'chai'; +import { isDeepStrictEqual } from 'util'; +import { + Binary, + BSONRegExp, + Document, + Double, + Int32, + Long, + ObjectId, + Timestamp +} from '../../../src'; +import { + CommandFailedEvent, + CommandStartedEvent, + CommandSucceededEvent +} from '../../../src/cmap/events'; +import { CommandEvent, EntitiesMap } from './entities'; +import { ExpectedEvent } from './schema'; + +export interface ExistsOperator { + $$exists: boolean; +} +export function isExistsOperator(value: unknown): value is ExistsOperator { + return typeof value === 'object' && value != null && '$$exists' in value; +} +export interface TypeOperator { + $$type: boolean; +} +export function isTypeOperator(value: unknown): value is TypeOperator { + return typeof value === 'object' && value != null && '$$type' in value; +} +export interface MatchesEntityOperator { + $$matchesEntity: string; +} +export function isMatchesEntityOperator(value: unknown): value is MatchesEntityOperator { + return typeof value === 'object' && value != null && '$$matchesEntity' in value; +} +export interface MatchesHexBytesOperator { + $$matchesHexBytes: string; +} +export function isMatchesHexBytesOperator(value: unknown): value is MatchesHexBytesOperator { + return typeof value === 'object' && value != null && '$$matchesHexBytes' in value; +} +export interface UnsetOrMatchesOperator { + $$unsetOrMatches: unknown; +} +export function isUnsetOrMatchesOperator(value: unknown): value is UnsetOrMatchesOperator { + return typeof value === 'object' && value != null && '$$unsetOrMatches' in value; +} +export interface SessionLsidOperator { + $$sessionLsid: string; +} +export function isSessionLsidOperator(value: unknown): value is SessionLsidOperator { + return typeof value === 'object' && value != null && '$$sessionLsid' in value; +} + +export const SpecialOperatorKeys = [ + '$$exists', + '$$type', + '$$matchesEntity', + '$$matchesHexBytes', + '$$unsetOrMatches', + '$$sessionLsid' +]; + +export type SpecialOperator = + | ExistsOperator + | TypeOperator + | MatchesEntityOperator + | MatchesHexBytesOperator + | UnsetOrMatchesOperator + | SessionLsidOperator; + +// eslint-disable-next-line @typescript-eslint/ban-types +type KeysOfUnion = T extends object ? keyof T : never; +export type SpecialOperatorKey = KeysOfUnion; +export function isSpecialOperator(value: unknown): value is SpecialOperator { + return ( + isExistsOperator(value) || + isTypeOperator(value) || + isMatchesEntityOperator(value) || + isMatchesHexBytesOperator(value) || + isUnsetOrMatchesOperator(value) || + isSessionLsidOperator(value) + ); +} + +const TYPE_MAP = new Map(); +function typeof_double(actual): actual is number | Double { + return typeof actual === 'number' || actual._bsontype === 'Double'; +} +function typeof_string(actual: unknown): actual is string { + return typeof actual === 'string'; +} +function typeof_object(actual: unknown): actual is Document { + return typeof actual === 'object' && actual !== null; +} +function typeof_array(actual: unknown): actual is unknown[] { + return Array.isArray(actual); +} +function typeof_binData(actual) { + return actual instanceof Binary; +} +function typeof_undefined(actual) { + return actual === undefined; +} +function typeof_objectId(actual) { + return actual instanceof ObjectId; +} +function typeof_bool(actual) { + return typeof actual === 'boolean'; +} +function typeof_date(actual) { + return actual instanceof Date; +} +function typeof_null(actual) { + return actual === null; +} +function typeof_regex(actual) { + return actual instanceof RegExp || actual._bsontype === 'BSONRegExp'; +} +function typeof_dbPointer(actual) { + return actual._bsontype === 'DBRef'; +} +function typeof_javascript(actual) { + return actual._bsontype === 'Code'; +} +function typeof_symbol(actual) { + return actual._bsontype === 'Symbol'; +} +function typeof_javascriptWithScope(actual) { + return actual._bsontype === 'Code' && actual.scope; +} +function typeof_int(actual): actual is number | Int32 { + return (typeof actual === 'number' && Number.isInteger(actual)) || actual._bsontype === 'Int32'; +} +function typeof_timestamp(actual: Timestamp, expected: Timestamp) { + expect(actual.equals(expected)).to.be.true; +} +function typeof_long(actual: unknown): actual is number | Long { + return (typeof actual === 'number' && Number.isInteger(actual)) || Long.isLong(actual); +} +function typeof_decimal(actual) { + return actual._bsontype === 'Decimal128'; +} +function typeof_minKey(actual) { + return actual._bsontype === 'MinKey'; +} +function typeof_maxKey(actual) { + return actual._bsontype === 'MaxKey'; +} + +TYPE_MAP.set('double', typeof_double); +TYPE_MAP.set('string', typeof_string); +TYPE_MAP.set('object', typeof_object); +TYPE_MAP.set('array', typeof_array); +TYPE_MAP.set('binData', typeof_binData); +TYPE_MAP.set('undefined', typeof_undefined); +TYPE_MAP.set('objectId', typeof_objectId); +TYPE_MAP.set('bool', typeof_bool); +TYPE_MAP.set('date', typeof_date); +TYPE_MAP.set('null', typeof_null); +TYPE_MAP.set('regex', typeof_regex); +TYPE_MAP.set('dbPointer', typeof_dbPointer); +TYPE_MAP.set('javascript', typeof_javascript); +TYPE_MAP.set('symbol', typeof_symbol); +TYPE_MAP.set('javascriptWithScope', typeof_javascriptWithScope); +TYPE_MAP.set('int', typeof_int); +TYPE_MAP.set('timestamp', typeof_timestamp); +TYPE_MAP.set('long', typeof_long); +TYPE_MAP.set('decimal', typeof_decimal); +TYPE_MAP.set('minKey', typeof_minKey); +TYPE_MAP.set('maxKey', typeof_maxKey); + +export function expectResultCheck( + actual: Document, + expected: Document | number | string | boolean, + entities: EntitiesMap, + path: string[] = [] +): boolean { + const result = resultCheck(actual, expected, entities, path); + if (result[0] === false) { + const path = result[1].join(''); + const expectedJSON = JSON.stringify(expected, undefined, 2); + const actualJSON = JSON.stringify(actual, undefined, 2); + expect.fail(`Unable to match ${expectedJSON} to ${actualJSON} at ${path}`); + } + return result[0]; +} + +export function resultCheck( + actual: Document, + expected: Document | number | string | boolean, + entities: EntitiesMap, + path: string[] +): [ok: boolean, path: string[]] { + if (typeof expected === 'object' && expected !== null) { + // Expected is an object + // either its a special operator or just an object to check equality against + + if (isSpecialOperator(expected)) { + // Special operation check is a base condition + // specialCheck may recurse depending upon the check ($$unsetOrMatches) + return [specialCheck(actual, expected, entities, path), path]; + } else { + // Just a plain object, however this object can contain special operations + // So we need to recurse over each key,value + let ok = true; + const expectedEntries = Object.entries(expected); + for (const [key, value] of expectedEntries) { + path.push(Array.isArray(expected) ? `[${key}]` : `.${key}`); // record what key we're at + ok &&= expectResultCheck(actual[key], value, entities, path); + path.pop(); // if the recursion was successful we can drop the tested key + } + return [ok, path]; + } + } else { + // Here's our recursion base case + // expected is: number | string | boolean | null + return [isDeepStrictEqual(actual, expected), path]; + } +} + +export function specialCheck( + actual: Document, + expected: SpecialOperator, + entities: EntitiesMap, + path: string[] = [] +): boolean { + let ok = false; + if (isUnsetOrMatchesOperator(expected)) { + // $$unsetOrMatches + ok = true; // start with true assumption + if (actual === null || actual === undefined) ok = true; + else ok &&= expectResultCheck(actual, expected.$$unsetOrMatches, entities, path); + } else if (isMatchesEntityOperator(expected)) { + // $$matchesEntity + const entity = entities.get(expected.$$matchesEntity); + if (!entity) ok = false; + else ok = isDeepStrictEqual(actual, entity); + } else if (isMatchesHexBytesOperator(expected)) { + // $$matchesHexBytes + const expectedBuffer = Buffer.from(expected.$$matchesHexBytes, 'hex'); + ok = expectedBuffer.every((byte, index) => byte === actual[index]); + } else if (isSessionLsidOperator(expected)) { + // $$sessionLsid + const session = entities.getEntity('session', expected.$$sessionLsid, false); + if (!session) ok = false; + else ok = session.id.id.buffer.equals(actual.lsid.id.buffer); + } else if (isTypeOperator(expected)) { + // $$type + const types = Array.isArray(expected.$$type) ? expected.$$type : [expected.$$type]; + for (const type of types) { + ok ||= TYPE_MAP.get(type)(actual); + } + } else if (isExistsOperator(expected)) { + // $$exists - unique, this op uses the path to check if the key is (not) in actual + const actualExists = actual !== undefined && actual !== null; + ok = (expected.$$exists && actualExists) || (!expected.$$exists && !actualExists); + } else { + throw new Error(`Unknown special operator: ${JSON.stringify(expected)}`); + } + + return ok; +} + +export function matchesEvents( + expected: ExpectedEvent[], + actual: CommandEvent[], + entities: EntitiesMap +): void { + // TODO: NodeJS Driver has extra events + // expect(actual).to.have.lengthOf(expected.length); + + for (const [index, actualEvent] of actual.entries()) { + const expectedEvent = expected[index]; + + if (expectedEvent.commandStartedEvent && actualEvent instanceof CommandStartedEvent) { + expectResultCheck(actualEvent, expectedEvent.commandStartedEvent, entities, [ + `events[${index}].commandStartedEvent` + ]); + } else if ( + expectedEvent.commandSucceededEvent && + actualEvent instanceof CommandSucceededEvent + ) { + expectResultCheck(actualEvent, expectedEvent.commandSucceededEvent, entities, [ + `events[${index}].commandSucceededEvent` + ]); + } else if (expectedEvent.commandFailedEvent && actualEvent instanceof CommandFailedEvent) { + expect(actualEvent.commandName).to.equal(expectedEvent.commandFailedEvent.commandName); + } else { + expect.fail(`Events must be one of the known types, got ${actualEvent}`); + } + } +} diff --git a/test/functional/unified-spec-runner/operations.ts b/test/functional/unified-spec-runner/operations.ts index 6697d690e9f..58788722241 100644 --- a/test/functional/unified-spec-runner/operations.ts +++ b/test/functional/unified-spec-runner/operations.ts @@ -1,18 +1,10 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ import { expect } from 'chai'; -import { Document, MongoError } from '../../../src'; -import type { EntitiesMap } from './entities'; +import { ChangeStream, Document, InsertOneOptions, MongoError } from '../../../src'; +import { EventCollector } from '../../tools/utils'; +import { EntitiesMap } from './entities'; +import { expectResultCheck } from './match'; import type * as uni from './schema'; -import { - isExistsOperator, - isMatchesEntityOperator, - isMatchesHexBytesOperator, - isSessionLsidOperator, - isSpecialOperator, - isTypeOperator, - isUnsetOrMatchesOperator, - SpecialOperator -} from './unified-utils'; export class UnifiedOperation { name: string; @@ -114,8 +106,31 @@ async function commitTransactionOperation( async function createChangeStreamOperation( entities: EntitiesMap, op: uni.OperationDescription -): Promise { - throw new Error('not implemented.'); +): Promise { + const watchable = entities.get(op.object); + if (!('watch' in watchable)) { + throw new Error(`Entity ${op.object} must be watchable`); + } + const changeStream = watchable.watch(op.arguments.pipeline, { + fullDocument: op.arguments.fullDocument, + maxAwaitTimeMS: op.arguments.maxAwaitTimeMS, + resumeAfter: op.arguments.resumeAfter, + startAfter: op.arguments.startAfter, + startAtOperationTime: op.arguments.startAtOperationTime, + batchSize: op.arguments.batchSize + }); + changeStream.eventCollector = new EventCollector(changeStream, ['init', 'change', 'error']); + + return new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + reject(new Error('Change stream never started')); + }, 2000); + + changeStream.cursor.once('init', () => { + clearTimeout(timeout); + resolve(changeStream); + }); + }); } async function createCollectionOperation( entities: EntitiesMap, @@ -151,7 +166,9 @@ async function findOperation( entities: EntitiesMap, op: uni.OperationDescription ): Promise { - throw new Error('not implemented.'); + const collection = entities.getEntity('collection', op.object); + const { filter, sort, batchSize, limit } = op.arguments; + return await collection.find(filter, { sort, batchSize, limit }).toArray(); } async function findOneAndReplaceOperation( entities: EntitiesMap, @@ -169,34 +186,48 @@ async function failPointOperation( entities: EntitiesMap, op: uni.OperationDescription ): Promise { - throw new Error('not implemented.'); + const client = entities.getEntity('client', op.arguments.client); + return client.enableFailPoint(op.arguments.failPoint); } async function insertOneOperation( entities: EntitiesMap, op: uni.OperationDescription ): Promise { const collection = entities.getEntity('collection', op.object); + const session = entities.getEntity('session', op.arguments.session, false); - const result = await collection.insertOne(op.arguments.document); - return result; + + const options = { + session + } as InsertOneOptions; + + return await collection.insertOne(op.arguments.document, options); } async function insertManyOperation( entities: EntitiesMap, op: uni.OperationDescription ): Promise { const collection = entities.getEntity('collection', op.object); + const session = entities.getEntity('session', op.arguments.session, false); + const options = { + session, ordered: op.arguments.ordered ?? true }; - const result = await collection.insertMany(op.arguments.documents, options); - return result; + + return await collection.insertMany(op.arguments.documents, options); } async function iterateUntilDocumentOrErrorOperation( entities: EntitiesMap, op: uni.OperationDescription ): Promise { - throw new Error('not implemented.'); + const changeStream = entities.getEntity('stream', op.object); + // Either change or error promise will finish + return Promise.race([ + changeStream.eventCollector.waitAndShiftEvent('change'), + changeStream.eventCollector.waitAndShiftEvent('error') + ]); } async function listDatabasesOperation( entities: EntitiesMap, @@ -294,71 +325,34 @@ export async function executeOperationAndCheck( operation: uni.OperationDescription, entities: EntitiesMap ): Promise { - const operationName = operation.name; - const opFunc = operations.get(operationName); - expect(opFunc, `Unknown operation: ${operationName}`).to.exist; - try { - const result = await opFunc(entities, operation); - - if (operation.expectError) { - expect.fail(`Operation ${operationName} succeeded but was not supposed to`); - } + const opFunc = operations.get(operation.name); + expect(opFunc, `Unknown operation: ${operation.name}`).to.exist; - if (operation.expectResult) { - if (isSpecialOperator(operation.expectResult)) { - specialCheck(result, operation.expectResult); - } else { - for (const [resultKey, resultValue] of Object.entries(operation.expectResult)) { - // each key/value expectation can be a special op - if (isSpecialOperator(resultValue)) { - specialCheck(result, resultValue); - } else { - expect(result[resultKey]).to.deep.equal(resultValue); - } - } - } - } + let result; - if (operation.saveResultAsEntity) { - entities.set(operation.saveResultAsEntity, result); - } + try { + result = await opFunc(entities, operation); } catch (error) { if (operation.expectError) { expect(error).to.be.instanceof(MongoError); - // TODO more checking of the error + // expectErrorCheck(error, operation.expectError); } else { - expect.fail(`Operation ${operationName} failed with ${error.message}`); + expect.fail(`Operation ${operation.name} failed with ${error.message}`); } + return; } -} -export function specialCheck(result: Document, check: SpecialOperator): void { - if (isUnsetOrMatchesOperator(check)) { - if (result == null) return; // acceptable unset - if (typeof check.$$unsetOrMatches === 'object') { - // We need to a "deep equals" check but the props can also point to special checks - for (const [k, v] of Object.entries(check.$$unsetOrMatches)) { - expect(result).to.have.property(k); - if (isSpecialOperator(v)) { - specialCheck(result[k], v); - } else { - expect(v).to.equal(check.$$unsetOrMatches); - } - } - } else { - expect(result).to.equal(check.$$unsetOrMatches); - } - } else if (isExistsOperator(check)) { - throw new Error('not implemented.'); - } else if (isMatchesEntityOperator(check)) { - throw new Error('not implemented.'); - } else if (isMatchesHexBytesOperator(check)) { - throw new Error('not implemented.'); - } else if (isSessionLsidOperator(check)) { - throw new Error('not implemented.'); - } else if (isTypeOperator(check)) { - throw new Error('not implemented.'); - } else { - throw new Error('not implemented.'); + // We check the positive outcome here so the try-catch above doesn't catch our chai assertions + + if (operation.expectError) { + expect.fail(`Operation ${operation.name} succeeded but was not supposed to`); + } + + if (operation.expectResult) { + expect(expectResultCheck(result, operation.expectResult, entities)).to.be.true; + } + + if (operation.saveResultAsEntity) { + entities.set(operation.saveResultAsEntity, result); } } diff --git a/test/functional/unified-spec-runner/schema.ts b/test/functional/unified-spec-runner/schema.ts index 55526faf025..90680f81b39 100644 --- a/test/functional/unified-spec-runner/schema.ts +++ b/test/functional/unified-spec-runner/schema.ts @@ -46,7 +46,7 @@ export interface ClientEntity { uriOptions?: Document; useMultipleMongoses?: boolean; observeEvents?: ObservableEventId[]; - ignoreCommandMonitoringEvents?: [string, ...string[]]; + ignoreCommandMonitoringEvents?: string[]; serverApi?: ServerApi; } export interface DatabaseEntity { diff --git a/test/functional/unified-spec-runner/unified-utils.ts b/test/functional/unified-spec-runner/unified-utils.ts index 572792f82c4..781598d1dcf 100644 --- a/test/functional/unified-spec-runner/unified-utils.ts +++ b/test/functional/unified-spec-runner/unified-utils.ts @@ -1,14 +1,9 @@ import { expect } from 'chai'; -import { - CommandFailedEvent, - CommandStartedEvent, - CommandSucceededEvent -} from '../../../src/cmap/events'; -import type { CommandEvent } from './entities'; -import type { CollectionOrDatabaseOptions, ExpectedEvent, RunOnRequirement } from './schema'; +import type { CollectionOrDatabaseOptions, RunOnRequirement } from './schema'; import type { TestConfiguration } from './unified.test'; import { gte as semverGte, lte as semverLte } from 'semver'; import { CollectionOptions, DbOptions } from '../../../src'; +import { isDeepStrictEqual } from 'util'; const ENABLE_UNIFIED_TEST_LOGGING = false; export function log(message: unknown, ...optionalParameters: unknown[]): void { @@ -38,9 +33,12 @@ export function getUnmetRequirements(config: TestConfiguration, r: RunOnRequirem } if (r.serverParameters) { - // for (const [name, value] of Object.entries(r.serverParameters)) { - // // TODO - // } + if (!config.parameters) throw new Error('Configuration does not have server parameters'); + for (const [name, value] of Object.entries(r.serverParameters)) { + if (name in config.parameters) { + ok &&= isDeepStrictEqual(config.parameters[name], value); + } + } } return ok; @@ -58,30 +56,6 @@ export function* zip( } } -export function matchesEvents(expected: ExpectedEvent[], actual: CommandEvent[]): void { - expect(expected).to.have.lengthOf(actual.length); - - for (const [index, actualEvent] of actual.entries()) { - const expectedEvent = expected[index]; - - if (expectedEvent.commandStartedEvent && actualEvent instanceof CommandStartedEvent) { - expect(actualEvent.commandName).to.equal(expectedEvent.commandStartedEvent.commandName); - expect(actualEvent.command).to.containSubset(expectedEvent.commandStartedEvent.command); - expect(actualEvent.databaseName).to.equal(expectedEvent.commandStartedEvent.databaseName); - } else if ( - expectedEvent.commandSucceededEvent && - actualEvent instanceof CommandSucceededEvent - ) { - expect(actualEvent.commandName).to.equal(expectedEvent.commandSucceededEvent.commandName); - expect(actualEvent.reply).to.containSubset(expectedEvent.commandSucceededEvent.reply); - } else if (expectedEvent.commandFailedEvent && actualEvent instanceof CommandFailedEvent) { - expect(actualEvent.commandName).to.equal(expectedEvent.commandFailedEvent.commandName); - } else { - expect.fail(`Events must be one of the known types, got ${actualEvent}`); - } - } -} - /** Correct schema version to be semver compliant */ export function patchVersion(version: string): string { expect(version).to.be.a('string'); @@ -98,71 +72,3 @@ export function patchCollectionOptions(options: CollectionOrDatabaseOptions): Co // TODO return { ...options } as CollectionOptions; } - -export interface ExistsOperator { - $$exists: boolean; -} -export function isExistsOperator(value: unknown): value is ExistsOperator { - return typeof value === 'object' && value != null && '$$exists' in value; -} -export interface TypeOperator { - $$type: boolean; -} -export function isTypeOperator(value: unknown): value is TypeOperator { - return typeof value === 'object' && value != null && '$$type' in value; -} -export interface MatchesEntityOperator { - $$matchesEntity: string; -} -export function isMatchesEntityOperator(value: unknown): value is MatchesEntityOperator { - return typeof value === 'object' && value != null && '$$matchesEntity' in value; -} -export interface MatchesHexBytesOperator { - $$matchesHexBytes: string; -} -export function isMatchesHexBytesOperator(value: unknown): value is MatchesHexBytesOperator { - return typeof value === 'object' && value != null && '$$matchesHexBytes' in value; -} -export interface UnsetOrMatchesOperator { - $$unsetOrMatches: unknown; -} -export function isUnsetOrMatchesOperator(value: unknown): value is UnsetOrMatchesOperator { - return typeof value === 'object' && value != null && '$$unsetOrMatches' in value; -} -export interface SessionLsidOperator { - $$sessionLsid: unknown; -} -export function isSessionLsidOperator(value: unknown): value is SessionLsidOperator { - return typeof value === 'object' && value != null && '$$sessionLsid' in value; -} - -export const SpecialOperatorKeys = [ - '$$exists', - '$$type', - '$$matchesEntity', - '$$matchesHexBytes', - '$$unsetOrMatches', - '$$sessionLsid' -]; - -export type SpecialOperator = - | ExistsOperator - | TypeOperator - | MatchesEntityOperator - | MatchesHexBytesOperator - | UnsetOrMatchesOperator - | SessionLsidOperator; - -// eslint-disable-next-line @typescript-eslint/ban-types -type KeysOfUnion = T extends object ? keyof T : never; -export type SpecialOperatorKey = KeysOfUnion; -export function isSpecialOperator(value: unknown): value is SpecialOperator { - return ( - isExistsOperator(value) || - isTypeOperator(value) || - isMatchesEntityOperator(value) || - isMatchesHexBytesOperator(value) || - isUnsetOrMatchesOperator(value) || - isSessionLsidOperator(value) - ); -} diff --git a/test/functional/unified-spec-runner/unified.test.ts b/test/functional/unified-spec-runner/unified.test.ts index 82e8d3cfe39..6551e14c492 100644 --- a/test/functional/unified-spec-runner/unified.test.ts +++ b/test/functional/unified-spec-runner/unified.test.ts @@ -2,11 +2,12 @@ import { expect } from 'chai'; import { ReadPreference } from '../../../src/read_preference'; import { loadSpecTests } from '../../spec/index'; import * as uni from './schema'; -import { getUnmetRequirements, matchesEvents, patchVersion, zip, log } from './unified-utils'; -import { EntitiesMap } from './entities'; +import { getUnmetRequirements, patchVersion, zip, log } from './unified-utils'; +import { CommandEvent, EntitiesMap } from './entities'; import { ns } from '../../../src/utils'; import { executeOperationAndCheck } from './operations'; import { satisfies as semverSatisfies } from 'semver'; +import { matchesEvents } from './match'; export type TestConfiguration = InstanceType< typeof import('../../tools/runner/config')['TestConfiguration'] @@ -37,6 +38,9 @@ async function runOne( await UTIL_CLIENT.connect(); ctx.defer(async () => await UTIL_CLIENT.close()); + // Must fetch parameters before checking runOnRequirements + ctx.configuration.parameters = await UTIL_CLIENT.db().admin().command({ getParameter: '*' }); + // If test.runOnRequirements is specified, the test runner MUST skip the test unless one or more // runOnRequirement objects are satisfied. if (test.runOnRequirements) { @@ -94,7 +98,7 @@ async function runOne( await executeOperationAndCheck(operation, entities); } - const clientEvents = new Map(); + const clientEvents = new Map(); // 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')) { @@ -107,7 +111,7 @@ async function runOne( const actualEvents = clientEvents.get(clientId); expect(actualEvents, `No client entity found with id ${clientId}`).to.exist; - matchesEvents(expectedEventList.events, actualEvents); + matchesEvents(expectedEventList.events, actualEvents, entities); } } @@ -144,6 +148,7 @@ describe('Unified test format', function unifiedTestRunner() { } catch (error) { if (error.message.includes('not implemented.')) { log(`${test.description}: was skipped due to missing functionality`); + log(error.stack); this.skip(); } else { throw error; diff --git a/test/tools/runner/config.js b/test/tools/runner/config.js index fd450365d56..b724a40e10b 100644 --- a/test/tools/runner/config.js +++ b/test/tools/runner/config.js @@ -28,6 +28,7 @@ class TestConfiguration { this.topologyType = context.topologyType; this.version = context.version; this.clientSideEncryption = context.clientSideEncryption; + this.parameters = undefined; this.options = { hosts, hostAddresses, diff --git a/test/tools/utils.js b/test/tools/utils.js index 3be582b2426..9f82abaaf65 100644 --- a/test/tools/utils.js +++ b/test/tools/utils.js @@ -177,7 +177,77 @@ function visualizeMonitoringEvents(client) { }); } +class EventCollector { + constructor(obj, events, options) { + this._events = Object.create(null); + this._timeout = options ? options.timeout : 5000; + + events.forEach(eventName => { + this._events[eventName] = []; + obj.on(eventName, event => this._events[eventName].push(event)); + }); + } + + waitForEvent(eventName, count, callback) { + if (typeof count === 'function') { + callback = count; + count = 1; + } + + this.waitForEventImpl(this, Date.now(), eventName, count, callback); + } + + /** + * Will only return one event at a time from the front of the list + * Useful for iterating over the events in the order they occurred + * + * @param {string} eventName + * @returns {Promise>} + */ + waitAndShiftEvent(eventName) { + return new Promise((resolve, reject) => { + if (this._events[eventName].length > 0) { + return resolve(this._events[eventName].shift()); + } + this.waitForEventImpl(this, Date.now(), eventName, 1, error => { + if (error) return reject(error); + resolve(this._events[eventName].shift()); + }); + }); + } + + reset(eventName) { + if (eventName == null) { + Object.keys(this._events).forEach(eventName => { + this._events[eventName] = []; + }); + + return; + } + + if (this._events[eventName] == null) { + throw new TypeError(`invalid event name "${eventName}" specified for reset`); + } + + this._events[eventName] = []; + } + + waitForEventImpl(collector, start, eventName, count, callback) { + const events = collector._events[eventName]; + if (events.length >= count) { + return callback(undefined, events); + } + + if (Date.now() - start >= collector._timeout) { + return callback(new Error(`timed out waiting for event "${eventName}"`)); + } + + setTimeout(() => this.waitForEventImpl(collector, start, eventName, count, callback), 10); + } +} + module.exports = { + EventCollector, makeTestFunction, ensureCalledWith, ClassWithLogger, From 510efd6393c22810658c95cb93eefdf7c0ea74aa Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Thu, 21 Jan 2021 18:50:58 -0500 Subject: [PATCH 5/8] fix: lint --- test/functional/unified-spec-runner/match.ts | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/test/functional/unified-spec-runner/match.ts b/test/functional/unified-spec-runner/match.ts index 1bb632f3ead..439f312dd12 100644 --- a/test/functional/unified-spec-runner/match.ts +++ b/test/functional/unified-spec-runner/match.ts @@ -1,15 +1,6 @@ import { expect } from 'chai'; import { isDeepStrictEqual } from 'util'; -import { - Binary, - BSONRegExp, - Document, - Double, - Int32, - Long, - ObjectId, - Timestamp -} from '../../../src'; +import { Binary, Document, Double, Int32, Long, ObjectId, Timestamp } from '../../../src'; import { CommandFailedEvent, CommandStartedEvent, From 62e48d4a175fd2ae5da9622bfcc2bdac5116c5c4 Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Fri, 22 Jan 2021 12:04:53 -0500 Subject: [PATCH 6/8] fix: run on requirement check --- test/functional/unified-spec-runner/unified-utils.ts | 2 +- test/functional/unified-spec-runner/unified.test.ts | 10 +++++++--- test/tools/runner/config.js | 2 +- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/test/functional/unified-spec-runner/unified-utils.ts b/test/functional/unified-spec-runner/unified-utils.ts index 781598d1dcf..6abb2148bb9 100644 --- a/test/functional/unified-spec-runner/unified-utils.ts +++ b/test/functional/unified-spec-runner/unified-utils.ts @@ -10,7 +10,7 @@ export function log(message: unknown, ...optionalParameters: unknown[]): void { if (ENABLE_UNIFIED_TEST_LOGGING) console.warn(message, ...optionalParameters); } -export function getUnmetRequirements(config: TestConfiguration, r: RunOnRequirement): boolean { +export function topologySatisfies(config: TestConfiguration, r: RunOnRequirement): boolean { let ok = true; if (r.minServerVersion) { const minVersion = patchVersion(r.minServerVersion); diff --git a/test/functional/unified-spec-runner/unified.test.ts b/test/functional/unified-spec-runner/unified.test.ts index 6551e14c492..2dc973d2105 100644 --- a/test/functional/unified-spec-runner/unified.test.ts +++ b/test/functional/unified-spec-runner/unified.test.ts @@ -2,7 +2,7 @@ import { expect } from 'chai'; import { ReadPreference } from '../../../src/read_preference'; import { loadSpecTests } from '../../spec/index'; import * as uni from './schema'; -import { getUnmetRequirements, patchVersion, zip, log } from './unified-utils'; +import { patchVersion, zip, log, topologySatisfies } from './unified-utils'; import { CommandEvent, EntitiesMap } from './entities'; import { ns } from '../../../src/utils'; import { executeOperationAndCheck } from './operations'; @@ -43,8 +43,12 @@ async function runOne( // If test.runOnRequirements is specified, the test runner MUST skip the test unless one or more // runOnRequirement objects are satisfied. - if (test.runOnRequirements) { - if (!test.runOnRequirements.some(r => getUnmetRequirements(ctx.configuration, r))) { + const allRequirements = [ + ...(unifiedSuite.runOnRequirements ?? []), + ...(test.runOnRequirements ?? []) + ]; + for (const requirement of allRequirements) { + if (!topologySatisfies(ctx.configuration, requirement)) { ctx.skip(); } } diff --git a/test/tools/runner/config.js b/test/tools/runner/config.js index b724a40e10b..ddc1ae92752 100644 --- a/test/tools/runner/config.js +++ b/test/tools/runner/config.js @@ -186,7 +186,7 @@ class TestConfiguration { } else { multipleHosts = this.options.hostAddresses .reduce((built, host) => { - built.push(host.type === 'tcp' ? `${host.host}:${host.port}` : host.host); + built.push(typeof host.port === 'number' ? `${host.host}:${host.port}` : host.host); return built; }, []) .join(','); From 402145bbc9c9ee9b6735f9527bb3d8b5e894eacc Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Mon, 25 Jan 2021 13:28:10 -0500 Subject: [PATCH 7/8] fix: nested documents must have exact keys Adds a check that asserts when at a recursion depth greater than 1 that documents do not have extra keys --- test/functional/unified-spec-runner/match.ts | 42 +++++++++++++------- 1 file changed, 28 insertions(+), 14 deletions(-) diff --git a/test/functional/unified-spec-runner/match.ts b/test/functional/unified-spec-runner/match.ts index 439f312dd12..0a7826d8699 100644 --- a/test/functional/unified-spec-runner/match.ts +++ b/test/functional/unified-spec-runner/match.ts @@ -168,24 +168,26 @@ export function expectResultCheck( actual: Document, expected: Document | number | string | boolean, entities: EntitiesMap, - path: string[] = [] + path: string[] = [], + depth = 0 ): boolean { - const result = resultCheck(actual, expected, entities, path); - if (result[0] === false) { - const path = result[1].join(''); + const ok = resultCheck(actual, expected, entities, path, depth); + if (ok === false) { + const pathString = path.join(''); const expectedJSON = JSON.stringify(expected, undefined, 2); const actualJSON = JSON.stringify(actual, undefined, 2); - expect.fail(`Unable to match ${expectedJSON} to ${actualJSON} at ${path}`); + expect.fail(`Unable to match ${expectedJSON} to ${actualJSON} at ${pathString}`); } - return result[0]; + return ok; } export function resultCheck( actual: Document, expected: Document | number | string | boolean, entities: EntitiesMap, - path: string[] -): [ok: boolean, path: string[]] { + path: string[], + depth = 0 +): boolean { if (typeof expected === 'object' && expected !== null) { // Expected is an object // either its a special operator or just an object to check equality against @@ -193,23 +195,30 @@ export function resultCheck( if (isSpecialOperator(expected)) { // Special operation check is a base condition // specialCheck may recurse depending upon the check ($$unsetOrMatches) - return [specialCheck(actual, expected, entities, path), path]; + return specialCheck(actual, expected, entities, path, depth); } else { // Just a plain object, however this object can contain special operations // So we need to recurse over each key,value let ok = true; const expectedEntries = Object.entries(expected); + + if (depth > 1 && Object.keys(actual).length !== Object.keys(expected).length) { + throw new Error(`[${Object.keys(actual)}] length !== [${Object.keys(expected)}]`); + } + for (const [key, value] of expectedEntries) { path.push(Array.isArray(expected) ? `[${key}]` : `.${key}`); // record what key we're at - ok &&= expectResultCheck(actual[key], value, entities, path); + depth += 1; + ok &&= expectResultCheck(actual[key], value, entities, path, depth); + depth -= 1; path.pop(); // if the recursion was successful we can drop the tested key } - return [ok, path]; + return ok; } } else { // Here's our recursion base case // expected is: number | string | boolean | null - return [isDeepStrictEqual(actual, expected), path]; + return isDeepStrictEqual(actual, expected); } } @@ -217,14 +226,19 @@ export function specialCheck( actual: Document, expected: SpecialOperator, entities: EntitiesMap, - path: string[] = [] + path: string[] = [], + depth = 0 ): boolean { let ok = false; if (isUnsetOrMatchesOperator(expected)) { // $$unsetOrMatches ok = true; // start with true assumption if (actual === null || actual === undefined) ok = true; - else ok &&= expectResultCheck(actual, expected.$$unsetOrMatches, entities, path); + else { + depth += 1; + ok &&= expectResultCheck(actual, expected.$$unsetOrMatches, entities, path, depth); + depth -= 1; + } } else if (isMatchesEntityOperator(expected)) { // $$matchesEntity const entity = entities.get(expected.$$matchesEntity); From f9a453bfeeedf5ab5576c9f4c11941532124ec24 Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Mon, 25 Jan 2021 17:52:47 -0500 Subject: [PATCH 8/8] fix: more terse type check functions --- test/functional/unified-spec-runner/match.ts | 113 +++++-------------- 1 file changed, 28 insertions(+), 85 deletions(-) diff --git a/test/functional/unified-spec-runner/match.ts b/test/functional/unified-spec-runner/match.ts index 0a7826d8699..b9781ed7cc4 100644 --- a/test/functional/unified-spec-runner/match.ts +++ b/test/functional/unified-spec-runner/match.ts @@ -1,6 +1,6 @@ import { expect } from 'chai'; import { isDeepStrictEqual } from 'util'; -import { Binary, Document, Double, Int32, Long, ObjectId, Timestamp } from '../../../src'; +import { Binary, Document, Long, ObjectId } from '../../../src'; import { CommandFailedEvent, CommandStartedEvent, @@ -78,91 +78,34 @@ export function isSpecialOperator(value: unknown): value is SpecialOperator { } const TYPE_MAP = new Map(); -function typeof_double(actual): actual is number | Double { - return typeof actual === 'number' || actual._bsontype === 'Double'; -} -function typeof_string(actual: unknown): actual is string { - return typeof actual === 'string'; -} -function typeof_object(actual: unknown): actual is Document { - return typeof actual === 'object' && actual !== null; -} -function typeof_array(actual: unknown): actual is unknown[] { - return Array.isArray(actual); -} -function typeof_binData(actual) { - return actual instanceof Binary; -} -function typeof_undefined(actual) { - return actual === undefined; -} -function typeof_objectId(actual) { - return actual instanceof ObjectId; -} -function typeof_bool(actual) { - return typeof actual === 'boolean'; -} -function typeof_date(actual) { - return actual instanceof Date; -} -function typeof_null(actual) { - return actual === null; -} -function typeof_regex(actual) { - return actual instanceof RegExp || actual._bsontype === 'BSONRegExp'; -} -function typeof_dbPointer(actual) { - return actual._bsontype === 'DBRef'; -} -function typeof_javascript(actual) { - return actual._bsontype === 'Code'; -} -function typeof_symbol(actual) { - return actual._bsontype === 'Symbol'; -} -function typeof_javascriptWithScope(actual) { - return actual._bsontype === 'Code' && actual.scope; -} -function typeof_int(actual): actual is number | Int32 { - return (typeof actual === 'number' && Number.isInteger(actual)) || actual._bsontype === 'Int32'; -} -function typeof_timestamp(actual: Timestamp, expected: Timestamp) { - expect(actual.equals(expected)).to.be.true; -} -function typeof_long(actual: unknown): actual is number | Long { - return (typeof actual === 'number' && Number.isInteger(actual)) || Long.isLong(actual); -} -function typeof_decimal(actual) { - return actual._bsontype === 'Decimal128'; -} -function typeof_minKey(actual) { - return actual._bsontype === 'MinKey'; -} -function typeof_maxKey(actual) { - return actual._bsontype === 'MaxKey'; -} -TYPE_MAP.set('double', typeof_double); -TYPE_MAP.set('string', typeof_string); -TYPE_MAP.set('object', typeof_object); -TYPE_MAP.set('array', typeof_array); -TYPE_MAP.set('binData', typeof_binData); -TYPE_MAP.set('undefined', typeof_undefined); -TYPE_MAP.set('objectId', typeof_objectId); -TYPE_MAP.set('bool', typeof_bool); -TYPE_MAP.set('date', typeof_date); -TYPE_MAP.set('null', typeof_null); -TYPE_MAP.set('regex', typeof_regex); -TYPE_MAP.set('dbPointer', typeof_dbPointer); -TYPE_MAP.set('javascript', typeof_javascript); -TYPE_MAP.set('symbol', typeof_symbol); -TYPE_MAP.set('javascriptWithScope', typeof_javascriptWithScope); -TYPE_MAP.set('int', typeof_int); -TYPE_MAP.set('timestamp', typeof_timestamp); -TYPE_MAP.set('long', typeof_long); -TYPE_MAP.set('decimal', typeof_decimal); -TYPE_MAP.set('minKey', typeof_minKey); -TYPE_MAP.set('maxKey', typeof_maxKey); +TYPE_MAP.set('double', actual => typeof actual === 'number' || actual._bsontype === 'Double'); +TYPE_MAP.set('string', actual => typeof actual === 'string'); +TYPE_MAP.set('object', actual => typeof actual === 'object' && actual !== null); +TYPE_MAP.set('array', actual => Array.isArray(actual)); +TYPE_MAP.set('binData', actual => actual instanceof Binary); +TYPE_MAP.set('undefined', actual => actual === undefined); +TYPE_MAP.set('objectId', actual => actual instanceof ObjectId); +TYPE_MAP.set('bool', actual => typeof actual === 'boolean'); +TYPE_MAP.set('date', actual => actual instanceof Date); +TYPE_MAP.set('null', actual => actual === null); +TYPE_MAP.set('regex', actual => actual instanceof RegExp || actual._bsontype === 'BSONRegExp'); +TYPE_MAP.set('dbPointer', actual => actual._bsontype === 'DBRef'); +TYPE_MAP.set('javascript', actual => actual._bsontype === 'Code'); +TYPE_MAP.set('symbol', actual => actual._bsontype === 'Symbol'); +TYPE_MAP.set('javascriptWithScope', actual => actual._bsontype === 'Code' && actual.scope); +TYPE_MAP.set('timestamp', actual => actual._bsontype === 'Timestamp'); +TYPE_MAP.set('decimal', actual => actual._bsontype === 'Decimal128'); +TYPE_MAP.set('minKey', actual => actual._bsontype === 'MinKey'); +TYPE_MAP.set('maxKey', actual => actual._bsontype === 'MaxKey'); +TYPE_MAP.set( + 'int', + actual => (typeof actual === 'number' && Number.isInteger(actual)) || actual._bsontype === 'Int32' +); +TYPE_MAP.set( + 'long', + actual => (typeof actual === 'number' && Number.isInteger(actual)) || Long.isLong(actual) +); export function expectResultCheck( actual: Document,