Fix failure
This commit is contained in:
parent
e151208e15
commit
909096969a
6 changed files with 95 additions and 32 deletions
|
|
@ -19,7 +19,9 @@ metadata, visible/durable disk images and CAS requests before cleanup.
|
||||||
|
|
||||||
## Workload and oracle
|
## Workload and oracle
|
||||||
|
|
||||||
Version 2 traces contain an editor-intent program and its compiled steps.
|
Version 2 traces store the editor-intent program once. Replay compiles its steps
|
||||||
|
and full-state checkpoints, avoiding repeated file contents in large artifacts.
|
||||||
|
Older version 2 traces with compiled steps still replay from their program.
|
||||||
`model.ts` independently interprets that intent into complete expected file sets:
|
`model.ts` independently interprets that intent into complete expected file sets:
|
||||||
exact bytes, paths and persistent identity keys. It imports no production merge
|
exact bytes, paths and persistent identity keys. It imports no production merge
|
||||||
implementation and never adopts observed output as the expected state. Deleted
|
implementation and never adopts observed output as the expected state. Deleted
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,12 @@ import { Logger } from "sync-client";
|
||||||
import { ServerControl } from "../../deterministic-tests/src/server-control";
|
import { ServerControl } from "../../deterministic-tests/src/server-control";
|
||||||
import { ServerManager } from "../../deterministic-tests/src/server-manager";
|
import { ServerManager } from "../../deterministic-tests/src/server-manager";
|
||||||
import { TestRunner } from "../../deterministic-tests/src/test-runner";
|
import { TestRunner } from "../../deterministic-tests/src/test-runner";
|
||||||
import { generateTrace, traceTest, type Trace } from "./workload";
|
import {
|
||||||
import { compileProgram } from "./model";
|
generateTrace,
|
||||||
|
serializeTrace,
|
||||||
|
traceTest,
|
||||||
|
type Trace
|
||||||
|
} from "./workload";
|
||||||
import { minimizeProgram } from "./minimize";
|
import { minimizeProgram } from "./minimize";
|
||||||
|
|
||||||
function integer(value: string, zero = false): number {
|
function integer(value: string, zero = false): number {
|
||||||
|
|
@ -44,8 +48,7 @@ async function main(): Promise<void> {
|
||||||
: fs.mkdtempSync(path.join(os.tmpdir(), "vault-link-fuzz-"));
|
: fs.mkdtempSync(path.join(os.tmpdir(), "vault-link-fuzz-"));
|
||||||
fs.mkdirSync(artifacts, { recursive: true });
|
fs.mkdirSync(artifacts, { recursive: true });
|
||||||
const tracePath = path.join(artifacts, `trace-${trace.seed}.json`);
|
const tracePath = path.join(artifacts, `trace-${trace.seed}.json`);
|
||||||
if (trace.version === 2) trace.steps = compileProgram(trace.program!);
|
fs.writeFileSync(tracePath, serializeTrace(trace));
|
||||||
fs.writeFileSync(tracePath, JSON.stringify(trace, null, 2));
|
|
||||||
console.log(`Replay: node ${__filename} --replay ${tracePath}`);
|
console.log(`Replay: node ${__filename} --replay ${tracePath}`);
|
||||||
const logger = new Logger();
|
const logger = new Logger();
|
||||||
logger.onLogEmitted.add((line) => console.log(line.message));
|
logger.onLogEmitted.add((line) => console.log(line.message));
|
||||||
|
|
@ -72,7 +75,7 @@ async function main(): Promise<void> {
|
||||||
JSON.stringify(result, null, 2)
|
JSON.stringify(result, null, 2)
|
||||||
);
|
);
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
if (trace.program && !values["no-shrink"]) {
|
if (trace.version === 2 && !values["no-shrink"]) {
|
||||||
try {
|
try {
|
||||||
const reduced = await minimizeProgram(
|
const reduced = await minimizeProgram(
|
||||||
trace.program,
|
trace.program,
|
||||||
|
|
@ -82,8 +85,7 @@ async function main(): Promise<void> {
|
||||||
await server.start();
|
await server.start();
|
||||||
const candidate = {
|
const candidate = {
|
||||||
...trace,
|
...trace,
|
||||||
program,
|
program
|
||||||
steps: compileProgram(program)
|
|
||||||
};
|
};
|
||||||
const outcome = await new TestRunner(
|
const outcome = await new TestRunner(
|
||||||
server,
|
server,
|
||||||
|
|
@ -104,17 +106,13 @@ async function main(): Promise<void> {
|
||||||
);
|
);
|
||||||
const minimized = {
|
const minimized = {
|
||||||
...trace,
|
...trace,
|
||||||
program: reduced.program,
|
program: reduced.program
|
||||||
steps: compileProgram(reduced.program)
|
|
||||||
};
|
};
|
||||||
const minimizedPath = path.join(
|
const minimizedPath = path.join(
|
||||||
artifacts,
|
artifacts,
|
||||||
`minimized-${trace.seed}.json`
|
`minimized-${trace.seed}.json`
|
||||||
);
|
);
|
||||||
fs.writeFileSync(
|
fs.writeFileSync(minimizedPath, serializeTrace(minimized));
|
||||||
minimizedPath,
|
|
||||||
JSON.stringify(minimized, null, 2)
|
|
||||||
);
|
|
||||||
console.log(
|
console.log(
|
||||||
`Reduced in ${reduced.attempts} attempts: node ${__filename} --replay ${minimizedPath}`
|
`Reduced in ${reduced.attempts} attempts: node ${__filename} --replay ${minimizedPath}`
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,42 @@
|
||||||
import assert from "node:assert/strict";
|
import assert from "node:assert/strict";
|
||||||
import { test } from "node:test";
|
import { test } from "node:test";
|
||||||
import { generateTrace, traceTest } from "./workload";
|
import { generateTrace, serializeTrace, traceTest } from "./workload";
|
||||||
import { compileProgram, type Program } from "./model";
|
import { compileProgram, type Program } from "./model";
|
||||||
import { minimizeProgram } from "./minimize";
|
import { minimizeProgram } from "./minimize";
|
||||||
|
|
||||||
|
test("seed 1329 at 500 iterations serializes a compact, complete replay", () => {
|
||||||
|
const trace = generateTrace(1329, 500);
|
||||||
|
const serialized = serializeTrace(trace);
|
||||||
|
assert.equal(serialized, JSON.stringify(trace, null, 2));
|
||||||
|
assert(
|
||||||
|
serialized.length < 10_000_000,
|
||||||
|
"Trace should store editor intent once"
|
||||||
|
);
|
||||||
|
const replay: unknown = JSON.parse(serialized);
|
||||||
|
assert.deepEqual(replay, trace);
|
||||||
|
const { steps } = traceTest(replay);
|
||||||
|
assert.equal(steps.length, 11883);
|
||||||
|
assert.equal(
|
||||||
|
steps.filter((step) => step.type === "assert-documents").length,
|
||||||
|
720
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("older version 2 traces discard compiled steps when saved and replay from intent", () => {
|
||||||
|
const trace = generateTrace(0, 1);
|
||||||
|
const expected = traceTest(trace);
|
||||||
|
const oldTrace = { ...trace, steps: expected.steps };
|
||||||
|
const serialized = serializeTrace(oldTrace);
|
||||||
|
assert.equal(serialized, serializeTrace(trace));
|
||||||
|
assert.deepEqual(traceTest(JSON.parse(serialized)), expected);
|
||||||
|
assert.equal(
|
||||||
|
oldTrace.steps,
|
||||||
|
expected.steps,
|
||||||
|
"Saving must not mutate the trace"
|
||||||
|
);
|
||||||
|
assert.deepEqual(traceTest({ ...oldTrace, steps: [] }), expected);
|
||||||
|
});
|
||||||
|
|
||||||
test("stateful programs replay primitive batches, crash boundaries, byte domains and conflict histories", () => {
|
test("stateful programs replay primitive batches, crash boundaries, byte domains and conflict histories", () => {
|
||||||
const seen = new Set<string>();
|
const seen = new Set<string>();
|
||||||
let large = false,
|
let large = false,
|
||||||
|
|
@ -19,9 +52,8 @@ test("stateful programs replay primitive batches, crash boundaries, byte domains
|
||||||
traceTest(trace),
|
traceTest(trace),
|
||||||
traceTest(JSON.parse(JSON.stringify(trace)))
|
traceTest(JSON.parse(JSON.stringify(trace)))
|
||||||
);
|
);
|
||||||
assert(trace.program);
|
|
||||||
large ||= trace.program.initial.length >= 70;
|
large ||= trace.program.initial.length >= 70;
|
||||||
for (const step of trace.steps) seen.add(step.type);
|
for (const step of traceTest(trace).steps) seen.add(step.type);
|
||||||
for (const epoch of trace.program.epochs) {
|
for (const epoch of trace.program.epochs) {
|
||||||
assert(epoch.actions.length);
|
assert(epoch.actions.length);
|
||||||
for (let i = 0; i < epoch.actions.length; i++) {
|
for (let i = 0; i < epoch.actions.length; i++) {
|
||||||
|
|
@ -123,7 +155,7 @@ test("shrinking retains the failing editor intent and regenerates a complete ind
|
||||||
|
|
||||||
test("shrinking rejects dependency-invalid histories and different failure mechanisms", async () => {
|
test("shrinking rejects dependency-invalid histories and different failure mechanisms", async () => {
|
||||||
const trace = generateTrace(0, 1);
|
const trace = generateTrace(0, 1);
|
||||||
const original = trace.program!;
|
const original = trace.program;
|
||||||
const reduced = await minimizeProgram(
|
const reduced = await minimizeProgram(
|
||||||
original,
|
original,
|
||||||
"Byte mismatch at a.md",
|
"Byte mismatch at a.md",
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,12 @@
|
||||||
import { Random } from "../../test-support/random";
|
import { Random } from "../../test-support/random";
|
||||||
import {
|
import {
|
||||||
compileProgram,
|
|
||||||
ReferenceModel,
|
ReferenceModel,
|
||||||
type Content,
|
type Content,
|
||||||
type Epoch,
|
type Epoch,
|
||||||
type Mutation,
|
type Mutation,
|
||||||
type Program
|
type Program
|
||||||
} from "./model";
|
} from "./model";
|
||||||
import type { Trace } from "./workload";
|
import type { StatefulTrace } from "./workload";
|
||||||
|
|
||||||
const words = [
|
const words = [
|
||||||
"alpha",
|
"alpha",
|
||||||
|
|
@ -42,7 +41,10 @@ export function randomContent(random: Random): Content {
|
||||||
|
|
||||||
/** Random primitive histories share a persistent model, including conflict
|
/** Random primitive histories share a persistent model, including conflict
|
||||||
* copies. Only checkpoints settle the system; each epoch composes 2–12 edits. */
|
* copies. Only checkpoints settle the system; each epoch composes 2–12 edits. */
|
||||||
export function generateStatefulTrace(seed: number, iterations: number): Trace {
|
export function generateStatefulTrace(
|
||||||
|
seed: number,
|
||||||
|
iterations: number
|
||||||
|
): StatefulTrace {
|
||||||
const random = new Random(seed);
|
const random = new Random(seed);
|
||||||
const clients = 3;
|
const clients = 3;
|
||||||
const model = new ReferenceModel();
|
const model = new ReferenceModel();
|
||||||
|
|
@ -475,7 +477,6 @@ export function generateStatefulTrace(seed: number, iterations: number): Trace {
|
||||||
seed,
|
seed,
|
||||||
iterations,
|
iterations,
|
||||||
clients,
|
clients,
|
||||||
program,
|
program
|
||||||
steps: compileProgram(program)
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,19 @@
|
||||||
import { test } from "node:test";
|
import { test } from "node:test";
|
||||||
import assert from "node:assert/strict";
|
import assert from "node:assert/strict";
|
||||||
import { generateLegacyTrace as generateTrace, traceTest } from "./workload";
|
import {
|
||||||
|
generateLegacyTrace as generateTrace,
|
||||||
|
serializeTrace,
|
||||||
|
traceTest
|
||||||
|
} from "./workload";
|
||||||
|
|
||||||
test("concrete traces replay all choices and retain preservation assertions with delayed events", () => {
|
test("concrete traces replay all choices and retain preservation assertions with delayed events", () => {
|
||||||
const trace = generateTrace(42, 12);
|
const trace = generateTrace(42, 12);
|
||||||
|
assert.equal(serializeTrace(trace), JSON.stringify(trace, null, 2));
|
||||||
assert.deepEqual(trace, generateTrace(42, 12));
|
assert.deepEqual(trace, generateTrace(42, 12));
|
||||||
assert.notDeepEqual(trace, generateTrace(43, 12));
|
assert.notDeepEqual(trace, generateTrace(43, 12));
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
traceTest(trace),
|
traceTest(trace),
|
||||||
traceTest(JSON.parse(JSON.stringify(trace)))
|
traceTest(JSON.parse(serializeTrace(trace)))
|
||||||
);
|
);
|
||||||
assert.equal(
|
assert.equal(
|
||||||
trace.steps.filter((step) => step.type === "assert-markers").length,
|
trace.steps.filter((step) => step.type === "assert-markers").length,
|
||||||
|
|
|
||||||
|
|
@ -7,23 +7,48 @@ import type {
|
||||||
} from "../../deterministic-tests/src/test-definition";
|
} from "../../deterministic-tests/src/test-definition";
|
||||||
import { Random } from "../../test-support/random";
|
import { Random } from "../../test-support/random";
|
||||||
|
|
||||||
export interface Trace {
|
interface TraceMetadata {
|
||||||
version: 1 | 2;
|
|
||||||
program?: Program;
|
|
||||||
seed: number;
|
seed: number;
|
||||||
iterations: number;
|
iterations: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LegacyTrace extends TraceMetadata {
|
||||||
|
version: 1;
|
||||||
/** Old traces omit this and retain their original two-client behavior. */
|
/** Old traces omit this and retain their original two-client behavior. */
|
||||||
clients?: number;
|
clients?: number;
|
||||||
steps: TestStep[];
|
steps: TestStep[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface StatefulTrace extends TraceMetadata {
|
||||||
|
version: 2;
|
||||||
|
clients: number;
|
||||||
|
program: Program;
|
||||||
|
/** Older artifacts included these; replay always compiles the program. */
|
||||||
|
steps?: TestStep[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Trace = LegacyTrace | StatefulTrace;
|
||||||
|
|
||||||
|
/** Full-state checkpoints can exceed V8's string limit on long campaigns.
|
||||||
|
* Persist their source program once, including when rewriting old artifacts. */
|
||||||
|
export function serializeTrace(trace: Trace): string {
|
||||||
|
return JSON.stringify(
|
||||||
|
trace.version === 2 ? { ...trace, steps: undefined } : trace,
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
type Document = ExpectedDocument & { content: string };
|
type Document = ExpectedDocument & { content: string };
|
||||||
|
|
||||||
/** Concrete actions and independent expected states are serialized together.
|
/** Concrete actions and independent expected states are serialized together.
|
||||||
* Every round checks the whole history, including earlier bytes and UUIDs.
|
* Every round checks the whole history, including earlier bytes and UUIDs.
|
||||||
* A shuffled cycle covers nine actions; most revisit existing documents.
|
* A shuffled cycle covers nine actions; most revisit existing documents.
|
||||||
*/
|
*/
|
||||||
export function generateLegacyTrace(seed: number, iterations: number): Trace {
|
export function generateLegacyTrace(
|
||||||
|
seed: number,
|
||||||
|
iterations: number
|
||||||
|
): LegacyTrace {
|
||||||
const random = new Random(seed);
|
const random = new Random(seed);
|
||||||
const clients = 2 + random.int(2);
|
const clients = 2 + random.int(2);
|
||||||
const steps: TestStep[] = [];
|
const steps: TestStep[] = [];
|
||||||
|
|
@ -297,12 +322,13 @@ export function generateLegacyTrace(seed: number, iterations: number): Trace {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function traceTest(trace: Trace): TestDefinition {
|
export function traceTest(trace: Trace): TestDefinition {
|
||||||
|
const program = trace.version === 2 ? trace.program : undefined;
|
||||||
if (
|
if (
|
||||||
![1, 2].includes(trace.version) ||
|
![1, 2].includes(trace.version) ||
|
||||||
(trace.version === 1 &&
|
(trace.version === 1 &&
|
||||||
(!Array.isArray(trace.steps) || !trace.steps.length)) ||
|
(!Array.isArray(trace.steps) || !trace.steps.length)) ||
|
||||||
(trace.version === 2 &&
|
(trace.version === 2 &&
|
||||||
(!trace.program || trace.clients !== trace.program.clients)) ||
|
(!program || trace.clients !== program.clients)) ||
|
||||||
(trace.clients !== undefined &&
|
(trace.clients !== undefined &&
|
||||||
(!Number.isInteger(trace.clients) || trace.clients < 2))
|
(!Number.isInteger(trace.clients) || trace.clients < 2))
|
||||||
)
|
)
|
||||||
|
|
@ -310,8 +336,7 @@ export function traceTest(trace: Trace): TestDefinition {
|
||||||
return {
|
return {
|
||||||
clients: trace.clients ?? 2,
|
clients: trace.clients ?? 2,
|
||||||
description: `Seed ${trace.seed}; ${trace.iterations} generated rounds`,
|
description: `Seed ${trace.seed}; ${trace.iterations} generated rounds`,
|
||||||
steps:
|
steps: trace.version === 2 ? compileProgram(trace.program) : trace.steps
|
||||||
trace.version === 2 ? compileProgram(trace.program!) : trace.steps
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue