Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support rebasing ops for testing. Part 1, the mock. #16163

Closed
wants to merge 39 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
221137f
Add mock for rebasing
andre4i Jun 27, 2023
c24f3e8
Add rebasing
andre4i Jun 27, 2023
e31d309
Remove empty function
andre4i Jun 27, 2023
c62d1d4
Update docs
andre4i Jun 27, 2023
748631d
Small test for map
andre4i Jun 27, 2023
309fb58
Update comment
andre4i Jun 27, 2023
8e8bf82
Update docs, override factory methods
andre4i Jun 27, 2023
b3541be
Better encapsulation
andre4i Jun 27, 2023
52f6329
Better encapsulation
andre4i Jun 27, 2023
f7ec14e
Rename test
andre4i Jun 27, 2023
140ce54
Standalone
andre4i Jun 28, 2023
9525c37
Merge branch 'main' into rebase-consistency-test-harness
andre4i Jun 28, 2023
a349de8
Track how many times the message is submitted
andre4i Jun 28, 2023
3eb38b3
Update comment
andre4i Jun 28, 2023
0f6f330
Updated docs
andre4i Jun 28, 2023
c22db7c
Merge branch 'main' into rebase-consistency-test-harness
andre4i Jun 29, 2023
2595ef9
Add some comments
andre4i Jun 29, 2023
2345a19
More subtle message processing in the test
andre4i Jun 29, 2023
e7f183b
Replay op test
andre4i Jun 30, 2023
b381f26
Fix resubmission
andre4i Jun 30, 2023
0bb980a
Adjust test
andre4i Jun 30, 2023
59984e5
Resubmit instead of submit
andre4i Jun 30, 2023
b6b7c2a
Small adjustments
andre4i Jun 30, 2023
dbff9a9
Update docs, mocks, etc.
andre4i Jun 30, 2023
81a64a1
Fix undefined reference
andre4i Jun 30, 2023
5031292
Fix slice
andre4i Jul 5, 2023
5579681
Rebase directly
andre4i Jul 5, 2023
8544f04
Minor adjustments
andre4i Jul 5, 2023
0a04870
Merge branch 'main' into rebase-consistency-test-harness
andre4i Jul 5, 2023
c6be0d9
Resubmit mock - working
andre4i Jul 5, 2023
35586b0
Fix map rebasing test
andre4i Jul 5, 2023
14b3fd8
Add onchange event handler
andre4i Jul 5, 2023
980360d
Simplify
andre4i Jul 5, 2023
90922d9
Make the scenario more interesting
andre4i Jul 5, 2023
7b5dde6
Don't resubmit twice
andre4i Jul 5, 2023
12795b1
Add test for shareddirectory
andre4i Jul 5, 2023
155f56d
Fix test
andre4i Jul 5, 2023
9dee177
Merge branch 'main' into rebase-consistency-test-harness
andre4i Jul 18, 2023
a69c719
Add assertions to ensure we're using the proper versions
andre4i Jul 18, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 36 additions & 1 deletion api-report/test-runtime-utils.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,8 @@ export class MockContainerRuntime {
// (undocumented)
process(message: ISequencedDocumentMessage): void;
// (undocumented)
protected get referenceSequenceNumber(): number;
// (undocumented)
submit(messageContent: any, localOpMetadata: unknown): number;
}

Expand Down Expand Up @@ -142,6 +144,20 @@ export class MockContainerRuntimeFactory {
sequenceNumber: number;
}

// @public
export class MockContainerRuntimeFactoryForRebasing extends MockContainerRuntimeFactory {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no idea if we have a place for it, but it would be nice to not publicly export this, as it increases our compat burden. this is especially bad as consumers do use this package, but it is not well factored/designed.

// (undocumented)
createContainerRuntime(dataStoreRuntime: MockFluidDataStoreRuntime, overrides?: {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just create() is good :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From MockContainerRuntimeFactory

minimumSequenceNumber?: number;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While I can see why you might want to create shape like that for last arg, I'd rather switch to this form when it's needed. Optional inside of optional is not great design pattern :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is all existing code and I agree it can be refactored into a better shape. However, I don't believe this change should address that.

}): MockContainerRuntimeForRebasing;
// (undocumented)
processAllMessages(): void;
// (undocumented)
processOneMessage(): void;
// (undocumented)
processSomeMessages(count: number): void;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you merge these 3 APIs into one? with count being optional (meaning all)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that's outside the scope of this change. These methods are coming from MockContainerRuntimeFactory

}

// @public
export class MockContainerRuntimeFactoryForReconnection extends MockContainerRuntimeFactory {
// (undocumented)
Expand All @@ -152,6 +168,21 @@ export class MockContainerRuntimeFactoryForReconnection extends MockContainerRun
}): MockContainerRuntimeForReconnection;
}

// @public
export class MockContainerRuntimeForRebasing extends MockContainerRuntime {
constructor(dataStoreRuntime: MockFluidDataStoreRuntime, factory: MockContainerRuntimeFactoryForRebasing, overrides?: {
minimumSequenceNumber?: number;
});
// (undocumented)
flush(): void;
// (undocumented)
process(message: ISequencedDocumentMessage): void;
// (undocumented)
rebase(): void;
// (undocumented)
submit(messageContent: any, localOpMetadata: unknown): number;
}

// @public
export class MockContainerRuntimeForReconnection extends MockContainerRuntime {
constructor(dataStoreRuntime: MockFluidDataStoreRuntime, factory: MockContainerRuntimeFactoryForReconnection, overrides?: {
Expand Down Expand Up @@ -391,8 +422,12 @@ export class MockFluidDataStoreRuntime extends EventEmitter implements IFluidDat
// (undocumented)
readonly connected = true;
// (undocumented)
containerRuntime?: MockContainerRuntime;
// (undocumented)
createChannel(id: string, type: string): IChannel;
// (undocumented)
createDeltaConnection?(): MockDeltaConnection;
// (undocumented)
deltaManager: MockDeltaManager;
// (undocumented)
dispose(): void;
Expand Down Expand Up @@ -444,7 +479,7 @@ export class MockFluidDataStoreRuntime extends EventEmitter implements IFluidDat
// (undocumented)
readonly path = "";
// (undocumented)
process(message: ISequencedDocumentMessage, local: boolean): void;
process(message: ISequencedDocumentMessage, local: boolean, localOpMetadata: unknown): void;
// (undocumented)
processSignal(message: any, local: boolean): void;
// (undocumented)
Expand Down
186 changes: 186 additions & 0 deletions packages/dds/map/src/test/mocha/rebasing.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
/*!
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
* Licensed under the MIT License.
*/

import { strict as assert } from "assert";
import {
MockFluidDataStoreRuntime,
MockContainerRuntimeFactoryForRebasing,
MockContainerRuntimeForRebasing,
MockStorage,
} from "@fluidframework/test-runtime-utils";
import { MapFactory, SharedMap } from "../../map";
import { DirectoryFactory, SharedDirectory } from "../../directory";
import { IDirectory } from "../../interfaces";

describe("Rebasing", () => {
let containerRuntimeFactory: MockContainerRuntimeFactoryForRebasing;
let containerRuntime1: MockContainerRuntimeForRebasing;
let containerRuntime2: MockContainerRuntimeForRebasing;

describe("SharedMap", () => {
let map1: SharedMap;
let map2: SharedMap;

beforeEach(async () => {
containerRuntimeFactory = new MockContainerRuntimeFactoryForRebasing();
const dataStoreRuntime1 = new MockFluidDataStoreRuntime();
containerRuntime1 = containerRuntimeFactory.createContainerRuntime(dataStoreRuntime1);
const services1 = {
deltaConnection: containerRuntime1.createDeltaConnection(),
objectStorage: new MockStorage(),
};
map1 = new SharedMap("shared-map-1", dataStoreRuntime1, MapFactory.Attributes);
map1.connect(services1);

const dataStoreRuntime2 = new MockFluidDataStoreRuntime();
containerRuntime2 = containerRuntimeFactory.createContainerRuntime(dataStoreRuntime2);
const services2 = {
deltaConnection: containerRuntime2.createDeltaConnection(),
objectStorage: new MockStorage(),
};
map2 = new SharedMap("shared-map-2", dataStoreRuntime2, MapFactory.Attributes);
map2.connect(services2);
});

it("Rebasing ops maintains eventual consistency", () => {
const keyCount = 10;
for (let i = 0; i < keyCount; i++) {
map1.set(`${i}`, map1.size);
}

containerRuntime1.rebase();
containerRuntime1.flush();
containerRuntimeFactory.processAllMessages();

for (let i = 0; i < keyCount; i++) {
assert.strictEqual(map1.get(`${i}`), i);
assert.strictEqual(map2.get(`${i}`), i);
}

const deleteThreshold = 5;
for (let i = 0; i < deleteThreshold - 1; i++) {
map2.delete(`${i}`);
}

map1.delete(`${deleteThreshold - 1}`);

containerRuntime2.rebase();
containerRuntime1.flush();
containerRuntime2.flush();
containerRuntimeFactory.processAllMessages();

for (let i = 0; i < 10; i++) {
const expected = i < deleteThreshold ? undefined : i;
assert.strictEqual(map1.get(`${i}`), expected);
assert.strictEqual(map2.get(`${i}`), expected);
}
});
});

describe("SharedDirectory", () => {
let dir1: SharedDirectory;
let dir2: SharedDirectory;

beforeEach(async () => {
containerRuntimeFactory = new MockContainerRuntimeFactoryForRebasing();
const dataStoreRuntime1 = new MockFluidDataStoreRuntime();
containerRuntime1 = containerRuntimeFactory.createContainerRuntime(dataStoreRuntime1);
const services1 = {
deltaConnection: containerRuntime1.createDeltaConnection(),
objectStorage: new MockStorage(),
};
dir1 = new SharedDirectory(
"shared-directory-1",
dataStoreRuntime1,
DirectoryFactory.Attributes,
);
dir1.connect(services1);

// Create the second SharedMap.
const dataStoreRuntime2 = new MockFluidDataStoreRuntime();
containerRuntime2 = containerRuntimeFactory.createContainerRuntime(dataStoreRuntime2);
const services2 = {
deltaConnection: containerRuntime2.createDeltaConnection(),
objectStorage: new MockStorage(),
};
dir2 = new SharedDirectory(
"shared-directory-2",
dataStoreRuntime2,
DirectoryFactory.Attributes,
);
dir2.connect(services2);
});

const areDirectoriesEqual = (a: IDirectory | undefined, b: IDirectory | undefined) => {
if (a === undefined || b === undefined) {
assert.strictEqual(a, b, "Both directories should be undefined");
return;
}

const leftKeys = Array.from(a.keys());
const rightKeys = Array.from(b.keys());
assert.strictEqual(
leftKeys.length,
rightKeys.length,
"Number of keys should be the same",
);
leftKeys.forEach((key) => {
assert.strictEqual(a.get(key), b.get(key), "Key values should be the same");
});

const leftSubdirectories = Array.from(a.subdirectories());
const rightSubdirectories = Array.from(b.subdirectories());
assert.strictEqual(
leftSubdirectories.length,
rightSubdirectories.length,
"Number of subdirectories should be the same",
);

leftSubdirectories.forEach(([name]) =>
areDirectoriesEqual(a.getSubDirectory(name), b.getSubDirectory(name)),
);
};

it("Rebasing ops maintains eventual consistency", () => {
dir2.on("valueChanged", (changed) => {
if (changed.key === "key") {
dir2.set("valueChanged", "valueChanged");
}
});
dir2.on("subDirectoryCreated", () => {
dir2.set("subDirectoryCreated1", "subDirectoryCreated");
dir2.set("subDirectoryCreated2", "subDirectoryCreated");
containerRuntime2.rebase();
});
const root1SubDir = dir1.createSubDirectory("testSubDir");
dir2.createSubDirectory("testSubDir");

containerRuntime1.flush();
containerRuntime2.flush();

root1SubDir.set("key1", "testValue1");
dir1.set("key", "value");
containerRuntime1.flush();
containerRuntimeFactory.processAllMessages();

dir2.deleteSubDirectory("testSubDir");
dir2.createSubDirectory("testSubDir");

containerRuntime2.rebase();
containerRuntime2.flush();
containerRuntimeFactory.processAllMessages();

const directory1SubDir = dir1.getSubDirectory("testSubDir");
const directory2SubDir = dir2.getSubDirectory("testSubDir");

assert(directory1SubDir !== undefined, "SubDirectory on dir 1 should be present");
assert(directory2SubDir !== undefined, "SubDirectory on dir 2 should be present");

assert.strictEqual(directory1SubDir.size, 0, "Dir 1 no key should exist");
assert.strictEqual(directory2SubDir.size, 0, "Dir 2 no key should exist");
areDirectoriesEqual(dir1, dir2);
});
});
});
91 changes: 91 additions & 0 deletions packages/dds/sequence/src/test/rebasing.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/*!
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
* Licensed under the MIT License.
*/

import { strict as assert } from "assert";
import {
MockFluidDataStoreRuntime,
MockContainerRuntimeFactoryForRebasing,
MockContainerRuntimeForRebasing,
MockStorage,
} from "@fluidframework/test-runtime-utils";
import { IMergeTreeInsertMsg } from "@fluidframework/merge-tree";
import { SharedString } from "../sharedString";
import { SharedStringFactory } from "../sequenceFactory";

describe("Rebasing", () => {
let containerRuntimeFactory: MockContainerRuntimeFactoryForRebasing;
let containerRuntime1: MockContainerRuntimeForRebasing;
let containerRuntime2: MockContainerRuntimeForRebasing;
let sharedString1: SharedString;
let sharedString2: SharedString;

const createSharedString = async (
id: string,
factory: MockContainerRuntimeFactoryForRebasing,
): Promise<[SharedString, MockContainerRuntimeForRebasing]> => {
const dataStoreRuntime = new MockFluidDataStoreRuntime();
dataStoreRuntime.local = false;
const containerRuntime = factory.createContainerRuntime(dataStoreRuntime);
const services = {
deltaConnection: containerRuntime.createDeltaConnection(),
objectStorage: new MockStorage(),
};
const sharedString = new SharedString(dataStoreRuntime, id, SharedStringFactory.Attributes);
sharedString.initializeLocal();
sharedString.connect(services);
return [sharedString, containerRuntime];
};

beforeEach(async () => {
containerRuntimeFactory = new MockContainerRuntimeFactoryForRebasing();
[sharedString1, containerRuntime1] = await createSharedString(
"shared-string-1",
containerRuntimeFactory,
);
[sharedString2, containerRuntime2] = await createSharedString(
"shared-string-2",
containerRuntimeFactory,
);
});

it("Rebasing ops maintains eventual consistency", async () => {
sharedString1.insertText(0, "ad");
sharedString1.insertText(1, "c");
containerRuntime1.flush();
containerRuntime2.flush();
containerRuntimeFactory.processAllMessages();

sharedString2.on("sequenceDelta", (sequenceDeltaEvent) => {
if ((sequenceDeltaEvent.opArgs.op as IMergeTreeInsertMsg).seg === "b") {
sharedString2.insertText(3, "u");
containerRuntime2.rebase();
}
});

sharedString1.insertText(1, "b");
sharedString2.insertText(0, "y");
sharedString2.insertText(1, "w");
containerRuntime1.flush();
containerRuntimeFactory.processOneMessage();
sharedString2.insertText(2, "v");

containerRuntime2.rebase();
containerRuntime1.flush();
containerRuntime2.flush();
containerRuntimeFactory.processAllMessages();

sharedString2.insertText(0, "z");
containerRuntime1.flush();
containerRuntime2.flush();
containerRuntimeFactory.processAllMessages();

assert.strictEqual(sharedString1.getText(), "zywvaubcd");
assert.strictEqual(
sharedString1.getText(),
sharedString2.getText(),
"SharedString eventual consistency broken",
);
});
});
4 changes: 4 additions & 0 deletions packages/runtime/test-runtime-utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,9 @@ export {
MockContainerRuntimeFactoryForReconnection,
MockContainerRuntimeForReconnection,
} from "./mocksForReconnection";
export {
MockContainerRuntimeFactoryForRebasing,
MockContainerRuntimeForRebasing,
} from "./mocksForRebasing";
export { MockStorage } from "./mockStorage";
export { validateAssertionError } from "./validateAssertionError";
Loading