-
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?
sharedString,map,cell: Validate event with rollback #25300
Conversation
30df064
to
41b2887
Compare
a513b02
to
c623bdb
Compare
c623bdb
to
d8fb934
Compare
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.
Pull Request Overview
This PR adds comprehensive rollback testing for SharedString, SharedMap, and SharedCell DDSes. The tests validate that rollback operations correctly emit expected delta/event notifications and restore proper state when operations (insert, remove, annotate) are rolled back.
- Refactors existing directory rollback tests to use common utilities
- Adds new rollback test utilities (
setupRollbackTest
,createAdditionalClient
) for multi-client scenarios - Implements extensive test coverage for SharedString rollback scenarios including multi-client coordination
Reviewed Changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated no comments.
Show a summary per file
File | Description |
---|---|
packages/dds/test-dds-utils/src/index.ts |
Exports new rollback test utilities |
packages/dds/test-dds-utils/src/ddsTestUtils.ts |
New utility functions for setting up multi-client rollback tests |
packages/dds/sequence/src/test/sharedString.rollback.spec.ts |
Comprehensive SharedString rollback tests including event validation |
packages/dds/map/src/test/mocha/directory.rollback.spec.ts |
Refactored to use common utilities and added event validation tests |
packages/dds/cell/src/test/cell.rollback.spec.ts |
New SharedCell rollback tests with multi-client scenarios |
fe59978
to
6998f8c
Compare
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 comment
The 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
function captureEvents(events: string[], emitter: {on:(e: string, l: ()=>void)=>void,off:(e: string, l: ()=>void)=>void}, act:()=>void){
const fired: string[]=[];
const handlers = new Map<string, ()=>void>();
for(const event of events){
const handler = ()=>fired.push(event)
emitter.on(event,handler);
handlers.set(event, handler);
}
try{
act();
}finally{
for(const [event,handler] of handlers){
emitter.off(event, handler)
}
}
return fired;
}
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 comment
The 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 comment
The reason will be displayed to describe this comment to others. Learn more.
I light disagree with this approach, in particular with the act()
callback here. Not a blocker to me either way but I'd prefer the PR's current approach.
Agree granularity is nice but is achieved more easily and blatantly obvious to a future reader by just clearing out the events
array periodically.
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 toHaveBeenNthCalledWith
would probably be a natural fit for this scenario. I'm less familiar with chai/sinon but I'm sure their options are fine too.
packages/dds/sequence/src/test/intervalCollection.event.rollback.spec.ts
Outdated
Show resolved
Hide resolved
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.
Mostly just reviewing the infra + cell + directory as I am less familiar with the sequence events.
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 comment
The reason will be displayed to describe this comment to others. Learn more.
I light disagree with this approach, in particular with the act()
callback here. Not a blocker to me either way but I'd prefer the PR's current approach.
Agree granularity is nice but is achieved more easily and blatantly obvious to a future reader by just clearing out the events
array periodically.
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 toHaveBeenNthCalledWith
would probably be a natural fit for this scenario. I'm less familiar with chai/sinon but I'm sure their options are fine too.
|
||
const events: (string | undefined)[] = []; | ||
|
||
cell.on("valueChanged", (value) => events.push("valueChanged")); |
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.
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.
const dataStoreRuntime = new MockFluidDataStoreRuntime({ clientId: id }); | ||
const containerRuntime = containerRuntimeFactory.createContainerRuntime(dataStoreRuntime); | ||
|
||
const dds = createDDS(dataStoreRuntime, id); |
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.
Seems maybe a little confusing to use the same id for the clientId and the DDS? Probably not problematic but a little bit unintuitive.
cell.set(42); | ||
containerRuntime.flush(); |
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.
Another approach for granularity is to shift the events off as you check them
cell.set(42); | |
containerRuntime.flush(); | |
cell.set(42); | |
assert.equal(events.shift(), "valueChanged"); | |
containerRuntime.flush(); |
// 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")); |
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.
Some of this repeated setup would probably be a good candidate to move to a beforeEach
block.
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.
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
assert.equal(cell2.get(), 42); | ||
|
||
// Rollback delete | ||
runtime1.rollback?.(); |
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.
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.
runtime1.rollback?.(); | |
runtime1.rollback?.(); | |
// After rollback, this flush/process should not affect cell2. | |
runtime1.flush(); | |
containerRuntimeFactory.processAllMessages(); |
Tests that SharedString correctly raises sequenceDelta events when operations (insert, remove, annotate) are rolled back. The tests ensure that rollback emits the expected deltas and that the resulting text state matches the pre-rollback state.
This change also includes test for map and cell
AB#43938
AB#43938