From ab5257ce230bd68f28479f2d89d2455d5e1b8706 Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Fri, 8 Jan 2021 17:47:35 -0500 Subject: [PATCH 1/3] 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 1013bf58f8c85f68410bc079641ab832aa4e1493 Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Thu, 21 Jan 2021 18:35:18 -0500 Subject: [PATCH 2/3] 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 --------------- 1 file changed, 15 deletions(-) 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", From dcf216d6eb2e7a672940641c52bef95fed37d58c Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Mon, 25 Jan 2021 17:59:03 -0500 Subject: [PATCH 3/3] test: Operations needed for versioned API tests Adds a number of operations to support versioned API tests. NODE-2287 --- test/functional/unified-spec-runner/match.ts | 61 +++++++- .../unified-spec-runner/operations.ts | 147 +++++++++++++++--- test/functional/unified-spec-runner/schema.ts | 4 +- 3 files changed, 183 insertions(+), 29 deletions(-) diff --git a/test/functional/unified-spec-runner/match.ts b/test/functional/unified-spec-runner/match.ts index b9781ed7cc4..9735063ac88 100644 --- a/test/functional/unified-spec-runner/match.ts +++ b/test/functional/unified-spec-runner/match.ts @@ -1,13 +1,13 @@ import { expect } from 'chai'; import { isDeepStrictEqual } from 'util'; -import { Binary, Document, Long, ObjectId } from '../../../src'; +import { Binary, Document, Long, ObjectId, MongoError } from '../../../src'; import { CommandFailedEvent, CommandStartedEvent, CommandSucceededEvent } from '../../../src/cmap/events'; import { CommandEvent, EntitiesMap } from './entities'; -import { ExpectedEvent } from './schema'; +import { ExpectedError, ExpectedEvent } from './schema'; export interface ExistsOperator { $$exists: boolean; @@ -242,3 +242,60 @@ export function matchesEvents( } } } + +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; + } + + if (expected.errorContains) { + if (error.message.includes(expected.errorContains)) { + throw new Error( + `Error message was supposed to contain '${expected.errorContains}' but had '${error.message}'` + ); + } + } + + if (!(error instanceof MongoError)) { + throw new Error(`Assertions need ${error} to be a MongoError`); + } + + if (expected.errorCode) { + if (error.code !== expected.errorCode) { + throw new Error(`${error} was supposed to have code '${expected.errorCode}'`); + } + } + + if (expected.errorCodeName) { + if (error.codeName !== expected.errorCodeName) { + throw new Error(`${error} was supposed to have '${expected.errorCodeName}' codeName`); + } + } + + if (expected.errorLabelsContain) { + for (const errorLabel of expected.errorLabelsContain) { + if (!error.hasErrorLabel(errorLabel)) { + throw new Error(`${error} was supposed to have '${errorLabel}'`); + } + } + } + + if (expected.errorLabelsOmit) { + for (const errorLabel of expected.errorLabelsOmit) { + if (error.hasErrorLabel(errorLabel)) { + throw new Error(`${error} was not supposed to have '${errorLabel}'`); + } + } + } + + if (expected.expectResult) { + if (!expectResultCheck(error, expected.expectResult, entities)) { + throw new Error(`${error} supposed to match result ${JSON.stringify(expected.expectResult)}`); + } + } +} diff --git a/test/functional/unified-spec-runner/operations.ts b/test/functional/unified-spec-runner/operations.ts index 58788722241..43e7851114c 100644 --- a/test/functional/unified-spec-runner/operations.ts +++ b/test/functional/unified-spec-runner/operations.ts @@ -1,18 +1,13 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ import { expect } from 'chai'; -import { ChangeStream, Document, InsertOneOptions, MongoError } from '../../../src'; +import { Collection, Db } from '../../../src'; +import { ChangeStream, Document, InsertOneOptions } from '../../../src'; +import { BulkWriteResult } from '../../../src/bulk/common'; import { EventCollector } from '../../tools/utils'; import { EntitiesMap } from './entities'; -import { expectResultCheck } from './match'; +import { expectErrorCheck, expectResultCheck } from './match'; import type * as uni from './schema'; -export class UnifiedOperation { - name: string; - constructor(op: uni.OperationDescription) { - this.name = op.name; - } -} - async function abortTransactionOperation( entities: EntitiesMap, op: uni.OperationDescription @@ -23,7 +18,22 @@ async function aggregateOperation( entities: EntitiesMap, op: uni.OperationDescription ): Promise { - throw new Error('not implemented.'); + const dbOrCollection = entities.get(op.object) as Db | Collection; + if (!(dbOrCollection instanceof Db || dbOrCollection instanceof Collection)) { + throw new Error(`Operation object '${op.object}' must be a db or collection`); + } + return dbOrCollection + .aggregate(op.arguments.pipeline, { + allowDiskUse: op.arguments.allowDiskUse, + batchSize: op.arguments.batchSize, + bypassDocumentValidation: op.arguments.bypassDocumentValidation, + maxTimeMS: op.arguments.maxTimeMS, + maxAwaitTimeMS: op.arguments.maxAwaitTimeMS, + collation: op.arguments.collation, + hint: op.arguments.hint, + out: op.arguments.out + }) + .toArray(); } async function assertCollectionExistsOperation( entities: EntitiesMap, @@ -94,14 +104,16 @@ async function assertSessionTransactionStateOperation( async function bulkWriteOperation( entities: EntitiesMap, op: uni.OperationDescription -): Promise { - throw new Error('not implemented.'); +): Promise { + const collection = entities.getEntity('collection', op.object); + return collection.bulkWrite(op.arguments.requests); } async function commitTransactionOperation( entities: EntitiesMap, op: uni.OperationDescription ): Promise { - throw new Error('not implemented.'); + const session = entities.getEntity('session', op.object); + return session.commitTransaction(); } async function createChangeStreamOperation( entities: EntitiesMap, @@ -148,7 +160,8 @@ async function deleteOneOperation( entities: EntitiesMap, op: uni.OperationDescription ): Promise { - throw new Error('not implemented.'); + const collection = entities.getEntity('collection', op.object); + return collection.deleteOne(op.arguments.filter); } async function dropCollectionOperation( entities: EntitiesMap, @@ -168,19 +181,24 @@ async function findOperation( ): Promise { const collection = entities.getEntity('collection', op.object); const { filter, sort, batchSize, limit } = op.arguments; - return await collection.find(filter, { sort, batchSize, limit }).toArray(); + return collection.find(filter, { sort, batchSize, limit }).toArray(); } async function findOneAndReplaceOperation( entities: EntitiesMap, op: uni.OperationDescription ): Promise { - throw new Error('not implemented.'); + const collection = entities.getEntity('collection', op.object); + return collection.findOneAndReplace(op.arguments.filter, op.arguments.replacement); } async function findOneAndUpdateOperation( entities: EntitiesMap, op: uni.OperationDescription ): Promise { - throw new Error('not implemented.'); + const collection = entities.getEntity('collection', op.object); + const returnOriginal = op.arguments.returnDocument === 'Before'; + return ( + await collection.findOneAndUpdate(op.arguments.filter, op.arguments.update, { returnOriginal }) + ).value; } async function failPointOperation( entities: EntitiesMap, @@ -201,7 +219,7 @@ async function insertOneOperation( session } as InsertOneOptions; - return await collection.insertOne(op.arguments.document, options); + return collection.insertOne(op.arguments.document, options); } async function insertManyOperation( entities: EntitiesMap, @@ -216,7 +234,7 @@ async function insertManyOperation( ordered: op.arguments.ordered ?? true }; - return await collection.insertMany(op.arguments.documents, options); + return collection.insertMany(op.arguments.documents, options); } async function iterateUntilDocumentOrErrorOperation( entities: EntitiesMap, @@ -239,13 +257,20 @@ async function replaceOneOperation( entities: EntitiesMap, op: uni.OperationDescription ): Promise { - throw new Error('not implemented.'); + const collection = entities.getEntity('collection', op.object); + return collection.replaceOne(op.arguments.filter, op.arguments.replacement, { + bypassDocumentValidation: op.arguments.bypassDocumentValidation, + collation: op.arguments.collation, + hint: op.arguments.hint, + upsert: op.arguments.upsert + }); } async function startTransactionOperation( entities: EntitiesMap, op: uni.OperationDescription -): Promise { - throw new Error('not implemented.'); +): Promise { + const session = entities.getEntity('session', op.object); + session.startTransaction(); } async function targetedFailPointOperation( entities: EntitiesMap, @@ -277,8 +302,67 @@ async function withTransactionOperation( ): Promise { throw new Error('not implemented.'); } +async function countDocumentsOperation( + entities: EntitiesMap, + op: uni.OperationDescription +): Promise { + const collection = entities.getEntity('collection', op.object); + return collection.countDocuments(op.arguments.filter as Document); +} +async function deleteManyOperation( + entities: EntitiesMap, + op: uni.OperationDescription +): Promise { + const collection = entities.getEntity('collection', op.object); + return collection.deleteMany(op.arguments.filter); +} +async function distinctOperation( + entities: EntitiesMap, + op: uni.OperationDescription +): Promise { + const collection = entities.getEntity('collection', op.object); + return collection.distinct(op.arguments.fieldName as string, op.arguments.filter as Document); +} +async function estimatedDocumentCountOperation( + entities: EntitiesMap, + op: uni.OperationDescription +): Promise { + const collection = entities.getEntity('collection', op.object); + return collection.estimatedDocumentCount(); +} +async function findOneAndDeleteOperation( + entities: EntitiesMap, + op: uni.OperationDescription +): Promise { + const collection = entities.getEntity('collection', op.object); + return collection.findOneAndDelete(op.arguments.filter); +} +async function runCommandOperation( + entities: EntitiesMap, + op: uni.OperationDescription +): Promise { + const db = entities.getEntity('db', op.object); + return db.command(op.arguments.command); +} +async function updateManyOperation( + entities: EntitiesMap, + op: uni.OperationDescription +): Promise { + const collection = entities.getEntity('collection', op.object); + return collection.updateMany(op.arguments.filter, op.arguments.update); +} +async function updateOneOperation( + entities: EntitiesMap, + op: uni.OperationDescription +): Promise { + const collection = entities.getEntity('collection', op.object); + return collection.updateOne(op.arguments.filter, op.arguments.update); +} -type RunOperationFn = (entities: EntitiesMap, op: uni.OperationDescription) => Promise; +type RunOperationFn = ( + entities: EntitiesMap, + op: uni.OperationDescription +) => Promise; export const operations = new Map(); operations.set('abortTransaction', abortTransactionOperation); @@ -321,6 +405,16 @@ operations.set('download', downloadOperation); operations.set('upload', uploadOperation); operations.set('withTransaction', withTransactionOperation); +// Versioned API adds these: +operations.set('countDocuments', countDocumentsOperation); +operations.set('deleteMany', deleteManyOperation); +operations.set('distinct', distinctOperation); +operations.set('estimatedDocumentCount', estimatedDocumentCountOperation); +operations.set('findOneAndDelete', findOneAndDeleteOperation); +operations.set('runCommand', runCommandOperation); +operations.set('updateMany', updateManyOperation); +operations.set('updateOne', updateOneOperation); + export async function executeOperationAndCheck( operation: uni.OperationDescription, entities: EntitiesMap @@ -333,9 +427,12 @@ export async function executeOperationAndCheck( try { result = await opFunc(entities, operation); } catch (error) { + // FIXME: Remove when project is done: + if (error.message === 'not implemented.') { + throw error; + } if (operation.expectError) { - expect(error).to.be.instanceof(MongoError); - // expectErrorCheck(error, operation.expectError); + expectErrorCheck(error, operation.expectError, entities); } else { expect.fail(`Operation ${operation.name} failed with ${error.message}`); } diff --git a/test/functional/unified-spec-runner/schema.ts b/test/functional/unified-spec-runner/schema.ts index 90680f81b39..caadd877cf6 100644 --- a/test/functional/unified-spec-runner/schema.ts +++ b/test/functional/unified-spec-runner/schema.ts @@ -140,7 +140,7 @@ export interface ExpectedError { errorContains?: string; errorCode?: number; errorCodeName?: string; - errorLabelsContain?: [string, ...string[]]; - errorLabelsOmit?: [string, ...string[]]; + errorLabelsContain?: string[]; + errorLabelsOmit?: string[]; expectResult?: unknown; }