diff --git a/api-report/mocha-test-setup.api.md b/api-report/mocha-test-setup.api.md index ed47a710abd9..8e1540c2fe41 100644 --- a/api-report/mocha-test-setup.api.md +++ b/api-report/mocha-test-setup.api.md @@ -4,14 +4,15 @@ ```ts +/// + // @public (undocumented) export const mochaHooks: { beforeAll(): void; - beforeEach(): void; - afterEach(): void; + beforeEach(this: Mocha.Context): void; + afterEach(this: Mocha.Context): void; }; - // (No @packageDocumentation comment for this package) ``` diff --git a/api-report/test-utils.api.md b/api-report/test-utils.api.md index 6b70c3351d40..0bececfe584d 100644 --- a/api-report/test-utils.api.md +++ b/api-report/test-utils.api.md @@ -337,7 +337,6 @@ export function timeoutPromise(executor: (resolve: (value: T | Promise // @public (undocumented) export interface TimeoutWithError { - // (undocumented) durationMs?: number; // (undocumented) errorMsg?: string; @@ -347,7 +346,6 @@ export interface TimeoutWithError { // @public (undocumented) export interface TimeoutWithValue { - // (undocumented) durationMs?: number; // (undocumented) reject: false; diff --git a/experimental/PropertyDDS/packages/property-dds/src/test/rebasing.spec.ts b/experimental/PropertyDDS/packages/property-dds/src/test/rebasing.spec.ts index 8a9480343e07..67fdbd85b535 100644 --- a/experimental/PropertyDDS/packages/property-dds/src/test/rebasing.spec.ts +++ b/experimental/PropertyDDS/packages/property-dds/src/test/rebasing.spec.ts @@ -12,987 +12,987 @@ import { requestFluidObject } from "@fluidframework/runtime-utils"; import { IUrlResolver } from "@fluidframework/driver-definitions"; import { LocalDeltaConnectionServer, ILocalDeltaConnectionServer } from "@fluidframework/server-local-server"; import { - createAndAttachContainer, - createLoader, - ITestFluidObject, - TestFluidObjectFactory, - LoaderContainerTracker, + createAndAttachContainer, + createLoader, + ITestFluidObject, + TestFluidObjectFactory, + LoaderContainerTracker, } from "@fluidframework/test-utils"; import { DeterministicRandomGenerator } from "@fluid-experimental/property-common"; import * as _ from "lodash"; import { - StringArrayProperty, - PropertyFactory, - ArrayProperty, - NamedProperty, + StringArrayProperty, + PropertyFactory, + ArrayProperty, + NamedProperty, Int32Property, } from "@fluid-experimental/property-properties"; import { assert } from "@fluidframework/common-utils"; import { SharedPropertyTree } from "../propertyTree"; function createLocalLoader( - packageEntries: Iterable<[IFluidCodeDetails, TestFluidObjectFactory]>, - deltaConnectionServer: ILocalDeltaConnectionServer, - urlResolver: IUrlResolver, - options?: ILoaderOptions, + packageEntries: Iterable<[IFluidCodeDetails, TestFluidObjectFactory]>, + deltaConnectionServer: ILocalDeltaConnectionServer, + urlResolver: IUrlResolver, + options?: ILoaderOptions, ): IHostLoader { - const documentServiceFactory = new LocalDocumentServiceFactory(deltaConnectionServer); + const documentServiceFactory = new LocalDocumentServiceFactory(deltaConnectionServer); - return createLoader(packageEntries, documentServiceFactory, urlResolver, undefined, options); + return createLoader(packageEntries, documentServiceFactory, urlResolver, undefined, options); } function createDerivedGuid(referenceGuid: string, identifier: string) { - const hash = crypto.createHash("sha1"); - hash.write(`${referenceGuid}:${identifier}`); - hash.end(); - - const hexHash = hash.digest("hex"); - return ( - `${hexHash.substr(0, 8)}-${hexHash.substr(8, 4)}-` + - `${hexHash.substr(12, 4)}-${hexHash.substr(16, 4)}-${hexHash.substr(20, 12)}` - ); + const hash = crypto.createHash("sha1"); + hash.write(`${referenceGuid}:${identifier}`); + hash.end(); + + const hexHash = hash.digest("hex"); + return ( + `${hexHash.substr(0, 8)}-${hexHash.substr(8, 4)}-` + + `${hexHash.substr(12, 4)}-${hexHash.substr(16, 4)}-${hexHash.substr(20, 12)}` + ); } console.assert = (condition: boolean, ...data: any[]) => { - assert(!!condition, "Console Assert"); + assert(!!condition, "Console Assert"); }; function getFunctionSource(fun: any): string { - let source = fun.toString() as string; - source = source.replace(/^.*=>\s*{?\n?\s*/m, ""); - source = source.replace(/}\s*$/m, ""); - source = source.replace(/^\s*/gm, ""); + let source = fun.toString() as string; + source = source.replace(/^.*=>\s*{?\n?\s*/m, ""); + source = source.replace(/}\s*$/m, ""); + source = source.replace(/^\s*/gm, ""); - return source; + return source; } describe("PropertyDDS", () => { const documentId = "localServerTest"; - const documentLoadUrl = `fluid-test://localhost/${documentId}`; - const propertyDdsId = "PropertyTree"; - const codeDetails: IFluidCodeDetails = { - package: "localServerTestPackage", - config: {}, - }; - const factory = new TestFluidObjectFactory([[propertyDdsId, SharedPropertyTree.getFactory()]]); - - let deltaConnectionServer: ILocalDeltaConnectionServer; - let urlResolver: LocalResolver; - let opProcessingController: LoaderContainerTracker; - let container1: IContainer; - let container2: IContainer; - let dataObject1: ITestFluidObject; - let dataObject2: ITestFluidObject; - let sharedPropertyTree1: SharedPropertyTree; - let sharedPropertyTree2: SharedPropertyTree; - - let errorHandler: (Error) => void; - - async function createContainer(): Promise { - const loader = createLocalLoader([[codeDetails, factory]], deltaConnectionServer, urlResolver); - opProcessingController.add(loader); - return createAndAttachContainer(codeDetails, loader, urlResolver.createCreateNewRequest(documentId)); - } - - async function loadContainer(): Promise { - const loader = createLocalLoader([[codeDetails, factory]], deltaConnectionServer, urlResolver); - opProcessingController.add(loader); - return loader.resolve({ url: documentLoadUrl }); - } - - function createRandomTests( - operations: { - getParameters: (random: DeterministicRandomGenerator) => Record void)>; - op: (parameters: Record) => Promise; - probability: number; - }[], - final: () => Promise, - count = 100, - startTest = 0, - maxOperations = 30, - ) { - for (let i = startTest; i < count; i++) { - const seed = createDerivedGuid("", String(i)); - it(`Generated Test Case #${i} (seed: ${seed})`, async function() { - this.timeout(10000); - let testString = ""; - - errorHandler = (err) => { - console.error(`Failed Test code: ${testString}`); - }; - const random = new DeterministicRandomGenerator(seed); - const operationCumSums = [] as number[]; - for (const operation of operations) { - operationCumSums.push( - (operationCumSums[operationCumSums.length - 1] ?? 0) + operation.probability, - ); - } - - try { - const numOperations = random.irandom(maxOperations); - const maxCount = operationCumSums[operationCumSums.length - 1]; - for (const j of _.range(numOperations)) { - const operationId = 1 + random.irandom(maxCount); - const selectedOperation = _.sortedIndex(operationCumSums, operationId); - - const parameters = operations[selectedOperation].getParameters(random); - - // Create the source code for the test - let operationSource = getFunctionSource(operations[selectedOperation].op.toString()); - for (const [key, value] of Object.entries(parameters)) { - const valueString = _.isFunction(value) ? getFunctionSource(value) : value.toString(); - operationSource = operationSource.replace( - new RegExp(`parameters.${key}\\(?\\)?`), - valueString, - ); - } - testString += operationSource; - - await operations[selectedOperation].op(parameters); - } - - testString += getFunctionSource(final); - await final(); - } catch (e) { - console.error(`Failed Test code: ${testString}`); - throw e; - } - }); - } - } - - async function setupContainers(mode = true) { - opProcessingController = new LoaderContainerTracker(); - deltaConnectionServer = LocalDeltaConnectionServer.create(); - urlResolver = new LocalResolver(); - - // Create a Container for the first client. - container1 = await createContainer(); - dataObject1 = await requestFluidObject(container1, "default"); - sharedPropertyTree1 = await dataObject1.getSharedObject(propertyDdsId); - (sharedPropertyTree1 as any).__id = 1; // Add an id to simplify debugging via conditional breakpoints - - // Load the Container that was created by the first client. - container2 = await loadContainer(); - dataObject2 = await requestFluidObject(container2, "default"); - sharedPropertyTree2 = await dataObject2.getSharedObject(propertyDdsId); - (sharedPropertyTree2 as any).__id = 2; // Add an id to simplify debugging via conditional breakpoints - - if (mode) { - // Submitting empty changeset to make sure both trees are in "write" mode, so the tests could control - // which commits are being synced at every point of time. - sharedPropertyTree1.commit(true); - sharedPropertyTree2.commit(true); - } + const documentLoadUrl = `fluid-test://localhost/${documentId}`; + const propertyDdsId = "PropertyTree"; + const codeDetails: IFluidCodeDetails = { + package: "localServerTestPackage", + config: {}, + }; + const factory = new TestFluidObjectFactory([[propertyDdsId, SharedPropertyTree.getFactory()]]); + + let deltaConnectionServer: ILocalDeltaConnectionServer; + let urlResolver: LocalResolver; + let opProcessingController: LoaderContainerTracker; + let container1: IContainer; + let container2: IContainer; + let dataObject1: ITestFluidObject; + let dataObject2: ITestFluidObject; + let sharedPropertyTree1: SharedPropertyTree; + let sharedPropertyTree2: SharedPropertyTree; + + let errorHandler: (Error) => void; + + async function createContainer(): Promise { + const loader = createLocalLoader([[codeDetails, factory]], deltaConnectionServer, urlResolver); + opProcessingController.add(loader); + return createAndAttachContainer(codeDetails, loader, urlResolver.createCreateNewRequest(documentId)); + } + + async function loadContainer(): Promise { + const loader = createLocalLoader([[codeDetails, factory]], deltaConnectionServer, urlResolver); + opProcessingController.add(loader); + return loader.resolve({ url: documentLoadUrl }); + } + + function createRandomTests( + operations: { + getParameters: (random: DeterministicRandomGenerator) => Record void)>; + op: (parameters: Record) => Promise; + probability: number; + }[], + final: () => Promise, + count = 100, + startTest = 0, + maxOperations = 30, + ) { + for (let i = startTest; i < count; i++) { + const seed = createDerivedGuid("", String(i)); + it(`Generated Test Case #${i} (seed: ${seed})`, async () => { + let testString = ""; + + errorHandler = (err) => { + console.error(`Failed Test code: ${testString}`); + }; + const random = new DeterministicRandomGenerator(seed); + const operationCumSums = [] as number[]; + for (const operation of operations) { + operationCumSums.push( + (operationCumSums[operationCumSums.length - 1] ?? 0) + operation.probability, + ); + } + + try { + const numOperations = random.irandom(maxOperations); + const maxCount = operationCumSums[operationCumSums.length - 1]; + for (const j of _.range(numOperations)) { + const operationId = 1 + random.irandom(maxCount); + const selectedOperation = _.sortedIndex(operationCumSums, operationId); + + const parameters = operations[selectedOperation].getParameters(random); + + // Create the source code for the test + let operationSource = getFunctionSource(operations[selectedOperation].op.toString()); + for (const [key, value] of Object.entries(parameters)) { + const valueString = _.isFunction(value) ? getFunctionSource(value) : value.toString(); + operationSource = operationSource.replace( + new RegExp(`parameters.${key}\\(?\\)?`), + valueString, + ); + } + testString += operationSource; - // Attach error handlers to make debugging easier and ensure that internal failures cause the test to fail - errorHandler = (err) => { }; // This enables the create random tests function to register its own handler - container1.on("closed", (err: any) => { - if (err !== undefined) { - errorHandler(err); - throw err; + await operations[selectedOperation].op(parameters); + } + + testString += getFunctionSource(final); + await final(); + } catch (e) { + console.error(`Failed Test code: ${testString}`); + throw e; } + }).timeout(10000); + } + } + + async function setupContainers(mode = true) { + opProcessingController = new LoaderContainerTracker(); + deltaConnectionServer = LocalDeltaConnectionServer.create(); + urlResolver = new LocalResolver(); + + // Create a Container for the first client. + container1 = await createContainer(); + dataObject1 = await requestFluidObject(container1, "default"); + sharedPropertyTree1 = await dataObject1.getSharedObject(propertyDdsId); + (sharedPropertyTree1 as any).__id = 1; // Add an id to simplify debugging via conditional breakpoints + + // Load the Container that was created by the first client. + container2 = await loadContainer(); + dataObject2 = await requestFluidObject(container2, "default"); + sharedPropertyTree2 = await dataObject2.getSharedObject(propertyDdsId); + (sharedPropertyTree2 as any).__id = 2; // Add an id to simplify debugging via conditional breakpoints + + if (mode) { + // Submitting empty changeset to make sure both trees are in "write" mode, so the tests could control + // which commits are being synced at every point of time. + sharedPropertyTree1.commit(true); + sharedPropertyTree2.commit(true); + } + + // Attach error handlers to make debugging easier and ensure that internal failures cause the test to fail + errorHandler = (err) => { }; // This enables the create random tests function to register its own handler + container1.on("closed", (err: any) => { + if (err !== undefined) { + errorHandler(err); + throw err; + } + }); + container2.on("closed", (err: any) => { + if (err !== undefined) { + errorHandler(err); + throw err; + } + }); + } + + function rebaseTests() { + describe("with non overlapping inserts", () => { + let ACount: number; + let CCount: number; + + beforeEach(async function() { + this.timeout(10000); + + // Insert and prepare an array within the container + sharedPropertyTree1.root.insert("array", PropertyFactory.create("String", "array")); + + const array = sharedPropertyTree1.root.get("array") as StringArrayProperty; + array.push("B1"); + array.push("B2"); + array.push("B3"); + sharedPropertyTree1.commit(); + + ACount = 0; + CCount = 0; + + // Make sure both shared trees are in sync + await opProcessingController.ensureSynchronized(); + await opProcessingController.pauseProcessing(); }); - container2.on("closed", (err: any) => { - if (err !== undefined) { - errorHandler(err); - throw err; + + afterEach(() => { + const result = _.range(1, ACount + 1) + .map((i) => `A${i}`) + .concat(["B1", "B2", "B3"]) + .concat(_.range(1, CCount + 1).map((i) => `C${i}`)); + + const array1 = sharedPropertyTree1.root.get("array") as StringArrayProperty; + const array2 = sharedPropertyTree2.root.get("array") as StringArrayProperty; + for (const array of [array1, array2]) { + for (const [i, value] of result.entries()) { + expect(array.get(i)).to.equal(value); + } } }); - } - function rebaseTests() { - describe("with non overlapping inserts", () => { - let ACount: number; - let CCount: number; + function insertInArray(tree: SharedPropertyTree, letter: string) { + const array = tree.root.get("array") as StringArrayProperty; - beforeEach(async function() { - this.timeout(10000); - // Insert and prepare an array within the container - sharedPropertyTree1.root.insert("array", PropertyFactory.create("String", "array")); + // Find the insert position + let insertPosition: number; + let insertString: string; + if (letter === "A") { + // We insert all As in front of B1 + const values: string[] = array.getValues(); + insertPosition = values.indexOf("B1"); - const array = sharedPropertyTree1.root.get("array") as StringArrayProperty; - array.push("B1"); - array.push("B2"); - array.push("B3"); - sharedPropertyTree1.commit(); + // For these letters we can just use the position to get the number for the inserted string + insertString = `A${insertPosition + 1}`; - ACount = 0; - CCount = 0; + ACount++; + } else { + // Alway insert B at the end + insertPosition = array.getLength(); - // Make sure both shared trees are in sync - await opProcessingController.ensureSynchronized(); - await opProcessingController.pauseProcessing(); - }); + // Get the number from the previous entry + const previous = array.get(insertPosition - 1) as string; + const entryNumber = previous[0] === "B" ? 1 : Number.parseInt(previous[1], 10) + 1; + insertString = `C${entryNumber}`; - afterEach(() => { - const result = _.range(1, ACount + 1) - .map((i) => `A${i}`) - .concat(["B1", "B2", "B3"]) - .concat(_.range(1, CCount + 1).map((i) => `C${i}`)); - - const array1 = sharedPropertyTree1.root.get("array") as StringArrayProperty; - const array2 = sharedPropertyTree2.root.get("array") as StringArrayProperty; - for (const array of [array1, array2]) { - for (const [i, value] of result.entries()) { - expect(array.get(i)).to.equal(value); - } - } - }); + CCount++; + } - function insertInArray(tree: SharedPropertyTree, letter: string) { - const array = tree.root.get("array") as StringArrayProperty; + array.insert(insertPosition, insertString); + tree.commit(); + } - // Find the insert position - let insertPosition: number; - let insertString: string; - if (letter === "A") { - // We insert all As in front of B1 - const values: string[] = array.getValues(); - insertPosition = values.indexOf("B1"); + it("Should work when doing two batches with synchronization in between", async () => { + insertInArray(sharedPropertyTree1, "A"); + insertInArray(sharedPropertyTree1, "A"); + insertInArray(sharedPropertyTree1, "A"); - // For these letters we can just use the position to get the number for the inserted string - insertString = `A${insertPosition + 1}`; + await opProcessingController.ensureSynchronized(); - ACount++; - } else { - // Alway insert B at the end - insertPosition = array.getLength(); + insertInArray(sharedPropertyTree2, "C"); + insertInArray(sharedPropertyTree2, "C"); + insertInArray(sharedPropertyTree2, "C"); - // Get the number from the previous entry - const previous = array.get(insertPosition - 1) as string; - const entryNumber = previous[0] === "B" ? 1 : Number.parseInt(previous[1], 10) + 1; - insertString = `C${entryNumber}`; + await opProcessingController.ensureSynchronized(); + }); - CCount++; - } + it("Should work when doing two batches without synchronization inbetween", async () => { + insertInArray(sharedPropertyTree1, "A"); + insertInArray(sharedPropertyTree1, "A"); + insertInArray(sharedPropertyTree1, "A"); - array.insert(insertPosition, insertString); - tree.commit(); - } + insertInArray(sharedPropertyTree2, "C"); + insertInArray(sharedPropertyTree2, "C"); + insertInArray(sharedPropertyTree2, "C"); - it("Should work when doing two batches with synchronization inbetween", async () => { - insertInArray(sharedPropertyTree1, "A"); - insertInArray(sharedPropertyTree1, "A"); - insertInArray(sharedPropertyTree1, "A"); + await opProcessingController.ensureSynchronized(); + }); - await opProcessingController.ensureSynchronized(); + it("Should work when creating local branches with different remote heads", async () => { + insertInArray(sharedPropertyTree2, "C"); + insertInArray(sharedPropertyTree1, "A"); + await opProcessingController.ensureSynchronized(); + insertInArray(sharedPropertyTree2, "C"); + insertInArray(sharedPropertyTree1, "A"); + await opProcessingController.ensureSynchronized(); + insertInArray(sharedPropertyTree2, "C"); + insertInArray(sharedPropertyTree1, "A"); + + await opProcessingController.ensureSynchronized(); + }); - insertInArray(sharedPropertyTree2, "C"); - insertInArray(sharedPropertyTree2, "C"); - insertInArray(sharedPropertyTree2, "C"); + it("Should work when synchronizing after each operation", async () => { + insertInArray(sharedPropertyTree1, "A"); + await opProcessingController.ensureSynchronized(); + insertInArray(sharedPropertyTree1, "A"); + await opProcessingController.ensureSynchronized(); + insertInArray(sharedPropertyTree1, "A"); + await opProcessingController.ensureSynchronized(); + + insertInArray(sharedPropertyTree2, "C"); + await opProcessingController.ensureSynchronized(); + insertInArray(sharedPropertyTree2, "C"); + await opProcessingController.ensureSynchronized(); + insertInArray(sharedPropertyTree2, "C"); + await opProcessingController.ensureSynchronized(); + }); - await opProcessingController.ensureSynchronized(); - }); + it("Should work when synchronizing after pairs of operations", async () => { + insertInArray(sharedPropertyTree1, "A"); + insertInArray(sharedPropertyTree2, "C"); + await opProcessingController.ensureSynchronized(); + insertInArray(sharedPropertyTree1, "A"); + insertInArray(sharedPropertyTree2, "C"); + await opProcessingController.ensureSynchronized(); + insertInArray(sharedPropertyTree1, "A"); + insertInArray(sharedPropertyTree2, "C"); + await opProcessingController.ensureSynchronized(); + }); + + it("works with overlapping sequences", async () => { + insertInArray(sharedPropertyTree2, "C"); + await opProcessingController.processOutgoing(container2); + + // Insert five operations to make this overlap with the insert position of C + insertInArray(sharedPropertyTree1, "A"); + insertInArray(sharedPropertyTree1, "A"); + insertInArray(sharedPropertyTree1, "A"); + insertInArray(sharedPropertyTree1, "A"); + insertInArray(sharedPropertyTree1, "A"); + await opProcessingController.processIncoming(container1); + insertInArray(sharedPropertyTree1, "A"); + await opProcessingController.processIncoming(container2); + + await opProcessingController.ensureSynchronized(); + }); - it("Should work when doing two batches without synchronization inbetween", async () => { - insertInArray(sharedPropertyTree1, "A"); - insertInArray(sharedPropertyTree1, "A"); - insertInArray(sharedPropertyTree1, "A"); + it("Should work when the remote head points to a change that is not the reference change", async () => { + insertInArray(sharedPropertyTree2, "C"); + await opProcessingController.processOutgoing(container2); + insertInArray(sharedPropertyTree1, "A"); + await opProcessingController.processOutgoing(container1); + insertInArray(sharedPropertyTree2, "C"); + await opProcessingController.processIncoming(container2); + insertInArray(sharedPropertyTree2, "C"); + insertInArray(sharedPropertyTree2, "C"); + + await opProcessingController.ensureSynchronized(); + }); - insertInArray(sharedPropertyTree2, "C"); - insertInArray(sharedPropertyTree2, "C"); - insertInArray(sharedPropertyTree2, "C"); + describe("Randomized Tests", () => { + const count = 100; + const startTest = 0; + const logTest = true; + + for (let i = startTest; i < count; i++) { + const seed = createDerivedGuid("", String(i)); + it(`Generated Test Case #${i} (seed: ${seed})`, async () => { + const random = new DeterministicRandomGenerator(seed); + let testString = ""; + + const numOperations = random.irandom(30); + for (const j of _.range(numOperations)) { + const operation = random.irandom(6); + switch (operation) { + case 0: + insertInArray(sharedPropertyTree1, "A"); + if (logTest) { + testString += 'insertInArray(sharedPropertyTree1, "A");\n'; + } + break; + case 1: + insertInArray(sharedPropertyTree2, "C"); + if (logTest) { + testString += 'insertInArray(sharedPropertyTree2, "C");\n'; + } + break; + case 2: + await opProcessingController.processOutgoing(container1); + if (logTest) { + testString += + "await opProcessingController.processOutgoing(container1);\n"; + } + break; + case 3: + await opProcessingController.processIncoming(container1); + if (logTest) { + testString += + "await opProcessingController.processIncoming(container1);\n"; + } + break; + case 4: + await opProcessingController.processOutgoing(container2); + if (logTest) { + testString += + "await opProcessingController.processOutgoing(container2);\n"; + } + break; + case 5: + await opProcessingController.processIncoming(container2); + if (logTest) { + testString += + "await opProcessingController.processIncoming(container2);\n"; + } + break; + default: + throw new Error("Should never happen"); + } + } - await opProcessingController.ensureSynchronized(); + await opProcessingController.ensureSynchronized(); + if (logTest) { + testString += + "await opProcessingController.ensureSynchronized();\n"; + } + }); + } + }); + }); + + describe("with inserts and deletes at arbitrary positions", () => { + let createdProperties: Set; + let deletedProperties: Set; + beforeEach(async () => { + createdProperties = new Set(); + deletedProperties = new Set(); + (PropertyFactory as any)._reregister({ + typeid: "test:namedEntry-1.0.0", + inherits: ["NamedProperty"], + properties: [], }); - it("Should work when creating local branches with different remote heads", async () => { - insertInArray(sharedPropertyTree2, "C"); - insertInArray(sharedPropertyTree1, "A"); - await opProcessingController.ensureSynchronized(); - insertInArray(sharedPropertyTree2, "C"); - insertInArray(sharedPropertyTree1, "A"); - await opProcessingController.ensureSynchronized(); - insertInArray(sharedPropertyTree2, "C"); - insertInArray(sharedPropertyTree1, "A"); + await opProcessingController.pauseProcessing(); + sharedPropertyTree1.root.insert("array", PropertyFactory.create("test:namedEntry-1.0.0", "array")); + sharedPropertyTree1.commit(); - await opProcessingController.ensureSynchronized(); - }); + // // Making sure that both trees are in write mode. + // sharedPropertyTree2.commit(true); + // Make sure both shared trees are in sync + await opProcessingController.ensureSynchronized(); + }); + afterEach(async () => { + // We expect the internal representation to be the same between both properties + expect((sharedPropertyTree1 as any).remoteTipView).to.deep.equal( + (sharedPropertyTree2 as any).remoteTipView, + ); + + // We expect the property tree to be the same between both + expect(sharedPropertyTree1.root.serialize()).to.deep.equal(sharedPropertyTree2.root.serialize()); + + // We expect the property tree to correspond to the remote tip view + expect((sharedPropertyTree1 as any).remoteTipView).to.deep.equal( + sharedPropertyTree2.root.serialize()); + + // We expect all properties from the set to be present + const array = sharedPropertyTree1.root.get("array"); + assert(array !== undefined, "property undefined"); + + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion + for (const property of array.getValues() as any[]) { + expect(!deletedProperties.has(property.guid)).to.be.true; + expect(createdProperties.has(property.guid)).to.be.true; + createdProperties.delete(property.guid); + } + expect(createdProperties.size).to.equal(0); + }); + function insertProperties(tree: SharedPropertyTree, index: number, count = 1, commit = true) { + for (let i = 0; i < count; i++) { + const property = PropertyFactory.create("test:namedEntry-1.0.0"); + tree.root.get("array")?.insert(index + i, property); + createdProperties.add(property.getGuid()); + } - it("Should work when synchronizing after each operation", async () => { - insertInArray(sharedPropertyTree1, "A"); - await opProcessingController.ensureSynchronized(); - insertInArray(sharedPropertyTree1, "A"); - await opProcessingController.ensureSynchronized(); - insertInArray(sharedPropertyTree1, "A"); - await opProcessingController.ensureSynchronized(); + if (commit) { + tree.commit(); + } + } + function removeProperties(tree: SharedPropertyTree, index: number, count = 1, commit = true) { + const array = tree.root.get("array"); + assert(array !== undefined, "property undefined"); - insertInArray(sharedPropertyTree2, "C"); - await opProcessingController.ensureSynchronized(); - insertInArray(sharedPropertyTree2, "C"); - await opProcessingController.ensureSynchronized(); - insertInArray(sharedPropertyTree2, "C"); + for (let i = 0; i < count; i++) { + if (index >= array.getLength()) { + break; + } + const property = array.get(index); + assert(property !== undefined, "property undefined"); + array.remove(index); + createdProperties.delete(property.getGuid()); + deletedProperties.add(property.getGuid()); + } + if (commit) { + tree.commit(); + } + } + + it("inserting properties into both trees", async () => { + insertProperties(sharedPropertyTree1, 0); + insertProperties(sharedPropertyTree1, 1); + insertProperties(sharedPropertyTree2, 0); + insertProperties(sharedPropertyTree2, 1); + await opProcessingController.ensureSynchronized(); + }); + + it("inserting properties in one tree and deleting in the other", async () => { + insertProperties(sharedPropertyTree1, 0); + insertProperties(sharedPropertyTree1, 1); + await opProcessingController.ensureSynchronized(); + removeProperties(sharedPropertyTree2, 0); + removeProperties(sharedPropertyTree2, 0); + await opProcessingController.ensureSynchronized(); + }); + + it("inserting properties in one tree and deleting in both", async () => { + insertProperties(sharedPropertyTree1, 0); + insertProperties(sharedPropertyTree1, 1); + await opProcessingController.ensureSynchronized(); + removeProperties(sharedPropertyTree1, 0); + removeProperties(sharedPropertyTree2, 0); + await opProcessingController.ensureSynchronized(); + }); + it("Multiple inserts in sequence in tree 1", async () => { + insertProperties(sharedPropertyTree1, 0, 1, true); + insertProperties(sharedPropertyTree1, 0, 1, true); + insertProperties(sharedPropertyTree1, 1, 1, true); + insertProperties(sharedPropertyTree2, 0, 1, true); + + await opProcessingController.ensureSynchronized(); + }); + + describe("Random tests", () => { + createRandomTests( + [ + { + getParameters: (random: DeterministicRandomGenerator) => { + const tree = random.irandom(2) === 0 ? + () => sharedPropertyTree1 : + () => sharedPropertyTree2; + const array = tree().root.get("array"); + return { + position: random.irandom(array?.getLength()) || 0, + count: (random.irandom(3)) + 1, + tree, + }; + }, + op: async (parameters) => { + insertProperties(parameters.tree(), parameters.position, parameters.count, true); + }, + probability: 1, + }, + { + getParameters: (random: DeterministicRandomGenerator) => { + const tree = random.irandom(2) === 0 ? + () => sharedPropertyTree1 : () => sharedPropertyTree2; + const array = tree().root.get("array"); + return { + position: random.irandom(array?.getLength()) || 0, + count: (random.irandom(3)) + 1, + tree, + }; + }, + op: async (parameters) => { + removeProperties(parameters.tree(), parameters.position, parameters.count, true); + }, + probability: 1, + }, + { + getParameters: (random: DeterministicRandomGenerator) => { + const container = random.irandom(2) === 0 ? () => container1 : () => container2; + return { + container, + }; + }, + op: async (parameters) => { + await opProcessingController.processOutgoing(parameters.container()); + }, + probability: 1, + }, + { + getParameters: (random: DeterministicRandomGenerator) => { + const container = random.irandom(2) === 0 ? () => container1 : () => container2; + return { + container, + }; + }, + op: async (parameters) => { + await opProcessingController.processIncoming(parameters.container()); + }, + probability: 1, + }, + ], + async () => { + await opProcessingController.ensureSynchronized(); + }, + 1000, + 0, + 25, + ); + }); + describe("Failed Random Tests", () => { + it("Test Failure 1", async () => { + insertProperties(sharedPropertyTree1, 0, 1, true); + insertProperties(sharedPropertyTree1, 0, 3, true); + insertProperties(sharedPropertyTree1, 3, 3, true); + insertProperties(sharedPropertyTree1, 6, 2, true); + insertProperties(sharedPropertyTree1, 0, 3, true); + insertProperties(sharedPropertyTree1, 0, 2, true); + insertProperties(sharedPropertyTree1, 2, 2, true); + insertProperties(sharedPropertyTree1, 8, 2, true); + insertProperties(sharedPropertyTree1, 2, 2, true); + insertProperties(sharedPropertyTree1, 16, 3, true); + insertProperties(sharedPropertyTree1, 9, 1, true); + insertProperties(sharedPropertyTree1, 4, 2, true); + insertProperties(sharedPropertyTree1, 13, 3, true); + insertProperties(sharedPropertyTree1, 9, 3, true); + insertProperties(sharedPropertyTree1, 16, 2, true); + insertProperties(sharedPropertyTree1, 12, 2, true); + insertProperties(sharedPropertyTree2, 0, 2, true); + insertProperties(sharedPropertyTree1, 12, 2, true); + insertProperties(sharedPropertyTree1, 12, 3, true); + insertProperties(sharedPropertyTree1, 25, 3, true); await opProcessingController.ensureSynchronized(); }); - it("Should work when synchronizing after pairs of operations", async () => { - insertInArray(sharedPropertyTree1, "A"); - insertInArray(sharedPropertyTree2, "C"); - await opProcessingController.ensureSynchronized(); - insertInArray(sharedPropertyTree1, "A"); - insertInArray(sharedPropertyTree2, "C"); - await opProcessingController.ensureSynchronized(); - insertInArray(sharedPropertyTree1, "A"); - insertInArray(sharedPropertyTree2, "C"); + it("Test Failure 2", async () => { + insertProperties(sharedPropertyTree1, 0, 1, true); + insertProperties(sharedPropertyTree1, 0, 1, true); + insertProperties(sharedPropertyTree1, 1, 1, true); + insertProperties(sharedPropertyTree1, 1, 1, true); + insertProperties(sharedPropertyTree1, 0, 1, true); + insertProperties(sharedPropertyTree1, 2, 1, true); + insertProperties(sharedPropertyTree1, 4, 1, true); + insertProperties(sharedPropertyTree1, 2, 1, true); + insertProperties(sharedPropertyTree1, 0, 1, true); + insertProperties(sharedPropertyTree1, 6, 1, true); + insertProperties(sharedPropertyTree1, 0, 1, true); + insertProperties(sharedPropertyTree1, 6, 1, true); + insertProperties(sharedPropertyTree1, 9, 1, true); + insertProperties(sharedPropertyTree1, 0, 1, true); + insertProperties(sharedPropertyTree1, 2, 1, true); + insertProperties(sharedPropertyTree1, 9, 1, true); + insertProperties(sharedPropertyTree2, 0, 1, true); + insertProperties(sharedPropertyTree1, 4, 1, true); + insertProperties(sharedPropertyTree1, 0, 1, true); + insertProperties(sharedPropertyTree1, 3, 1, true); await opProcessingController.ensureSynchronized(); }); - it("works with overlapping sequences", async () => { - insertInArray(sharedPropertyTree2, "C"); - await opProcessingController.processOutgoing(container2); - - // Insert five operations to make this overlap with the insert position of C - insertInArray(sharedPropertyTree1, "A"); - insertInArray(sharedPropertyTree1, "A"); - insertInArray(sharedPropertyTree1, "A"); - insertInArray(sharedPropertyTree1, "A"); - insertInArray(sharedPropertyTree1, "A"); - await opProcessingController.processIncoming(container1); - insertInArray(sharedPropertyTree1, "A"); - await opProcessingController.processIncoming(container2); + it("Test Failure 3", async () => { + insertProperties(sharedPropertyTree1, 0, 1, true); + insertProperties(sharedPropertyTree2, 0, 1, true); + insertProperties(sharedPropertyTree2, 0, 1, true); await opProcessingController.ensureSynchronized(); }); - it("Should work when the remote head points to a change that is not the reference change", async () => { - insertInArray(sharedPropertyTree2, "C"); - await opProcessingController.processOutgoing(container2); - insertInArray(sharedPropertyTree1, "A"); - await opProcessingController.processOutgoing(container1); - insertInArray(sharedPropertyTree2, "C"); - await opProcessingController.processIncoming(container2); - insertInArray(sharedPropertyTree2, "C"); - insertInArray(sharedPropertyTree2, "C"); + it("Test Failure 4", async () => { + insertProperties(sharedPropertyTree1, 0, 1, true); + insertProperties(sharedPropertyTree2, 0, 1, true); + insertProperties(sharedPropertyTree2, 0, 1, true); + insertProperties(sharedPropertyTree2, 1, 1, true); await opProcessingController.ensureSynchronized(); }); - describe("Randomized Tests", () => { - const count = 100; - const startTest = 0; - const logTest = true; - - for (let i = startTest; i < count; i++) { - const seed = createDerivedGuid("", String(i)); - it(`Generated Test Case #${i} (seed: ${seed})`, async () => { - const random = new DeterministicRandomGenerator(seed); - let testString = ""; - - const numOperations = random.irandom(30); - for (const j of _.range(numOperations)) { - const operation = random.irandom(6); - switch (operation) { - case 0: - insertInArray(sharedPropertyTree1, "A"); - if (logTest) { - testString += 'insertInArray(sharedPropertyTree1, "A");\n'; - } - break; - case 1: - insertInArray(sharedPropertyTree2, "C"); - if (logTest) { - testString += 'insertInArray(sharedPropertyTree2, "C");\n'; - } - break; - case 2: - await opProcessingController.processOutgoing(container1); - if (logTest) { - testString += - "await opProcessingController.processOutgoing(container1);\n"; - } - break; - case 3: - await opProcessingController.processIncoming(container1); - if (logTest) { - testString += - "await opProcessingController.processIncoming(container1);\n"; - } - break; - case 4: - await opProcessingController.processOutgoing(container2); - if (logTest) { - testString += - "await opProcessingController.processOutgoing(container2);\n"; - } - break; - case 5: - await opProcessingController.processIncoming(container2); - if (logTest) { - testString += - "await opProcessingController.processIncoming(container2);\n"; - } - break; - default: - throw new Error("Should never happen"); - } - } + it("Test Failure 5", async () => { + insertProperties(sharedPropertyTree1, 0, 8, true); + removeProperties(sharedPropertyTree1, 4, 3, true); - await opProcessingController.ensureSynchronized(); - if (logTest) { - testString += - "await opProcessingController.ensureSynchronized();\n"; - } - }); - } + await opProcessingController.ensureSynchronized(); }); - }); - describe("with inserts and deletes at arbitrary positions", () => { - let createdProperties: Set; - let deletedProperties: Set; - beforeEach(async () => { - createdProperties = new Set(); - deletedProperties = new Set(); - (PropertyFactory as any)._reregister({ - typeid: "test:namedEntry-1.0.0", - inherits: ["NamedProperty"], - properties: [], - }); - - await opProcessingController.pauseProcessing(); - sharedPropertyTree1.root.insert("array", PropertyFactory.create("test:namedEntry-1.0.0", "array")); - sharedPropertyTree1.commit(); + it("Test Failure 6", async () => { + insertProperties(sharedPropertyTree1, 0, 2, true); + removeProperties(sharedPropertyTree1, 0, 2, true); + insertProperties(sharedPropertyTree2, 0, 1, true); + removeProperties(sharedPropertyTree2, 0, 1, true); - // // Making sure that both trees are in write mode. - // sharedPropertyTree2.commit(true); - // Make sure both shared trees are in sync await opProcessingController.ensureSynchronized(); }); - afterEach(async () => { - // We expect the internal representation to be the same between both properties - expect((sharedPropertyTree1 as any).remoteTipView).to.deep.equal( - (sharedPropertyTree2 as any).remoteTipView, - ); - // We expect the property tree to be the same between both - expect(sharedPropertyTree1.root.serialize()).to.deep.equal(sharedPropertyTree2.root.serialize()); + it("Test Failure 7", async () => { + insertProperties(sharedPropertyTree2, 0, 8, true); + insertProperties(sharedPropertyTree1, 0, 2, true); + await opProcessingController.processOutgoing(container1); + await opProcessingController.processOutgoing(container2); + insertProperties(sharedPropertyTree1, 1, 4, true); + await opProcessingController.processOutgoing(container1); + removeProperties(sharedPropertyTree1, 4, 3, true); + removeProperties(sharedPropertyTree1, 0, 2, true); + insertProperties(sharedPropertyTree1, 0, 4, true); - // We expect the property tree to correspond to the remote tip view - expect((sharedPropertyTree1 as any).remoteTipView).to.deep.equal( - sharedPropertyTree2.root.serialize()); + await opProcessingController.ensureSynchronized(); + }); - // We expect all properties from the set to be present - const array = sharedPropertyTree1.root.get("array"); - assert(array !== undefined, "property undefined"); + it("Test Failure 8", async () => { + insertProperties(sharedPropertyTree2, 0, 3, true); + await opProcessingController.processOutgoing(container2); + await opProcessingController.processIncoming(container1); + removeProperties(sharedPropertyTree2, 0, 3, true); + insertProperties(sharedPropertyTree2, 0, 3, true); + await opProcessingController.processOutgoing(container2); + insertProperties(sharedPropertyTree1, 0, 1, true); - // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion - for (const property of array.getValues() as any[]) { - expect(!deletedProperties.has(property.guid)).to.be.true; - expect(createdProperties.has(property.guid)).to.be.true; - createdProperties.delete(property.guid); - } - expect(createdProperties.size).to.equal(0); + await opProcessingController.ensureSynchronized(); }); - function insertProperties(tree: SharedPropertyTree, index: number, count = 1, commit = true) { - for (let i = 0; i < count; i++) { - const property = PropertyFactory.create("test:namedEntry-1.0.0"); - tree.root.get("array")?.insert(index + i, property); - createdProperties.add(property.getGuid()); - } - if (commit) { - tree.commit(); - } - } - function removeProperties(tree: SharedPropertyTree, index: number, count = 1, commit = true) { - const array = tree.root.get("array"); - assert(array !== undefined, "property undefined"); - - for (let i = 0; i < count; i++) { - if (index >= array.getLength()) { - break; - } - const property = array.get(index); - assert(property !== undefined, "property undefined"); - array.remove(index); - createdProperties.delete(property.getGuid()); - deletedProperties.add(property.getGuid()); - } - if (commit) { - tree.commit(); - } - } + it("Test Failure 9", async () => { + insertProperties(sharedPropertyTree2, 0, 9, true); + insertProperties(sharedPropertyTree2, 4, 1, true); + await opProcessingController.processOutgoing(container2); + insertProperties(sharedPropertyTree2, 0, 1, true); + await opProcessingController.processIncoming(container2); + insertProperties(sharedPropertyTree2, 1, 2, true); - it("inserting properties into both trees", async () => { - insertProperties(sharedPropertyTree1, 0); - insertProperties(sharedPropertyTree1, 1); - insertProperties(sharedPropertyTree2, 0); - insertProperties(sharedPropertyTree2, 1); await opProcessingController.ensureSynchronized(); }); - it("inserting properties in one tree and deleting in the other", async () => { - insertProperties(sharedPropertyTree1, 0); - insertProperties(sharedPropertyTree1, 1); + it("Test Failure 10", async () => { + insertProperties(sharedPropertyTree2, 0, 3, true); + await opProcessingController.processOutgoing(container2); + await opProcessingController.processIncoming(container1); + removeProperties(sharedPropertyTree2, 0, 3, true); + insertProperties(sharedPropertyTree2, 0, 3, true); + await opProcessingController.processOutgoing(container2); + insertProperties(sharedPropertyTree1, 0, 1, true); + removeProperties(sharedPropertyTree1, 0, 1, true); await opProcessingController.ensureSynchronized(); - removeProperties(sharedPropertyTree2, 0); - removeProperties(sharedPropertyTree2, 0); + }); + it("Test Failure 11", async () => { + insertProperties(sharedPropertyTree2, 0, 6, true); + insertProperties(sharedPropertyTree1, 0, 1, true); + await opProcessingController.processOutgoing(container1); + await opProcessingController.processIncoming(container2); + insertProperties(sharedPropertyTree2, 4, 2, true); await opProcessingController.ensureSynchronized(); }); - - it("inserting properties in one tree and deleting in both", async () => { - insertProperties(sharedPropertyTree1, 0); - insertProperties(sharedPropertyTree1, 1); + it("Test Failure 12", async () => { + insertProperties(sharedPropertyTree1, 0, 2, true); + insertProperties(sharedPropertyTree2, 0, 3, true); + await opProcessingController.processOutgoing(container1); + await opProcessingController.processOutgoing(container2); + removeProperties(sharedPropertyTree2, 2, 2, true); + await opProcessingController.processIncoming(container2); + insertProperties(sharedPropertyTree2, 1, 2, true); await opProcessingController.ensureSynchronized(); - removeProperties(sharedPropertyTree1, 0); - removeProperties(sharedPropertyTree2, 0); + }); + it("Test Failure 13", async () => { + insertProperties(sharedPropertyTree1, 0, 2, true); + await opProcessingController.processOutgoing(container1); + await opProcessingController.processIncoming(container2); + insertProperties(sharedPropertyTree2, 1, 3, true); + removeProperties(sharedPropertyTree2, 4, 1, true); + insertProperties(sharedPropertyTree2, 4, 3, true); + insertProperties(sharedPropertyTree1, 1, 2, true); + await opProcessingController.processOutgoing(container2); + await opProcessingController.processOutgoing(container1); await opProcessingController.ensureSynchronized(); }); - it("Multiple inserts in sequence in tree 1", async () => { + it("Test Failure 14", async () => { insertProperties(sharedPropertyTree1, 0, 1, true); + await opProcessingController.processOutgoing(container1); + insertProperties(sharedPropertyTree2, 0, 2, true); + await opProcessingController.processIncoming(container2); + removeProperties(sharedPropertyTree2, 0, 1, true); + await opProcessingController.processOutgoing(container2); insertProperties(sharedPropertyTree1, 0, 1, true); - insertProperties(sharedPropertyTree1, 1, 1, true); + await opProcessingController.ensureSynchronized(); + }); + it("Test Failure 15", async () => { insertProperties(sharedPropertyTree2, 0, 1, true); - + await opProcessingController.processOutgoing(container2); + await opProcessingController.processIncoming(container1); + insertProperties(sharedPropertyTree2, 0, 2, true); + await opProcessingController.processOutgoing(container2); + insertProperties(sharedPropertyTree1, 0, 2, true); + removeProperties(sharedPropertyTree1, 1, 3, true); + insertProperties(sharedPropertyTree1, 0, 1, true); await opProcessingController.ensureSynchronized(); }); - - describe("Random tests", () => { - createRandomTests( - [ - { - getParameters: (random: DeterministicRandomGenerator) => { - const tree = random.irandom(2) === 0 ? - () => sharedPropertyTree1 : - () => sharedPropertyTree2; - const array = tree().root.get("array"); - return { - position: random.irandom(array?.getLength()) || 0, - count: (random.irandom(3)) + 1, - tree, - }; - }, - op: async (parameters) => { - insertProperties(parameters.tree(), parameters.position, parameters.count, true); - }, - probability: 1, - }, - { - getParameters: (random: DeterministicRandomGenerator) => { - const tree = random.irandom(2) === 0 ? - () => sharedPropertyTree1 : () => sharedPropertyTree2; - const array = tree().root.get("array"); - return { - position: random.irandom(array?.getLength()) || 0, - count: (random.irandom(3)) + 1, - tree, - }; - }, - op: async (parameters) => { - removeProperties(parameters.tree(), parameters.position, parameters.count, true); - }, - probability: 1, - }, - { - getParameters: (random: DeterministicRandomGenerator) => { - const container = random.irandom(2) === 0 ? () => container1 : () => container2; - return { - container, - }; - }, - op: async (parameters) => { - await opProcessingController.processOutgoing(parameters.container()); - }, - probability: 1, - }, - { - getParameters: (random: DeterministicRandomGenerator) => { - const container = random.irandom(2) === 0 ? () => container1 : () => container2; - return { - container, - }; - }, - op: async (parameters) => { - await opProcessingController.processIncoming(parameters.container()); - }, - probability: 1, - }, - ], - async () => { - await opProcessingController.ensureSynchronized(); - }, - 1000, - 0, - 25, - ); + it("Test Failure 16", async () => { + insertProperties(sharedPropertyTree1, 0, 3, true); + await opProcessingController.processOutgoing(container1); + insertProperties(sharedPropertyTree2, 0, 1, true); + removeProperties(sharedPropertyTree2, 0, 1, true); + removeProperties(sharedPropertyTree1, 0, 3, true); + insertProperties(sharedPropertyTree1, 0, 3, true); + await opProcessingController.processIncoming(container2); + insertProperties(sharedPropertyTree2, 2, 2, true); + await opProcessingController.ensureSynchronized(); }); - describe("Failed Random Tests", () => { - it("Test Failure 1", async () => { - insertProperties(sharedPropertyTree1, 0, 1, true); - insertProperties(sharedPropertyTree1, 0, 3, true); - insertProperties(sharedPropertyTree1, 3, 3, true); - insertProperties(sharedPropertyTree1, 6, 2, true); - insertProperties(sharedPropertyTree1, 0, 3, true); - insertProperties(sharedPropertyTree1, 0, 2, true); - insertProperties(sharedPropertyTree1, 2, 2, true); - insertProperties(sharedPropertyTree1, 8, 2, true); - insertProperties(sharedPropertyTree1, 2, 2, true); - insertProperties(sharedPropertyTree1, 16, 3, true); - insertProperties(sharedPropertyTree1, 9, 1, true); - insertProperties(sharedPropertyTree1, 4, 2, true); - insertProperties(sharedPropertyTree1, 13, 3, true); - insertProperties(sharedPropertyTree1, 9, 3, true); - insertProperties(sharedPropertyTree1, 16, 2, true); - insertProperties(sharedPropertyTree1, 12, 2, true); - insertProperties(sharedPropertyTree2, 0, 2, true); - insertProperties(sharedPropertyTree1, 12, 2, true); - insertProperties(sharedPropertyTree1, 12, 3, true); - insertProperties(sharedPropertyTree1, 25, 3, true); - await opProcessingController.ensureSynchronized(); - }); - - it("Test Failure 2", async () => { - insertProperties(sharedPropertyTree1, 0, 1, true); - insertProperties(sharedPropertyTree1, 0, 1, true); - insertProperties(sharedPropertyTree1, 1, 1, true); - insertProperties(sharedPropertyTree1, 1, 1, true); - insertProperties(sharedPropertyTree1, 0, 1, true); - insertProperties(sharedPropertyTree1, 2, 1, true); - insertProperties(sharedPropertyTree1, 4, 1, true); - insertProperties(sharedPropertyTree1, 2, 1, true); - insertProperties(sharedPropertyTree1, 0, 1, true); - insertProperties(sharedPropertyTree1, 6, 1, true); - insertProperties(sharedPropertyTree1, 0, 1, true); - insertProperties(sharedPropertyTree1, 6, 1, true); - insertProperties(sharedPropertyTree1, 9, 1, true); - insertProperties(sharedPropertyTree1, 0, 1, true); - insertProperties(sharedPropertyTree1, 2, 1, true); - insertProperties(sharedPropertyTree1, 9, 1, true); - insertProperties(sharedPropertyTree2, 0, 1, true); - insertProperties(sharedPropertyTree1, 4, 1, true); - insertProperties(sharedPropertyTree1, 0, 1, true); - insertProperties(sharedPropertyTree1, 3, 1, true); - await opProcessingController.ensureSynchronized(); - }); - - it("Test Failure 3", async () => { - insertProperties(sharedPropertyTree1, 0, 1, true); - insertProperties(sharedPropertyTree2, 0, 1, true); - insertProperties(sharedPropertyTree2, 0, 1, true); - - await opProcessingController.ensureSynchronized(); - }); - - it("Test Failure 4", async () => { - insertProperties(sharedPropertyTree1, 0, 1, true); - insertProperties(sharedPropertyTree2, 0, 1, true); - insertProperties(sharedPropertyTree2, 0, 1, true); - insertProperties(sharedPropertyTree2, 1, 1, true); - - await opProcessingController.ensureSynchronized(); - }); - - it("Test Failure 5", async () => { - insertProperties(sharedPropertyTree1, 0, 8, true); - removeProperties(sharedPropertyTree1, 4, 3, true); - - await opProcessingController.ensureSynchronized(); - }); - - it("Test Failure 6", async () => { - insertProperties(sharedPropertyTree1, 0, 2, true); - removeProperties(sharedPropertyTree1, 0, 2, true); - insertProperties(sharedPropertyTree2, 0, 1, true); - removeProperties(sharedPropertyTree2, 0, 1, true); - - await opProcessingController.ensureSynchronized(); - }); - - it("Test Failure 7", async () => { - insertProperties(sharedPropertyTree2, 0, 8, true); - insertProperties(sharedPropertyTree1, 0, 2, true); - await opProcessingController.processOutgoing(container1); - await opProcessingController.processOutgoing(container2); - insertProperties(sharedPropertyTree1, 1, 4, true); - await opProcessingController.processOutgoing(container1); - removeProperties(sharedPropertyTree1, 4, 3, true); - removeProperties(sharedPropertyTree1, 0, 2, true); - insertProperties(sharedPropertyTree1, 0, 4, true); - - await opProcessingController.ensureSynchronized(); - }); - - it("Test Failure 8", async () => { - insertProperties(sharedPropertyTree2, 0, 3, true); - await opProcessingController.processOutgoing(container2); - await opProcessingController.processIncoming(container1); - removeProperties(sharedPropertyTree2, 0, 3, true); - insertProperties(sharedPropertyTree2, 0, 3, true); - await opProcessingController.processOutgoing(container2); - insertProperties(sharedPropertyTree1, 0, 1, true); - - await opProcessingController.ensureSynchronized(); - }); - - it("Test Failure 9", async () => { - insertProperties(sharedPropertyTree2, 0, 9, true); - insertProperties(sharedPropertyTree2, 4, 1, true); - await opProcessingController.processOutgoing(container2); - insertProperties(sharedPropertyTree2, 0, 1, true); - await opProcessingController.processIncoming(container2); - insertProperties(sharedPropertyTree2, 1, 2, true); - - await opProcessingController.ensureSynchronized(); - }); - - it("Test Failure 10", async () => { - insertProperties(sharedPropertyTree2, 0, 3, true); - await opProcessingController.processOutgoing(container2); - await opProcessingController.processIncoming(container1); - removeProperties(sharedPropertyTree2, 0, 3, true); - insertProperties(sharedPropertyTree2, 0, 3, true); - await opProcessingController.processOutgoing(container2); - insertProperties(sharedPropertyTree1, 0, 1, true); - removeProperties(sharedPropertyTree1, 0, 1, true); - await opProcessingController.ensureSynchronized(); - }); - it("Test Failure 11", async () => { - insertProperties(sharedPropertyTree2, 0, 6, true); - insertProperties(sharedPropertyTree1, 0, 1, true); - await opProcessingController.processOutgoing(container1); - await opProcessingController.processIncoming(container2); - insertProperties(sharedPropertyTree2, 4, 2, true); - await opProcessingController.ensureSynchronized(); - }); - it("Test Failure 12", async () => { - insertProperties(sharedPropertyTree1, 0, 2, true); - insertProperties(sharedPropertyTree2, 0, 3, true); - await opProcessingController.processOutgoing(container1); - await opProcessingController.processOutgoing(container2); - removeProperties(sharedPropertyTree2, 2, 2, true); - await opProcessingController.processIncoming(container2); - insertProperties(sharedPropertyTree2, 1, 2, true); - await opProcessingController.ensureSynchronized(); - }); - it("Test Failure 13", async () => { - insertProperties(sharedPropertyTree1, 0, 2, true); - await opProcessingController.processOutgoing(container1); - await opProcessingController.processIncoming(container2); - insertProperties(sharedPropertyTree2, 1, 3, true); - removeProperties(sharedPropertyTree2, 4, 1, true); - insertProperties(sharedPropertyTree2, 4, 3, true); - insertProperties(sharedPropertyTree1, 1, 2, true); - await opProcessingController.processOutgoing(container2); - await opProcessingController.processOutgoing(container1); - await opProcessingController.ensureSynchronized(); - }); - it("Test Failure 14", async () => { - insertProperties(sharedPropertyTree1, 0, 1, true); - await opProcessingController.processOutgoing(container1); - insertProperties(sharedPropertyTree2, 0, 2, true); - await opProcessingController.processIncoming(container2); - removeProperties(sharedPropertyTree2, 0, 1, true); - await opProcessingController.processOutgoing(container2); - insertProperties(sharedPropertyTree1, 0, 1, true); - await opProcessingController.ensureSynchronized(); - }); - it("Test Failure 15", async () => { - insertProperties(sharedPropertyTree2, 0, 1, true); - await opProcessingController.processOutgoing(container2); - await opProcessingController.processIncoming(container1); - insertProperties(sharedPropertyTree2, 0, 2, true); - await opProcessingController.processOutgoing(container2); - insertProperties(sharedPropertyTree1, 0, 2, true); - removeProperties(sharedPropertyTree1, 1, 3, true); - insertProperties(sharedPropertyTree1, 0, 1, true); - await opProcessingController.ensureSynchronized(); - }); - it("Test Failure 16", async () => { - insertProperties(sharedPropertyTree1, 0, 3, true); - await opProcessingController.processOutgoing(container1); - insertProperties(sharedPropertyTree2, 0, 1, true); - removeProperties(sharedPropertyTree2, 0, 1, true); - removeProperties(sharedPropertyTree1, 0, 3, true); - insertProperties(sharedPropertyTree1, 0, 3, true); - await opProcessingController.processIncoming(container2); - insertProperties(sharedPropertyTree2, 2, 2, true); - await opProcessingController.ensureSynchronized(); - }); - it("Test Failure 17", async () => { - insertProperties(sharedPropertyTree1, 0, 3, true); - await opProcessingController.processOutgoing(container1); - insertProperties(sharedPropertyTree1, 2, 4, true); - removeProperties(sharedPropertyTree1, 0, 3, true); - removeProperties(sharedPropertyTree1, 1, 3, true); - insertProperties(sharedPropertyTree1, 0, 2, true); + it("Test Failure 17", async () => { + insertProperties(sharedPropertyTree1, 0, 3, true); + await opProcessingController.processOutgoing(container1); + insertProperties(sharedPropertyTree1, 2, 4, true); + removeProperties(sharedPropertyTree1, 0, 3, true); + removeProperties(sharedPropertyTree1, 1, 3, true); + insertProperties(sharedPropertyTree1, 0, 2, true); - await opProcessingController.processIncoming(container2); - insertProperties(sharedPropertyTree2, 1, 1, true); + await opProcessingController.processIncoming(container2); + insertProperties(sharedPropertyTree2, 1, 1, true); - await opProcessingController.ensureSynchronized(); - }); - it("Test Failure 18", async () => { - insertProperties(sharedPropertyTree2, 0, 3, true); - await opProcessingController.processOutgoing(container2); - await opProcessingController.processIncoming(container1); - removeProperties(sharedPropertyTree2, 0, 3, true); - insertProperties(sharedPropertyTree2, 0, 3, true); - await opProcessingController.processOutgoing(container2); - removeProperties(sharedPropertyTree1, 1, 2, true); - insertProperties(sharedPropertyTree1, 0, 1, true); + await opProcessingController.ensureSynchronized(); + }); + it("Test Failure 18", async () => { + insertProperties(sharedPropertyTree2, 0, 3, true); + await opProcessingController.processOutgoing(container2); + await opProcessingController.processIncoming(container1); + removeProperties(sharedPropertyTree2, 0, 3, true); + insertProperties(sharedPropertyTree2, 0, 3, true); + await opProcessingController.processOutgoing(container2); + removeProperties(sharedPropertyTree1, 1, 2, true); + insertProperties(sharedPropertyTree1, 0, 1, true); - await opProcessingController.ensureSynchronized(); - }); - it("Test Failure 19", async () => { - insertProperties(sharedPropertyTree1, 0, 3, true); - insertProperties(sharedPropertyTree2, 0, 1, true); - await opProcessingController.processOutgoing(container2); - await opProcessingController.processOutgoing(container1); - await opProcessingController.processIncoming(container2); - removeProperties(sharedPropertyTree2, 1, 3, true); - await opProcessingController.processOutgoing(container1); - removeProperties(sharedPropertyTree1, 0, 1, true); - insertProperties(sharedPropertyTree1, 0, 3, true); - removeProperties(sharedPropertyTree2, 0, 2, true); + await opProcessingController.ensureSynchronized(); + }); + it("Test Failure 19", async () => { + insertProperties(sharedPropertyTree1, 0, 3, true); + insertProperties(sharedPropertyTree2, 0, 1, true); + await opProcessingController.processOutgoing(container2); + await opProcessingController.processOutgoing(container1); + await opProcessingController.processIncoming(container2); + removeProperties(sharedPropertyTree2, 1, 3, true); + await opProcessingController.processOutgoing(container1); + removeProperties(sharedPropertyTree1, 0, 1, true); + insertProperties(sharedPropertyTree1, 0, 3, true); + removeProperties(sharedPropertyTree2, 0, 2, true); - await opProcessingController.ensureSynchronized(); - }); + await opProcessingController.ensureSynchronized(); + }); - it("Test Failure 20", async () => { - insertProperties(sharedPropertyTree1, 0, 2, true); - await opProcessingController.processOutgoing(container2); - insertProperties(sharedPropertyTree2, 0, 2, true); - await opProcessingController.processOutgoing(container2); - removeProperties(sharedPropertyTree2, 1, 3, true); - await opProcessingController.processIncoming(container1); - removeProperties(sharedPropertyTree1, 1, 3, true); - insertProperties(sharedPropertyTree1, 1, 2, true); - removeProperties(sharedPropertyTree2, 0, 1, true); + it("Test Failure 20", async () => { + insertProperties(sharedPropertyTree1, 0, 2, true); + await opProcessingController.processOutgoing(container2); + insertProperties(sharedPropertyTree2, 0, 2, true); + await opProcessingController.processOutgoing(container2); + removeProperties(sharedPropertyTree2, 1, 3, true); + await opProcessingController.processIncoming(container1); + removeProperties(sharedPropertyTree1, 1, 3, true); + insertProperties(sharedPropertyTree1, 1, 2, true); + removeProperties(sharedPropertyTree2, 0, 1, true); - await opProcessingController.ensureSynchronized(); - }); + await opProcessingController.ensureSynchronized(); + }); - it("Test Failure 21", async () => { - insertProperties(sharedPropertyTree1, 0, 7, true); - await opProcessingController.processOutgoing(container1); - await opProcessingController.processIncoming(container2); - insertProperties(sharedPropertyTree1, 4, 2, true); - await opProcessingController.processOutgoing(container1); - insertProperties(sharedPropertyTree2, 5, 1, true); - removeProperties(sharedPropertyTree2, 6, 2, true); - insertProperties(sharedPropertyTree1, 6, 1, true); - removeProperties(sharedPropertyTree1, 8, 1, true); - await opProcessingController.processOutgoing(container2); - removeProperties(sharedPropertyTree1, 7, 2, true); + it("Test Failure 21", async () => { + insertProperties(sharedPropertyTree1, 0, 7, true); + await opProcessingController.processOutgoing(container1); + await opProcessingController.processIncoming(container2); + insertProperties(sharedPropertyTree1, 4, 2, true); + await opProcessingController.processOutgoing(container1); + insertProperties(sharedPropertyTree2, 5, 1, true); + removeProperties(sharedPropertyTree2, 6, 2, true); + insertProperties(sharedPropertyTree1, 6, 1, true); + removeProperties(sharedPropertyTree1, 8, 1, true); + await opProcessingController.processOutgoing(container2); + removeProperties(sharedPropertyTree1, 7, 2, true); - await opProcessingController.ensureSynchronized(); - }); + await opProcessingController.ensureSynchronized(); + }); - it("Test Failure 22", async () => { - insertProperties(sharedPropertyTree2, 0, 3, true); - await opProcessingController.processIncoming(container2); - insertProperties(sharedPropertyTree2, 1, 2, true); - removeProperties(sharedPropertyTree1, 0, 2, true); - await opProcessingController.processOutgoing(container1); - insertProperties(sharedPropertyTree1, 0, 1, true); - await opProcessingController.processOutgoing(container1); - await opProcessingController.processOutgoing(container2); - await opProcessingController.processIncoming(container1); - await opProcessingController.ensureSynchronized(); - insertProperties(sharedPropertyTree1, 5, 2, true); - await opProcessingController.processIncoming(container1); - await opProcessingController.processOutgoing(container2); - removeProperties(sharedPropertyTree2, 1, 1, true); - removeProperties(sharedPropertyTree1, 6, 2, true); - await opProcessingController.processIncoming(container2); - insertProperties(sharedPropertyTree1, 3, 2, true); - await opProcessingController.processOutgoing(container2); - removeProperties(sharedPropertyTree2, 2, 3, true); - removeProperties(sharedPropertyTree1, 3, 2, true); - insertProperties(sharedPropertyTree2, 1, 3, true); - await opProcessingController.ensureSynchronized(); - }); + it("Test Failure 22", async () => { + insertProperties(sharedPropertyTree2, 0, 3, true); + await opProcessingController.processIncoming(container2); + insertProperties(sharedPropertyTree2, 1, 2, true); + removeProperties(sharedPropertyTree1, 0, 2, true); + await opProcessingController.processOutgoing(container1); + insertProperties(sharedPropertyTree1, 0, 1, true); + await opProcessingController.processOutgoing(container1); + await opProcessingController.processOutgoing(container2); + await opProcessingController.processIncoming(container1); + await opProcessingController.ensureSynchronized(); + insertProperties(sharedPropertyTree1, 5, 2, true); + await opProcessingController.processIncoming(container1); + await opProcessingController.processOutgoing(container2); + removeProperties(sharedPropertyTree2, 1, 1, true); + removeProperties(sharedPropertyTree1, 6, 2, true); + await opProcessingController.processIncoming(container2); + insertProperties(sharedPropertyTree1, 3, 2, true); + await opProcessingController.processOutgoing(container2); + removeProperties(sharedPropertyTree2, 2, 3, true); + removeProperties(sharedPropertyTree1, 3, 2, true); + insertProperties(sharedPropertyTree2, 1, 3, true); + await opProcessingController.ensureSynchronized(); + }); - it("Test failure 23", async () => { - insertProperties(sharedPropertyTree2, 0, 4, true); - insertProperties(sharedPropertyTree1, 0, 3, true); - await opProcessingController.processOutgoing(container2); - insertProperties(sharedPropertyTree2, 1, 3, true); - removeProperties(sharedPropertyTree2, 0, 2, true); - await opProcessingController.ensureSynchronized(); - }); + it("Test failure 23", async () => { + insertProperties(sharedPropertyTree2, 0, 4, true); + insertProperties(sharedPropertyTree1, 0, 3, true); + await opProcessingController.processOutgoing(container2); + insertProperties(sharedPropertyTree2, 1, 3, true); + removeProperties(sharedPropertyTree2, 0, 2, true); + await opProcessingController.ensureSynchronized(); + }); - it("Test failure 24", async () => { - insertProperties(sharedPropertyTree2, 0, 6, true); - await opProcessingController.processOutgoing(container2); - await opProcessingController.processIncoming(container1); - removeProperties(sharedPropertyTree2, 4, 1, true); - removeProperties(sharedPropertyTree1, 3, 3, true); - await opProcessingController.processOutgoing(container2); - await opProcessingController.processIncoming(container1); - removeProperties(sharedPropertyTree1, 2, 3, true); + it("Test failure 24", async () => { + insertProperties(sharedPropertyTree2, 0, 6, true); + await opProcessingController.processOutgoing(container2); + await opProcessingController.processIncoming(container1); + removeProperties(sharedPropertyTree2, 4, 1, true); + removeProperties(sharedPropertyTree1, 3, 3, true); + await opProcessingController.processOutgoing(container2); + await opProcessingController.processIncoming(container1); + removeProperties(sharedPropertyTree1, 2, 3, true); - await opProcessingController.ensureSynchronized(); - }); + await opProcessingController.ensureSynchronized(); + }); - it("Test failure 25", async () => { - insertProperties(sharedPropertyTree1, 0, 3, true); - await opProcessingController.processOutgoing(container2); - await opProcessingController.processOutgoing(container1); - insertProperties(sharedPropertyTree2, 0, 3, true); - await opProcessingController.processIncoming(container2); - insertProperties(sharedPropertyTree2, 2, 1, true); - removeProperties(sharedPropertyTree2, 0, 1, true); - await opProcessingController.processOutgoing(container1); - await opProcessingController.processIncoming(container2); - insertProperties(sharedPropertyTree1, 1, 2, true); - removeProperties(sharedPropertyTree2, 1, 2, true); - await opProcessingController.processIncoming(container1); - await opProcessingController.ensureSynchronized(); - }); + it("Test failure 25", async () => { + insertProperties(sharedPropertyTree1, 0, 3, true); + await opProcessingController.processOutgoing(container2); + await opProcessingController.processOutgoing(container1); + insertProperties(sharedPropertyTree2, 0, 3, true); + await opProcessingController.processIncoming(container2); + insertProperties(sharedPropertyTree2, 2, 1, true); + removeProperties(sharedPropertyTree2, 0, 1, true); + await opProcessingController.processOutgoing(container1); + await opProcessingController.processIncoming(container2); + insertProperties(sharedPropertyTree1, 1, 2, true); + removeProperties(sharedPropertyTree2, 1, 2, true); + await opProcessingController.processIncoming(container1); + await opProcessingController.ensureSynchronized(); }); }); + }); - describe("Rebase with pending changes.", () => { - it("Int32", async () => { - const val1 = 600; - const val2 = 500; + describe("Rebase with pending changes.", () => { + it("Int32", async () => { + const val1 = 600; + const val2 = 500; - await opProcessingController.pauseProcessing(); + await opProcessingController.pauseProcessing(); - const prop = PropertyFactory.create("Int32"); - sharedPropertyTree1.root.insert("int32Prop", prop); + const prop = PropertyFactory.create("Int32"); + sharedPropertyTree1.root.insert("int32Prop", prop); - sharedPropertyTree1.commit(); - // Make sure both shared trees are in sync - await opProcessingController.ensureSynchronized(); - await opProcessingController.pauseProcessing(); + sharedPropertyTree1.commit(); + // Make sure both shared trees are in sync + await opProcessingController.ensureSynchronized(); + await opProcessingController.pauseProcessing(); - // Make local changes for both collaborators - prop.setValue(val1); - sharedPropertyTree2.root.get("int32Prop")?.setValue(val2); + // Make local changes for both collaborators + prop.setValue(val1); + sharedPropertyTree2.root.get("int32Prop")?.setValue(val2); - sharedPropertyTree1.commit(); - await opProcessingController.ensureSynchronized(); - await opProcessingController.pauseProcessing(); + sharedPropertyTree1.commit(); + await opProcessingController.ensureSynchronized(); + await opProcessingController.pauseProcessing(); - // This collaborator should still have pending changes after rebase the incoming commits - expect(Object.keys( - sharedPropertyTree2.root.getPendingChanges().getSerializedChangeSet()).length).to.not.equal(0); + // This collaborator should still have pending changes after rebase the incoming commits + expect(Object.keys( + sharedPropertyTree2.root.getPendingChanges().getSerializedChangeSet()).length).to.not.equal(0); - // Committing the new pending change - sharedPropertyTree2.commit(); - await opProcessingController.ensureSynchronized(); + // Committing the new pending change + sharedPropertyTree2.commit(); + await opProcessingController.ensureSynchronized(); - // The pending change val2 should be now the new value cross collaborators - expect(prop.getValue()).to.equal(val2); - }); + // The pending change val2 should be now the new value cross collaborators + expect(prop.getValue()).to.equal(val2); }); - } + }); + } - describe("Rebasing", () => { + describe("Rebasing", () => { beforeEach(async () => { - await setupContainers(); - }); + await setupContainers(); + }); - rebaseTests(); - }); + rebaseTests(); + }); describe("Rebasing with reconnection", () => { beforeEach(async () => { - await setupContainers(false); - }); + await setupContainers(false); + }); - rebaseTests(); - }); + rebaseTests(); + }); }); diff --git a/packages/drivers/odsp-driver/src/test/test.ts b/packages/drivers/odsp-driver/src/test/test.ts index da4f97dcfe30..3de42768679c 100644 --- a/packages/drivers/odsp-driver/src/test/test.ts +++ b/packages/drivers/odsp-driver/src/test/test.ts @@ -40,5 +40,5 @@ describe("Binary WireFormat perf", () => { const parseTime = performance.now() - start; console.log("Binary Format medium snapshot parse time ", parseTime); - }); + }).timeout(3000); // This can take more time if running in parallel }); diff --git a/packages/test/mocha-test-setup/mocharc-common.js b/packages/test/mocha-test-setup/mocharc-common.js index a3075cb49348..a973c8631d9f 100644 --- a/packages/test/mocha-test-setup/mocharc-common.js +++ b/packages/test/mocha-test-setup/mocharc-common.js @@ -13,10 +13,10 @@ function getFluidTestMochaConfig(packageDir, additionalRequiredModules, testRepo const moduleDir = `${packageDir}/node_modules`; const requiredModules = [ - ...(additionalRequiredModules ? additionalRequiredModules : []), - // General mocha setup e.g. suppresses console.log - // Moved to last in required modules, so that aria logger will be ready to access in mochaHooks.ts + // General mocha setup e.g. suppresses console.log, + // This has to be before others (except logger) so that registerMochaTestWrapperFuncs is available `@fluidframework/mocha-test-setup`, + ...(additionalRequiredModules ? additionalRequiredModules : []), ]; // mocha install node_modules directory might not be the same as the module required because of hoisting @@ -32,7 +32,7 @@ function getFluidTestMochaConfig(packageDir, additionalRequiredModules, testRepo }); if (process.env.FLUID_TEST_LOGGER_PKG_PATH) { - // Inject implementation of getTestLogger + // Inject implementation of getTestLogger, put it first before mocha-test-setup requiredModulePaths.unshift(process.env.FLUID_TEST_LOGGER_PKG_PATH); } diff --git a/packages/test/mocha-test-setup/src/mochaHooks.ts b/packages/test/mocha-test-setup/src/mochaHooks.ts index 19a1d5c341a2..3e730c525447 100644 --- a/packages/test/mocha-test-setup/src/mochaHooks.ts +++ b/packages/test/mocha-test-setup/src/mochaHooks.ts @@ -5,7 +5,7 @@ import { ITelemetryBufferedLogger } from "@fluidframework/test-driver-definitions"; import { ITelemetryBaseEvent } from "@fluidframework/common-definitions"; -import { Context } from "mocha"; +import * as mochaModule from "mocha"; import { pkgName } from "./packageVersion"; const testVariant = process.env.FLUID_TEST_VARIANT; @@ -52,7 +52,7 @@ export const mochaHooks = { return currentTestLogger ?? originalLogger; }; }, - beforeEach() { + beforeEach(this: Mocha.Context) { // Suppress console.log if not verbose mode if (process.env.FLUID_TEST_VERBOSE === undefined) { console.log = () => { }; @@ -60,8 +60,7 @@ export const mochaHooks = { console.warn = () => { }; } // save the test name can and clear the previous logger (if afterEach didn't get ran and it got left behind) - const context = this as any as Context; - currentTestName = context.currentTest?.fullTitle(); + currentTestName = this.currentTest?.fullTitle(); currentTestLogger = undefined; // send event on test start @@ -73,16 +72,15 @@ export const mochaHooks = { hostName: pkgName, }); }, - afterEach() { + afterEach(this: Mocha.Context) { // send event on test end - const context = this as any as Context; originalLogger.send({ category: "generic", eventName: "fluid:telemetry:Test_end", testName: currentTestName, - state: context.currentTest?.state, - duration: context.currentTest?.duration, - timedOut: context.currentTest?.timedOut, + state: this.currentTest?.state, + duration: this.currentTest?.duration, + timedOut: this.currentTest?.timedOut, testVariant, hostName: pkgName, }); @@ -96,3 +94,7 @@ export const mochaHooks = { currentTestName = undefined; }, }; + +globalThis.getMochaModule = () => { + return mochaModule; +}; diff --git a/packages/test/test-end-to-end-tests/src/test/blobs.spec.ts b/packages/test/test-end-to-end-tests/src/test/blobs.spec.ts index 2944d48ea998..fef147900095 100644 --- a/packages/test/test-end-to-end-tests/src/test/blobs.spec.ts +++ b/packages/test/test-end-to-end-tests/src/test/blobs.spec.ts @@ -130,7 +130,7 @@ describeFullCompat("blobs", (getTestObjectProvider) => { const container2 = await provider.loadTestContainer(testContainerConfig); const dataStore2 = await requestFluidObject(container2, "default"); - await provider.ensureSynchronized(this.timeout() / 3); + await provider.ensureSynchronized(); const blobHandle = dataStore2._root.get>(testKey); assert(blobHandle); @@ -181,7 +181,7 @@ describeFullCompat("blobs", (getTestObjectProvider) => { // validate on remote container, local container, and container loaded from summary for (const container of [container1, container2, await provider.loadTestContainer(testContainerConfig)]) { const dataStore2 = await requestFluidObject(container, "default"); - await provider.ensureSynchronized(this.timeout() / 3); + await provider.ensureSynchronized(); const handle = dataStore2._root.get>("sharedString"); assert(handle); const sharedString2 = await handle.get(); diff --git a/packages/test/test-end-to-end-tests/src/test/counterEndToEndTests.spec.ts b/packages/test/test-end-to-end-tests/src/test/counterEndToEndTests.spec.ts index 6fe6f9cf5c18..09f2d7f3c5ca 100644 --- a/packages/test/test-end-to-end-tests/src/test/counterEndToEndTests.spec.ts +++ b/packages/test/test-end-to-end-tests/src/test/counterEndToEndTests.spec.ts @@ -127,7 +127,7 @@ describeFullCompat("SharedCounter", (getTestObjectProvider) => { // do the increment incrementer.increment(incrementAmount); - await provider.ensureSynchronized(this.timeout() / 3); + await provider.ensureSynchronized(); // event count is correct assert.equal(eventCount1, expectedEventCount); diff --git a/packages/test/test-end-to-end-tests/src/test/deRehydrateContainerTests.spec.ts b/packages/test/test-end-to-end-tests/src/test/deRehydrateContainerTests.spec.ts index 41f69da5c750..b7765b657b21 100644 --- a/packages/test/test-end-to-end-tests/src/test/deRehydrateContainerTests.spec.ts +++ b/packages/test/test-end-to-end-tests/src/test/deRehydrateContainerTests.spec.ts @@ -608,7 +608,7 @@ describeFullCompat(`Dehydrate Rehydrate Container Test`, (getTestObjectProvider) // Create and reference another dataStore const { peerDataStore: dataStore2 } = await createPeerDataStore(defaultDataStore.context.containerRuntime); defaultDataStore.root.set("dataStore2", dataStore2.handle); - await provider.ensureSynchronized(this.timeout() / 3); + await provider.ensureSynchronized(); const sharedMap1 = await dataStore2.getSharedObject(sharedMapId); sharedMap1.set("0", "A"); diff --git a/packages/test/test-end-to-end-tests/src/test/directoryEndToEndTests.spec.ts b/packages/test/test-end-to-end-tests/src/test/directoryEndToEndTests.spec.ts index 90f9b4dfdd64..81d57eae3926 100644 --- a/packages/test/test-end-to-end-tests/src/test/directoryEndToEndTests.spec.ts +++ b/packages/test/test-end-to-end-tests/src/test/directoryEndToEndTests.spec.ts @@ -247,7 +247,7 @@ describeFullCompat("SharedDirectory", (getTestObjectProvider) => { expectAllBeforeValues("testKey3", "/", "value3.1", "value3.2", undefined); - await provider.ensureSynchronized(this.timeout() / 3); + await provider.ensureSynchronized(); expectAllAfterValues("testKey3", "/", undefined); }); @@ -511,7 +511,7 @@ describeFullCompat("SharedDirectory", (getTestObjectProvider) => { expectAllBeforeValues("testKey3", "/testSubDir", "value3.1", "value3.2", undefined); - await provider.ensureSynchronized(this.timeout() / 3); + await provider.ensureSynchronized(); expectAllAfterValues("testKey3", "/testSubDir", undefined); }); @@ -576,7 +576,7 @@ describeFullCompat("SharedDirectory", (getTestObjectProvider) => { const root2SubDir = sharedDirectory2.createSubDirectory("testSubDir"); root2SubDir.set("testKey2", "testValue2"); - await provider.ensureSynchronized(this.timeout() / 2); + await provider.ensureSynchronized(); assert.strictEqual(sharedDirectory1.getSubDirectory("testSubDir"), root1SubDir, "Created two separate subdirectories in root1"); diff --git a/packages/test/test-end-to-end-tests/src/test/localLoader.spec.ts b/packages/test/test-end-to-end-tests/src/test/localLoader.spec.ts index e911773a0d48..8190fbb4f4f7 100644 --- a/packages/test/test-end-to-end-tests/src/test/localLoader.spec.ts +++ b/packages/test/test-end-to-end-tests/src/test/localLoader.spec.ts @@ -8,7 +8,8 @@ import { ContainerRuntimeFactoryWithDefaultDataStore, DataObject, DataObjectFactory, - IDataObjectProps } from "@fluidframework/aqueduct"; + IDataObjectProps, +} from "@fluidframework/aqueduct"; import { IContainer, IFluidCodeDetails } from "@fluidframework/container-definitions"; import { IFluidHandle, IRequest } from "@fluidframework/core-interfaces"; import { SharedCounter } from "@fluidframework/counter"; diff --git a/packages/test/test-utils/package.json b/packages/test/test-utils/package.json index 1b19fb9d74e9..3a3347952ee9 100644 --- a/packages/test/test-utils/package.json +++ b/packages/test/test-utils/package.json @@ -29,6 +29,9 @@ "eslint:fix": "eslint --format stylish src --fix --fix-type problem,suggestion,layout", "lint": "npm run eslint", "lint:fix": "npm run eslint:fix", + "test": "npm run test:mocha", + "test:mocha": "mocha --unhandled-rejections=strict --recursive dist/test/*.spec.js --exit --project src/test/tsconfig.json -r node_modules/@fluidframework/mocha-test-setup", + "test:mocha:verbose": "cross-env FLUID_TEST_VERBOSE=1 npm run test:mocha", "tsc": "tsc", "tsfmt": "tsfmt --verify", "tsfmt:fix": "tsfmt --replace", @@ -69,6 +72,7 @@ "@fluidframework/driver-utils": ">=2.0.0-internal.3.0.0 <2.0.0-internal.4.0.0", "@fluidframework/local-driver": ">=2.0.0-internal.3.0.0 <2.0.0-internal.4.0.0", "@fluidframework/map": ">=2.0.0-internal.3.0.0 <2.0.0-internal.4.0.0", + "@fluidframework/mocha-test-setup": ">=2.0.0-internal.3.0.0 <2.0.0-internal.4.0.0", "@fluidframework/protocol-definitions": "^1.1.0-97957", "@fluidframework/request-handler": ">=2.0.0-internal.3.0.0 <2.0.0-internal.4.0.0", "@fluidframework/routerlicious-driver": ">=2.0.0-internal.3.0.0 <2.0.0-internal.4.0.0", @@ -94,6 +98,7 @@ "@types/random-js": "^1.0.31", "concurrently": "^6.2.0", "copyfiles": "^2.4.1", + "cross-env": "^7.0.2", "diff": "^3.5.0", "eslint": "~8.6.0", "mocha": "^10.0.0", @@ -102,9 +107,5 @@ "rimraf": "^2.6.2", "typescript": "~4.5.5", "typescript-formatter": "7.1.0" - }, - "typeValidation": { - "version": "2.0.0", - "broken": {} } } diff --git a/packages/test/test-utils/src/TestSummaryUtils.ts b/packages/test/test-utils/src/TestSummaryUtils.ts index 0d7a9683e81e..879bf10a284f 100644 --- a/packages/test/test-utils/src/TestSummaryUtils.ts +++ b/packages/test/test-utils/src/TestSummaryUtils.ts @@ -23,6 +23,7 @@ import { requestFluidObject } from "@fluidframework/runtime-utils"; import { IConfigProviderBase } from "@fluidframework/telemetry-utils"; import { ITestContainerConfig, ITestObjectProvider } from "./testObjectProvider"; import { mockConfigProvider } from "./TestConfigs"; +import { timeoutAwait } from "./timeoutUtils"; const summarizerClientType = "summarizer"; @@ -117,16 +118,16 @@ export async function createSummarizer( export async function summarizeNow(summarizer: ISummarizer, reason: string = "end-to-end test") { const result = summarizer.summarizeOnDemand({ reason }); - const submitResult = await result.summarySubmitted; + const submitResult = await timeoutAwait(result.summarySubmitted); assert(submitResult.success, "on-demand summary should submit"); assert(submitResult.data.stage === "submit", "on-demand summary submitted data stage should be submit"); assert(submitResult.data.summaryTree !== undefined, "summary tree should exist"); - const broadcastResult = await result.summaryOpBroadcasted; + const broadcastResult = await timeoutAwait(result.summaryOpBroadcasted); assert(broadcastResult.success, "summary op should be broadcast"); - const ackNackResult = await result.receivedSummaryAckOrNack; + const ackNackResult = await timeoutAwait(result.receivedSummaryAckOrNack); assert(ackNackResult.success, "summary op should be acked"); await new Promise((resolve) => process.nextTick(resolve)); diff --git a/packages/test/test-utils/src/loaderContainerTracker.ts b/packages/test/test-utils/src/loaderContainerTracker.ts index b2cc7c8c0bfe..bfe92a7f4d54 100644 --- a/packages/test/test-utils/src/loaderContainerTracker.ts +++ b/packages/test/test-utils/src/loaderContainerTracker.ts @@ -2,8 +2,6 @@ * Copyright (c) Microsoft Corporation and contributors. All rights reserved. * Licensed under the MIT License. */ -/* eslint-disable @typescript-eslint/strict-boolean-expressions */ - import { assert } from "@fluidframework/common-utils"; import { IContainer, IDeltaQueue, IHostLoader } from "@fluidframework/container-definitions"; import { Container } from "@fluidframework/container-loader"; @@ -16,9 +14,6 @@ import { timeoutAwait, timeoutPromise } from "./timeoutUtils"; const debugOp = debug.extend("ops"); const debugWait = debug.extend("wait"); -// set the maximum timeout value as 5 mins -const defaultMaxTimeout = 5 * 6000; - interface ContainerRecord { // A short number for debug output index: number; @@ -187,7 +182,6 @@ export class LoaderContainerTracker implements IOpProcessingController { * - Trailing NoOp is tracked and don't count as pending ops. */ private async processSynchronized(timeoutDuration: number | undefined, ...containers: IContainer[]) { - const start = Date.now(); const resumed = this.resumeProcessing(...containers); let waitingSequenceNumberSynchronized = false; @@ -214,14 +208,12 @@ export class LoaderContainerTracker implements IOpProcessingController { waitingSequenceNumberSynchronized = true; debugWait("Waiting for sequence number synchronized"); await timeoutAwait(this.waitForAnyInboundOps(containersToApply), { - durationMs: timeoutDuration ? timeoutDuration - (Date.now() - start) : defaultMaxTimeout, errorMsg: "Timeout on waiting for sequence number synchronized", }); } } else { waitingSequenceNumberSynchronized = false; await timeoutAwait(this.waitForPendingClients(pendingClients), { - durationMs: timeoutDuration ? timeoutDuration - (Date.now() - start) : defaultMaxTimeout, errorMsg: "Timeout on waiting for pending join or leave op", }); } @@ -230,12 +222,10 @@ export class LoaderContainerTracker implements IOpProcessingController { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion debugWait(`Waiting container to be saved ${dirtyContainers.map((c) => this.containers.get(c)!.index)}`); waitingSequenceNumberSynchronized = false; - const remainedDuration = timeoutDuration ? timeoutDuration - (Date.now() - start) : defaultMaxTimeout; await Promise.all(dirtyContainers.map(async (c) => Promise.race( [timeoutPromise( (resolve) => c.once("saved", () => resolve()), { - durationMs: remainedDuration, errorMsg: "Timeout on waiting a container to be saved", }, ), @@ -252,7 +242,6 @@ export class LoaderContainerTracker implements IOpProcessingController { // don't call pause if resumed is empty and pause everything, which is not what we want if (resumed.length !== 0) { await timeoutAwait(this.pauseProcessing(...resumed), { - durationMs: timeoutDuration ? timeoutDuration - (Date.now() - start) : defaultMaxTimeout, errorMsg: "Timeout on waiting for pausing all resumed containers", }); } diff --git a/packages/test/test-utils/src/test/timeoutUtils.spec.ts b/packages/test/test-utils/src/test/timeoutUtils.spec.ts new file mode 100644 index 000000000000..349d73337b3d --- /dev/null +++ b/packages/test/test-utils/src/test/timeoutUtils.spec.ts @@ -0,0 +1,158 @@ +/*! + * Copyright (c) Microsoft Corporation and contributors. All rights reserved. + * Licensed under the MIT License. + */ + +import { strict as assert } from "assert"; +import { timeoutPromise } from "../timeoutUtils"; + +describe("TimeoutPromise", () => { + beforeEach(async () => { + // Make sure there are no timeout set left behind, wait larger then the test timeout + await timeoutPromise((resolve) => setTimeout(resolve, 50)); + }); + + afterEach(async () => { + // Make sure there are no timeout set left behind, wait larger then the test timeout + await timeoutPromise((resolve) => setTimeout(resolve, 50)); + }); + + let runCount = 0; + + it("No timeout", async () => { + if (runCount++ !== 1) { + // only timeout the first time to test, but pass the second one, + // to test the behavior of test timed out by mocha + // to ensure that we reset the timeoutPromise state. + const value = await timeoutPromise((resolve) => { resolve(3); }); + assert(value === 3, "Value not returned"); + } + }).timeout(25).retries(1); + + it("Timeout", async () => { + if (runCount++ !== 1) { + // only timeout the first time to test, but pass the second one, + // to test the behavior of test timed out by mocha + // to ensure that we reset the timeoutPromise state. + return new Promise(() => { }); + } + }).timeout(25).retries(1); + + it("Timeout with no options", async () => { + try { + await timeoutPromise(() => { }); + assert(false, "should have timed out"); + } catch (e: any) { + assert(e.message.startsWith("Test timed out ("), `expected timeout error message: got ${e.message}`); + } + }).timeout(25); + + it("Timeout with no duration", async () => { + try { + await timeoutPromise(() => { }, {}); + assert(false, "should have timed out"); + } catch (e: any) { + assert(e.message.startsWith("Test timed out ("), `expected timeout error message: got ${e.message}`); + } + }).timeout(25); + + it("Timeout with duration", async () => { + try { + await timeoutPromise(() => { }, { durationMs: 1 }); + assert(false, "should have timed out"); + } catch (e: any) { + assert(e.message.startsWith("Timed out ("), `expected timeout error message: got ${e.message}`); + } + }).timeout(25); + + it("No timeout with zero duration", async () => { + try { + await timeoutPromise((resolve) => { + setTimeout(resolve, 10); + }, { durationMs: 0 }); + } catch (e: any) { + assert(false, `should not have timed out: ${e.message}`); + } + }).timeout(25); + + it("No timeout with negative duration", async function() { + // Make sure resetTimeout in the test works + this.timeout(100); + try { + await timeoutPromise((resolve) => { + setTimeout(resolve, 50); + }, { durationMs: -1 }); + } catch (e: any) { + assert(false, `should not have timed out: ${e.message}`); + } + }).timeout(25); + + it("No timeout with Infinity duration", async function() { + // Make sure resetTimeout in the test works + this.timeout(100); + try { + await timeoutPromise((resolve) => { + setTimeout(resolve, 50); + }, { durationMs: Infinity }); + } catch (e: any) { + assert(false, `should not have timed out: ${e.message}`); + } + }).timeout(25); + + it("No timeout with valid duration", async function() { + // Make sure resetTimeout in the test works + this.timeout(100); + try { + await timeoutPromise((resolve) => { setTimeout(resolve, 50); }, { durationMs: 75 }); + } catch (e: any) { + assert(false, `should not have timed out: ${e.message}`); + } + }).timeout(25); + + it("No timeout with throw", async function() { + // Make sure resetTimeout in the test works + this.timeout(100); + try { + await timeoutPromise((resolve, reject) => { reject(new Error("blah")); }); + assert(false, "should have thrown"); + } catch (e: any) { + assert(e.message === "blah", `should not have timed out: ${e.message}`); + } + }).timeout(25); + + it("Timeout with valid duration", async function() { + // Make sure resetTimeout in the test works + this.timeout(100); + try { + await timeoutPromise((resolve) => { setTimeout(resolve, 75); }, { durationMs: 50 }); + assert(false, "should have timed out"); + } catch (e: any) { + assert(e.message.startsWith("Timed out ("), "expected timeout error message"); + } + }).timeout(25); + + it("Timeout with no reject option", async () => { + try { + const value = await timeoutPromise(() => { }, { + durationMs: 1, + reject: false, + value: 1, + }); + assert(value === 1, "expect timeout to return value given in option"); + } catch (e: any) { + assert(false, `should not have timed out: ${e.message}`); + } + }).timeout(25); + + it("Timeout rejection with error option", async () => { + try { + await timeoutPromise(() => { }, { + durationMs: 1, + errorMsg: "hello", + }); + assert(false, "should have timed out"); + } catch (e: any) { + assert(e.message.startsWith("hello"), "expected timeout reject error message given in option"); + } + }).timeout(25); +}); diff --git a/packages/test/test-utils/src/test/tsconfig.json b/packages/test/test-utils/src/test/tsconfig.json index 913f09ed3b1b..710a3b4b93b2 100644 --- a/packages/test/test-utils/src/test/tsconfig.json +++ b/packages/test/test-utils/src/test/tsconfig.json @@ -3,10 +3,12 @@ "compilerOptions": { "rootDir": "./", "outDir": "../../dist/test", + "types": [ + "mocha", + ], "declaration": false, "declarationMap": false, "skipLibCheck": true, - "noEmit": true, }, "include": [ "./**/*" diff --git a/packages/test/test-utils/src/testObjectProvider.ts b/packages/test/test-utils/src/testObjectProvider.ts index 64b77bce7f92..21d158862313 100644 --- a/packages/test/test-utils/src/testObjectProvider.ts +++ b/packages/test/test-utils/src/testObjectProvider.ts @@ -140,7 +140,7 @@ export class EventAndErrorTrackingLogger extends TelemetryLogger { private readonly expectedEvents: ({ index: number; event: ITelemetryGenericEvent | undefined; } | undefined)[] = []; private readonly unexpectedErrors: ITelemetryBaseEvent[] = []; - public registerExpectedEvent(... orderedExpectedEvents: ITelemetryGenericEvent[]) { + public registerExpectedEvent(...orderedExpectedEvents: ITelemetryGenericEvent[]) { if (this.expectedEvents.length !== 0) { // we don't have to error here. just no reason not to. given the events must be // ordered it could be tricky to figure out problems around multiple registrations. @@ -148,7 +148,7 @@ export class EventAndErrorTrackingLogger extends TelemetryLogger { "Expected events already registered.\n" + "Call reportAndClearTrackedEvents to clear them before registering more"); } - this.expectedEvents.push(... orderedExpectedEvents.map((event, index) => ({ index, event }))); + this.expectedEvents.push(...orderedExpectedEvents.map((event, index) => ({ index, event }))); } send(event: ITelemetryBaseEvent): void { @@ -219,14 +219,14 @@ export class TestObjectProvider implements ITestObjectProvider { if (this._logger === undefined) { this._logger = new EventAndErrorTrackingLogger( ChildLogger.create(getTestLogger?.(), undefined, - { - all: { - driverType: this.driver.type, - driverEndpointName: this.driver.endpointName, - driverTenantName: this.driver.tenantName, - driverUserIndex: this.driver.userIndex, - }, - })); + { + all: { + driverType: this.driver.type, + driverEndpointName: this.driver.endpointName, + driverTenantName: this.driver.tenantName, + driverUserIndex: this.driver.userIndex, + }, + })); } return this._logger; } @@ -280,7 +280,7 @@ export class TestObjectProvider implements ITestObjectProvider { } const loader = new this.LoaderConstructor({ - ... loaderProps, + ...loaderProps, logger: multiSinkLogger, codeLoader: loaderProps?.codeLoader ?? new LocalCodeLoader(packageEntries), urlResolver: loaderProps?.urlResolver ?? this.urlResolver, @@ -394,10 +394,9 @@ export class TestObjectProvider implements ITestObjectProvider { } public async ensureSynchronized(timeoutDuration?: number): Promise { - // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions - return !timeoutDuration - ? this._loaderContainerTracker.ensureSynchronized() - : this._loaderContainerTracker.ensureSynchronizedWithTimeout?.(timeoutDuration); + return this._loaderContainerTracker.ensureSynchronizedWithTimeout + ? this._loaderContainerTracker.ensureSynchronizedWithTimeout(timeoutDuration) + : this._loaderContainerTracker.ensureSynchronized(); } public async waitContainerToCatchUp(container: IContainer) { diff --git a/packages/test/test-utils/src/timeoutUtils.ts b/packages/test/test-utils/src/timeoutUtils.ts index 4fcc765633a0..bc4dd86a18b2 100644 --- a/packages/test/test-utils/src/timeoutUtils.ts +++ b/packages/test/test-utils/src/timeoutUtils.ts @@ -4,15 +4,116 @@ */ import { Container } from "@fluidframework/container-loader"; +import { assert, Deferred } from "@fluidframework/common-utils"; +// @deprecated this value is no longer used export const defaultTimeoutDurationMs = 250; +// TestTimeout class manage tracking of test timeout. It create a timer when timeout is in effect, +// and provide a promise that will be reject before the test timeout happen with a `timeBuffer` of 15 ms. +// Once rejected, a new TestTimeout object will be create for the timeout. + +const timeBuffer = 15; // leave 15 ms leeway for finish processing + +class TestTimeout { + private timeout: number = 0; + private timer: NodeJS.Timeout | undefined; + private readonly deferred: Deferred; + private rejected = false; + + private static instance: TestTimeout = new TestTimeout(); + public static reset(runnable: Mocha.Runnable) { + TestTimeout.clear(); + TestTimeout.instance.resetTimer(runnable); + } + + public static clear() { + if (TestTimeout.instance.rejected) { + TestTimeout.instance = new TestTimeout(); + } else { + TestTimeout.instance.clearTimer(); + } + } + + public static getInstance() { + return TestTimeout.instance; + } + + public async getPromise() { + return this.deferred.promise; + } + + public getTimeout() { + return this.timeout; + } + + private constructor() { + this.deferred = new Deferred(); + // Ignore rejection for timeout promise if no one is waiting for it. + this.deferred.promise.catch(() => { }); + } + + private resetTimer(runnable: Mocha.Runnable) { + assert(!this.timer, "clearTimer should have been called before reset"); + assert(!this.deferred.isCompleted, "can't reset a completed TestTimeout"); + + // Check the test timeout setting + const timeout = runnable.timeout(); + if (!(Number.isFinite(timeout) && timeout > 0)) { return; } + + // subtract a buffer + this.timeout = Math.max(timeout - timeBuffer, 1); + + // Set up timer to reject near the test timeout. + this.timer = setTimeout(() => { + this.deferred.reject(this); + this.rejected = true; + }, this.timeout); + } + private clearTimer() { + if (this.timer) { + clearTimeout(this.timer); + this.timer = undefined; + } + } +} + +// only register if we are running with mocha-test-setup loaded +if (globalThis.getMochaModule !== undefined) { + // patching resetTimeout and clearTimeout on the runnable object + // so we can track when test timeout are enforced + const mochaModule = globalThis.getMochaModule() as typeof Mocha; + const runnablePrototype = mochaModule.Runnable.prototype; + // eslint-disable-next-line @typescript-eslint/unbound-method + const oldResetTimeoutFunc = runnablePrototype.resetTimeout; + runnablePrototype.resetTimeout = function(this: Mocha.Runnable) { + oldResetTimeoutFunc.call(this); + TestTimeout.reset(this); + }; + // eslint-disable-next-line @typescript-eslint/unbound-method + const oldClearTimeoutFunc = runnablePrototype.clearTimeout; + runnablePrototype.clearTimeout = function(this: Mocha.Runnable) { + TestTimeout.clear(); + oldClearTimeoutFunc.call(this); + }; +} + export interface TimeoutWithError { + /** + * Timeout duration in milliseconds, if it is great than 0 and not Infinity + * If it is undefined, then it will use test timeout if we are in side the test function + * Otherwise, there is no timeout + */ durationMs?: number; reject?: true; errorMsg?: string; } export interface TimeoutWithValue { + /** + * Timeout duration in milliseconds, if it is great than 0 and not Infinity + * If it is undefined, then it will use test timeout if we are in side the test function + * Otherwise, there is no timeout + */ durationMs?: number; reject: false; value: T; @@ -31,23 +132,26 @@ export async function ensureContainerConnected(container: Container): Promise( +// Create a promise based on the timeout options +async function getTimeoutPromise( executor: (resolve: (value: T | PromiseLike) => void, reject: (reason?: any) => void) => void, - timeoutOptions: TimeoutWithError | TimeoutWithValue = {}, -): Promise { - const timeout = - timeoutOptions.durationMs !== undefined - && Number.isFinite(timeoutOptions.durationMs) - && timeoutOptions.durationMs > 0 - ? timeoutOptions.durationMs : defaultTimeoutDurationMs; - // create the timeout error outside the async task, so its callstack includes - // the original call site, this makes it easier to debug - const err = timeoutOptions.reject === false - ? undefined - : new Error(`${timeoutOptions.errorMsg ?? "Timed out"}(${timeout}ms)`); + timeoutOptions: TimeoutWithError | TimeoutWithValue, + err: Error | undefined, +) { + const timeout = timeoutOptions.durationMs ?? 0; + if (timeout <= 0 || !Number.isFinite(timeout)) { + return new Promise(executor); + } + return new Promise((resolve, reject) => { + const timeoutRejections = () => { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const errorObject = err!; + errorObject.message = `${errorObject.message} (${timeout}ms)`; + reject(err); + }; const timer = setTimeout( - () => timeoutOptions.reject === false ? resolve(timeoutOptions.value) : reject(err), + () => timeoutOptions.reject === false ? resolve(timeoutOptions.value) : timeoutRejections(), timeout); executor( @@ -61,3 +165,34 @@ export async function timeoutPromise( }); }); } + +// Create a promise based on test timeout and the timeout options +export async function timeoutPromise( + executor: (resolve: (value: T | PromiseLike) => void, reject: (reason?: any) => void) => void, + timeoutOptions: TimeoutWithError | TimeoutWithValue = {}, +): Promise { + // create the timeout error outside the async task, so its callstack includes + // the original call site, this makes it easier to debug + const err = timeoutOptions.reject === false + ? undefined + : new Error(timeoutOptions.errorMsg ?? "Timed out"); + const executorPromise = getTimeoutPromise(executor, timeoutOptions, err); + + const currentTestTimeout = TestTimeout.getInstance(); + if (currentTestTimeout === undefined) { return executorPromise; } + + return Promise.race([executorPromise, currentTestTimeout.getPromise()]).catch((e) => { + if (e === currentTestTimeout) { + if (timeoutOptions.reject !== false) { + // If the rejection is because of the timeout then + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const errorObject = err!; + errorObject.message = + `${timeoutOptions.errorMsg ?? "Test timed out"} (${currentTestTimeout.getTimeout()}ms)`; + throw errorObject; + } + return timeoutOptions.value; + } + throw e; + }) as Promise; +} diff --git a/packages/test/test-utils/tsconfig.json b/packages/test/test-utils/tsconfig.json index f156559ebdf6..aa4bc4eb8d3c 100644 --- a/packages/test/test-utils/tsconfig.json +++ b/packages/test/test-utils/tsconfig.json @@ -6,7 +6,10 @@ "compilerOptions": { "rootDir": "./src", "outDir": "./dist", - "composite": true + "composite": true, + "types": [ + "mocha", + ] }, "include": [ "src/**/*" diff --git a/packages/test/test-version-utils/src/versionUtils.ts b/packages/test/test-version-utils/src/versionUtils.ts index 4ff55f91c9e0..95703ee76c55 100644 --- a/packages/test/test-version-utils/src/versionUtils.ts +++ b/packages/test/test-version-utils/src/versionUtils.ts @@ -101,7 +101,7 @@ export function resolveVersion(requested: string, installed: boolean) { } if (installed) { - // Check the install directory instad of asking NPM for it. + // Check the install directory instead of asking NPM for it. const files = readdirSync(baseModulePath, { withFileTypes: true }); let found: string | undefined; files.map((dirent) => { @@ -122,7 +122,7 @@ export function resolveVersion(requested: string, installed: boolean) { ); // if we are requesting an x.x.0-0 prerelease and failed the first try, try // again using the virtualPatch schema - if (result === "") { + if (result === "" && !requested.includes("internal")) { const requestedVersion = new semver.SemVer(requested.substring(requested.indexOf("^") + 1)); if (requestedVersion.patch === 0 && requestedVersion.prerelease.length > 0) { const retryVersion = `^${requestedVersion.major}.${requestedVersion.minor}.1000-0`; @@ -134,14 +134,17 @@ export function resolveVersion(requested: string, installed: boolean) { } try { - const versions: string | string[] = JSON.parse(result); + const versions: string | string[] = result !== "" ? JSON.parse(result) : ""; const version = Array.isArray(versions) ? versions.sort(semver.rcompare)[0] : versions; - if (!version) { throw new Error(`No version found for ${requested}`); } - resolutionCache.set(requested, version); - return version; + if (version) { + resolutionCache.set(requested, version); + return version; + } } catch (e) { throw new Error(`Error parsing versions for ${requested}`); } + + throw new Error(`No version found for ${requested}`); } }