From 67b9933549fcd2f3e3eb125448571ab7645bb037 Mon Sep 17 00:00:00 2001 From: Sonali Deshpande <48232592+sonalideshpandemsft@users.noreply.github.com> Date: Wed, 10 Sep 2025 01:20:13 +0000 Subject: [PATCH 01/11] wip --- packages/dds/map/src/test/directroyOracle.ts | 84 ++++++++ .../src/test/mocha/directoryFuzzTests.spec.ts | 185 ++++++++++-------- packages/dds/map/src/test/mocha/fuzzUtils.ts | 13 +- .../dds/map/src/test/mocha/oracleUtils.ts | 25 +++ 4 files changed, 223 insertions(+), 84 deletions(-) create mode 100644 packages/dds/map/src/test/directroyOracle.ts create mode 100644 packages/dds/map/src/test/mocha/oracleUtils.ts diff --git a/packages/dds/map/src/test/directroyOracle.ts b/packages/dds/map/src/test/directroyOracle.ts new file mode 100644 index 000000000000..9dfcf8e0af59 --- /dev/null +++ b/packages/dds/map/src/test/directroyOracle.ts @@ -0,0 +1,84 @@ +/*! + * Copyright (c) Microsoft Corporation and contributors. All rights reserved. + * Licensed under the MIT License. + */ + +import { strict as assert } from "node:assert"; + +import type { SharedDirectory } from "../directoryFactory.js"; +import type { IDirectory, IDirectoryValueChanged, ISharedDirectory } from "../interfaces.js"; + +/** + * Oracle for directroy + * @internal + */ +export class SharedDirectoryOracle { + private readonly model = new Map(); + + public constructor(private readonly shared: SharedDirectory) { + // Snapshot + this.snapshotDirectory(shared, ""); + + // Subscribe + this.shared.on("valueChanged", this.onValueChanged); + this.shared.on("subDirectoryCreated", this.onSubDirCreated); + this.shared.on("subDirectoryDeleted", this.onSubDirDeleted); + } + + private snapshotDirectory(dir: IDirectory, prefix: string): void { + for (const [key, value] of dir.entries()) { + this.model.set(`${prefix}${key}`, value); + } + for (const [subName, subDir] of dir.subdirectories()) { + this.snapshotDirectory(subDir, `${prefix}${subName}/`); + } + } + + private readonly onValueChanged = (change: IDirectoryValueChanged): void => { + const pathKey = `${change.path}/${change.key}`; + if (this.shared.has(pathKey)) { + this.model.set(pathKey, this.shared.get(pathKey)); + } else { + this.model.delete(pathKey); + } + }; + + private readonly onSubDirCreated = (subDir: ISharedDirectory, path: string): void => { + this.snapshotDirectory(subDir, `${path}/`); + }; + + private readonly onSubDirDeleted = (path: string): void => { + for (const key of [...this.model.keys()]) { + if (key.startsWith(`${path}/`)) { + this.model.delete(key); + } + } + }; + + public validate(): void { + // Rebuild snapshot + const actualMap = new Map(); + this.snapshotDirectory(this.shared, ""); + + assert.strictEqual( + actualMap.size, + this.model.size, + `SharedDirectoryOracle mismatch: expected size=${this.model.size}, actual=${actualMap.size}`, + ); + + for (const [key, expectedValue] of this.model.entries()) { + const actualValue = actualMap.get(key); + assert.deepStrictEqual( + expectedValue, + actualValue, + `SharedDirectoryOracle mismatch at path="${key}"`, + ); + } + } + + public dispose(): void { + this.shared.off("valueChanged", this.onValueChanged); + this.shared.off("subDirectoryCreated", this.onSubDirCreated); + this.shared.off("subDirectoryDeleted", this.onSubDirDeleted); + } +} diff --git a/packages/dds/map/src/test/mocha/directoryFuzzTests.spec.ts b/packages/dds/map/src/test/mocha/directoryFuzzTests.spec.ts index 5305ccb5c444..e0f43de6d1b2 100644 --- a/packages/dds/map/src/test/mocha/directoryFuzzTests.spec.ts +++ b/packages/dds/map/src/test/mocha/directoryFuzzTests.spec.ts @@ -5,22 +5,37 @@ import * as dirPath from "node:path"; +import { TypedEventEmitter } from "@fluid-internal/client-utils"; import { takeAsync } from "@fluid-private/stochastic-test-utils"; -import { type DDSFuzzModel, createDDSFuzzSuite } from "@fluid-private/test-dds-utils"; -import { FlushMode } from "@fluidframework/runtime-definitions/internal"; +import { + type DDSFuzzHarnessEvents, + type DDSFuzzModel, + createDDSFuzzSuite, +} from "@fluid-private/test-dds-utils"; +// import { FlushMode } from "@fluidframework/runtime-definitions/internal"; import { DirectoryFactory } from "../../index.js"; +import { SharedDirectoryOracle } from "../directroyOracle.js"; import { assertEquivalentDirectories } from "./directoryEquivalenceUtils.js"; import { _dirname } from "./dirname.cjs"; import { - baseDirModel, + // baseDirModel, dirDefaultOptions, makeDirOperationGenerator, makeDirReducer, type DirOperation, type DirOperationGenerationConfig, } from "./fuzzUtils.js"; +import type { ISharedDirectoryWithOracle } from "./oracleUtils.js"; + +const oracleEmitter = new TypedEventEmitter(); + +oracleEmitter.on("clientCreate", (client) => { + const channel = client.channel as ISharedDirectoryWithOracle; + const directroyOracle = new SharedDirectoryOracle(channel); + channel.sharedDirectoryOracle = directroyOracle; +}); describe("SharedDirectory fuzz Create/Delete concentrated", () => { const options: DirOperationGenerationConfig = { @@ -56,91 +71,95 @@ describe("SharedDirectory fuzz Create/Delete concentrated", () => { stashableClientProbability: 0.2, }, defaultTestCount: 25, + // emitter: oracleEmitter, // Uncomment this line to replay a specific seed from its failure file: // replay: 21, saveFailures: { directory: dirPath.join(_dirname, "../../../src/test/mocha/results/1") }, }); - createDDSFuzzSuite( - { ...model, workloadName: "default directory 1 with rebasing" }, - { - validationStrategy: { - type: "random", - probability: 0.4, - }, - rebaseProbability: 0.2, - reconnectProbability: 0.5, - // We prevent handles from being generated on the creation/deletion tests since the set operations are disabled. - handleGenerationDisabled: true, - containerRuntimeOptions: { - flushMode: FlushMode.TurnBased, - enableGroupedBatching: true, - }, - numberOfClients: 3, - clientJoinOptions: { - maxNumberOfClients: 3, - clientAddProbability: 0.08, - stashableClientProbability: undefined, - }, - defaultTestCount: 200, - // Uncomment this line to replay a specific seed from its failure file: - // replay: 0, - saveFailures: { - directory: dirPath.join(_dirname, "../../../src/test/mocha/results/1"), - }, - }, - ); + // createDDSFuzzSuite( + // { ...model, workloadName: "default directory 1 with rebasing" }, + // { + // validationStrategy: { + // type: "random", + // probability: 0.4, + // }, + // rebaseProbability: 0.2, + // reconnectProbability: 0.5, + // // We prevent handles from being generated on the creation/deletion tests since the set operations are disabled. + // handleGenerationDisabled: true, + // containerRuntimeOptions: { + // flushMode: FlushMode.TurnBased, + // enableGroupedBatching: true, + // }, + // numberOfClients: 3, + // clientJoinOptions: { + // maxNumberOfClients: 3, + // clientAddProbability: 0.08, + // stashableClientProbability: undefined, + // }, + // defaultTestCount: 200, + // // emitter: oracleEmitter, + // // Uncomment this line to replay a specific seed from its failure file: + // // replay: 0, + // saveFailures: { + // directory: dirPath.join(_dirname, "../../../src/test/mocha/results/1"), + // }, + // }, + // ); }); -describe("SharedDirectory fuzz", () => { - createDDSFuzzSuite(baseDirModel, { - validationStrategy: { - type: "fixedInterval", - interval: dirDefaultOptions.validateInterval, - }, - reconnectProbability: 0.15, - numberOfClients: 3, - clientJoinOptions: { - // Note: if tests are slow, we may want to tune this down. This mimics behavior before this suite - // was refactored to use the DDS fuzz harness. - maxNumberOfClients: Number.MAX_SAFE_INTEGER, - clientAddProbability: 0.08, - stashableClientProbability: 0.2, - }, - defaultTestCount: 25, - // Uncomment this line to replay a specific seed from its failure file: - // replay: 0, - saveFailures: { directory: dirPath.join(_dirname, "../../../src/test/mocha/results/2") }, - skip: [], - }); +// describe("SharedDirectory fuzz", () => { +// createDDSFuzzSuite(baseDirModel, { +// validationStrategy: { +// type: "fixedInterval", +// interval: dirDefaultOptions.validateInterval, +// }, +// reconnectProbability: 0.15, +// numberOfClients: 3, +// clientJoinOptions: { +// // Note: if tests are slow, we may want to tune this down. This mimics behavior before this suite +// // was refactored to use the DDS fuzz harness. +// maxNumberOfClients: Number.MAX_SAFE_INTEGER, +// clientAddProbability: 0.08, +// stashableClientProbability: 0.2, +// }, +// defaultTestCount: 25, +// // emitter: oracleEmitter, +// // Uncomment this line to replay a specific seed from its failure file: +// // replay: 0, +// saveFailures: { directory: dirPath.join(_dirname, "../../../src/test/mocha/results/2") }, +// skip: [], +// }); - createDDSFuzzSuite( - { ...baseDirModel, workloadName: "default directory 2 with rebasing" }, - { - validationStrategy: { - type: "random", - probability: 0.4, - }, - rebaseProbability: 0.2, - reconnectProbability: 0.5, - containerRuntimeOptions: { - flushMode: FlushMode.TurnBased, - enableGroupedBatching: true, - }, - numberOfClients: 3, - clientJoinOptions: { - // Note: if tests are slow, we may want to tune this down. This mimics behavior before this suite - // was refactored to use the DDS fuzz harness. - maxNumberOfClients: Number.MAX_SAFE_INTEGER, - clientAddProbability: 0.08, - stashableClientProbability: undefined, - }, - defaultTestCount: 200, - // Uncomment this line to replay a specific seed from its failure file: - // replay: 0, - saveFailures: { - directory: dirPath.join(_dirname, "../../../src/test/mocha/results/2"), - }, - }, - ); -}); +// createDDSFuzzSuite( +// { ...baseDirModel, workloadName: "default directory 2 with rebasing" }, +// { +// validationStrategy: { +// type: "random", +// probability: 0.4, +// }, +// rebaseProbability: 0.2, +// reconnectProbability: 0.5, +// containerRuntimeOptions: { +// flushMode: FlushMode.TurnBased, +// enableGroupedBatching: true, +// }, +// numberOfClients: 3, +// clientJoinOptions: { +// // Note: if tests are slow, we may want to tune this down. This mimics behavior before this suite +// // was refactored to use the DDS fuzz harness. +// maxNumberOfClients: Number.MAX_SAFE_INTEGER, +// clientAddProbability: 0.08, +// stashableClientProbability: undefined, +// }, +// defaultTestCount: 200, +// // emitter: oracleEmitter, +// // Uncomment this line to replay a specific seed from its failure file: +// // replay: 0, +// saveFailures: { +// directory: dirPath.join(_dirname, "../../../src/test/mocha/results/2"), +// }, +// }, +// ); +// }); diff --git a/packages/dds/map/src/test/mocha/fuzzUtils.ts b/packages/dds/map/src/test/mocha/fuzzUtils.ts index 008bd2c94b0a..96b11b641b8b 100644 --- a/packages/dds/map/src/test/mocha/fuzzUtils.ts +++ b/packages/dds/map/src/test/mocha/fuzzUtils.ts @@ -29,6 +29,7 @@ import { } from "../../index.js"; import { assertEquivalentDirectories } from "./directoryEquivalenceUtils.js"; +import { hasSharedDirectroyOracle } from "./oracleUtils.js"; /** * Represents a map clear operation. @@ -508,7 +509,17 @@ export const baseDirModel: DDSFuzzModel = { workloadName: "default directory 1", generatorFactory: () => takeAsync(100, makeDirOperationGenerator(dirDefaultOptions)), reducer: makeDirReducer({ clientIds: ["A", "B", "C"], printConsoleLogs: false }), - validateConsistency: async (a, b) => assertEquivalentDirectories(a.channel, b.channel), + validateConsistency: async (a, b) => { + if (hasSharedDirectroyOracle(a.channel)) { + a.channel.sharedDirectoryOracle.validate(); + } + + if (hasSharedDirectroyOracle(b.channel)) { + b.channel.sharedDirectoryOracle.validate(); + } + // eslint-disable-next-line no-void + void assertEquivalentDirectories(a.channel, b.channel); + }, factory: new DirectoryFactory(), minimizationTransforms: [ (op: DirOperation): void => { diff --git a/packages/dds/map/src/test/mocha/oracleUtils.ts b/packages/dds/map/src/test/mocha/oracleUtils.ts new file mode 100644 index 000000000000..067a71fe5a16 --- /dev/null +++ b/packages/dds/map/src/test/mocha/oracleUtils.ts @@ -0,0 +1,25 @@ +/*! + * Copyright (c) Microsoft Corporation and contributors. All rights reserved. + * Licensed under the MIT License. + */ + +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +import type { SharedDirectory } from "../../directoryFactory.js"; +import { SharedDirectoryOracle } from "../directroyOracle.js"; + +/** + * @internal + */ +export interface ISharedDirectoryWithOracle extends SharedDirectory { + sharedDirectoryOracle: SharedDirectoryOracle; +} + +/** + * Type guard for directory + * @internal + */ +export function hasSharedDirectroyOracle(s: SharedDirectory): s is ISharedDirectoryWithOracle { + return ( + "sharedDirectoryOracle" in s && s.sharedDirectoryOracle instanceof SharedDirectoryOracle + ); +} From 0145b440e651687483b68463d6372479e8be3d6f Mon Sep 17 00:00:00 2001 From: Sonali Deshpande <48232592+sonalideshpandemsft@users.noreply.github.com> Date: Fri, 12 Sep 2025 19:44:58 +0000 Subject: [PATCH 02/11] wip --- packages/dds/map/src/test/directroyOracle.ts | 78 +++++++++---------- .../src/test/mocha/directoryFuzzTests.spec.ts | 2 +- 2 files changed, 39 insertions(+), 41 deletions(-) diff --git a/packages/dds/map/src/test/directroyOracle.ts b/packages/dds/map/src/test/directroyOracle.ts index 9dfcf8e0af59..239775d4f052 100644 --- a/packages/dds/map/src/test/directroyOracle.ts +++ b/packages/dds/map/src/test/directroyOracle.ts @@ -3,51 +3,50 @@ * Licensed under the MIT License. */ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ + import { strict as assert } from "node:assert"; -import type { SharedDirectory } from "../directoryFactory.js"; import type { IDirectory, IDirectoryValueChanged, ISharedDirectory } from "../interfaces.js"; /** - * Oracle for directroy + * Oracle for directory * @internal */ export class SharedDirectoryOracle { private readonly model = new Map(); - public constructor(private readonly shared: SharedDirectory) { - // Snapshot - this.snapshotDirectory(shared, ""); - - // Subscribe + public constructor(private readonly shared: ISharedDirectory) { this.shared.on("valueChanged", this.onValueChanged); + this.shared.on("clear", this.onClear); this.shared.on("subDirectoryCreated", this.onSubDirCreated); this.shared.on("subDirectoryDeleted", this.onSubDirDeleted); } - private snapshotDirectory(dir: IDirectory, prefix: string): void { - for (const [key, value] of dir.entries()) { - this.model.set(`${prefix}${key}`, value); - } - for (const [subName, subDir] of dir.subdirectories()) { - this.snapshotDirectory(subDir, `${prefix}${subName}/`); - } - } - - private readonly onValueChanged = (change: IDirectoryValueChanged): void => { + private readonly onValueChanged = (change: IDirectoryValueChanged) => { const pathKey = `${change.path}/${change.key}`; - if (this.shared.has(pathKey)) { - this.model.set(pathKey, this.shared.get(pathKey)); - } else { + const dir = this.shared.getWorkingDirectory(change.path); + if (!dir) return; + + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const newValue = dir.get(change.key); + + if (newValue === undefined) { this.model.delete(pathKey); + } else { + this.model.set(pathKey, newValue); } }; - private readonly onSubDirCreated = (subDir: ISharedDirectory, path: string): void => { - this.snapshotDirectory(subDir, `${path}/`); + private readonly onClear = () => { + this.model.clear(); + }; + + private readonly onSubDirCreated = (path: string) => { + // No need to recursively snapshot—subsequent events will populate keys }; - private readonly onSubDirDeleted = (path: string): void => { + private readonly onSubDirDeleted = (path: string) => { for (const key of [...this.model.keys()]) { if (key.startsWith(`${path}/`)) { this.model.delete(key); @@ -56,29 +55,28 @@ export class SharedDirectoryOracle { }; public validate(): void { - // Rebuild snapshot - const actualMap = new Map(); - this.snapshotDirectory(this.shared, ""); - - assert.strictEqual( - actualMap.size, - this.model.size, - `SharedDirectoryOracle mismatch: expected size=${this.model.size}, actual=${actualMap.size}`, - ); - - for (const [key, expectedValue] of this.model.entries()) { - const actualValue = actualMap.get(key); - assert.deepStrictEqual( - expectedValue, - actualValue, - `SharedDirectoryOracle mismatch at path="${key}"`, - ); + // Compare oracle with current shared directory via events + for (const [key, value] of this.model.entries()) { + const parts = key.split("/"); + assert(parts.length === 0, "Invalid path, cannot extract key"); + const leafKey = parts.pop(); + let dir: IDirectory | undefined = this.shared; + for (const part of parts) { + dir = dir.getSubDirectory(part); + if (!dir) break; + } + assert(leafKey !== undefined, "leaf key is undefined"); + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const actual = dir?.get(leafKey); + assert.deepStrictEqual(actual, value, `SharedDirectoryOracle mismatch at path="${key}"`); } } public dispose(): void { this.shared.off("valueChanged", this.onValueChanged); + this.shared.off("clear", this.onClear); this.shared.off("subDirectoryCreated", this.onSubDirCreated); this.shared.off("subDirectoryDeleted", this.onSubDirDeleted); + this.model.clear(); } } diff --git a/packages/dds/map/src/test/mocha/directoryFuzzTests.spec.ts b/packages/dds/map/src/test/mocha/directoryFuzzTests.spec.ts index e0f43de6d1b2..3b6aa1ab7a3e 100644 --- a/packages/dds/map/src/test/mocha/directoryFuzzTests.spec.ts +++ b/packages/dds/map/src/test/mocha/directoryFuzzTests.spec.ts @@ -71,7 +71,7 @@ describe("SharedDirectory fuzz Create/Delete concentrated", () => { stashableClientProbability: 0.2, }, defaultTestCount: 25, - // emitter: oracleEmitter, + emitter: oracleEmitter, // Uncomment this line to replay a specific seed from its failure file: // replay: 21, saveFailures: { directory: dirPath.join(_dirname, "../../../src/test/mocha/results/1") }, From f34b302bc8e041b0629791fa06939070a4ee9338 Mon Sep 17 00:00:00 2001 From: Sonali Deshpande <48232592+sonalideshpandemsft@users.noreply.github.com> Date: Wed, 17 Sep 2025 15:47:14 +0000 Subject: [PATCH 03/11] wip --- packages/dds/map/src/test/directroyOracle.ts | 93 +++++++-- .../src/test/mocha/directoryFuzzTests.spec.ts | 186 ++++++++++-------- packages/dds/map/src/test/mocha/fuzzUtils.ts | 3 +- .../dds/map/src/test/mocha/oracleUtils.ts | 2 +- 4 files changed, 174 insertions(+), 110 deletions(-) diff --git a/packages/dds/map/src/test/directroyOracle.ts b/packages/dds/map/src/test/directroyOracle.ts index 239775d4f052..3d04c321ce22 100644 --- a/packages/dds/map/src/test/directroyOracle.ts +++ b/packages/dds/map/src/test/directroyOracle.ts @@ -7,7 +7,12 @@ import { strict as assert } from "node:assert"; -import type { IDirectory, IDirectoryValueChanged, ISharedDirectory } from "../interfaces.js"; +import type { + IDirectory, + IDirectoryValueChanged, + ISharedDirectory, + IValueChanged, +} from "../interfaces.js"; /** * Oracle for directory @@ -16,20 +21,40 @@ import type { IDirectory, IDirectoryValueChanged, ISharedDirectory } from "../in export class SharedDirectoryOracle { private readonly model = new Map(); - public constructor(private readonly shared: ISharedDirectory) { - this.shared.on("valueChanged", this.onValueChanged); - this.shared.on("clear", this.onClear); - this.shared.on("subDirectoryCreated", this.onSubDirCreated); - this.shared.on("subDirectoryDeleted", this.onSubDirDeleted); + public constructor(private readonly sharedDir: ISharedDirectory) { + this.sharedDir.on("valueChanged", this.onValueChanged); + this.sharedDir.on("clear", this.onClear); + this.sharedDir.on("subDirectoryCreated", this.onSubDirCreated); + this.sharedDir.on("subDirectoryDeleted", this.onSubDirDeleted); + this.sharedDir.on("containedValueChanged", this.onContainedValueChanged); + + this.captureInitialSnapshot(sharedDir); + } + + private captureInitialSnapshot(dir: IDirectory, prefix = ""): void { + // Capture all keys directly in this directory + for (const [key, value] of dir.entries()) { + const pathKey = prefix === "" ? key : `${prefix}/${key}`; + this.model.set(pathKey, value); + } + + // Recursively capture subdirectories + for (const [subDirName, subDir] of dir.subdirectories()) { + const subPrefix = prefix === "" ? subDirName : `${prefix}/${subDirName}`; + this.model.set(`${subPrefix}/`, {}); + this.captureInitialSnapshot(subDir, subPrefix); + } } private readonly onValueChanged = (change: IDirectoryValueChanged) => { - const pathKey = `${change.path}/${change.key}`; - const dir = this.shared.getWorkingDirectory(change.path); + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const { path, key, previousValue } = change; + const pathKey = `${path}/${key}`; + const dir = this.sharedDir.getWorkingDirectory(path); if (!dir) return; // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const newValue = dir.get(change.key); + const newValue = dir.get(key); if (newValue === undefined) { this.model.delete(pathKey); @@ -42,8 +67,16 @@ export class SharedDirectoryOracle { this.model.clear(); }; - private readonly onSubDirCreated = (path: string) => { - // No need to recursively snapshot—subsequent events will populate keys + private readonly onSubDirCreated = ( + subdirName: string, + local: boolean, + target: ISharedDirectory, + ) => { + const pathKey = + target.absolutePath === "" ? subdirName : `${target.absolutePath}/${subdirName}`; + if (!this.model.has(pathKey)) { + this.model.set(`${pathKey}/`, {}); + } }; private readonly onSubDirDeleted = (path: string) => { @@ -54,29 +87,49 @@ export class SharedDirectoryOracle { } }; + private readonly onContainedValueChanged = ( + change: IValueChanged, + local: boolean, + target: IDirectory, + ) => { + const path = target.absolutePath; + const pathKey = path === "" ? change.key : `${path}/${change.key}`; + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const newValue = target.get(change.key); + + if (newValue === undefined) { + this.model.delete(pathKey); + } else { + this.model.set(pathKey, newValue); + } + }; + public validate(): void { // Compare oracle with current shared directory via events for (const [key, value] of this.model.entries()) { const parts = key.split("/"); - assert(parts.length === 0, "Invalid path, cannot extract key"); + assert(parts.length > 0, "Invalid path, cannot extract key"); const leafKey = parts.pop(); - let dir: IDirectory | undefined = this.shared; + let dir: IDirectory | undefined = this.sharedDir; for (const part of parts) { dir = dir.getSubDirectory(part); if (!dir) break; } assert(leafKey !== undefined, "leaf key is undefined"); - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const actual = dir?.get(leafKey); - assert.deepStrictEqual(actual, value, `SharedDirectoryOracle mismatch at path="${key}"`); + assert.deepStrictEqual( + dir?.get(leafKey), + value, + `SharedDirectoryOracle mismatch at path="${key}"`, + ); } } public dispose(): void { - this.shared.off("valueChanged", this.onValueChanged); - this.shared.off("clear", this.onClear); - this.shared.off("subDirectoryCreated", this.onSubDirCreated); - this.shared.off("subDirectoryDeleted", this.onSubDirDeleted); + this.sharedDir.off("valueChanged", this.onValueChanged); + this.sharedDir.off("clear", this.onClear); + this.sharedDir.off("subDirectoryCreated", this.onSubDirCreated); + this.sharedDir.off("subDirectoryDeleted", this.onSubDirDeleted); + this.sharedDir.off("containedValueChanged", this.onContainedValueChanged); this.model.clear(); } } diff --git a/packages/dds/map/src/test/mocha/directoryFuzzTests.spec.ts b/packages/dds/map/src/test/mocha/directoryFuzzTests.spec.ts index 3b6aa1ab7a3e..39f4349ad673 100644 --- a/packages/dds/map/src/test/mocha/directoryFuzzTests.spec.ts +++ b/packages/dds/map/src/test/mocha/directoryFuzzTests.spec.ts @@ -12,7 +12,7 @@ import { type DDSFuzzModel, createDDSFuzzSuite, } from "@fluid-private/test-dds-utils"; -// import { FlushMode } from "@fluidframework/runtime-definitions/internal"; +import { FlushMode } from "@fluidframework/runtime-definitions/internal"; import { DirectoryFactory } from "../../index.js"; import { SharedDirectoryOracle } from "../directroyOracle.js"; @@ -20,14 +20,14 @@ import { SharedDirectoryOracle } from "../directroyOracle.js"; import { assertEquivalentDirectories } from "./directoryEquivalenceUtils.js"; import { _dirname } from "./dirname.cjs"; import { - // baseDirModel, + baseDirModel, dirDefaultOptions, makeDirOperationGenerator, makeDirReducer, type DirOperation, type DirOperationGenerationConfig, } from "./fuzzUtils.js"; -import type { ISharedDirectoryWithOracle } from "./oracleUtils.js"; +import { hasSharedDirectroyOracle, type ISharedDirectoryWithOracle } from "./oracleUtils.js"; const oracleEmitter = new TypedEventEmitter(); @@ -52,7 +52,16 @@ describe("SharedDirectory fuzz Create/Delete concentrated", () => { workloadName: "default directory 1", generatorFactory: () => takeAsync(100, makeDirOperationGenerator(options)), reducer: makeDirReducer({ clientIds: ["A", "B", "C"], printConsoleLogs: false }), - validateConsistency: async (a, b) => assertEquivalentDirectories(a.channel, b.channel), + validateConsistency: async (a, b) => { + if (hasSharedDirectroyOracle(a.channel)) { + a.channel.sharedDirectoryOracle.validate(); + } + + if (hasSharedDirectroyOracle(b.channel)) { + b.channel.sharedDirectoryOracle.validate(); + } + return assertEquivalentDirectories(a.channel, b.channel); + }, factory: new DirectoryFactory(), }; @@ -77,89 +86,92 @@ describe("SharedDirectory fuzz Create/Delete concentrated", () => { saveFailures: { directory: dirPath.join(_dirname, "../../../src/test/mocha/results/1") }, }); - // createDDSFuzzSuite( - // { ...model, workloadName: "default directory 1 with rebasing" }, - // { - // validationStrategy: { - // type: "random", - // probability: 0.4, - // }, - // rebaseProbability: 0.2, - // reconnectProbability: 0.5, - // // We prevent handles from being generated on the creation/deletion tests since the set operations are disabled. - // handleGenerationDisabled: true, - // containerRuntimeOptions: { - // flushMode: FlushMode.TurnBased, - // enableGroupedBatching: true, - // }, - // numberOfClients: 3, - // clientJoinOptions: { - // maxNumberOfClients: 3, - // clientAddProbability: 0.08, - // stashableClientProbability: undefined, - // }, - // defaultTestCount: 200, - // // emitter: oracleEmitter, - // // Uncomment this line to replay a specific seed from its failure file: - // // replay: 0, - // saveFailures: { - // directory: dirPath.join(_dirname, "../../../src/test/mocha/results/1"), - // }, - // }, - // ); + createDDSFuzzSuite( + { ...model, workloadName: "default directory 1 with rebasing" }, + { + validationStrategy: { + type: "random", + probability: 0.4, + }, + rebaseProbability: 0.2, + reconnectProbability: 0.5, + // We prevent handles from being generated on the creation/deletion tests since the set operations are disabled. + handleGenerationDisabled: true, + containerRuntimeOptions: { + flushMode: FlushMode.TurnBased, + enableGroupedBatching: true, + }, + numberOfClients: 3, + clientJoinOptions: { + maxNumberOfClients: 3, + clientAddProbability: 0.08, + stashableClientProbability: undefined, + }, + defaultTestCount: 200, + emitter: oracleEmitter, + // emitter: oracleEmitter, + // Uncomment this line to replay a specific seed from its failure file: + // replay: 0, + saveFailures: { + directory: dirPath.join(_dirname, "../../../src/test/mocha/results/1"), + }, + }, + ); }); -// describe("SharedDirectory fuzz", () => { -// createDDSFuzzSuite(baseDirModel, { -// validationStrategy: { -// type: "fixedInterval", -// interval: dirDefaultOptions.validateInterval, -// }, -// reconnectProbability: 0.15, -// numberOfClients: 3, -// clientJoinOptions: { -// // Note: if tests are slow, we may want to tune this down. This mimics behavior before this suite -// // was refactored to use the DDS fuzz harness. -// maxNumberOfClients: Number.MAX_SAFE_INTEGER, -// clientAddProbability: 0.08, -// stashableClientProbability: 0.2, -// }, -// defaultTestCount: 25, -// // emitter: oracleEmitter, -// // Uncomment this line to replay a specific seed from its failure file: -// // replay: 0, -// saveFailures: { directory: dirPath.join(_dirname, "../../../src/test/mocha/results/2") }, -// skip: [], -// }); +describe("SharedDirectory fuzz", () => { + createDDSFuzzSuite(baseDirModel, { + validationStrategy: { + type: "fixedInterval", + interval: dirDefaultOptions.validateInterval, + }, + reconnectProbability: 0.15, + numberOfClients: 3, + clientJoinOptions: { + // Note: if tests are slow, we may want to tune this down. This mimics behavior before this suite + // was refactored to use the DDS fuzz harness. + maxNumberOfClients: Number.MAX_SAFE_INTEGER, + clientAddProbability: 0.08, + stashableClientProbability: 0.2, + }, + defaultTestCount: 25, + emitter: oracleEmitter, + // emitter: oracleEmitter, + // Uncomment this line to replay a specific seed from its failure file: + // replay: 0, + saveFailures: { directory: dirPath.join(_dirname, "../../../src/test/mocha/results/2") }, + skip: [], + }); -// createDDSFuzzSuite( -// { ...baseDirModel, workloadName: "default directory 2 with rebasing" }, -// { -// validationStrategy: { -// type: "random", -// probability: 0.4, -// }, -// rebaseProbability: 0.2, -// reconnectProbability: 0.5, -// containerRuntimeOptions: { -// flushMode: FlushMode.TurnBased, -// enableGroupedBatching: true, -// }, -// numberOfClients: 3, -// clientJoinOptions: { -// // Note: if tests are slow, we may want to tune this down. This mimics behavior before this suite -// // was refactored to use the DDS fuzz harness. -// maxNumberOfClients: Number.MAX_SAFE_INTEGER, -// clientAddProbability: 0.08, -// stashableClientProbability: undefined, -// }, -// defaultTestCount: 200, -// // emitter: oracleEmitter, -// // Uncomment this line to replay a specific seed from its failure file: -// // replay: 0, -// saveFailures: { -// directory: dirPath.join(_dirname, "../../../src/test/mocha/results/2"), -// }, -// }, -// ); -// }); + createDDSFuzzSuite( + { ...baseDirModel, workloadName: "default directory 2 with rebasing" }, + { + validationStrategy: { + type: "random", + probability: 0.4, + }, + rebaseProbability: 0.2, + reconnectProbability: 0.5, + containerRuntimeOptions: { + flushMode: FlushMode.TurnBased, + enableGroupedBatching: true, + }, + numberOfClients: 3, + clientJoinOptions: { + // Note: if tests are slow, we may want to tune this down. This mimics behavior before this suite + // was refactored to use the DDS fuzz harness. + maxNumberOfClients: Number.MAX_SAFE_INTEGER, + clientAddProbability: 0.08, + stashableClientProbability: undefined, + }, + defaultTestCount: 200, + emitter: oracleEmitter, + // emitter: oracleEmitter, + // Uncomment this line to replay a specific seed from its failure file: + // replay: 0, + saveFailures: { + directory: dirPath.join(_dirname, "../../../src/test/mocha/results/2"), + }, + }, + ); +}); diff --git a/packages/dds/map/src/test/mocha/fuzzUtils.ts b/packages/dds/map/src/test/mocha/fuzzUtils.ts index 5b4596267370..cdcf9f427665 100644 --- a/packages/dds/map/src/test/mocha/fuzzUtils.ts +++ b/packages/dds/map/src/test/mocha/fuzzUtils.ts @@ -526,8 +526,7 @@ export const baseDirModel: DDSFuzzModel = { if (hasSharedDirectroyOracle(b.channel)) { b.channel.sharedDirectoryOracle.validate(); } - // eslint-disable-next-line no-void - void assertEquivalentDirectories(a.channel, b.channel); + return assertEquivalentDirectories(a.channel, b.channel); }, factory: new DirectoryFactory(), minimizationTransforms: [ diff --git a/packages/dds/map/src/test/mocha/oracleUtils.ts b/packages/dds/map/src/test/mocha/oracleUtils.ts index 7763a98759f3..3c21aa7b167c 100644 --- a/packages/dds/map/src/test/mocha/oracleUtils.ts +++ b/packages/dds/map/src/test/mocha/oracleUtils.ts @@ -4,9 +4,9 @@ */ import type { SharedDirectory } from "../../directoryFactory.js"; -import { SharedDirectoryOracle } from "../directroyOracle.js"; import type { ISharedMap } from "../../interfaces.js"; import type { SharedMap } from "../../mapFactory.js"; +import { SharedDirectoryOracle } from "../directroyOracle.js"; import { SharedMapOracle } from "../mapOracle.js"; /** From 6270c9850def8df7d36f2cad836a469466cb1b4c Mon Sep 17 00:00:00 2001 From: Sonali Deshpande <48232592+sonalideshpandemsft@users.noreply.github.com> Date: Wed, 17 Sep 2025 19:37:35 +0000 Subject: [PATCH 04/11] wip --- packages/dds/map/src/test/directroyOracle.ts | 61 +++++++++--------- .../src/test/mocha/directoryFuzzTests.spec.ts | 64 +++++++++---------- 2 files changed, 61 insertions(+), 64 deletions(-) diff --git a/packages/dds/map/src/test/directroyOracle.ts b/packages/dds/map/src/test/directroyOracle.ts index 3d04c321ce22..55d458ba25d8 100644 --- a/packages/dds/map/src/test/directroyOracle.ts +++ b/packages/dds/map/src/test/directroyOracle.ts @@ -31,40 +31,36 @@ export class SharedDirectoryOracle { this.captureInitialSnapshot(sharedDir); } - private captureInitialSnapshot(dir: IDirectory, prefix = ""): void { - // Capture all keys directly in this directory + private captureInitialSnapshot(dir: IDirectory): void { + // Capture keys for (const [key, value] of dir.entries()) { - const pathKey = prefix === "" ? key : `${prefix}/${key}`; - this.model.set(pathKey, value); + this.model.set(`${dir.absolutePath}/${key}`, value); } - // Recursively capture subdirectories - for (const [subDirName, subDir] of dir.subdirectories()) { - const subPrefix = prefix === "" ? subDirName : `${prefix}/${subDirName}`; - this.model.set(`${subPrefix}/`, {}); - this.captureInitialSnapshot(subDir, subPrefix); + for (const [, subDir] of dir.subdirectories()) { + // Just recurse to capture keys inside the subdir + this.captureInitialSnapshot(subDir); } } private readonly onValueChanged = (change: IDirectoryValueChanged) => { - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const { path, key, previousValue } = change; - const pathKey = `${path}/${key}`; - const dir = this.sharedDir.getWorkingDirectory(path); - if (!dir) return; - - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const newValue = dir.get(key); + const { path, key } = change; + const fuzzDir = this.sharedDir.getWorkingDirectory(path); + if (!fuzzDir) return; - if (newValue === undefined) { - this.model.delete(pathKey); + if (fuzzDir.has(key)) { + this.model.set(`${path}/${key}`, fuzzDir.get(key)); } else { - this.model.set(pathKey, newValue); + this.model.delete(`${path}/${key}`); } }; - private readonly onClear = () => { + private readonly onClear = (local: boolean) => { this.model.clear(); + + // if (!local) { + // this.captureInitialSnapshot(this.sharedDir); + // } }; private readonly onSubDirCreated = ( @@ -75,14 +71,14 @@ export class SharedDirectoryOracle { const pathKey = target.absolutePath === "" ? subdirName : `${target.absolutePath}/${subdirName}`; if (!this.model.has(pathKey)) { - this.model.set(`${pathKey}/`, {}); + this.model.set(`${pathKey}/`, undefined); } }; private readonly onSubDirDeleted = (path: string) => { for (const key of [...this.model.keys()]) { if (key.startsWith(`${path}/`)) { - this.model.delete(key); + this.model.delete(`${path}/${key}`); } } }; @@ -92,8 +88,8 @@ export class SharedDirectoryOracle { local: boolean, target: IDirectory, ) => { - const path = target.absolutePath; - const pathKey = path === "" ? change.key : `${path}/${change.key}`; + const pathKey = + target.absolutePath === "" ? change.key : `${target.absolutePath}/${change.key}`; // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const newValue = target.get(change.key); @@ -105,21 +101,24 @@ export class SharedDirectoryOracle { }; public validate(): void { - // Compare oracle with current shared directory via events - for (const [key, value] of this.model.entries()) { - const parts = key.split("/"); + for (const [pathKey, value] of this.model.entries()) { + const parts = pathKey.split("/").filter((p) => p.length > 0); assert(parts.length > 0, "Invalid path, cannot extract key"); + const leafKey = parts.pop(); let dir: IDirectory | undefined = this.sharedDir; for (const part of parts) { dir = dir.getSubDirectory(part); if (!dir) break; } - assert(leafKey !== undefined, "leaf key is undefined"); + + assert(leafKey !== undefined && leafKey.length > 0, "Leaf key is undefined"); + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const actual = dir?.get(leafKey); assert.deepStrictEqual( - dir?.get(leafKey), + actual, value, - `SharedDirectoryOracle mismatch at path="${key}"`, + `SharedDirectoryOracle mismatch at path="${pathKey}" with actual value = ${actual} and oracle value = ${value}}`, ); } } diff --git a/packages/dds/map/src/test/mocha/directoryFuzzTests.spec.ts b/packages/dds/map/src/test/mocha/directoryFuzzTests.spec.ts index 39f4349ad673..27b6a87985f8 100644 --- a/packages/dds/map/src/test/mocha/directoryFuzzTests.spec.ts +++ b/packages/dds/map/src/test/mocha/directoryFuzzTests.spec.ts @@ -136,42 +136,40 @@ describe("SharedDirectory fuzz", () => { }, defaultTestCount: 25, emitter: oracleEmitter, - // emitter: oracleEmitter, // Uncomment this line to replay a specific seed from its failure file: - // replay: 0, + replay: 0, saveFailures: { directory: dirPath.join(_dirname, "../../../src/test/mocha/results/2") }, skip: [], }); - createDDSFuzzSuite( - { ...baseDirModel, workloadName: "default directory 2 with rebasing" }, - { - validationStrategy: { - type: "random", - probability: 0.4, - }, - rebaseProbability: 0.2, - reconnectProbability: 0.5, - containerRuntimeOptions: { - flushMode: FlushMode.TurnBased, - enableGroupedBatching: true, - }, - numberOfClients: 3, - clientJoinOptions: { - // Note: if tests are slow, we may want to tune this down. This mimics behavior before this suite - // was refactored to use the DDS fuzz harness. - maxNumberOfClients: Number.MAX_SAFE_INTEGER, - clientAddProbability: 0.08, - stashableClientProbability: undefined, - }, - defaultTestCount: 200, - emitter: oracleEmitter, - // emitter: oracleEmitter, - // Uncomment this line to replay a specific seed from its failure file: - // replay: 0, - saveFailures: { - directory: dirPath.join(_dirname, "../../../src/test/mocha/results/2"), - }, - }, - ); + // createDDSFuzzSuite( + // { ...baseDirModel, workloadName: "default directory 2 with rebasing" }, + // { + // validationStrategy: { + // type: "random", + // probability: 0.4, + // }, + // rebaseProbability: 0.2, + // reconnectProbability: 0.5, + // containerRuntimeOptions: { + // flushMode: FlushMode.TurnBased, + // enableGroupedBatching: true, + // }, + // numberOfClients: 3, + // clientJoinOptions: { + // // Note: if tests are slow, we may want to tune this down. This mimics behavior before this suite + // // was refactored to use the DDS fuzz harness. + // maxNumberOfClients: Number.MAX_SAFE_INTEGER, + // clientAddProbability: 0.08, + // stashableClientProbability: undefined, + // }, + // defaultTestCount: 200, + // emitter: oracleEmitter, + // // Uncomment this line to replay a specific seed from its failure file: + // // replay: 0, + // saveFailures: { + // directory: dirPath.join(_dirname, "../../../src/test/mocha/results/2"), + // }, + // }, + // ); }); From 8a5cb9753440460d18f200a027bfc9264ca9b3b9 Mon Sep 17 00:00:00 2001 From: Sonali Deshpande <48232592+sonalideshpandemsft@users.noreply.github.com> Date: Wed, 17 Sep 2025 21:55:41 +0000 Subject: [PATCH 05/11] wip --- packages/dds/map/src/test/directroyOracle.ts | 32 +++++----- .../src/test/mocha/directoryFuzzTests.spec.ts | 62 +++++++++---------- 2 files changed, 46 insertions(+), 48 deletions(-) diff --git a/packages/dds/map/src/test/directroyOracle.ts b/packages/dds/map/src/test/directroyOracle.ts index 55d458ba25d8..79489d7d4675 100644 --- a/packages/dds/map/src/test/directroyOracle.ts +++ b/packages/dds/map/src/test/directroyOracle.ts @@ -44,7 +44,8 @@ export class SharedDirectoryOracle { } private readonly onValueChanged = (change: IDirectoryValueChanged) => { - const { path, key } = change; + const { key } = change; + const path = change.path ?? ""; const fuzzDir = this.sharedDir.getWorkingDirectory(path); if (!fuzzDir) return; @@ -57,10 +58,6 @@ export class SharedDirectoryOracle { private readonly onClear = (local: boolean) => { this.model.clear(); - - // if (!local) { - // this.captureInitialSnapshot(this.sharedDir); - // } }; private readonly onSubDirCreated = ( @@ -68,17 +65,19 @@ export class SharedDirectoryOracle { local: boolean, target: ISharedDirectory, ) => { - const pathKey = - target.absolutePath === "" ? subdirName : `${target.absolutePath}/${subdirName}`; - if (!this.model.has(pathKey)) { - this.model.set(`${pathKey}/`, undefined); + if (!this.model.has(`${target.absolutePath}${subdirName}`)) { + this.model.set(`${target.absolutePath}${subdirName}`, undefined); } }; private readonly onSubDirDeleted = (path: string) => { + const absPath = path.startsWith("/") ? path : `/${path}`; for (const key of [...this.model.keys()]) { - if (key.startsWith(`${path}/`)) { - this.model.delete(`${path}/${key}`); + if (key.startsWith(absPath)) { + const deleted = this.model.delete(key); + if (!deleted) { + assert("not deleted"); + } } } }; @@ -88,15 +87,13 @@ export class SharedDirectoryOracle { local: boolean, target: IDirectory, ) => { - const pathKey = - target.absolutePath === "" ? change.key : `${target.absolutePath}/${change.key}`; // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const newValue = target.get(change.key); if (newValue === undefined) { - this.model.delete(pathKey); + this.model.delete(`${target.absolutePath}${change.key}`); } else { - this.model.set(pathKey, newValue); + this.model.set(`${target.absolutePath}${change.key}`, newValue); } }; @@ -105,14 +102,15 @@ export class SharedDirectoryOracle { const parts = pathKey.split("/").filter((p) => p.length > 0); assert(parts.length > 0, "Invalid path, cannot extract key"); - const leafKey = parts.pop(); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const leafKey = parts.pop()!; // The actual key let dir: IDirectory | undefined = this.sharedDir; + for (const part of parts) { dir = dir.getSubDirectory(part); if (!dir) break; } - assert(leafKey !== undefined && leafKey.length > 0, "Leaf key is undefined"); // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const actual = dir?.get(leafKey); assert.deepStrictEqual( diff --git a/packages/dds/map/src/test/mocha/directoryFuzzTests.spec.ts b/packages/dds/map/src/test/mocha/directoryFuzzTests.spec.ts index 27b6a87985f8..ce3795749064 100644 --- a/packages/dds/map/src/test/mocha/directoryFuzzTests.spec.ts +++ b/packages/dds/map/src/test/mocha/directoryFuzzTests.spec.ts @@ -137,39 +137,39 @@ describe("SharedDirectory fuzz", () => { defaultTestCount: 25, emitter: oracleEmitter, // Uncomment this line to replay a specific seed from its failure file: - replay: 0, + only: 13, saveFailures: { directory: dirPath.join(_dirname, "../../../src/test/mocha/results/2") }, skip: [], }); - // createDDSFuzzSuite( - // { ...baseDirModel, workloadName: "default directory 2 with rebasing" }, - // { - // validationStrategy: { - // type: "random", - // probability: 0.4, - // }, - // rebaseProbability: 0.2, - // reconnectProbability: 0.5, - // containerRuntimeOptions: { - // flushMode: FlushMode.TurnBased, - // enableGroupedBatching: true, - // }, - // numberOfClients: 3, - // clientJoinOptions: { - // // Note: if tests are slow, we may want to tune this down. This mimics behavior before this suite - // // was refactored to use the DDS fuzz harness. - // maxNumberOfClients: Number.MAX_SAFE_INTEGER, - // clientAddProbability: 0.08, - // stashableClientProbability: undefined, - // }, - // defaultTestCount: 200, - // emitter: oracleEmitter, - // // Uncomment this line to replay a specific seed from its failure file: - // // replay: 0, - // saveFailures: { - // directory: dirPath.join(_dirname, "../../../src/test/mocha/results/2"), - // }, - // }, - // ); + createDDSFuzzSuite( + { ...baseDirModel, workloadName: "default directory 2 with rebasing" }, + { + validationStrategy: { + type: "random", + probability: 0.4, + }, + rebaseProbability: 0.2, + reconnectProbability: 0.5, + containerRuntimeOptions: { + flushMode: FlushMode.TurnBased, + enableGroupedBatching: true, + }, + numberOfClients: 3, + clientJoinOptions: { + // Note: if tests are slow, we may want to tune this down. This mimics behavior before this suite + // was refactored to use the DDS fuzz harness. + maxNumberOfClients: Number.MAX_SAFE_INTEGER, + clientAddProbability: 0.08, + stashableClientProbability: undefined, + }, + defaultTestCount: 200, + emitter: oracleEmitter, + // Uncomment this line to replay a specific seed from its failure file: + // replay: 0, + saveFailures: { + directory: dirPath.join(_dirname, "../../../src/test/mocha/results/2"), + }, + }, + ); }); From e5b9d7d6d16cb78c61a1009731b60744bda31012 Mon Sep 17 00:00:00 2001 From: Sonali Deshpande <48232592+sonalideshpandemsft@users.noreply.github.com> Date: Thu, 18 Sep 2025 15:28:23 +0000 Subject: [PATCH 06/11] skip seed --- packages/dds/map/src/test/mocha/directoryFuzzTests.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/dds/map/src/test/mocha/directoryFuzzTests.spec.ts b/packages/dds/map/src/test/mocha/directoryFuzzTests.spec.ts index ce3795749064..9202bb205bc2 100644 --- a/packages/dds/map/src/test/mocha/directoryFuzzTests.spec.ts +++ b/packages/dds/map/src/test/mocha/directoryFuzzTests.spec.ts @@ -137,9 +137,9 @@ describe("SharedDirectory fuzz", () => { defaultTestCount: 25, emitter: oracleEmitter, // Uncomment this line to replay a specific seed from its failure file: - only: 13, + // replay: 0, saveFailures: { directory: dirPath.join(_dirname, "../../../src/test/mocha/results/2") }, - skip: [], + skip: [13], }); createDDSFuzzSuite( From 5daccaf1e62613fd1fc47f15b6f169978e6d1751 Mon Sep 17 00:00:00 2001 From: Sonali Deshpande <48232592+sonalideshpandemsft@users.noreply.github.com> Date: Thu, 18 Sep 2025 17:24:22 +0000 Subject: [PATCH 07/11] failing seed --- packages/dds/map/src/test/directroyOracle.ts | 45 ++++++++++++++----- .../src/test/mocha/directoryFuzzTests.spec.ts | 3 +- 2 files changed, 37 insertions(+), 11 deletions(-) diff --git a/packages/dds/map/src/test/directroyOracle.ts b/packages/dds/map/src/test/directroyOracle.ts index 79489d7d4675..8766369bb4f8 100644 --- a/packages/dds/map/src/test/directroyOracle.ts +++ b/packages/dds/map/src/test/directroyOracle.ts @@ -34,7 +34,9 @@ export class SharedDirectoryOracle { private captureInitialSnapshot(dir: IDirectory): void { // Capture keys for (const [key, value] of dir.entries()) { - this.model.set(`${dir.absolutePath}/${key}`, value); + const pathKey = dir.absolutePath === "/" ? `/${key}` : `${dir.absolutePath}/${key}`; + + this.model.set(pathKey, value); } for (const [, subDir] of dir.subdirectories()) { @@ -44,15 +46,25 @@ export class SharedDirectoryOracle { } private readonly onValueChanged = (change: IDirectoryValueChanged) => { - const { key } = change; + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const { key, previousValue } = change; const path = change.path ?? ""; + + const pathKey = path === "/" ? `/${key}` : `${path}/${key}`; + + assert.strictEqual( + previousValue, + this.model.get(pathKey), + `Mismatch on previous value for key="${key}"`, + ); + const fuzzDir = this.sharedDir.getWorkingDirectory(path); if (!fuzzDir) return; if (fuzzDir.has(key)) { - this.model.set(`${path}/${key}`, fuzzDir.get(key)); + this.model.set(pathKey, fuzzDir.get(key)); } else { - this.model.delete(`${path}/${key}`); + this.model.delete(pathKey); } }; @@ -65,8 +77,9 @@ export class SharedDirectoryOracle { local: boolean, target: ISharedDirectory, ) => { - if (!this.model.has(`${target.absolutePath}${subdirName}`)) { - this.model.set(`${target.absolutePath}${subdirName}`, undefined); + const { absolutePath } = target; + if (!this.model.has(`${absolutePath}${subdirName}`)) { + this.model.set(`${absolutePath}${subdirName}`, undefined); } }; @@ -88,12 +101,24 @@ export class SharedDirectoryOracle { target: IDirectory, ) => { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const newValue = target.get(change.key); + const { key, previousValue } = change; + const { absolutePath } = target; + + const pathKey = absolutePath === "/" ? `/${key}` : `${absolutePath}/${key}`; + + assert.strictEqual( + previousValue, + this.model.get(pathKey), + `Mismatch on previous value for key="${key}"`, + ); + + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const newValue = target.get(key); if (newValue === undefined) { - this.model.delete(`${target.absolutePath}${change.key}`); + this.model.delete(pathKey); } else { - this.model.set(`${target.absolutePath}${change.key}`, newValue); + this.model.set(pathKey, newValue); } }; @@ -116,7 +141,7 @@ export class SharedDirectoryOracle { assert.deepStrictEqual( actual, value, - `SharedDirectoryOracle mismatch at path="${pathKey}" with actual value = ${actual} and oracle value = ${value}}`, + `SharedDirectoryOracle mismatch at path="${pathKey}" with actual value = ${actual} and oracle value = ${value} with model entries = ${JSON.stringify(this.model.entries())}}`, ); } } diff --git a/packages/dds/map/src/test/mocha/directoryFuzzTests.spec.ts b/packages/dds/map/src/test/mocha/directoryFuzzTests.spec.ts index 9202bb205bc2..13ff6722c0da 100644 --- a/packages/dds/map/src/test/mocha/directoryFuzzTests.spec.ts +++ b/packages/dds/map/src/test/mocha/directoryFuzzTests.spec.ts @@ -125,6 +125,7 @@ describe("SharedDirectory fuzz", () => { type: "fixedInterval", interval: dirDefaultOptions.validateInterval, }, + skipMinimization: true, reconnectProbability: 0.15, numberOfClients: 3, clientJoinOptions: { @@ -139,7 +140,7 @@ describe("SharedDirectory fuzz", () => { // Uncomment this line to replay a specific seed from its failure file: // replay: 0, saveFailures: { directory: dirPath.join(_dirname, "../../../src/test/mocha/results/2") }, - skip: [13], + skip: [], }); createDDSFuzzSuite( From b92a838f6d6841b1ac9b4214330715b1b85ad9ba Mon Sep 17 00:00:00 2001 From: Sonali Deshpande <48232592+sonalideshpandemsft@users.noreply.github.com> Date: Thu, 18 Sep 2025 23:47:53 +0000 Subject: [PATCH 08/11] rename file --- .../dds/map/src/test/{directroyOracle.ts => directoryOracle.ts} | 0 packages/dds/map/src/test/mocha/directoryFuzzTests.spec.ts | 2 +- packages/dds/map/src/test/mocha/oracleUtils.ts | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename packages/dds/map/src/test/{directroyOracle.ts => directoryOracle.ts} (100%) diff --git a/packages/dds/map/src/test/directroyOracle.ts b/packages/dds/map/src/test/directoryOracle.ts similarity index 100% rename from packages/dds/map/src/test/directroyOracle.ts rename to packages/dds/map/src/test/directoryOracle.ts diff --git a/packages/dds/map/src/test/mocha/directoryFuzzTests.spec.ts b/packages/dds/map/src/test/mocha/directoryFuzzTests.spec.ts index 13ff6722c0da..6506d2ba15c3 100644 --- a/packages/dds/map/src/test/mocha/directoryFuzzTests.spec.ts +++ b/packages/dds/map/src/test/mocha/directoryFuzzTests.spec.ts @@ -15,7 +15,7 @@ import { import { FlushMode } from "@fluidframework/runtime-definitions/internal"; import { DirectoryFactory } from "../../index.js"; -import { SharedDirectoryOracle } from "../directroyOracle.js"; +import { SharedDirectoryOracle } from "../directoryOracle.js"; import { assertEquivalentDirectories } from "./directoryEquivalenceUtils.js"; import { _dirname } from "./dirname.cjs"; diff --git a/packages/dds/map/src/test/mocha/oracleUtils.ts b/packages/dds/map/src/test/mocha/oracleUtils.ts index 3c21aa7b167c..09381b26b473 100644 --- a/packages/dds/map/src/test/mocha/oracleUtils.ts +++ b/packages/dds/map/src/test/mocha/oracleUtils.ts @@ -6,7 +6,7 @@ import type { SharedDirectory } from "../../directoryFactory.js"; import type { ISharedMap } from "../../interfaces.js"; import type { SharedMap } from "../../mapFactory.js"; -import { SharedDirectoryOracle } from "../directroyOracle.js"; +import { SharedDirectoryOracle } from "../directoryOracle.js"; import { SharedMapOracle } from "../mapOracle.js"; /** From 85c3d4295e562824faf945874f716338f219f69b Mon Sep 17 00:00:00 2001 From: Sonali Deshpande <48232592+sonalideshpandemsft@users.noreply.github.com> Date: Wed, 24 Sep 2025 15:49:30 +0000 Subject: [PATCH 09/11] wip --- packages/dds/map/src/test/directoryOracle.ts | 123 ++++++++---------- .../src/test/mocha/directoryFuzzTests.spec.ts | 4 +- 2 files changed, 58 insertions(+), 69 deletions(-) diff --git a/packages/dds/map/src/test/directoryOracle.ts b/packages/dds/map/src/test/directoryOracle.ts index 8766369bb4f8..74b06c7cb3f6 100644 --- a/packages/dds/map/src/test/directoryOracle.ts +++ b/packages/dds/map/src/test/directoryOracle.ts @@ -2,11 +2,10 @@ * Copyright (c) Microsoft Corporation and contributors. All rights reserved. * Licensed under the MIT License. */ - -/* eslint-disable @typescript-eslint/explicit-function-return-type */ - import { strict as assert } from "node:assert"; +import type { IEventThisPlaceHolder } from "@fluidframework/core-interfaces"; + import type { IDirectory, IDirectoryValueChanged, @@ -28,97 +27,87 @@ export class SharedDirectoryOracle { this.sharedDir.on("subDirectoryDeleted", this.onSubDirDeleted); this.sharedDir.on("containedValueChanged", this.onContainedValueChanged); - this.captureInitialSnapshot(sharedDir); + this.takeSnapshot(sharedDir); } - private captureInitialSnapshot(dir: IDirectory): void { - // Capture keys - for (const [key, value] of dir.entries()) { - const pathKey = dir.absolutePath === "/" ? `/${key}` : `${dir.absolutePath}/${key}`; - - this.model.set(pathKey, value); - } - - for (const [, subDir] of dir.subdirectories()) { - // Just recurse to capture keys inside the subdir - this.captureInitialSnapshot(subDir); + private takeSnapshot(dir: ISharedDirectory | IDirectory): void { + for (const [k, v] of this.sharedDir.entries()) { + this.model.set(k, v); } } - private readonly onValueChanged = (change: IDirectoryValueChanged) => { + private readonly onValueChanged = ( + changed: IDirectoryValueChanged, + local: boolean, + target: IEventThisPlaceHolder, + ): void => { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const { key, previousValue } = change; - const path = change.path ?? ""; - - const pathKey = path === "/" ? `/${key}` : `${path}/${key}`; - - assert.strictEqual( - previousValue, - this.model.get(pathKey), - `Mismatch on previous value for key="${key}"`, - ); + const { path, key, previousValue } = changed; + const fullPath = path === "/" ? `/${key}` : `${path}/${key}`; + + if (this.model.has(fullPath)) { + const prevVal = this.model.get(fullPath); + assert.strictEqual( + prevVal, + previousValue, + `previous value mismatch at ${fullPath}: expected: ${prevVal}, actual: ${previousValue}`, + ); + } - const fuzzDir = this.sharedDir.getWorkingDirectory(path); - if (!fuzzDir) return; + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const newVal = this.sharedDir.get(fullPath); - if (fuzzDir.has(key)) { - this.model.set(pathKey, fuzzDir.get(key)); + if (newVal === undefined) { + // deletion + this.model.delete(fullPath); } else { - this.model.delete(pathKey); + this.model.set(fullPath, newVal); } }; - private readonly onClear = (local: boolean) => { + private readonly onClear = (local: boolean, target: IEventThisPlaceHolder): void => { this.model.clear(); }; private readonly onSubDirCreated = ( - subdirName: string, + path: string, local: boolean, - target: ISharedDirectory, - ) => { - const { absolutePath } = target; - if (!this.model.has(`${absolutePath}${subdirName}`)) { - this.model.set(`${absolutePath}${subdirName}`, undefined); - } + target: IEventThisPlaceHolder, + ): void => { + this.model.set(path, undefined); }; - private readonly onSubDirDeleted = (path: string) => { - const absPath = path.startsWith("/") ? path : `/${path}`; - for (const key of [...this.model.keys()]) { - if (key.startsWith(absPath)) { - const deleted = this.model.delete(key); - if (!deleted) { - assert("not deleted"); - } - } - } + private readonly onSubDirDeleted = ( + path: string, + local: boolean, + target: IEventThisPlaceHolder, + ): void => { + this.model.delete(path); }; private readonly onContainedValueChanged = ( - change: IValueChanged, + changed: IValueChanged, local: boolean, - target: IDirectory, - ) => { + target: IEventThisPlaceHolder, + ): void => { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const { key, previousValue } = change; - const { absolutePath } = target; - - const pathKey = absolutePath === "/" ? `/${key}` : `${absolutePath}/${key}`; - - assert.strictEqual( - previousValue, - this.model.get(pathKey), - `Mismatch on previous value for key="${key}"`, - ); + const { key, previousValue } = changed; + + if (this.model.has(key)) { + const prevVal = this.model.get(key); + assert.strictEqual( + prevVal, + previousValue, + `contained previous value mismatch at ${key}: expected: ${prevVal}, actual: ${previousValue}`, + ); + } // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const newValue = target.get(key); - - if (newValue === undefined) { - this.model.delete(pathKey); + const newVal = this.sharedDir.get(key); + if (newVal === undefined) { + this.model.delete(key); } else { - this.model.set(pathKey, newValue); + this.model.set(key, newVal); } }; diff --git a/packages/dds/map/src/test/mocha/directoryFuzzTests.spec.ts b/packages/dds/map/src/test/mocha/directoryFuzzTests.spec.ts index 6506d2ba15c3..bf912d852b4c 100644 --- a/packages/dds/map/src/test/mocha/directoryFuzzTests.spec.ts +++ b/packages/dds/map/src/test/mocha/directoryFuzzTests.spec.ts @@ -11,6 +11,7 @@ import { type DDSFuzzHarnessEvents, type DDSFuzzModel, createDDSFuzzSuite, + registerOracle, } from "@fluid-private/test-dds-utils"; import { FlushMode } from "@fluidframework/runtime-definitions/internal"; @@ -35,6 +36,7 @@ oracleEmitter.on("clientCreate", (client) => { const channel = client.channel as ISharedDirectoryWithOracle; const directroyOracle = new SharedDirectoryOracle(channel); channel.sharedDirectoryOracle = directroyOracle; + registerOracle(directroyOracle); }); describe("SharedDirectory fuzz Create/Delete concentrated", () => { @@ -109,7 +111,6 @@ describe("SharedDirectory fuzz Create/Delete concentrated", () => { }, defaultTestCount: 200, emitter: oracleEmitter, - // emitter: oracleEmitter, // Uncomment this line to replay a specific seed from its failure file: // replay: 0, saveFailures: { @@ -125,7 +126,6 @@ describe("SharedDirectory fuzz", () => { type: "fixedInterval", interval: dirDefaultOptions.validateInterval, }, - skipMinimization: true, reconnectProbability: 0.15, numberOfClients: 3, clientJoinOptions: { From 1e0cfacd9d4d72d2e79953ee2a1179675cd676bf Mon Sep 17 00:00:00 2001 From: Sonali Deshpande <48232592+sonalideshpandemsft@users.noreply.github.com> Date: Wed, 24 Sep 2025 20:01:03 +0000 Subject: [PATCH 10/11] fix ci --- packages/dds/map/src/test/directoryOracle.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/dds/map/src/test/directoryOracle.ts b/packages/dds/map/src/test/directoryOracle.ts index 74b06c7cb3f6..e38c5b77da4b 100644 --- a/packages/dds/map/src/test/directoryOracle.ts +++ b/packages/dds/map/src/test/directoryOracle.ts @@ -2,6 +2,7 @@ * Copyright (c) Microsoft Corporation and contributors. All rights reserved. * Licensed under the MIT License. */ + import { strict as assert } from "node:assert"; import type { IEventThisPlaceHolder } from "@fluidframework/core-interfaces"; From 2ba78e1ddeffc33c8fa5e3df35795bf588c89631 Mon Sep 17 00:00:00 2001 From: Sonali Deshpande <48232592+sonalideshpandemsft@users.noreply.github.com> Date: Mon, 29 Sep 2025 15:12:41 +0000 Subject: [PATCH 11/11] feedback --- packages/dds/map/src/test/directoryOracle.ts | 31 +++++++++---------- .../src/test/mocha/directoryFuzzTests.spec.ts | 12 +++---- packages/dds/map/src/test/mocha/fuzzUtils.ts | 6 ++-- .../dds/map/src/test/mocha/oracleUtils.ts | 2 +- 4 files changed, 24 insertions(+), 27 deletions(-) diff --git a/packages/dds/map/src/test/directoryOracle.ts b/packages/dds/map/src/test/directoryOracle.ts index e38c5b77da4b..201c67357ce6 100644 --- a/packages/dds/map/src/test/directoryOracle.ts +++ b/packages/dds/map/src/test/directoryOracle.ts @@ -14,6 +14,11 @@ import type { IValueChanged, } from "../interfaces.js"; +interface OracleDir { + keys: Map; + subdirs: Map; +} + /** * Oracle for directory * @internal @@ -28,10 +33,10 @@ export class SharedDirectoryOracle { this.sharedDir.on("subDirectoryDeleted", this.onSubDirDeleted); this.sharedDir.on("containedValueChanged", this.onContainedValueChanged); - this.takeSnapshot(sharedDir); + this.takeSnapshot(); } - private takeSnapshot(dir: ISharedDirectory | IDirectory): void { + private takeSnapshot(): void { for (const [k, v] of this.sharedDir.entries()) { this.model.set(k, v); } @@ -55,8 +60,9 @@ export class SharedDirectoryOracle { ); } + const workingDir = this.sharedDir.getWorkingDirectory(fullPath); // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const newVal = this.sharedDir.get(fullPath); + const newVal = workingDir?.get(key); if (newVal === undefined) { // deletion @@ -89,22 +95,13 @@ export class SharedDirectoryOracle { private readonly onContainedValueChanged = ( changed: IValueChanged, local: boolean, - target: IEventThisPlaceHolder, + target, ): void => { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const { key, previousValue } = changed; - if (this.model.has(key)) { - const prevVal = this.model.get(key); - assert.strictEqual( - prevVal, - previousValue, - `contained previous value mismatch at ${key}: expected: ${prevVal}, actual: ${previousValue}`, - ); - } - - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const newVal = this.sharedDir.get(key); + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call + const newVal = target.get(key); if (newVal === undefined) { this.model.delete(key); } else { @@ -122,7 +119,7 @@ export class SharedDirectoryOracle { let dir: IDirectory | undefined = this.sharedDir; for (const part of parts) { - dir = dir.getSubDirectory(part); + dir = dir.getWorkingDirectory(part); if (!dir) break; } @@ -131,7 +128,7 @@ export class SharedDirectoryOracle { assert.deepStrictEqual( actual, value, - `SharedDirectoryOracle mismatch at path="${pathKey}" with actual value = ${actual} and oracle value = ${value} with model entries = ${JSON.stringify(this.model.entries())}}`, + `SharedDirectoryOracle mismatch at path="${pathKey}" with actual value = ${actual} and oracle value = ${value}}}`, ); } } diff --git a/packages/dds/map/src/test/mocha/directoryFuzzTests.spec.ts b/packages/dds/map/src/test/mocha/directoryFuzzTests.spec.ts index bf912d852b4c..9b7845c6f558 100644 --- a/packages/dds/map/src/test/mocha/directoryFuzzTests.spec.ts +++ b/packages/dds/map/src/test/mocha/directoryFuzzTests.spec.ts @@ -28,15 +28,15 @@ import { type DirOperation, type DirOperationGenerationConfig, } from "./fuzzUtils.js"; -import { hasSharedDirectroyOracle, type ISharedDirectoryWithOracle } from "./oracleUtils.js"; +import { hasSharedDirectoryOracle, type ISharedDirectoryWithOracle } from "./oracleUtils.js"; const oracleEmitter = new TypedEventEmitter(); oracleEmitter.on("clientCreate", (client) => { const channel = client.channel as ISharedDirectoryWithOracle; - const directroyOracle = new SharedDirectoryOracle(channel); - channel.sharedDirectoryOracle = directroyOracle; - registerOracle(directroyOracle); + const directoryOracle = new SharedDirectoryOracle(channel); + channel.sharedDirectoryOracle = directoryOracle; + registerOracle(directoryOracle); }); describe("SharedDirectory fuzz Create/Delete concentrated", () => { @@ -55,11 +55,11 @@ describe("SharedDirectory fuzz Create/Delete concentrated", () => { generatorFactory: () => takeAsync(100, makeDirOperationGenerator(options)), reducer: makeDirReducer({ clientIds: ["A", "B", "C"], printConsoleLogs: false }), validateConsistency: async (a, b) => { - if (hasSharedDirectroyOracle(a.channel)) { + if (hasSharedDirectoryOracle(a.channel)) { a.channel.sharedDirectoryOracle.validate(); } - if (hasSharedDirectroyOracle(b.channel)) { + if (hasSharedDirectoryOracle(b.channel)) { b.channel.sharedDirectoryOracle.validate(); } return assertEquivalentDirectories(a.channel, b.channel); diff --git a/packages/dds/map/src/test/mocha/fuzzUtils.ts b/packages/dds/map/src/test/mocha/fuzzUtils.ts index cdcf9f427665..ceb5f1ba37f7 100644 --- a/packages/dds/map/src/test/mocha/fuzzUtils.ts +++ b/packages/dds/map/src/test/mocha/fuzzUtils.ts @@ -29,7 +29,7 @@ import { } from "../../index.js"; import { assertEquivalentDirectories } from "./directoryEquivalenceUtils.js"; -import { hasSharedMapOracle, hasSharedDirectroyOracle } from "./oracleUtils.js"; +import { hasSharedMapOracle, hasSharedDirectoryOracle } from "./oracleUtils.js"; /** * Represents a map clear operation. @@ -519,11 +519,11 @@ export const baseDirModel: DDSFuzzModel = { generatorFactory: () => takeAsync(100, makeDirOperationGenerator(dirDefaultOptions)), reducer: makeDirReducer({ clientIds: ["A", "B", "C"], printConsoleLogs: false }), validateConsistency: async (a, b) => { - if (hasSharedDirectroyOracle(a.channel)) { + if (hasSharedDirectoryOracle(a.channel)) { a.channel.sharedDirectoryOracle.validate(); } - if (hasSharedDirectroyOracle(b.channel)) { + if (hasSharedDirectoryOracle(b.channel)) { b.channel.sharedDirectoryOracle.validate(); } return assertEquivalentDirectories(a.channel, b.channel); diff --git a/packages/dds/map/src/test/mocha/oracleUtils.ts b/packages/dds/map/src/test/mocha/oracleUtils.ts index 09381b26b473..f40ba6baf62a 100644 --- a/packages/dds/map/src/test/mocha/oracleUtils.ts +++ b/packages/dds/map/src/test/mocha/oracleUtils.ts @@ -27,7 +27,7 @@ export interface ISharedMapWithOracle extends ISharedMap { * Type guard for directory * @internal */ -export function hasSharedDirectroyOracle(s: SharedDirectory): s is ISharedDirectoryWithOracle { +export function hasSharedDirectoryOracle(s: SharedDirectory): s is ISharedDirectoryWithOracle { return ( "sharedDirectoryOracle" in s && s.sharedDirectoryOracle instanceof SharedDirectoryOracle );