-
Notifications
You must be signed in to change notification settings - Fork 532
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
Changes from all commits
221137f
c24f3e8
e31d309
c62d1d4
748631d
309fb58
8e8bf82
b3541be
52f6329
f7ec14e
140ce54
9525c37
a349de8
3eb38b3
0f6f330
c22db7c
2595ef9
2345a19
e7f183b
b381f26
0bb980a
59984e5
b6b7c2a
dbff9a9
81a64a1
5031292
5579681
8544f04
0a04870
c6be0d9
35586b0
14b3fd8
980360d
90922d9
7b5dde6
12795b1
155f56d
9dee177
a69c719
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -115,6 +115,8 @@ export class MockContainerRuntime { | |
// (undocumented) | ||
process(message: ISequencedDocumentMessage): void; | ||
// (undocumented) | ||
protected get referenceSequenceNumber(): number; | ||
// (undocumented) | ||
submit(messageContent: any, localOpMetadata: unknown): number; | ||
} | ||
|
||
|
@@ -142,6 +144,20 @@ export class MockContainerRuntimeFactory { | |
sequenceNumber: number; | ||
} | ||
|
||
// @public | ||
export class MockContainerRuntimeFactoryForRebasing extends MockContainerRuntimeFactory { | ||
// (undocumented) | ||
createContainerRuntime(dataStoreRuntime: MockFluidDataStoreRuntime, overrides?: { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. just create() is good :) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. From |
||
minimumSequenceNumber?: number; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 :) There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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)? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
} | ||
|
||
// @public | ||
export class MockContainerRuntimeFactoryForReconnection extends MockContainerRuntimeFactory { | ||
// (undocumented) | ||
|
@@ -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?: { | ||
|
@@ -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; | ||
|
@@ -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) | ||
|
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); | ||
}); | ||
}); | ||
}); |
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", | ||
); | ||
}); | ||
}); |
There was a problem hiding this comment.
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.