-
Notifications
You must be signed in to change notification settings - Fork 554
sharedString,map,cell: Validate event with rollback #25300
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
base: main
Are you sure you want to change the base?
Changes from all commits
40c572d
9bda8bb
4078565
8617906
b54d016
9451072
4b50116
e0fd9f0
80f07d8
d8fb934
6998f8c
3d58f53
e4ec91a
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 | ||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,173 @@ | ||||||||||||
/*! | ||||||||||||
* Copyright (c) Microsoft Corporation and contributors. All rights reserved. | ||||||||||||
* Licensed under the MIT License. | ||||||||||||
*/ | ||||||||||||
|
||||||||||||
import { strict as assert } from "node:assert"; | ||||||||||||
|
||||||||||||
import { setupRollbackTest, createAdditionalClient } from "@fluid-private/test-dds-utils"; | ||||||||||||
|
||||||||||||
import { CellFactory } from "../cellFactory.js"; | ||||||||||||
import type { ISharedCell } from "../interfaces.js"; | ||||||||||||
|
||||||||||||
const cellFactory = new CellFactory(); | ||||||||||||
|
||||||||||||
describe("Cell with rollback", () => { | ||||||||||||
it("should emit valueChanged on set and rollback should re-emit previous value", async () => { | ||||||||||||
const { dds: cell, containerRuntime } = setupRollbackTest<ISharedCell>( | ||||||||||||
"cell-1", | ||||||||||||
(rt, id): ISharedCell => cellFactory.create(rt, id), | ||||||||||||
); | ||||||||||||
|
||||||||||||
const events: (string | undefined)[] = []; | ||||||||||||
|
||||||||||||
cell.on("valueChanged", (value) => events.push("valueChanged")); | ||||||||||||
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. These tests should also test event callback params, as these are particularly interesting in rollback cases (they need to reflect the inverse operation as well, which sometimes requires special bookkeeping in the DDS). Consider instead of just having an array of strings for the events, push some structure that also includes the args that the callback received (example with map). EDIT: I just realized cell's event params are different and maybe less interesting since they don't provide the previousValue, but still good to verify as a practice. |
||||||||||||
|
||||||||||||
cell.on("delete", () => events.push("delete")); | ||||||||||||
|
||||||||||||
cell.set(10); | ||||||||||||
assert.equal(cell.get(), 10); | ||||||||||||
|
||||||||||||
containerRuntime.rollback?.(); | ||||||||||||
|
||||||||||||
assert.equal(cell.get(), undefined); | ||||||||||||
|
||||||||||||
assert.deepEqual(events, ["valueChanged", "delete"]); | ||||||||||||
}); | ||||||||||||
|
||||||||||||
it("should emit delete on delete, and rollback should re-emit last valueChanged", async () => { | ||||||||||||
const { | ||||||||||||
dds: cell, | ||||||||||||
containerRuntimeFactory, | ||||||||||||
containerRuntime, | ||||||||||||
} = setupRollbackTest<ISharedCell>( | ||||||||||||
"cell-1", | ||||||||||||
(rt, id): ISharedCell => cellFactory.create(rt, id), | ||||||||||||
); | ||||||||||||
|
||||||||||||
const events: (string | undefined)[] = []; | ||||||||||||
|
||||||||||||
cell.on("valueChanged", (value) => events.push("valueChanged")); | ||||||||||||
cell.on("delete", () => events.push("delete")); | ||||||||||||
|
||||||||||||
cell.set(42); | ||||||||||||
assert.equal(events.shift(), "valueChanged"); | ||||||||||||
containerRuntime.flush(); | ||||||||||||
containerRuntimeFactory.processAllMessages(); | ||||||||||||
|
||||||||||||
cell.delete(); | ||||||||||||
assert(cell.empty()); | ||||||||||||
|
||||||||||||
// rollback delete | ||||||||||||
containerRuntime.rollback?.(); | ||||||||||||
assert.equal(cell.get(), 42); | ||||||||||||
|
||||||||||||
// delete triggers delete event, rollback restores valueChanged | ||||||||||||
assert.deepEqual(events, ["valueChanged", "delete", "valueChanged"]); | ||||||||||||
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 these tests would be easier to follow if they had more granular event validation, rather than doing it all at the end. to make that easier we could add a helper method like
this would make it easy to get the events per action, and validate per action, rather than doing it all at the end. this would be quite a bit of refactoring. i'd probably try writing the helper method manually. migrating one test in the file, and then seeing if copilot could refactor the rest. it might save some time. be sure to commit incrementally, as copilot can also go off the rails, and you don't want to lose work. 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 might be hard to do for the more advance tests, i think could help with the simper test 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 light disagree with this approach, in particular with the Agree granularity is nice but is achieved more easily and blatantly obvious to a future reader by just clearing out the Or if we do ultimately want a more reusable approach, the best option is probably to use an existing testing library's function mocks or spies rather than writing our own, something like |
||||||||||||
}); | ||||||||||||
}); | ||||||||||||
|
||||||||||||
describe("SharedCell rollback events with multiple clients", () => { | ||||||||||||
it("should emit valueChanged on set and rollback should re-emit previous value across clients", async () => { | ||||||||||||
// Setup two clients | ||||||||||||
const { | ||||||||||||
dds: cell1, | ||||||||||||
containerRuntimeFactory, | ||||||||||||
containerRuntime: runtime1, | ||||||||||||
} = setupRollbackTest<ISharedCell>( | ||||||||||||
"client-1", | ||||||||||||
(rt, id): ISharedCell => cellFactory.create(rt, id), | ||||||||||||
); | ||||||||||||
const { dds: cell2 } = createAdditionalClient( | ||||||||||||
containerRuntimeFactory, | ||||||||||||
"client-2", | ||||||||||||
(rt, id): ISharedCell => cellFactory.create(rt, `cell-${id}`), | ||||||||||||
); | ||||||||||||
|
||||||||||||
const events1: string[] = []; | ||||||||||||
const events2: string[] = []; | ||||||||||||
|
||||||||||||
// Listen to valueChanged events on both clients | ||||||||||||
cell1.on("valueChanged", () => events1.push("valueChanged")); | ||||||||||||
cell2.on("valueChanged", () => events2.push("valueChanged")); | ||||||||||||
|
||||||||||||
// Client 1 sets a value | ||||||||||||
cell1.set(10); | ||||||||||||
assert.equal(cell1.get(), 10); | ||||||||||||
|
||||||||||||
// Propagate ops to client 2 | ||||||||||||
runtime1.flush(); | ||||||||||||
containerRuntimeFactory.processAllMessages(); | ||||||||||||
|
||||||||||||
assert.equal(cell2.get(), 10); | ||||||||||||
|
||||||||||||
cell1.set(100); | ||||||||||||
assert.equal(cell1.get(), 100); | ||||||||||||
assert.equal(cell2.get(), 10); | ||||||||||||
|
||||||||||||
// Rollback on client 1 | ||||||||||||
runtime1.rollback?.(); | ||||||||||||
|
||||||||||||
assert.equal(cell1.get(), 10); | ||||||||||||
assert.equal(cell2.get(), 10); | ||||||||||||
|
||||||||||||
// Both clients should have seen the events | ||||||||||||
assert.deepEqual(events1, ["valueChanged", "valueChanged", "valueChanged"]); | ||||||||||||
assert.deepEqual(events2, ["valueChanged"]); | ||||||||||||
}); | ||||||||||||
|
||||||||||||
it("should emit delete on delete, and rollback should re-emit last valueChanged across clients", async () => { | ||||||||||||
// Setup two clients | ||||||||||||
const { | ||||||||||||
dds: cell1, | ||||||||||||
containerRuntimeFactory, | ||||||||||||
containerRuntime: runtime1, | ||||||||||||
} = setupRollbackTest<ISharedCell>( | ||||||||||||
"client-1", | ||||||||||||
(rt, id): ISharedCell => cellFactory.create(rt, id), | ||||||||||||
); | ||||||||||||
const { dds: cell2 } = createAdditionalClient( | ||||||||||||
containerRuntimeFactory, | ||||||||||||
"client-2", | ||||||||||||
(rt, id): ISharedCell => cellFactory.create(rt, `cell-${id}`), | ||||||||||||
); | ||||||||||||
|
||||||||||||
const events1: string[] = []; | ||||||||||||
const events2: string[] = []; | ||||||||||||
|
||||||||||||
// Attach listeners | ||||||||||||
cell1.on("valueChanged", () => events1.push("valueChanged")); | ||||||||||||
cell1.on("delete", () => events1.push("delete")); | ||||||||||||
|
||||||||||||
cell2.on("valueChanged", () => events2.push("valueChanged")); | ||||||||||||
cell2.on("delete", () => events2.push("delete")); | ||||||||||||
Comment on lines
+120
to
+143
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. Some of this repeated setup would probably be a good candidate to move to a 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 was removed in the previous iteration as per: #25300 (comment). Overall it seems the test is easier to follow with the initialization kept inside the test itself, even though some setup is repeated |
||||||||||||
|
||||||||||||
// Set initial value and propagate | ||||||||||||
cell1.set(42); | ||||||||||||
runtime1.flush(); | ||||||||||||
containerRuntimeFactory.processAllMessages(); | ||||||||||||
|
||||||||||||
assert.equal(cell2.get(), 42); | ||||||||||||
|
||||||||||||
// Delete the value | ||||||||||||
cell1.delete(); | ||||||||||||
|
||||||||||||
assert(cell1.empty()); | ||||||||||||
assert.equal(cell2.get(), 42); | ||||||||||||
|
||||||||||||
// Rollback delete | ||||||||||||
runtime1.rollback?.(); | ||||||||||||
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. Would probably be good to do a final flush/process (that we expect to be a no-op) to verify nothing slips out that ends up in cell2/events2.
Suggested change
|
||||||||||||
|
||||||||||||
// After rollback, this flush/process should not affect cell2. | ||||||||||||
runtime1.flush(); | ||||||||||||
containerRuntimeFactory.processAllMessages(); | ||||||||||||
|
||||||||||||
// After rollback, value is restored | ||||||||||||
assert.equal(cell1.get(), 42); | ||||||||||||
assert.equal(cell2.get(), 42); | ||||||||||||
|
||||||||||||
// Event order | ||||||||||||
assert.deepEqual(events1, ["valueChanged", "delete", "valueChanged"]); | ||||||||||||
assert.deepEqual(events2, ["valueChanged"]); | ||||||||||||
}); | ||||||||||||
}); |
Uh oh!
There was an error while loading. Please reload this page.