Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/chapter_6.md
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,7 @@ The regression file stores failures grouped by test type (property-based tests v
"seed": 901717247,
"dial": "example/sip010.cjs",
"numRuns": 15,
"counterexamplePath": "2:1:0",
"timestamp": 1767886534833
},
{
Expand Down Expand Up @@ -341,6 +342,8 @@ Each failure record includes:
- `numRuns` – Number of test iterations needed for the failure to occur
- `timestamp` – When the failure was discovered (Unix timestamp in milliseconds)
- `dial` (optional) – Path to the dialer file used during the test
- `counterexamplePath` (optional) – fast-check shrink metadata for diagnostics;
it is not an exact replay trace

Failures are sorted by timestamp in descending order, with the most recent failures first.

Expand Down
3 changes: 2 additions & 1 deletion heatstroke.tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,7 @@ describe("Custom reporter logging", () => {
});

const runDetails = {
counterexamplePath: `${r.path}:preferred`,
path: r.path,
failed: r.failed,
numRuns: r.numRuns,
Expand Down Expand Up @@ -320,7 +321,7 @@ describe("Custom reporter logging", () => {
const expectedMessages = [
`\nError: Property failed after ${r.numRuns} tests.`,
`Seed : ${r.seed}`,
`Path : ${r.path}`,
`Path : ${r.path}:preferred`,
`\nCounterexample:`,
`- Contract : ${getContractNameFromContractId(
rendezvousContractId,
Expand Down
9 changes: 6 additions & 3 deletions heatstroke.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ import { getContractNameFromContractId } from "./shared";
* @property `runDetails.counterexample`: The input that caused the failure.
* @property `runDetails.numRuns`: The number of test cases that were run.
* @property `runDetails.seed`: The replay seed for reproducing the failure.
* @property `runDetails.path`: The replay path for reproducing the failure.
* @property `runDetails.counterexamplePath`: Fast-check shrink metadata for
* diagnostics; it is not an exact replay trace.
* @property `runDetails.path`: The legacy path fallback for older callers.
* @property `runDetails.error`: The error thrown during the test.
* @param radio The event emitter to log messages.
* @param type The type of test that failed: invariant or property.
Expand All @@ -42,6 +44,7 @@ export const reporter = (

if (failed) {
const error = runDetails.errorInstance || runDetails.error;
const counterexamplePathValue = runDetails.counterexamplePath ?? path;

// Extract the actual Clarity error once for both error types.
const clarityError =
Expand All @@ -56,8 +59,8 @@ export const reporter = (
`\nError: Property failed after ${numRuns} tests.`,
);
radio.emit("logFailure", `Seed : ${seed}`);
if (path) {
radio.emit("logFailure", `Path : ${path}`);
if (counterexamplePathValue) {
radio.emit("logFailure", `Path : ${counterexamplePathValue}`);
}
switch (type) {
case "invariant": {
Expand Down
1 change: 1 addition & 0 deletions heatstroke.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export interface RunDetails {
counterexample: CounterExample[];
numRuns: number;
seed: number;
counterexamplePath?: string | null;
path?: string;
error?: Error;
errorInstance?: Error;
Expand Down
47 changes: 46 additions & 1 deletion persistence.tests.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { mkdirSync, rmSync, statSync } from "node:fs";
import { mkdirSync, rmSync, statSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";

Expand Down Expand Up @@ -409,6 +409,51 @@ describe("Failure Persistence", () => {
),
);
});

it("persists paths and reads records from older files", () => {
const customBaseDir = createTemporaryCustomTestBaseDir("paths");
const options = { baseDir: customBaseDir };

try {
persistFailure(
{
...createMockRunDetails(1),
counterexamplePath: "0:1",
path: "legacy",
},
"test",
TEST_CONTRACT_ID,
undefined,
options,
);
persistFailure(
{ ...createMockRunDetails(2), path: "legacy" },
"test",
TEST_CONTRACT_ID,
undefined,
options,
);

const failures = loadFailures(TEST_CONTRACT_ID, "test", options);
expect(
failures.find(({ seed }) => seed === 1)?.counterexamplePath,
).toBe("0:1");
expect(
failures.find(({ seed }) => seed === 2)?.counterexamplePath,
).toBe("legacy");

const record = { seed: 17, numRuns: 20, timestamp: 123 };
writeFileSync(
getFailureFilePath(TEST_CONTRACT_ID, customBaseDir),
JSON.stringify({ invariant: [record], test: [] }),
);
expect(loadFailures(TEST_CONTRACT_ID, "invariant", options)).toEqual([
record,
]);
} finally {
rmSync(customBaseDir, { recursive: true, force: true });
}
});
});

describe("Loading Failures", () => {
Expand Down
3 changes: 3 additions & 0 deletions persistence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ interface FailureRecord {
seed: number;
/** The number of runs needed to reproduce the failure*/
numRuns: number;
/** The fast-check path to the counterexample */
counterexamplePath?: string | null;
/** The path to the dialer file used for this test run */
dial: string | undefined;
/** Timestamp when the failure was recorded */
Expand Down Expand Up @@ -115,6 +117,7 @@ export const persistFailure = (
seed: runDetails.seed,
dial: dial,
numRuns: runDetails.numRuns,
counterexamplePath: runDetails.counterexamplePath ?? runDetails.path,
timestamp: Date.now(),
};

Expand Down