Skip to content
144 changes: 144 additions & 0 deletions packages/dds/map/src/test/directoryOracle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
/*!
* 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";

import type {
IDirectory,
IDirectoryValueChanged,
ISharedDirectory,
IValueChanged,
} from "../interfaces.js";

interface OracleDir {
keys: Map<string, unknown>;
subdirs: Map<string, OracleDir>;
}

/**
* Oracle for directory
* @internal
*/
export class SharedDirectoryOracle {
private readonly model = new Map<string, unknown>();

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.takeSnapshot();
}

private takeSnapshot(): void {
for (const [k, v] of this.sharedDir.entries()) {
this.model.set(k, v);
}
}

private readonly onValueChanged = (
changed: IDirectoryValueChanged,
local: boolean,
target: IEventThisPlaceHolder,
): void => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
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 workingDir = this.sharedDir.getWorkingDirectory(fullPath);
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const newVal = workingDir?.get(key);

if (newVal === undefined) {
// deletion
this.model.delete(fullPath);
} else {
this.model.set(fullPath, newVal);
}
};

private readonly onClear = (local: boolean, target: IEventThisPlaceHolder): void => {
this.model.clear();
};

private readonly onSubDirCreated = (
path: string,
local: boolean,
target: IEventThisPlaceHolder,
): void => {
this.model.set(path, undefined);
};

private readonly onSubDirDeleted = (
path: string,
local: boolean,
target: IEventThisPlaceHolder,
): void => {
this.model.delete(path);
};

private readonly onContainedValueChanged = (
changed: IValueChanged,
local: boolean,
target,
): void => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const { key, previousValue } = changed;

// 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 {
this.model.set(key, newVal);
}
};

public validate(): void {
for (const [pathKey, value] of this.model.entries()) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i'm not sure this is correct. i think entries just returns the keys of the directory and you have to use subdirectories to get the subdirectories

Copy link
Contributor Author

@sonalideshpandemsft sonalideshpandemsft Sep 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

model map stores subdirectories as separate keys, so each subdirectory has its own entry

nvm - subdirs are not pushed to the snapshot. looking into it...

const parts = pathKey.split("/").filter((p) => p.length > 0);
assert(parts.length > 0, "Invalid path, cannot extract key");
Copy link

Copilot AI Sep 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use a string literal for the assert error message instead of a descriptive string

Copilot generated this review using guidance from repository custom instructions.

// 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.getWorkingDirectory(part);
if (!dir) break;
}

// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const actual = dir?.get(leafKey);
assert.deepStrictEqual(
actual,
value,
`SharedDirectoryOracle mismatch at path="${pathKey}" with actual value = ${actual} and oracle value = ${value}}}`,
);
}
}

public dispose(): void {
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();
}
}
34 changes: 32 additions & 2 deletions packages/dds/map/src/test/mocha/directoryFuzzTests.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,18 @@

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 {
type DDSFuzzHarnessEvents,
type DDSFuzzModel,
createDDSFuzzSuite,
registerOracle,
} from "@fluid-private/test-dds-utils";
import { FlushMode } from "@fluidframework/runtime-definitions/internal";

import { DirectoryFactory } from "../../index.js";
import { SharedDirectoryOracle } from "../directoryOracle.js";

import { assertEquivalentDirectories } from "./directoryEquivalenceUtils.js";
import { _dirname } from "./dirname.cjs";
Expand All @@ -21,6 +28,16 @@ import {
type DirOperation,
type DirOperationGenerationConfig,
} from "./fuzzUtils.js";
import { hasSharedDirectoryOracle, type ISharedDirectoryWithOracle } from "./oracleUtils.js";

const oracleEmitter = new TypedEventEmitter<DDSFuzzHarnessEvents>();

oracleEmitter.on("clientCreate", (client) => {
const channel = client.channel as ISharedDirectoryWithOracle;
const directoryOracle = new SharedDirectoryOracle(channel);
channel.sharedDirectoryOracle = directoryOracle;
registerOracle(directoryOracle);
});

describe("SharedDirectory fuzz Create/Delete concentrated", () => {
const options: DirOperationGenerationConfig = {
Expand All @@ -37,7 +54,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 (hasSharedDirectoryOracle(a.channel)) {
a.channel.sharedDirectoryOracle.validate();
}

if (hasSharedDirectoryOracle(b.channel)) {
b.channel.sharedDirectoryOracle.validate();
}
return assertEquivalentDirectories(a.channel, b.channel);
},
factory: new DirectoryFactory(),
};

Expand All @@ -56,6 +82,7 @@ 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") },
Expand Down Expand Up @@ -83,6 +110,7 @@ describe("SharedDirectory fuzz Create/Delete concentrated", () => {
stashableClientProbability: undefined,
},
defaultTestCount: 200,
emitter: oracleEmitter,
// Uncomment this line to replay a specific seed from its failure file:
// replay: 0,
saveFailures: {
Expand All @@ -108,6 +136,7 @@ describe("SharedDirectory fuzz", () => {
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") },
Expand Down Expand Up @@ -136,6 +165,7 @@ describe("SharedDirectory fuzz", () => {
stashableClientProbability: undefined,
},
defaultTestCount: 200,
emitter: oracleEmitter,
// Uncomment this line to replay a specific seed from its failure file:
// replay: 0,
saveFailures: {
Expand Down
13 changes: 11 additions & 2 deletions packages/dds/map/src/test/mocha/fuzzUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import {
} from "../../index.js";

import { assertEquivalentDirectories } from "./directoryEquivalenceUtils.js";
import { hasSharedMapOracle } from "./oracleUtils.js";
import { hasSharedMapOracle, hasSharedDirectoryOracle } from "./oracleUtils.js";

/**
* Represents a map clear operation.
Expand Down Expand Up @@ -518,7 +518,16 @@ export const baseDirModel: DDSFuzzModel<DirectoryFactory, DirOperation> = {
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 (hasSharedDirectoryOracle(a.channel)) {
a.channel.sharedDirectoryOracle.validate();
}

if (hasSharedDirectoryOracle(b.channel)) {
b.channel.sharedDirectoryOracle.validate();
}
return assertEquivalentDirectories(a.channel, b.channel);
},
factory: new DirectoryFactory(),
minimizationTransforms: [
(op: DirOperation): void => {
Expand Down
19 changes: 19 additions & 0 deletions packages/dds/map/src/test/mocha/oracleUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,36 @@
* Licensed under the MIT License.
*/

import type { SharedDirectory } from "../../directoryFactory.js";
import type { ISharedMap } from "../../interfaces.js";
import type { SharedMap } from "../../mapFactory.js";
import { SharedDirectoryOracle } from "../directoryOracle.js";
import { SharedMapOracle } from "../mapOracle.js";

/**
* @internal
*/
export interface ISharedDirectoryWithOracle extends SharedDirectory {
sharedDirectoryOracle: SharedDirectoryOracle;
}

/**
* @internal
*/
export interface ISharedMapWithOracle extends ISharedMap {
sharedMapOracle: SharedMapOracle;
}

/**
* Type guard for directory
* @internal
*/
export function hasSharedDirectoryOracle(s: SharedDirectory): s is ISharedDirectoryWithOracle {
return (
"sharedDirectoryOracle" in s && s.sharedDirectoryOracle instanceof SharedDirectoryOracle
);
}

/**
* Type guard for map
* @internal
Expand Down
Loading