Skip to content

Commit

Permalink
test(tree): Convert fuzz test to integration test (#14753)
Browse files Browse the repository at this point in the history
  • Loading branch information
yann-achard-MS authored Mar 28, 2023
1 parent 59d52e5 commit 71d2c21
Show file tree
Hide file tree
Showing 5 changed files with 167 additions and 222 deletions.
128 changes: 128 additions & 0 deletions packages/dds/tree/src/test/objMerge.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
/*!
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
* Licensed under the MIT License.
*/

import { fail } from "../util";

// eslint-disable-next-line @typescript-eslint/no-extraneous-class
export class Merged {}

export class Conflicted extends Merged {
public constructor(o: unknown) {
super();
Object.assign(this, o);
}
}

export class Conflict extends Merged {
public constructor(public readonly lhs: unknown, public readonly rhs: unknown) {
super();
}
}

/**
* Utility function for comparing two objects.
* Supports data that could be roundtrip through `JSON.stringify`/`JSON.parse`.
* @returns An object that represents a merged view of the given objects.
*/
export function merge<T>(lhs: T, rhs: T): Conflicted | Conflict | T {
if (lhs instanceof Merged || rhs instanceof Merged) {
fail("This function does not accept its output type as an input type");
}

// === is not reflective because of how NaN is handled, so use Object.is instead.
// This treats -0 and +0 as different.
// Since -0 is not preserved in serialization round trips,
// it can be handed in any way that is reflective and commutative, so this is fine.
if (Object.is(lhs, rhs)) {
return lhs;
}

// Primitives which are equal would have early returned above, so now if the values are not both objects,
// they are unequal.
if (typeof lhs !== "object" || typeof rhs !== "object") {
return new Conflict(lhs, rhs);
}

// null is of type object, and needs to be treated as distinct from the empty object.
// Handling it early also avoids type errors trying to access its keys.
// Rationale: 'undefined' payloads are reserved for future use (see 'SetValue' interface).
if (lhs === null || rhs === null) {
return new Conflict(lhs, rhs);
}

// Special case IFluidHandles, comparing them only by their absolutePath
// Detect them using JavaScript feature detection pattern: they have a `IFluidHandle`
// field that is set to the parent object.
{
const aHandle = lhs as unknown as { IFluidHandle?: unknown; absolutePath?: string };
const bHandle = rhs as unknown as { IFluidHandle?: unknown; absolutePath?: string };
if (aHandle.IFluidHandle === aHandle) {
if (bHandle.IFluidHandle !== bHandle) {
return new Conflict(lhs, rhs);
}
return aHandle.absolutePath === bHandle.absolutePath ? lhs : new Conflict(lhs, rhs);
}
}

if (Array.isArray(lhs) !== Array.isArray(rhs)) {
return new Conflict(lhs, rhs);
}
if (Array.isArray(lhs) && Array.isArray(rhs)) {
let same = true;
const out = [];
for (let i = 0; i < lhs.length; i += 1) {
const d = merge(lhs[i], rhs[i]);
same = same && d instanceof Merged === false;
out.push(d);
}
for (let i = lhs.length; i < rhs.length; i += 1) {
const d = merge(lhs[i], rhs[i]);
same = same && d instanceof Merged === false;
out.push(d);
}
return same ? out : new Conflicted(out);
}

{
// Fluid Serialization (like Json) only keeps enumerable properties, so we can ignore non-enumerable ones.
const lhsKeys = Object.keys(lhs);
const rhsKeys = Object.keys(rhs);
const selfKeys: string[] = [];

const lhsObj = lhs as Record<string, unknown>;
const rhsObj = rhs as Record<string, unknown>;
let same = true;
const out: Record<string, unknown> = {};
for (const key of lhsKeys) {
if (key in rhs === false) {
same = false;
out[key] = new Conflict(lhsObj[key], undefined);
} else {
// The JavaScript feature detection pattern, used for IFluidHandle, uses a field that is set to the
// parent object.
// Detect this pattern and special case it to avoid infinite recursion.
const aSelf = Object.is(lhsObj[key], lhsObj);
const bSelf = Object.is(rhsObj[key], rhsObj);
if (aSelf === true && bSelf === true) {
selfKeys.push(key);
}
const d = merge(lhsObj[key], rhsObj[key]);
same = same && d instanceof Merged === false;
out[key] = d;
}
}
for (const key of rhsKeys) {
if (key in lhs === false) {
same = false;
out[key] = new Conflict(undefined, rhsObj[key]);
}
}
const final = same ? out : new Conflicted(out);
for (const key of selfKeys) {
out[key] = final;
}
return final;
}
}
24 changes: 24 additions & 0 deletions packages/dds/tree/src/test/shared-tree/editing.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,30 @@ describe("Editing", () => {
expectJsonTree([tree1, tree2, tree3, tree4], ["x", "y"]);
});

// TODO: enable once muted marks are supported
it.skip("can rebase change under a node whose insertion is also rebased", () => {
const sequencer = new Sequencer();
const tree1 = TestTree.fromJson(["B"]);
const tree2 = tree1.fork();
const tree3 = tree1.fork();

const addC = insert(tree2, 1, "C");
const addA = insert(tree1, 0, "A");
const nestedChange = tree1.runTransaction((forest, editor) => {
editor.setValue(
{ parent: undefined, parentField: rootFieldKeySymbol, parentIndex: 0 },
"a",
);
});

const sequenced = sequencer.sequence([addC, addA, nestedChange]);
tree1.receive(sequenced);
tree2.receive(sequenced);
tree3.receive(sequenced);

expectJsonTree([tree1, tree2, tree3], ["a", "B", "C"]);
});

it("can handle competing deletes", () => {
for (const index of [0, 1, 2, 3]) {
const sequencer = new Sequencer();
Expand Down
206 changes: 0 additions & 206 deletions packages/dds/tree/src/test/shared-tree/sharedTree.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ import {
EditManager,
ValueSchema,
} from "../../core";
import { checkTreesAreSynchronized } from "./sharedTreeFuzzTests";

const fooKey: FieldKey = brand("foo");
const globalFieldKey: GlobalFieldKey = brand("globalFieldKey");
Expand Down Expand Up @@ -1875,211 +1874,6 @@ describe("SharedTree", () => {
anchorPath = tree.locate(firstAnchor);
assert(compareUpPaths(expectedPath, anchorPath));
});
it("ensureSynchronized shows diverged trees", async () => {
const provider = await TestTreeProvider.create(4, SummarizeType.onDemand);
const initialTreeState: JsonableTree = {
type: brand("Node"),
fields: {
foo: [
{ type: brand("Number"), value: 0 },
{ type: brand("Number"), value: 1 },
{ type: brand("Number"), value: 2 },
],
foo2: [
{ type: brand("Number"), value: 0 },
{ type: brand("Number"), value: 1 },
{ type: brand("Number"), value: 2 },
],
},
};
initializeTestTree(provider.trees[0], initialTreeState, testSchema);
await provider.ensureSynchronized();

const tree0 = provider.trees[0];
const tree1 = provider.trees[1];
const tree2 = provider.trees[2];

const rootPath = {
parent: undefined,
parentField: rootFieldKeySymbol,
parentIndex: 0,
};

let path: UpPath;
// edit 1
await provider.ensureSynchronized();

// edit 2
let readCursor = tree2.forest.allocateCursor();
moveToDetachedField(tree2.forest, readCursor);
let actual = mapCursorField(readCursor, jsonableTreeFromCursor);
readCursor.free();
runSynchronous(tree2, () => {
const field = tree2.editor.sequenceField(undefined, rootFieldKeySymbol);
field.insert(
1,
singleTextCursor({ type: brand("Test"), value: -9007199254740991 }),
);
});
readCursor = tree2.forest.allocateCursor();
moveToDetachedField(tree2.forest, readCursor);
actual = mapCursorField(readCursor, jsonableTreeFromCursor);
readCursor.free();

// edit 3
readCursor = tree0.forest.allocateCursor();
moveToDetachedField(tree0.forest, readCursor);
actual = mapCursorField(readCursor, jsonableTreeFromCursor);
readCursor.free();
runSynchronous(tree0, () => {
const field = tree0.editor.sequenceField(rootPath, brand("Test"));
field.insert(
0,
singleTextCursor({ type: brand("Test"), value: -9007199254740991 }),
);
});
readCursor = tree0.forest.allocateCursor();
moveToDetachedField(tree0.forest, readCursor);
actual = mapCursorField(readCursor, jsonableTreeFromCursor);
readCursor.free();

// edit 4
readCursor = tree0.forest.allocateCursor();
moveToDetachedField(tree0.forest, readCursor);
actual = mapCursorField(readCursor, jsonableTreeFromCursor);
readCursor.free();
runSynchronous(tree0, () => {
const field = tree0.editor.sequenceField(undefined, rootFieldKeySymbol);
field.insert(
0,
singleTextCursor({ type: brand("Test"), value: -9007199254740991 }),
);
});
readCursor = tree0.forest.allocateCursor();
moveToDetachedField(tree0.forest, readCursor);
actual = mapCursorField(readCursor, jsonableTreeFromCursor);
readCursor.free();

// edit 5
readCursor = tree1.forest.allocateCursor();
moveToDetachedField(tree1.forest, readCursor);
actual = mapCursorField(readCursor, jsonableTreeFromCursor);
readCursor.free();
runSynchronous(tree1, () => {
const field = tree1.editor.sequenceField(rootPath, brand("Test"));
field.insert(
0,
singleTextCursor({ type: brand("Test"), value: -9007199254740991 }),
);
});
readCursor = tree1.forest.allocateCursor();
moveToDetachedField(tree1.forest, readCursor);
actual = mapCursorField(readCursor, jsonableTreeFromCursor);
readCursor.free();

// edit 6
readCursor = tree0.forest.allocateCursor();
moveToDetachedField(tree0.forest, readCursor);
actual = mapCursorField(readCursor, jsonableTreeFromCursor);
readCursor.free();
runSynchronous(tree0, () => {
const field = tree0.editor.sequenceField(rootPath, brand("Test"));
field.insert(
0,
singleTextCursor({ type: brand("Test"), value: -9007199254740991 }),
);
});
readCursor = tree0.forest.allocateCursor();
moveToDetachedField(tree0.forest, readCursor);
actual = mapCursorField(readCursor, jsonableTreeFromCursor);
readCursor.free();

// edit 7
readCursor = tree2.forest.allocateCursor();
moveToDetachedField(tree2.forest, readCursor);
actual = mapCursorField(readCursor, jsonableTreeFromCursor);
readCursor.free();
runSynchronous(tree2, () => {
const field = tree2.editor.sequenceField(undefined, rootFieldKeySymbol);
field.insert(
0,
singleTextCursor({ type: brand("Test"), value: -9007199254740991 }),
);
});
readCursor = tree2.forest.allocateCursor();
moveToDetachedField(tree2.forest, readCursor);
actual = mapCursorField(readCursor, jsonableTreeFromCursor);
readCursor.free();

// edit 8
readCursor = tree2.forest.allocateCursor();
moveToDetachedField(tree2.forest, readCursor);
actual = mapCursorField(readCursor, jsonableTreeFromCursor);
readCursor.free();
remove(tree2, 0, 1);
readCursor = tree2.forest.allocateCursor();
moveToDetachedField(tree2.forest, readCursor);
actual = mapCursorField(readCursor, jsonableTreeFromCursor);
readCursor.free();

// edit 9
readCursor = tree0.forest.allocateCursor();
moveToDetachedField(tree0.forest, readCursor);
actual = mapCursorField(readCursor, jsonableTreeFromCursor);
readCursor.free();
// eslint-disable-next-line prefer-const
path = {
parent: rootPath,
parentField: brand("Test"),
parentIndex: 0,
};
runSynchronous(tree0, () => {
tree0.editor.setValue(path, 3969223090210651);
});
readCursor = tree0.forest.allocateCursor();
moveToDetachedField(tree0.forest, readCursor);
actual = mapCursorField(readCursor, jsonableTreeFromCursor);
readCursor.free();

// edit 10
readCursor = tree0.forest.allocateCursor();
moveToDetachedField(tree0.forest, readCursor);
actual = mapCursorField(readCursor, jsonableTreeFromCursor);
readCursor.free();
runSynchronous(tree0, () => {
const field = tree0.editor.sequenceField(rootPath, brand("Test"));
field.insert(
0,
singleTextCursor({ type: brand("Test"), value: -9007199254740991 }),
);
});
readCursor = tree0.forest.allocateCursor();
moveToDetachedField(tree0.forest, readCursor);
actual = mapCursorField(readCursor, jsonableTreeFromCursor);
readCursor.free();

// edit 10
readCursor = tree2.forest.allocateCursor();
moveToDetachedField(tree2.forest, readCursor);
actual = mapCursorField(readCursor, jsonableTreeFromCursor);
readCursor.free();
runSynchronous(tree2, () => {
const field = tree2.editor.sequenceField(undefined, rootFieldKeySymbol);
field.insert(
0,
singleTextCursor({ type: brand("Test"), value: -9007199254740991 }),
);
});
readCursor = tree2.forest.allocateCursor();
moveToDetachedField(tree2.forest, readCursor);
actual = mapCursorField(readCursor, jsonableTreeFromCursor);
readCursor.free();

// edit 11
await provider.ensureSynchronized();

checkTreesAreSynchronized(provider);
});
});
});

Expand Down
Loading

0 comments on commit 71d2c21

Please sign in to comment.