From 3655962b948d7599e15f79b131d69d1a7b032229 Mon Sep 17 00:00:00 2001 From: Curtis Man Date: Wed, 5 Oct 2022 13:10:39 -0700 Subject: [PATCH 01/12] Enable timeout ensureSynchronized everywhere --- .../property-dds/src/test/rebasing.spec.ts | 1708 ++++++++--------- lerna-package-lock.json | 30 +- package-lock.json | 30 +- packages/drivers/odsp-driver/src/test/test.ts | 2 +- .../src/test/blobs.spec.ts | 4 +- .../src/test/counterEndToEndTests.spec.ts | 2 +- .../test/deRehydrateContainerTests.spec.ts | 2 +- .../src/test/directoryEndToEndTests.spec.ts | 6 +- .../src/test/localLoader.spec.ts | 3 +- packages/test/test-utils/package.json | 9 +- .../test-utils/src/loaderContainerTracker.ts | 11 - .../test-utils/src/test/timeoutUtils.spec.ts | 99 + .../test/test-utils/src/test/tsconfig.json | 4 +- .../test/test-utils/src/testObjectProvider.ts | 29 +- packages/test/test-utils/src/timeoutUtils.ts | 104 +- packages/test/test-utils/tsconfig.json | 5 +- 16 files changed, 1117 insertions(+), 931 deletions(-) create mode 100644 packages/test/test-utils/src/test/timeoutUtils.spec.ts 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/lerna-package-lock.json b/lerna-package-lock.json index 9af1686c6b38..d26db3172885 100644 --- a/lerna-package-lock.json +++ b/lerna-package-lock.json @@ -33084,7 +33084,7 @@ "color-name": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" }, "color-string": { "version": "1.9.1", @@ -36413,7 +36413,7 @@ "escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==" + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" }, "escodegen": { "version": "2.0.0", @@ -39522,7 +39522,7 @@ "has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==" + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" }, "has-glob": { "version": "1.0.0", @@ -41181,7 +41181,7 @@ "is-plain-obj": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", - "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==" + "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=" }, "is-plain-object": { "version": "2.0.4", @@ -41252,7 +41252,7 @@ "is-stream": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==" + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=" }, "is-string": { "version": "1.0.7", @@ -41373,7 +41373,7 @@ "isarray": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==" + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" }, "isemail": { "version": "3.2.0", @@ -49161,7 +49161,7 @@ "npm-run-path": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", - "integrity": "sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==", + "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", "requires": { "path-key": "^2.0.0" } @@ -53323,7 +53323,7 @@ "read-pkg-up": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-3.0.0.tgz", - "integrity": "sha512-YFzFrVvpC6frF1sz8psoHDBGF7fLPc+llq/8NB43oagqWkx8ar5zYtsTORtOjw9W2RHLpWP+zTWwBvf1bCmcSw==", + "integrity": "sha1-PtSWaF26D4/hGNBpHcUfSh/5bwc=", "dev": true, "requires": { "find-up": "^2.0.0", @@ -53333,7 +53333,7 @@ "find-up": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", - "integrity": "sha512-NWzkk0jSJtTt08+FBFMvXoeZnOJD+jTtsRmBYbAIzJdX6l7dLgR7CTubCM5/eDdPUBvLCeVasP1brfVR/9/EZQ==", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", "dev": true, "requires": { "locate-path": "^2.0.0" @@ -53354,7 +53354,7 @@ "locate-path": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", - "integrity": "sha512-NCI2kiDkyR7VeEKm27Kda/iQHyKJe1Bu0FlTbYp3CqJu+9IFe9bLyAjMxf5ZDDbEg+iMPzB5zYyUTSm8wVTKmA==", + "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", "dev": true, "requires": { "p-locate": "^2.0.0", @@ -53373,7 +53373,7 @@ "p-locate": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", - "integrity": "sha512-nQja7m7gSKuewoVRen45CtVfODR3crN3goVQ0DDZ9N3yHxgpkuBhZqsaiotSQRrADUrne346peY7kT3TSACykg==", + "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", "dev": true, "requires": { "p-limit": "^1.1.0" @@ -53382,7 +53382,7 @@ "p-try": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", - "integrity": "sha512-U1etNYuMJoIz3ZXSrrySFjsXQTWOx2/jdi86L+2pRvph/qMKL6sbcCYdH23fqsbm8TH2Gn0OybpT4eSFlCVHww==", + "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", "dev": true }, "read-pkg": { @@ -53401,7 +53401,7 @@ "readable-stream": { "version": "1.0.34", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", - "integrity": "sha512-ok1qVCJuRkNmvebYikljxJA/UEsKwLl2nI1OmaqAu4/UE+h0wKCHok4XkL/gvi39OacXvw59RJUOFUkDib2rHg==", + "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.1", @@ -56808,7 +56808,7 @@ "string_decoder": { "version": "0.10.31", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==" + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" }, "strip-ansi": { "version": "4.0.0", @@ -58818,7 +58818,7 @@ "tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", "requires": { "safe-buffer": "^5.0.1" } diff --git a/package-lock.json b/package-lock.json index 45a2caa559ff..40a1cc50d144 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5022,7 +5022,7 @@ "color-name": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", "dev": true }, "color-support": { @@ -6077,7 +6077,7 @@ "escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", "dev": true }, "eslint-scope": { @@ -6817,7 +6817,7 @@ "has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", "dev": true }, "has-symbols": { @@ -7353,7 +7353,7 @@ "is-plain-obj": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", - "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", + "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=", "dev": true }, "is-plain-object": { @@ -7392,7 +7392,7 @@ "is-stream": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", "dev": true }, "is-text-path": { @@ -7443,7 +7443,7 @@ "isarray": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", "dev": true }, "isexe": { @@ -9460,7 +9460,7 @@ "npm-run-path": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", - "integrity": "sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==", + "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", "dev": true, "requires": { "path-key": "^2.0.0" @@ -10512,7 +10512,7 @@ "read-pkg-up": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-3.0.0.tgz", - "integrity": "sha512-YFzFrVvpC6frF1sz8psoHDBGF7fLPc+llq/8NB43oagqWkx8ar5zYtsTORtOjw9W2RHLpWP+zTWwBvf1bCmcSw==", + "integrity": "sha1-PtSWaF26D4/hGNBpHcUfSh/5bwc=", "dev": true, "requires": { "find-up": "^2.0.0", @@ -10522,7 +10522,7 @@ "find-up": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", - "integrity": "sha512-NWzkk0jSJtTt08+FBFMvXoeZnOJD+jTtsRmBYbAIzJdX6l7dLgR7CTubCM5/eDdPUBvLCeVasP1brfVR/9/EZQ==", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", "dev": true, "requires": { "locate-path": "^2.0.0" @@ -10543,7 +10543,7 @@ "locate-path": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", - "integrity": "sha512-NCI2kiDkyR7VeEKm27Kda/iQHyKJe1Bu0FlTbYp3CqJu+9IFe9bLyAjMxf5ZDDbEg+iMPzB5zYyUTSm8wVTKmA==", + "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", "dev": true, "requires": { "p-locate": "^2.0.0", @@ -10562,7 +10562,7 @@ "p-locate": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", - "integrity": "sha512-nQja7m7gSKuewoVRen45CtVfODR3crN3goVQ0DDZ9N3yHxgpkuBhZqsaiotSQRrADUrne346peY7kT3TSACykg==", + "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", "dev": true, "requires": { "p-limit": "^1.1.0" @@ -10571,7 +10571,7 @@ "p-try": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", - "integrity": "sha512-U1etNYuMJoIz3ZXSrrySFjsXQTWOx2/jdi86L+2pRvph/qMKL6sbcCYdH23fqsbm8TH2Gn0OybpT4eSFlCVHww==", + "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", "dev": true }, "parse-json": { @@ -10612,7 +10612,7 @@ "readable-stream": { "version": "1.0.34", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", - "integrity": "sha512-ok1qVCJuRkNmvebYikljxJA/UEsKwLl2nI1OmaqAu4/UE+h0wKCHok4XkL/gvi39OacXvw59RJUOFUkDib2rHg==", + "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", "dev": true, "requires": { "core-util-is": "~1.0.0", @@ -11320,7 +11320,7 @@ "string_decoder": { "version": "0.10.31", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", "dev": true }, "strip-ansi": { @@ -11639,7 +11639,7 @@ "tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", "dev": true, "requires": { "safe-buffer": "^5.0.1" 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/test-end-to-end-tests/src/test/blobs.spec.ts b/packages/test/test-end-to-end-tests/src/test/blobs.spec.ts index e1d1d8cc492d..445210f04878 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 @@ -122,7 +122,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); @@ -173,7 +173,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/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..e670ae0da236 --- /dev/null +++ b/packages/test/test-utils/src/test/timeoutUtils.spec.ts @@ -0,0 +1,99 @@ +/*! + * Copyright (c) Microsoft Corporation and contributors. All rights reserved. + * Licensed under the MIT License. + */ + +import { strict as assert } from "assert"; +import { timeoutPromise, defaultTimeoutDurationMs } from "../timeoutUtils"; + +describe("TimeoutPromise", () => { + it("Timeout with no options", async () => { + try { + await timeoutPromise(() => {}); + assert(false, "should have timed out"); + } catch (e: any) { + assert(e.message.startsWith("Timed out"), "expected timeout error message"); + } + }); + + it("Timeout with no duration", async () => { + try { + await timeoutPromise(() => { }, {}); + assert(false, "should have timed out"); + } catch (e: any) { + assert(e.message.startsWith("Timed out"), "expected timeout error message"); + } + }); + + it("Timeout with duration", async () => { + try { + await timeoutPromise(() => { }, { durationMs: 1 }); + assert(false, "should have timed out"); + } catch { + + } + }); + + it("Timeout with zero duration", async () => { + try { + await timeoutPromise((resolve) => { + setTimeout(resolve, defaultTimeoutDurationMs + 50); + }, { durationMs: 0 }); + } catch { + assert(false, "should not have timed out"); + } + }); + + it("Timeout with negative duration", async () => { + try { + await timeoutPromise((resolve) => { + setTimeout(resolve, defaultTimeoutDurationMs + 50); + }, { durationMs: -1 }); + } catch { + assert(false, "should not have timed out"); + } + }); + + it("Timeout with Infinity duration", async () => { + try { + await timeoutPromise((resolve) => { + setTimeout(resolve, defaultTimeoutDurationMs + 50); + }, { durationMs: Infinity }); + } catch { + assert(false, "should not have timed out"); + } + }); + + it("no timeout", async () => { + try { + await timeoutPromise((resolve) => { setTimeout(resolve, 1); }, { durationMs: 100 }); + } catch { + assert(false, "should not have timed out"); + } + }); + + 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 { + assert(false, "should not have timed out"); + } + }); + + 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"); + } + }); +}); 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..45dc0bd86b01 100644 --- a/packages/test/test-utils/src/timeoutUtils.ts +++ b/packages/test/test-utils/src/timeoutUtils.ts @@ -4,15 +4,108 @@ */ import { Container } from "@fluidframework/container-loader"; +import { assert } from "@fluidframework/common-utils"; export const defaultTimeoutDurationMs = 250; +// Patch mocha so we can timeout promises based on how much time is left in the test. +let currentTestEndTimeId = 0; +let currentTestEndTime = 0; // 0 means it is not set, -1 means timeout is disabled, otherwise, it is the test end time +const timeBuffer = 30; // leave 30ms leeway for finish processing + +function getCurrentTestTimeout() { + if (currentTestEndTime === -1) { + return -1; + } + if (currentTestEndTime === 0) { + return defaultTimeoutDurationMs; + } + // Even if we are passed our timeout, return 1ms so that we will still wait + return Math.max(currentTestEndTime - Date.now(), 1); +} + +function setTestEndTime(timeout: number) { + const now = Date.now(); + // Either the test timed out (so the test end time is less then now, or the promise resolved) + assert(currentTestEndTime < now && currentTestEndTime !== -1, "Unexpected nested tests detected"); + const hasTimeout = Number.isFinite(timeout) && timeout > 0; + currentTestEndTime = hasTimeout ? now + timeout - timeBuffer : -1; + return ++currentTestEndTimeId; +} + +function clearTestEndTime(id) { + if (id === currentTestEndTimeId) { + currentTestEndTime = 0; + } +} + +function getWrappedFunction(fn: Mocha.Func | Mocha.AsyncFunc) { + if (fn.length > 0) { + return function(this: Mocha.Context, done) { + const id = setTestEndTime(this.timeout()); + try { + (fn as Mocha.Func).call(this, done); + } finally { + clearTestEndTime(id); + } + }; + } + return function(this: Mocha.Context) { + const id = setTestEndTime(this.timeout()); + + let ret: PromiseLike | void; + try { + ret = (fn as Mocha.AsyncFunc).call(this); + } finally { + clearTestEndTime(id); + } + + if (typeof ret?.then === "function") { + // Start the timer again to wait for async + const asyncId = setTestEndTime(this.timeout()); + // Clear the timer if the promise resolves. + // use the id to avoid clearing the end time if it resolves after timing out + const clearFunc = () => { clearTestEndTime(asyncId); }; + ret?.then(clearFunc, clearFunc); + } + return ret; + }; +} + +let newTestFunction: Mocha.TestFunction | undefined; +function setupMocha() { + const currentTestFunction = globalThis.it; + // the function `it` is reassign per test files. Trap it. + Object.defineProperty(globalThis, "it", { + get: () => { return newTestFunction; }, + set: (oldTestFunction: Mocha.TestFunction | undefined) => { + if (oldTestFunction === undefined) { newTestFunction = undefined; return; } + newTestFunction = ((title: string, fn?: Mocha.Func | Mocha.AsyncFunc) => { + return oldTestFunction(title, fn && typeof fn.call === "function" ? + getWrappedFunction(fn) + : fn); + }) as Mocha.TestFunction; + newTestFunction.skip = oldTestFunction.skip; + newTestFunction.only = oldTestFunction.only; + }, + }); + globalThis.it = currentTestFunction; +} + +setupMocha(); + export interface TimeoutWithError { + // Timeout duration in milliseconds. + // If it is undefined, then the default is 250. + // If it is <= 0 or infinity, then there is no timeout durationMs?: number; reject?: true; errorMsg?: string; } export interface TimeoutWithValue { + // Timeout duration in milliseconds. + // If it is undefined, then the default is 250. + // If it is <= 0 or infinity, then there is no timeout durationMs?: number; reject: false; value: T; @@ -35,16 +128,15 @@ export async function timeoutPromise( 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; + const timeout = timeoutOptions.durationMs ?? getCurrentTestTimeout(); + if (timeout <= 0 || !Number.isFinite(timeout)) { + return new Promise(executor); + } // 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)`); + : new Error(`${timeoutOptions.errorMsg ?? "Timed out"} (${timeout}ms)`); return new Promise((resolve, reject) => { const timer = setTimeout( () => timeoutOptions.reject === false ? resolve(timeoutOptions.value) : reject(err), 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/**/*" From 7736c35f62cd5d5b420a1906b68668c19cd42181 Mon Sep 17 00:00:00 2001 From: Curtis Man Date: Thu, 6 Oct 2022 15:17:37 -0700 Subject: [PATCH 02/12] Refactor the patch to a general utilities, and fix issue with trying to patch with jest --- .../test/mocha-test-setup/mocharc-common.js | 8 +- .../test/mocha-test-setup/src/mochaHooks.ts | 75 +++++++++++++++++++ packages/test/test-utils/src/timeoutUtils.ts | 63 ++-------------- 3 files changed, 86 insertions(+), 60 deletions(-) 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..989ce4297720 100644 --- a/packages/test/mocha-test-setup/src/mochaHooks.ts +++ b/packages/test/mocha-test-setup/src/mochaHooks.ts @@ -96,3 +96,78 @@ export const mochaHooks = { currentTestName = undefined; }, }; + +let newTestFunction: Mocha.TestFunction | undefined; + +type BeforeTestFunc = (this: Mocha.Context) => T; +type AfterTestFunc = (this: Mocha.Context, beforeTestResult: T) => void; + +const testFuncs: { beforeTestFunc: BeforeTestFunc; afterTestFunc: AfterTestFunc; }[] = []; +function runBeforeTestFuncs(context: Mocha.Context): any[] { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return testFuncs.map((v) => v.beforeTestFunc.call(context)); +} + +function runAfterTestFuncs(context: Mocha.Context, values: any[]) { + for (let index = testFuncs.length - 1; index >= 0; index--) { + testFuncs[index].afterTestFunc.call(context, values[index]); + } +} +function getWrappedFunction(fn: Mocha.Func | Mocha.AsyncFunc) { + if (fn.length > 0) { + return function(this: Mocha.Context, done) { + const values = runBeforeTestFuncs(this); + try { + (fn as Mocha.Func).call(this, done); + } finally { + runAfterTestFuncs(this, values); + } + }; + } + return function(this: Mocha.Context) { + const values = runBeforeTestFuncs(this); + + let ret: PromiseLike | void; + try { + ret = (fn as Mocha.AsyncFunc).call(this); + } finally { + runAfterTestFuncs(this, values); + } + + if (typeof ret?.then === "function") { + // Start the timer again to wait for async + const asyncValues = runBeforeTestFuncs(this); + // Clear the timer if the promise resolves. + // use the id to avoid clearing the end time if it resolves after timing out + const clearFunc = () => { runAfterTestFuncs(this, asyncValues); }; + ret?.then(clearFunc, clearFunc); + } + return ret; + }; +} + +function setupCustomTestHooks() { + const currentTestFunction = globalThis.it; + // the function `it` is reassign per test files. Trap it. + Object.defineProperty(globalThis, "it", { + get: () => { return newTestFunction; }, + set: (oldTestFunction: Mocha.TestFunction | undefined) => { + if (oldTestFunction === undefined) { newTestFunction = undefined; return; } + newTestFunction = ((title: string, fn?: Mocha.Func | Mocha.AsyncFunc) => { + return oldTestFunction(title, fn && typeof fn.call === "function" ? + getWrappedFunction(fn) + : fn); + }) as Mocha.TestFunction; + newTestFunction.skip = oldTestFunction.skip; + newTestFunction.only = oldTestFunction.only; + }, + }); + globalThis.it = currentTestFunction; +} + +setupCustomTestHooks(); + +globalThis.registerMochaTestWrapperFuncs = + function (beforeTestFunc: BeforeTestFunc, afterTestFunc: AfterTestFunc) { + testFuncs.push({ beforeTestFunc, afterTestFunc }); + }; diff --git a/packages/test/test-utils/src/timeoutUtils.ts b/packages/test/test-utils/src/timeoutUtils.ts index 45dc0bd86b01..cee6368d90ea 100644 --- a/packages/test/test-utils/src/timeoutUtils.ts +++ b/packages/test/test-utils/src/timeoutUtils.ts @@ -24,7 +24,8 @@ function getCurrentTestTimeout() { return Math.max(currentTestEndTime - Date.now(), 1); } -function setTestEndTime(timeout: number) { +function setTestEndTime(this: Mocha.Context) { + const timeout = this.timeout(); const now = Date.now(); // Either the test timed out (so the test end time is less then now, or the promise resolved) assert(currentTestEndTime < now && currentTestEndTime !== -1, "Unexpected nested tests detected"); @@ -33,67 +34,17 @@ function setTestEndTime(timeout: number) { return ++currentTestEndTimeId; } -function clearTestEndTime(id) { - if (id === currentTestEndTimeId) { +function clearTestEndTime(this: Mocha.Context, value: number) { + if (value === currentTestEndTimeId) { currentTestEndTime = 0; } } -function getWrappedFunction(fn: Mocha.Func | Mocha.AsyncFunc) { - if (fn.length > 0) { - return function(this: Mocha.Context, done) { - const id = setTestEndTime(this.timeout()); - try { - (fn as Mocha.Func).call(this, done); - } finally { - clearTestEndTime(id); - } - }; - } - return function(this: Mocha.Context) { - const id = setTestEndTime(this.timeout()); - - let ret: PromiseLike | void; - try { - ret = (fn as Mocha.AsyncFunc).call(this); - } finally { - clearTestEndTime(id); - } - - if (typeof ret?.then === "function") { - // Start the timer again to wait for async - const asyncId = setTestEndTime(this.timeout()); - // Clear the timer if the promise resolves. - // use the id to avoid clearing the end time if it resolves after timing out - const clearFunc = () => { clearTestEndTime(asyncId); }; - ret?.then(clearFunc, clearFunc); - } - return ret; - }; +// only register if we are running with mocha-test-setup loaded +if (globalThis.registerMochaTestWrapperFuncs !== undefined) { + globalThis.registerMochaTestWrapperFuncs(setTestEndTime, clearTestEndTime); } -let newTestFunction: Mocha.TestFunction | undefined; -function setupMocha() { - const currentTestFunction = globalThis.it; - // the function `it` is reassign per test files. Trap it. - Object.defineProperty(globalThis, "it", { - get: () => { return newTestFunction; }, - set: (oldTestFunction: Mocha.TestFunction | undefined) => { - if (oldTestFunction === undefined) { newTestFunction = undefined; return; } - newTestFunction = ((title: string, fn?: Mocha.Func | Mocha.AsyncFunc) => { - return oldTestFunction(title, fn && typeof fn.call === "function" ? - getWrappedFunction(fn) - : fn); - }) as Mocha.TestFunction; - newTestFunction.skip = oldTestFunction.skip; - newTestFunction.only = oldTestFunction.only; - }, - }); - globalThis.it = currentTestFunction; -} - -setupMocha(); - export interface TimeoutWithError { // Timeout duration in milliseconds. // If it is undefined, then the default is 250. From 5656e9b3b88840c28743964d7d18dde580cbd472 Mon Sep 17 00:00:00 2001 From: Curtis Man Date: Thu, 6 Oct 2022 15:25:00 -0700 Subject: [PATCH 03/12] Add comment --- packages/test/mocha-test-setup/src/mochaHooks.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/test/mocha-test-setup/src/mochaHooks.ts b/packages/test/mocha-test-setup/src/mochaHooks.ts index 989ce4297720..511bac47ddb2 100644 --- a/packages/test/mocha-test-setup/src/mochaHooks.ts +++ b/packages/test/mocha-test-setup/src/mochaHooks.ts @@ -97,6 +97,12 @@ export const mochaHooks = { }, }; +// Add ability to register code to run before and after the test code. This is different +// than beforeEach/AfterEach in that the injected code is counted as part of the test. +// It would be good for code that needs to run closer to the test (e.g. tracking timeout). +// Also, error in this code count as error of the test, instead of part of the hook, which +// would avoid rest of tests in the suite being skipped because of the hook error. + let newTestFunction: Mocha.TestFunction | undefined; type BeforeTestFunc = (this: Mocha.Context) => T; From b4e406e0193768aeee66962686600d7899a30cd7 Mon Sep 17 00:00:00 2001 From: Curtis Man Date: Thu, 6 Oct 2022 19:38:31 -0700 Subject: [PATCH 04/12] simplify --- .../test/mocha-test-setup/src/mochaHooks.ts | 75 ++++++---- .../test-utils/src/test/timeoutUtils.spec.ts | 129 +++++++++++++----- packages/test/test-utils/src/timeoutUtils.ts | 118 ++++++++++++---- 3 files changed, 227 insertions(+), 95 deletions(-) diff --git a/packages/test/mocha-test-setup/src/mochaHooks.ts b/packages/test/mocha-test-setup/src/mochaHooks.ts index 511bac47ddb2..ddeae5d080e0 100644 --- a/packages/test/mocha-test-setup/src/mochaHooks.ts +++ b/packages/test/mocha-test-setup/src/mochaHooks.ts @@ -103,55 +103,71 @@ export const mochaHooks = { // Also, error in this code count as error of the test, instead of part of the hook, which // would avoid rest of tests in the suite being skipped because of the hook error. -let newTestFunction: Mocha.TestFunction | undefined; +type AfterTestFunc = (context: Mocha.Context) => void; +type BeforeTestFunc = (context: Mocha.Context) => AfterTestFunc | undefined; + +const testFuncs: BeforeTestFunc[] = []; +function patchAndRunBeforeTestFuncs(context: Mocha.Context, done: Mocha.Done) { + const afterTestFuncs: (AfterTestFunc | undefined)[] = []; + // Run after test funcs when done is call + const newDone = function(err?: any) { + runAfterTestFuncs(context, afterTestFuncs); + done(err); + }; -type BeforeTestFunc = (this: Mocha.Context) => T; -type AfterTestFunc = (this: Mocha.Context, beforeTestResult: T) => void; + // patch the timeout callback; + context.runnable().callback = newDone; -const testFuncs: { beforeTestFunc: BeforeTestFunc; afterTestFunc: AfterTestFunc; }[] = []; -function runBeforeTestFuncs(context: Mocha.Context): any[] { - // eslint-disable-next-line @typescript-eslint/no-unsafe-return - return testFuncs.map((v) => v.beforeTestFunc.call(context)); + // Actually run it and capture the variable + testFuncs.forEach((v) => afterTestFuncs.unshift(v(context))); + return { afterTestFuncs, newDone }; } -function runAfterTestFuncs(context: Mocha.Context, values: any[]) { - for (let index = testFuncs.length - 1; index >= 0; index--) { - testFuncs[index].afterTestFunc.call(context, values[index]); - } +function runAfterTestFuncs(context: Mocha.Context, afterTestFuncs: (AfterTestFunc | undefined)[]) { + afterTestFuncs.forEach((func) => { if (func) { func(context); } }); } + function getWrappedFunction(fn: Mocha.Func | Mocha.AsyncFunc) { if (fn.length > 0) { - return function(this: Mocha.Context, done) { - const values = runBeforeTestFuncs(this); + return function(this: Mocha.Context, done: Mocha.Done) { + // Run before test funcs + const { afterTestFuncs, newDone } = patchAndRunBeforeTestFuncs(this, done); + let success = false; try { - (fn as Mocha.Func).call(this, done); + // call the actual test function + (fn as Mocha.Func).call(this, newDone); + success = true; } finally { - runAfterTestFuncs(this, values); + if (!success) { + // run the after test funcs if there is an exception + runAfterTestFuncs(this, afterTestFuncs); + } } }; } return function(this: Mocha.Context) { - const values = runBeforeTestFuncs(this); + // Run the before test funcs + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const { afterTestFuncs } = patchAndRunBeforeTestFuncs(this, this.runnable().callback!); let ret: PromiseLike | void; try { + // Run the test ret = (fn as Mocha.AsyncFunc).call(this); } finally { - runAfterTestFuncs(this, values); - } - - if (typeof ret?.then === "function") { - // Start the timer again to wait for async - const asyncValues = runBeforeTestFuncs(this); - // Clear the timer if the promise resolves. - // use the id to avoid clearing the end time if it resolves after timing out - const clearFunc = () => { runAfterTestFuncs(this, asyncValues); }; - ret?.then(clearFunc, clearFunc); + if (typeof ret?.then === "function") { + // Wait for the promise to resolve + const clearFunc = () => { runAfterTestFuncs(this, afterTestFuncs); }; + ret?.then(clearFunc, clearFunc); + } else { + runAfterTestFuncs(this, afterTestFuncs); + } } return ret; }; } +let newTestFunction: Mocha.TestFunction | undefined; function setupCustomTestHooks() { const currentTestFunction = globalThis.it; // the function `it` is reassign per test files. Trap it. @@ -173,7 +189,6 @@ function setupCustomTestHooks() { setupCustomTestHooks(); -globalThis.registerMochaTestWrapperFuncs = - function (beforeTestFunc: BeforeTestFunc, afterTestFunc: AfterTestFunc) { - testFuncs.push({ beforeTestFunc, afterTestFunc }); - }; +globalThis.registerMochaTestWrapperFunc = (beforeTestFunc: BeforeTestFunc) => { + testFuncs.push(beforeTestFunc); +}; diff --git a/packages/test/test-utils/src/test/timeoutUtils.spec.ts b/packages/test/test-utils/src/test/timeoutUtils.spec.ts index e670ae0da236..83cea2996006 100644 --- a/packages/test/test-utils/src/test/timeoutUtils.spec.ts +++ b/packages/test/test-utils/src/test/timeoutUtils.spec.ts @@ -4,90 +4,149 @@ */ import { strict as assert } from "assert"; -import { timeoutPromise, defaultTimeoutDurationMs } from "../timeoutUtils"; +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(() => {}); + await timeoutPromise(() => { }); assert(false, "should have timed out"); } catch (e: any) { - assert(e.message.startsWith("Timed out"), "expected timeout error message"); + assert(e.message.startsWith("Test timed out ("), "expected timeout error 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("Timed out"), "expected timeout error message"); + assert(e.message.startsWith("Test timed out ("), "expected timeout error message"); } - }); + }).timeout(25); it("Timeout with duration", async () => { try { await timeoutPromise(() => { }, { durationMs: 1 }); assert(false, "should have timed out"); - } catch { - + } catch (e: any) { + assert(e.message.startsWith("Timed out ("), "expected timeout error message"); } - }); + }).timeout(25); - it("Timeout with zero duration", async () => { + it("No timeout with zero duration", async () => { try { await timeoutPromise((resolve) => { - setTimeout(resolve, defaultTimeoutDurationMs + 50); + setTimeout(resolve, 10); }, { durationMs: 0 }); - } catch { - assert(false, "should not have timed out"); + } catch (e: any) { + assert(false, `should not have timed out: ${e.message}`); } - }); + }).timeout(25); - it("Timeout with negative duration", async () => { + it("No timeout with negative duration", async function() { + // The original 25 timeout will be used by timeoutPromise + this.timeout(100); try { await timeoutPromise((resolve) => { - setTimeout(resolve, defaultTimeoutDurationMs + 50); + setTimeout(resolve, 50); }, { durationMs: -1 }); - } catch { - assert(false, "should not have timed out"); + } catch (e: any) { + assert(false, `should not have timed out: ${e.message}`); } - }); + }).timeout(25); - it("Timeout with Infinity duration", async () => { + it("No timeout with Infinity duration", async function() { + // The original 25 timeout will be used by timeoutPromise + this.timeout(100); try { await timeoutPromise((resolve) => { - setTimeout(resolve, defaultTimeoutDurationMs + 50); + setTimeout(resolve, 50); }, { durationMs: Infinity }); - } catch { - assert(false, "should not have timed out"); + } catch (e: any) { + assert(false, `should not have timed out: ${e.message}`); } - }); + }).timeout(25); - it("no timeout", async () => { + it("No timeout with valid duration", async function() { + // The original 25 timeout will be used by timeoutPromise + this.timeout(100); try { - await timeoutPromise((resolve) => { setTimeout(resolve, 1); }, { durationMs: 100 }); - } catch { - assert(false, "should not have timed out"); + 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() { + // The original 25 timeout will be used by timeoutPromise + 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() { + // The original 25 timeout will be used by timeoutPromise + 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(() => {}, { + const value = await timeoutPromise(() => { }, { durationMs: 1, reject: false, value: 1, }); assert(value === 1, "expect timeout to return value given in option"); - } catch { - assert(false, "should not have timed out"); + } catch (e: any) { + assert(false, `should not have timed out: ${e.message}`); } - }); + }).timeout(25); it("Timeout rejection with error option", async () => { try { - await timeoutPromise(() => {}, { + await timeoutPromise(() => { }, { durationMs: 1, errorMsg: "hello", }); @@ -95,5 +154,5 @@ describe("TimeoutPromise", () => { } 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/timeoutUtils.ts b/packages/test/test-utils/src/timeoutUtils.ts index cee6368d90ea..c530255fc495 100644 --- a/packages/test/test-utils/src/timeoutUtils.ts +++ b/packages/test/test-utils/src/timeoutUtils.ts @@ -6,43 +6,75 @@ import { Container } from "@fluidframework/container-loader"; import { assert } from "@fluidframework/common-utils"; +// @deprecated this value is no longer used export const defaultTimeoutDurationMs = 250; // Patch mocha so we can timeout promises based on how much time is left in the test. -let currentTestEndTimeId = 0; -let currentTestEndTime = 0; // 0 means it is not set, -1 means timeout is disabled, otherwise, it is the test end time -const timeBuffer = 30; // leave 30ms leeway for finish processing +let timeoutPromiseInstance: Promise | undefined; +let testTimeout: number; +const timeBuffer = 15; // leave 15 ms leeway for finish processing -function getCurrentTestTimeout() { - if (currentTestEndTime === -1) { - return -1; - } - if (currentTestEndTime === 0) { - return defaultTimeoutDurationMs; - } - // Even if we are passed our timeout, return 1ms so that we will still wait - return Math.max(currentTestEndTime - Date.now(), 1); -} +function trackTestEndTime(context: Mocha.Context) { + assert(timeoutPromiseInstance === undefined, "Unexpected nested tests detected"); + let timeoutRejection: ((reason?: any) => void) | undefined; + let timer: NodeJS.Timeout; + timeoutPromiseInstance = new Promise((resolve, reject) => timeoutRejection = reject); -function setTestEndTime(this: Mocha.Context) { - const timeout = this.timeout(); - const now = Date.now(); - // Either the test timed out (so the test end time is less then now, or the promise resolved) - assert(currentTestEndTime < now && currentTestEndTime !== -1, "Unexpected nested tests detected"); - const hasTimeout = Number.isFinite(timeout) && timeout > 0; - currentTestEndTime = hasTimeout ? now + timeout - timeBuffer : -1; - return ++currentTestEndTimeId; -} + // Ignore rejection for timeout promise if no one is waiting for it. + timeoutPromiseInstance.catch(() => {}); + + const runnable = context.runnable(); + + // function to reset the timer + const resetTimer = () => { + // clear current timer if there is one + clearTimeout(timer); -function clearTestEndTime(this: Mocha.Context, value: number) { - if (value === currentTestEndTimeId) { - currentTestEndTime = 0; + // Check the test timeout setting + const timeout = context.timeout(); + if (!(Number.isFinite(timeout) && timeout > 0)) { return; } + + // subtract a buffer + testTimeout = Math.max(timeout - timeBuffer, 1); + + // Set up timer to reject near the test timeout. + timer = setTimeout(() => { + if (timeoutRejection) { + timeoutRejection(timeoutPromiseInstance); + timeoutRejection = undefined; + } + }, testTimeout); + }; + + // patching resetTimeout and clearTimeout on the runnable object + // eslint-disable-next-line @typescript-eslint/unbound-method + const oldResetTimeoutFunc = runnable.resetTimeout; + runnable.resetTimeout = function(this: Mocha.Runnable) { + oldResetTimeoutFunc.call(this); + resetTimer(); + }; + // eslint-disable-next-line @typescript-eslint/unbound-method + const oldClearTimeoutFunc = runnable.clearTimeout; + runnable.clearTimeout = function(this: Mocha.Runnable) { + clearTimeout(timer); + oldClearTimeoutFunc.call(this); + }; + + if (runnable.timer !== undefined) { + // set up the timer is already started + resetTimer(); } + + // clean up after the test is done + return (c: Mocha.Context) => { + timeoutPromiseInstance = undefined; + clearTimeout(timer); + }; } // only register if we are running with mocha-test-setup loaded -if (globalThis.registerMochaTestWrapperFuncs !== undefined) { - globalThis.registerMochaTestWrapperFuncs(setTestEndTime, clearTestEndTime); +if (globalThis.registerMochaTestWrapperFunc !== undefined) { + globalThis.registerMochaTestWrapperFunc(trackTestEndTime); } export interface TimeoutWithError { @@ -75,11 +107,12 @@ 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 ?? getCurrentTestTimeout(); +) { + const timeout = timeoutOptions.durationMs ?? 0; if (timeout <= 0 || !Number.isFinite(timeout)) { return new Promise(executor); } @@ -104,3 +137,28 @@ 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 ?? "Test timed out"} (${testTimeout}ms)`); + const executorPromise = getTimeoutPromise(executor, timeoutOptions); + if (timeoutPromiseInstance === undefined) { return executorPromise; } + return Promise.race([executorPromise, timeoutPromiseInstance]).catch((e) => { + if (e === timeoutPromiseInstance) { + if (timeoutOptions.reject !== false) { + // If the rejection is because of the timeout then + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + throw err!; + } + return timeoutOptions.value; + } + throw e; + }) as Promise; +} From 6140f830a150745951614f019c121e4f3a55c273 Mon Sep 17 00:00:00 2001 From: Curtis Man Date: Fri, 7 Oct 2022 21:37:19 -0700 Subject: [PATCH 05/12] update comment --- packages/test/test-utils/src/timeoutUtils.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/test/test-utils/src/timeoutUtils.ts b/packages/test/test-utils/src/timeoutUtils.ts index c530255fc495..f1cf620bddc4 100644 --- a/packages/test/test-utils/src/timeoutUtils.ts +++ b/packages/test/test-utils/src/timeoutUtils.ts @@ -78,17 +78,17 @@ if (globalThis.registerMochaTestWrapperFunc !== undefined) { } export interface TimeoutWithError { - // Timeout duration in milliseconds. - // If it is undefined, then the default is 250. - // If it is <= 0 or infinity, then there is no timeout + // Timeout duration in milliseconds, if it is > 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 undefined, then the default is 250. - // If it is <= 0 or infinity, then there is no timeout + // Timeout duration in milliseconds, if it is > 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; From 78520076d1a1dc475d057bd0d59c7b095fe83476 Mon Sep 17 00:00:00 2001 From: Curtis Man Date: Sun, 9 Oct 2022 15:08:43 -0700 Subject: [PATCH 06/12] Patch prototype --- api-report/mocha-test-setup.api.md | 5 +- .../test/mocha-test-setup/src/mochaHooks.ts | 45 +++--- packages/test/test-utils/src/timeoutUtils.ts | 136 +++++++++++------- 3 files changed, 104 insertions(+), 82 deletions(-) diff --git a/api-report/mocha-test-setup.api.md b/api-report/mocha-test-setup.api.md index ed47a710abd9..ebbf6bc76d66 100644 --- a/api-report/mocha-test-setup.api.md +++ b/api-report/mocha-test-setup.api.md @@ -7,11 +7,10 @@ // @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/packages/test/mocha-test-setup/src/mochaHooks.ts b/packages/test/mocha-test-setup/src/mochaHooks.ts index ddeae5d080e0..88ed0ac9e9d2 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, }); @@ -167,28 +165,19 @@ function getWrappedFunction(fn: Mocha.Func | Mocha.AsyncFunc) { }; } -let newTestFunction: Mocha.TestFunction | undefined; function setupCustomTestHooks() { - const currentTestFunction = globalThis.it; - // the function `it` is reassign per test files. Trap it. - Object.defineProperty(globalThis, "it", { - get: () => { return newTestFunction; }, - set: (oldTestFunction: Mocha.TestFunction | undefined) => { - if (oldTestFunction === undefined) { newTestFunction = undefined; return; } - newTestFunction = ((title: string, fn?: Mocha.Func | Mocha.AsyncFunc) => { - return oldTestFunction(title, fn && typeof fn.call === "function" ? - getWrappedFunction(fn) - : fn); - }) as Mocha.TestFunction; - newTestFunction.skip = oldTestFunction.skip; - newTestFunction.only = oldTestFunction.only; - }, - }); - globalThis.it = currentTestFunction; + // eslint-disable-next-line @typescript-eslint/unbound-method + const oldAddTest = mochaModule.Suite.prototype.addTest; + mochaModule.Suite.prototype.addTest = function(test: Mocha.Test) { + if (test.fn && typeof test.fn.call === "function") { + test.fn = getWrappedFunction(test.fn); + } + return oldAddTest.call(this, test); + }; } setupCustomTestHooks(); -globalThis.registerMochaTestWrapperFunc = (beforeTestFunc: BeforeTestFunc) => { - testFuncs.push(beforeTestFunc); +globalThis.getMochaModule = () => { + return mochaModule; }; diff --git a/packages/test/test-utils/src/timeoutUtils.ts b/packages/test/test-utils/src/timeoutUtils.ts index f1cf620bddc4..0d305ea65bc7 100644 --- a/packages/test/test-utils/src/timeoutUtils.ts +++ b/packages/test/test-utils/src/timeoutUtils.ts @@ -9,72 +9,101 @@ import { assert } from "@fluidframework/common-utils"; // @deprecated this value is no longer used export const defaultTimeoutDurationMs = 250; -// Patch mocha so we can timeout promises based on how much time is left in the test. -let timeoutPromiseInstance: Promise | undefined; -let testTimeout: number; +// 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. + +type PromiseRejectFunc = (reason?: any) => void; const timeBuffer = 15; // leave 15 ms leeway for finish processing -function trackTestEndTime(context: Mocha.Context) { - assert(timeoutPromiseInstance === undefined, "Unexpected nested tests detected"); - let timeoutRejection: ((reason?: any) => void) | undefined; - let timer: NodeJS.Timeout; - timeoutPromiseInstance = new Promise((resolve, reject) => timeoutRejection = reject); +class TestTimeout { + private timeout: number = 0; + private timer: NodeJS.Timeout | undefined; + private reject: PromiseRejectFunc = () => { }; + private readonly promise: Promise; + private rejected = false; + + private static instance: TestTimeout; + public static initialize() { + TestTimeout.instance = new TestTimeout(); + } + public static reset(runnable: Mocha.Runnable) { + TestTimeout.clear(); + TestTimeout.instance.resetTimer(runnable); + } - // Ignore rejection for timeout promise if no one is waiting for it. - timeoutPromiseInstance.catch(() => {}); + public static clear() { + if (TestTimeout.instance.rejected) { + TestTimeout.instance = new TestTimeout(); + } else { + TestTimeout.instance.clearTimer(); + } + } - const runnable = context.runnable(); + public static getInstance() { + return TestTimeout.instance; + } - // function to reset the timer - const resetTimer = () => { - // clear current timer if there is one - clearTimeout(timer); + public async getPromise() { + return this.promise; + } + + public getTimeout() { + return this.timeout; + } + + private constructor() { + this.promise + = new Promise((resolve, reject: PromiseRejectFunc) => { this.reject = reject; }); + // Ignore rejection for timeout promise if no one is waiting for it. + this.promise.catch(() => { }); + } + + private resetTimer(runnable: Mocha.Runnable) { + assert(!this.timer, "clearTimer should have been called before reset"); + assert(!this.rejected, "can't reset a rejected TestTimeout"); // Check the test timeout setting - const timeout = context.timeout(); + const timeout = runnable.timeout(); if (!(Number.isFinite(timeout) && timeout > 0)) { return; } // subtract a buffer - testTimeout = Math.max(timeout - timeBuffer, 1); + this.timeout = Math.max(timeout - timeBuffer, 1); // Set up timer to reject near the test timeout. - timer = setTimeout(() => { - if (timeoutRejection) { - timeoutRejection(timeoutPromiseInstance); - timeoutRejection = undefined; - } - }, testTimeout); - }; + this.timer = setTimeout(() => { + this.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) { + TestTimeout.initialize(); // 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 = runnable.resetTimeout; - runnable.resetTimeout = function(this: Mocha.Runnable) { + const oldResetTimeoutFunc = runnablePrototype.resetTimeout; + runnablePrototype.resetTimeout = function(this: Mocha.Runnable) { oldResetTimeoutFunc.call(this); - resetTimer(); + TestTimeout.reset(this); }; // eslint-disable-next-line @typescript-eslint/unbound-method - const oldClearTimeoutFunc = runnable.clearTimeout; - runnable.clearTimeout = function(this: Mocha.Runnable) { - clearTimeout(timer); + const oldClearTimeoutFunc = runnablePrototype.clearTimeout; + runnablePrototype.clearTimeout = function(this: Mocha.Runnable) { + TestTimeout.clear(); oldClearTimeoutFunc.call(this); }; - - if (runnable.timer !== undefined) { - // set up the timer is already started - resetTimer(); - } - - // clean up after the test is done - return (c: Mocha.Context) => { - timeoutPromiseInstance = undefined; - clearTimeout(timer); - }; -} - -// only register if we are running with mocha-test-setup loaded -if (globalThis.registerMochaTestWrapperFunc !== undefined) { - globalThis.registerMochaTestWrapperFunc(trackTestEndTime); } export interface TimeoutWithError { @@ -147,15 +176,20 @@ export async function timeoutPromise( // the original call site, this makes it easier to debug const err = timeoutOptions.reject === false ? undefined - : new Error(`${timeoutOptions.errorMsg ?? "Test timed out"} (${testTimeout}ms)`); + : new Error(timeoutOptions.errorMsg ?? "Test timed out"); const executorPromise = getTimeoutPromise(executor, timeoutOptions); - if (timeoutPromiseInstance === undefined) { return executorPromise; } - return Promise.race([executorPromise, timeoutPromiseInstance]).catch((e) => { - if (e === timeoutPromiseInstance) { + + 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 - throw err!; + const errorObject = err!; + errorObject.message = `${errorObject.message} (${currentTestTimeout.getTimeout()}ms)`; + throw errorObject; } return timeoutOptions.value; } From b19e04f4a0ab6673d0570bf850174fcfad262020 Mon Sep 17 00:00:00 2001 From: Curtis Man Date: Sun, 9 Oct 2022 15:40:37 -0700 Subject: [PATCH 07/12] Remove unnecessary code --- .../test/mocha-test-setup/src/mochaHooks.ts | 83 ------------------- .../test-version-utils/src/versionUtils.ts | 15 ++-- 2 files changed, 9 insertions(+), 89 deletions(-) diff --git a/packages/test/mocha-test-setup/src/mochaHooks.ts b/packages/test/mocha-test-setup/src/mochaHooks.ts index 88ed0ac9e9d2..3e730c525447 100644 --- a/packages/test/mocha-test-setup/src/mochaHooks.ts +++ b/packages/test/mocha-test-setup/src/mochaHooks.ts @@ -95,89 +95,6 @@ export const mochaHooks = { }, }; -// Add ability to register code to run before and after the test code. This is different -// than beforeEach/AfterEach in that the injected code is counted as part of the test. -// It would be good for code that needs to run closer to the test (e.g. tracking timeout). -// Also, error in this code count as error of the test, instead of part of the hook, which -// would avoid rest of tests in the suite being skipped because of the hook error. - -type AfterTestFunc = (context: Mocha.Context) => void; -type BeforeTestFunc = (context: Mocha.Context) => AfterTestFunc | undefined; - -const testFuncs: BeforeTestFunc[] = []; -function patchAndRunBeforeTestFuncs(context: Mocha.Context, done: Mocha.Done) { - const afterTestFuncs: (AfterTestFunc | undefined)[] = []; - // Run after test funcs when done is call - const newDone = function(err?: any) { - runAfterTestFuncs(context, afterTestFuncs); - done(err); - }; - - // patch the timeout callback; - context.runnable().callback = newDone; - - // Actually run it and capture the variable - testFuncs.forEach((v) => afterTestFuncs.unshift(v(context))); - return { afterTestFuncs, newDone }; -} - -function runAfterTestFuncs(context: Mocha.Context, afterTestFuncs: (AfterTestFunc | undefined)[]) { - afterTestFuncs.forEach((func) => { if (func) { func(context); } }); -} - -function getWrappedFunction(fn: Mocha.Func | Mocha.AsyncFunc) { - if (fn.length > 0) { - return function(this: Mocha.Context, done: Mocha.Done) { - // Run before test funcs - const { afterTestFuncs, newDone } = patchAndRunBeforeTestFuncs(this, done); - let success = false; - try { - // call the actual test function - (fn as Mocha.Func).call(this, newDone); - success = true; - } finally { - if (!success) { - // run the after test funcs if there is an exception - runAfterTestFuncs(this, afterTestFuncs); - } - } - }; - } - return function(this: Mocha.Context) { - // Run the before test funcs - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const { afterTestFuncs } = patchAndRunBeforeTestFuncs(this, this.runnable().callback!); - - let ret: PromiseLike | void; - try { - // Run the test - ret = (fn as Mocha.AsyncFunc).call(this); - } finally { - if (typeof ret?.then === "function") { - // Wait for the promise to resolve - const clearFunc = () => { runAfterTestFuncs(this, afterTestFuncs); }; - ret?.then(clearFunc, clearFunc); - } else { - runAfterTestFuncs(this, afterTestFuncs); - } - } - return ret; - }; -} - -function setupCustomTestHooks() { - // eslint-disable-next-line @typescript-eslint/unbound-method - const oldAddTest = mochaModule.Suite.prototype.addTest; - mochaModule.Suite.prototype.addTest = function(test: Mocha.Test) { - if (test.fn && typeof test.fn.call === "function") { - test.fn = getWrappedFunction(test.fn); - } - return oldAddTest.call(this, test); - }; -} - -setupCustomTestHooks(); - globalThis.getMochaModule = () => { return mochaModule; }; 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}`); } } From 455122dabc32708e977af4a00d3806c32cba2979 Mon Sep 17 00:00:00 2001 From: Curtis Man Date: Tue, 11 Oct 2022 15:22:53 -0700 Subject: [PATCH 08/12] use deferred --- .../test/test-utils/src/TestSummaryUtils.ts | 7 ++++--- packages/test/test-utils/src/timeoutUtils.ts | 17 +++++++---------- 2 files changed, 11 insertions(+), 13 deletions(-) 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/timeoutUtils.ts b/packages/test/test-utils/src/timeoutUtils.ts index 0d305ea65bc7..22a9466d8910 100644 --- a/packages/test/test-utils/src/timeoutUtils.ts +++ b/packages/test/test-utils/src/timeoutUtils.ts @@ -4,7 +4,7 @@ */ import { Container } from "@fluidframework/container-loader"; -import { assert } from "@fluidframework/common-utils"; +import { assert, Deferred } from "@fluidframework/common-utils"; // @deprecated this value is no longer used export const defaultTimeoutDurationMs = 250; @@ -13,14 +13,12 @@ export const defaultTimeoutDurationMs = 250; // 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. -type PromiseRejectFunc = (reason?: any) => void; const timeBuffer = 15; // leave 15 ms leeway for finish processing class TestTimeout { private timeout: number = 0; private timer: NodeJS.Timeout | undefined; - private reject: PromiseRejectFunc = () => { }; - private readonly promise: Promise; + private readonly deferred: Deferred; private rejected = false; private static instance: TestTimeout; @@ -45,7 +43,7 @@ class TestTimeout { } public async getPromise() { - return this.promise; + return this.deferred.promise; } public getTimeout() { @@ -53,15 +51,14 @@ class TestTimeout { } private constructor() { - this.promise - = new Promise((resolve, reject: PromiseRejectFunc) => { this.reject = reject; }); + this.deferred = new Deferred(); // Ignore rejection for timeout promise if no one is waiting for it. - this.promise.catch(() => { }); + this.deferred.promise.catch(() => { }); } private resetTimer(runnable: Mocha.Runnable) { assert(!this.timer, "clearTimer should have been called before reset"); - assert(!this.rejected, "can't reset a rejected TestTimeout"); + assert(!this.deferred.isCompleted, "can't reset a completed TestTimeout"); // Check the test timeout setting const timeout = runnable.timeout(); @@ -72,7 +69,7 @@ class TestTimeout { // Set up timer to reject near the test timeout. this.timer = setTimeout(() => { - this.reject(this); + this.deferred.reject(this); this.rejected = true; }, this.timeout); } From 40f3e0de6a281478833383a75194336a9e13bd07 Mon Sep 17 00:00:00 2001 From: Curtis Man Date: Thu, 13 Oct 2022 11:21:10 -0700 Subject: [PATCH 09/12] update api-report --- api-report/mocha-test-setup.api.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/api-report/mocha-test-setup.api.md b/api-report/mocha-test-setup.api.md index ebbf6bc76d66..8e1540c2fe41 100644 --- a/api-report/mocha-test-setup.api.md +++ b/api-report/mocha-test-setup.api.md @@ -4,6 +4,8 @@ ```ts +/// + // @public (undocumented) export const mochaHooks: { beforeAll(): void; From e0d0caa3ee1c0b9f68ac95b8193cbbb5f526e053 Mon Sep 17 00:00:00 2001 From: Curtis Man Date: Thu, 13 Oct 2022 14:25:40 -0700 Subject: [PATCH 10/12] Revert lock files --- lerna-package-lock.json | 62 ++++++++++++++++++++--------------------- package-lock.json | 34 +++++++++++----------- 2 files changed, 48 insertions(+), 48 deletions(-) diff --git a/lerna-package-lock.json b/lerna-package-lock.json index 02385eb10e83..6d367591fae6 100644 --- a/lerna-package-lock.json +++ b/lerna-package-lock.json @@ -33041,7 +33041,7 @@ "color-name": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" }, "color-string": { "version": "1.9.1", @@ -33240,7 +33240,7 @@ "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" }, "concat-stream": { "version": "1.6.2", @@ -36365,7 +36365,7 @@ "escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==" }, "escodegen": { "version": "2.0.0", @@ -38451,7 +38451,7 @@ "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" }, "fsevents": { "version": "2.3.2", @@ -39474,7 +39474,7 @@ "has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==" }, "has-glob": { "version": "1.0.0", @@ -40423,7 +40423,7 @@ "inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", "requires": { "once": "^1.3.0", "wrappy": "1" @@ -41133,7 +41133,7 @@ "is-plain-obj": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", - "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=" + "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==" }, "is-plain-object": { "version": "2.0.4", @@ -41204,7 +41204,7 @@ "is-stream": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=" + "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==" }, "is-string": { "version": "1.0.7", @@ -41325,7 +41325,7 @@ "isarray": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==" }, "isemail": { "version": "3.2.0", @@ -41338,7 +41338,7 @@ "isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" }, "isobject": { "version": "3.0.1", @@ -43127,7 +43127,7 @@ "jju": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/jju/-/jju-1.4.0.tgz", - "integrity": "sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==" + "integrity": "sha1-o6vicYryQaKykE+EpiWXDzia4yo=" }, "jmespath": { "version": "0.15.0", @@ -44910,7 +44910,7 @@ "lodash.get": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", - "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==" + "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=" }, "lodash.groupby": { "version": "4.6.0", @@ -44935,7 +44935,7 @@ "lodash.isequal": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", - "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==" + "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=" }, "lodash.isfunction": { "version": "3.0.9", @@ -49113,7 +49113,7 @@ "npm-run-path": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", - "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", + "integrity": "sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==", "requires": { "path-key": "^2.0.0" } @@ -50065,7 +50065,7 @@ "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", "requires": { "wrappy": "1" } @@ -51030,7 +51030,7 @@ "parse-json": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", + "integrity": "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==", "requires": { "error-ex": "^1.3.1", "json-parse-better-errors": "^1.0.1" @@ -51194,7 +51194,7 @@ "path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==" + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" }, "path-is-inside": { "version": "1.0.2", @@ -53282,7 +53282,7 @@ "read-pkg-up": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-3.0.0.tgz", - "integrity": "sha1-PtSWaF26D4/hGNBpHcUfSh/5bwc=", + "integrity": "sha512-YFzFrVvpC6frF1sz8psoHDBGF7fLPc+llq/8NB43oagqWkx8ar5zYtsTORtOjw9W2RHLpWP+zTWwBvf1bCmcSw==", "dev": true, "requires": { "find-up": "^2.0.0", @@ -53292,7 +53292,7 @@ "find-up": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", - "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "integrity": "sha512-NWzkk0jSJtTt08+FBFMvXoeZnOJD+jTtsRmBYbAIzJdX6l7dLgR7CTubCM5/eDdPUBvLCeVasP1brfVR/9/EZQ==", "dev": true, "requires": { "locate-path": "^2.0.0" @@ -53313,7 +53313,7 @@ "locate-path": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", - "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "integrity": "sha512-NCI2kiDkyR7VeEKm27Kda/iQHyKJe1Bu0FlTbYp3CqJu+9IFe9bLyAjMxf5ZDDbEg+iMPzB5zYyUTSm8wVTKmA==", "dev": true, "requires": { "p-locate": "^2.0.0", @@ -53332,7 +53332,7 @@ "p-locate": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", - "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "integrity": "sha512-nQja7m7gSKuewoVRen45CtVfODR3crN3goVQ0DDZ9N3yHxgpkuBhZqsaiotSQRrADUrne346peY7kT3TSACykg==", "dev": true, "requires": { "p-limit": "^1.1.0" @@ -53341,7 +53341,7 @@ "p-try": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", - "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", + "integrity": "sha512-U1etNYuMJoIz3ZXSrrySFjsXQTWOx2/jdi86L+2pRvph/qMKL6sbcCYdH23fqsbm8TH2Gn0OybpT4eSFlCVHww==", "dev": true }, "read-pkg": { @@ -53360,7 +53360,7 @@ "readable-stream": { "version": "1.0.34", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", - "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", + "integrity": "sha512-ok1qVCJuRkNmvebYikljxJA/UEsKwLl2nI1OmaqAu4/UE+h0wKCHok4XkL/gvi39OacXvw59RJUOFUkDib2rHg==", "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.1", @@ -54178,7 +54178,7 @@ "require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==" + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" }, "require-from-string": { "version": "2.0.2", @@ -55565,7 +55565,7 @@ "spawn-command": { "version": "0.0.2-1", "resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2-1.tgz", - "integrity": "sha512-n98l9E2RMSJ9ON1AKisHzz7V42VDiBQGY6PB1BwRglz99wpVsSuGzQ+jOi6lFXBGVTCrRpltvjm+/XA+tpeJrg==", + "integrity": "sha1-YvXpRmmBwbeW3Fkpk34RycaSG9A=", "dev": true }, "spawn-wrap": { @@ -55788,7 +55788,7 @@ "sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==" + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" }, "sshpk": { "version": "1.16.1", @@ -56767,7 +56767,7 @@ "string_decoder": { "version": "0.10.31", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==" }, "strip-ansi": { "version": "4.0.0", @@ -56787,7 +56787,7 @@ "strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=" + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==" }, "strip-eof": { "version": "1.0.0", @@ -58017,7 +58017,7 @@ "timsort": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz", - "integrity": "sha512-qsdtZH+vMoCARQtyod4imc2nIJwg9Cc7lPRrw9CzF8ZKR0khdr8+2nX80PBhET3tcyTtJDxAffGh2rXH4tyU8A==", + "integrity": "sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=", "dev": true }, "tiny-warning": { @@ -58777,7 +58777,7 @@ "tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", "requires": { "safe-buffer": "^5.0.1" } @@ -61270,7 +61270,7 @@ "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, "write-file-atomic": { "version": "2.4.3", diff --git a/package-lock.json b/package-lock.json index a6571fa1c0a3..701d93584691 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4236,7 +4236,7 @@ "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", "dev": true }, "concat-stream": { @@ -5691,7 +5691,7 @@ "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", "dev": true }, "fsevents": { @@ -6319,7 +6319,7 @@ "inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", "dev": true, "requires": { "once": "^1.3.0", @@ -6690,7 +6690,7 @@ "isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", "dev": true }, "isobject": { @@ -6805,7 +6805,7 @@ "jju": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/jju/-/jju-1.4.0.tgz", - "integrity": "sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==", + "integrity": "sha1-o6vicYryQaKykE+EpiWXDzia4yo=", "dev": true }, "js-tokens": { @@ -7271,7 +7271,7 @@ "lodash.get": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", - "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=", "dev": true }, "lodash.includes": { @@ -7289,7 +7289,7 @@ "lodash.isequal": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", - "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", + "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=", "dev": true }, "lodash.isinteger": { @@ -8570,7 +8570,7 @@ "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", "dev": true, "requires": { "wrappy": "1" @@ -8898,7 +8898,7 @@ "parse-json": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", + "integrity": "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==", "dev": true, "requires": { "error-ex": "^1.3.1", @@ -8965,7 +8965,7 @@ "path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", "dev": true }, "path-key": { @@ -9524,7 +9524,7 @@ "require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", "dev": true }, "require-from-string": { @@ -9924,7 +9924,7 @@ "spawn-command": { "version": "0.0.2-1", "resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2-1.tgz", - "integrity": "sha512-n98l9E2RMSJ9ON1AKisHzz7V42VDiBQGY6PB1BwRglz99wpVsSuGzQ+jOi6lFXBGVTCrRpltvjm+/XA+tpeJrg==", + "integrity": "sha1-YvXpRmmBwbeW3Fkpk34RycaSG9A=", "dev": true }, "spdx-correct": { @@ -9999,7 +9999,7 @@ "sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", "dev": true }, "ssri": { @@ -10065,7 +10065,7 @@ "strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", "dev": true }, "strip-eof": { @@ -10276,7 +10276,7 @@ "timsort": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz", - "integrity": "sha512-qsdtZH+vMoCARQtyod4imc2nIJwg9Cc7lPRrw9CzF8ZKR0khdr8+2nX80PBhET3tcyTtJDxAffGh2rXH4tyU8A==", + "integrity": "sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=", "dev": true }, "tmp": { @@ -10370,7 +10370,7 @@ "tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", "dev": true, "requires": { "safe-buffer": "^5.0.1" @@ -10798,7 +10798,7 @@ "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", "dev": true }, "write-file-atomic": { From 962a8bcefcbb8ab954d7b54ee8768c40f972ac0c Mon Sep 17 00:00:00 2001 From: Curtis Man Date: Fri, 14 Oct 2022 16:27:53 -0700 Subject: [PATCH 11/12] PR feedback --- api-report/test-utils.api.md | 2 -- packages/test/test-utils/src/timeoutUtils.ts | 35 +++++++++----------- 2 files changed, 15 insertions(+), 22 deletions(-) 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/packages/test/test-utils/src/timeoutUtils.ts b/packages/test/test-utils/src/timeoutUtils.ts index 22a9466d8910..75884be6cdae 100644 --- a/packages/test/test-utils/src/timeoutUtils.ts +++ b/packages/test/test-utils/src/timeoutUtils.ts @@ -21,10 +21,7 @@ class TestTimeout { private readonly deferred: Deferred; private rejected = false; - private static instance: TestTimeout; - public static initialize() { - TestTimeout.instance = new TestTimeout(); - } + private static instance: TestTimeout = new TestTimeout(); public static reset(runnable: Mocha.Runnable) { TestTimeout.clear(); TestTimeout.instance.resetTimer(runnable); @@ -83,8 +80,6 @@ class TestTimeout { // only register if we are running with mocha-test-setup loaded if (globalThis.getMochaModule !== undefined) { - TestTimeout.initialize(); - // patching resetTimeout and clearTimeout on the runnable object // so we can track when test timeout are enforced const mochaModule = globalThis.getMochaModule() as typeof Mocha; @@ -104,17 +99,21 @@ if (globalThis.getMochaModule !== undefined) { } export interface TimeoutWithError { - // Timeout duration in milliseconds, if it is > 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 + /** + * 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 > 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 + /** + * 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; @@ -136,17 +135,13 @@ export async function ensureContainerConnected(container: Container): Promise( executor: (resolve: (value: T | PromiseLike) => void, reject: (reason?: any) => void) => void, - timeoutOptions: TimeoutWithError | TimeoutWithValue = {}, + timeoutOptions: TimeoutWithError | TimeoutWithValue, + err: Error | undefined, ) { const timeout = timeoutOptions.durationMs ?? 0; if (timeout <= 0 || !Number.isFinite(timeout)) { return new Promise(executor); } - // 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)`); return new Promise((resolve, reject) => { const timer = setTimeout( () => timeoutOptions.reject === false ? resolve(timeoutOptions.value) : reject(err), @@ -173,8 +168,8 @@ export async function timeoutPromise( // the original call site, this makes it easier to debug const err = timeoutOptions.reject === false ? undefined - : new Error(timeoutOptions.errorMsg ?? "Test timed out"); - const executorPromise = getTimeoutPromise(executor, timeoutOptions); + : new Error(timeoutOptions.errorMsg ?? "Timed out"); + const executorPromise = getTimeoutPromise(executor, timeoutOptions, err); const currentTestTimeout = TestTimeout.getInstance(); if (currentTestTimeout === undefined) { return executorPromise; } From cd4379f9af854e391092995244a221a823f6c234 Mon Sep 17 00:00:00 2001 From: Curtis Man Date: Sat, 15 Oct 2022 14:21:48 -0700 Subject: [PATCH 12/12] Fix test message --- .../test-utils/src/test/timeoutUtils.spec.ts | 16 ++++++++-------- packages/test/test-utils/src/timeoutUtils.ts | 12 ++++++++++-- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/packages/test/test-utils/src/test/timeoutUtils.spec.ts b/packages/test/test-utils/src/test/timeoutUtils.spec.ts index 83cea2996006..349d73337b3d 100644 --- a/packages/test/test-utils/src/test/timeoutUtils.spec.ts +++ b/packages/test/test-utils/src/test/timeoutUtils.spec.ts @@ -43,7 +43,7 @@ describe("TimeoutPromise", () => { await timeoutPromise(() => { }); assert(false, "should have timed out"); } catch (e: any) { - assert(e.message.startsWith("Test timed out ("), "expected timeout error message"); + assert(e.message.startsWith("Test timed out ("), `expected timeout error message: got ${e.message}`); } }).timeout(25); @@ -52,7 +52,7 @@ describe("TimeoutPromise", () => { await timeoutPromise(() => { }, {}); assert(false, "should have timed out"); } catch (e: any) { - assert(e.message.startsWith("Test timed out ("), "expected timeout error message"); + assert(e.message.startsWith("Test timed out ("), `expected timeout error message: got ${e.message}`); } }).timeout(25); @@ -61,7 +61,7 @@ describe("TimeoutPromise", () => { await timeoutPromise(() => { }, { durationMs: 1 }); assert(false, "should have timed out"); } catch (e: any) { - assert(e.message.startsWith("Timed out ("), "expected timeout error message"); + assert(e.message.startsWith("Timed out ("), `expected timeout error message: got ${e.message}`); } }).timeout(25); @@ -76,7 +76,7 @@ describe("TimeoutPromise", () => { }).timeout(25); it("No timeout with negative duration", async function() { - // The original 25 timeout will be used by timeoutPromise + // Make sure resetTimeout in the test works this.timeout(100); try { await timeoutPromise((resolve) => { @@ -88,7 +88,7 @@ describe("TimeoutPromise", () => { }).timeout(25); it("No timeout with Infinity duration", async function() { - // The original 25 timeout will be used by timeoutPromise + // Make sure resetTimeout in the test works this.timeout(100); try { await timeoutPromise((resolve) => { @@ -100,7 +100,7 @@ describe("TimeoutPromise", () => { }).timeout(25); it("No timeout with valid duration", async function() { - // The original 25 timeout will be used by timeoutPromise + // Make sure resetTimeout in the test works this.timeout(100); try { await timeoutPromise((resolve) => { setTimeout(resolve, 50); }, { durationMs: 75 }); @@ -110,7 +110,7 @@ describe("TimeoutPromise", () => { }).timeout(25); it("No timeout with throw", async function() { - // The original 25 timeout will be used by timeoutPromise + // Make sure resetTimeout in the test works this.timeout(100); try { await timeoutPromise((resolve, reject) => { reject(new Error("blah")); }); @@ -121,7 +121,7 @@ describe("TimeoutPromise", () => { }).timeout(25); it("Timeout with valid duration", async function() { - // The original 25 timeout will be used by timeoutPromise + // Make sure resetTimeout in the test works this.timeout(100); try { await timeoutPromise((resolve) => { setTimeout(resolve, 75); }, { durationMs: 50 }); diff --git a/packages/test/test-utils/src/timeoutUtils.ts b/packages/test/test-utils/src/timeoutUtils.ts index 75884be6cdae..bc4dd86a18b2 100644 --- a/packages/test/test-utils/src/timeoutUtils.ts +++ b/packages/test/test-utils/src/timeoutUtils.ts @@ -142,9 +142,16 @@ async function getTimeoutPromise( 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( @@ -180,7 +187,8 @@ export async function timeoutPromise( // If the rejection is because of the timeout then // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const errorObject = err!; - errorObject.message = `${errorObject.message} (${currentTestTimeout.getTimeout()}ms)`; + errorObject.message = + `${timeoutOptions.errorMsg ?? "Test timed out"} (${currentTestTimeout.getTimeout()}ms)`; throw errorObject; } return timeoutOptions.value;