UNPKG

@firfi/quint-connect

Version:

Model-based testing framework connecting Quint specifications to TypeScript implementations

117 lines 5.1 kB
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