@firfi/quint-connect
Version:
Model-based testing framework connecting Quint specifications to TypeScript implementations
117 lines • 5.1 kB
JavaScript
import * as Command from "@effect/platform/Command";
import * as FileSystem from "@effect/platform/FileSystem";
import * as Path from "@effect/platform/Path";
import { Effect, Schema, Stream } from "effect";
import { ItfTrace } from "../itf/schema.js";
export class QuintError extends Schema.TaggedError()("QuintError", {
message: Schema.String,
stderr: Schema.optional(Schema.String),
exitCode: Schema.optional(Schema.Number)
}) {
}
export class QuintNotFoundError extends Schema.TaggedError()("QuintNotFoundError", {
message: Schema.String
}) {
}
const DEFAULT_N_TRACES = 10;
// quint defaults maxSamples to 1 when --seed is provided; use the non-seed default instead
const DEFAULT_MAX_SAMPLES = 10000;
const buildRunArgs = (opts, outDir) => {
const nTraces = opts.nTraces ?? DEFAULT_N_TRACES;
const args = [
"run",
opts.spec,
"--mbt",
"--n-traces",
String(nTraces),
"--out-itf",
`${outDir}/trace_{seq}.itf.json`
];
const envBackend = process.env["QUINT_BACKEND"];
const backend = opts.backend ?? (envBackend === "typescript" || envBackend === "rust" ? envBackend : "typescript");
args.push("--backend", backend);
if (opts.seed !== undefined) {
args.push("--seed", opts.seed);
}
if (opts.maxSteps !== undefined) {
args.push("--max-steps", String(opts.maxSteps));
}
if (opts.maxSamples !== undefined) {
args.push("--max-samples", String(opts.maxSamples));
}
else if (opts.seed !== undefined) {
args.push("--max-samples", String(DEFAULT_MAX_SAMPLES));
}
if (opts.init !== undefined) {
args.push("--init", opts.init);
}
if (opts.step !== undefined) {
args.push("--step", opts.step);
}
if (opts.main !== undefined) {
args.push("--main", opts.main);
}
if (opts.invariants !== undefined) {
for (const inv of opts.invariants) {
args.push("--invariant", inv);
}
}
if (opts.witnesses !== undefined) {
for (const wit of opts.witnesses) {
args.push("--witness", wit);
}
}
return args;
};
const runAndReadTraces = (opts, outDir) => Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const args = buildRunArgs(opts, outDir);
const baseCmd = Command.make("npx", "@informalsystems/quint", ...args);
const cmd = opts.verbose === true ? Command.env(baseCmd, { QUINT_VERBOSE: "true" }) : baseCmd;
const proc = yield* Effect.mapError(Command.start(cmd), (e) => new QuintNotFoundError({ message: `Failed to start quint: ${e}` }));
const decoder = new TextDecoder();
const [exitCode, , stderr] = yield* Effect.mapError(Effect.all([
proc.exitCode,
Stream.runDrain(proc.stdout),
Stream.runFold(proc.stderr, "", (acc, chunk) => acc + decoder.decode(chunk))
], { concurrency: "unbounded" }), (e) => new QuintError({ message: `quint process error: ${e}` }));
if (exitCode !== 0) {
return yield* new QuintError({
message: stderr
? `quint run failed with exit code ${exitCode}:\n${stderr.trim()}`
: `quint run failed with exit code ${exitCode}`,
stderr,
exitCode
});
}
const files = yield* Effect.mapError(fs.readDirectory(outDir), (e) => new QuintError({ message: `Failed to read trace directory: ${e}` }));
const traceFiles = files
.filter((f) => f.endsWith(".itf.json"))
.sort();
const traces = [];
for (const file of traceFiles) {
const content = yield* Effect.mapError(fs.readFileString(path.join(outDir, file)), (e) => new QuintError({ message: `Failed to read trace file ${file}: ${e}` }));
const json = yield* Effect.try({
try: () => JSON.parse(content),
catch: (e) => new QuintError({ message: `Invalid JSON in trace file ${file}: ${e}` })
});
const trace = yield* Effect.mapError(Schema.decodeUnknown(ItfTrace)(json), (e) => new QuintError({ message: `Failed to parse ITF trace ${file}: ${e}` }));
traces.push(trace);
}
return traces;
});
const generateTracesWithTraceDir = (opts, traceDir) => Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
yield* Effect.mapError(fs.makeDirectory(traceDir, { recursive: true }), (e) => new QuintError({ message: `Failed to create trace directory: ${e}` }));
return yield* runAndReadTraces(opts, traceDir);
}).pipe(Effect.scoped);
const generateTracesWithTempDir = (opts) => Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const tmpDir = yield* Effect.mapError(fs.makeTempDirectoryScoped(), (e) => new QuintError({ message: `Failed to create temp directory: ${e}` }));
return yield* runAndReadTraces(opts, tmpDir);
}).pipe(Effect.scoped);
export const generateTraces = (opts) => opts.traceDir !== undefined
? generateTracesWithTraceDir(opts, opts.traceDir)
: generateTracesWithTempDir(opts);
//# sourceMappingURL=quint.js.map