@firfi/quint-connect
Version:
Model-based testing framework connecting Quint specifications to TypeScript implementations
113 lines • 5.62 kB
JavaScript
import spawn from "cross-spawn";
import { Effect } from "effect";
import { existsSync, readdirSync } from "node:fs";
import { readFile } from "node:fs/promises";
import { cpus, homedir } from "node:os";
import { join } from "node:path";
import { decodeCompiledEvaluatorInput, makeRandomSeedHex, patchCompiledEvaluatorInput } from "./compiled-evaluator-input.js";
import { normalizeEvaluatorOutput } from "./compiled-evaluator-output.js";
import { QuintError, QuintNotFoundError } from "./errors.js";
import { runManagedProcess } from "./managed-process.js";
import { platformProcess } from "./platform-process.js";
import { readTraceFiles, writeTraceFiles } from "./trace-files.js";
import { isCompiledEvaluatorPolicy, resolveTraceGenerationPolicy, validateTraceGenerationConfiguration } from "./trace-generation-policy.js";
const getRustEvaluatorPath = (processBoundary = platformProcess) => {
const quintDir = join(homedir(), ".quint");
if (!existsSync(quintDir)) {
throw new Error(`Quint home directory not found: ${quintDir}`);
}
const preferredVersion = process.env["QUINT_EVALUATOR_VERSION"];
const dirs = readdirSync(quintDir).filter((dir) => dir.startsWith("rust-evaluator-")).sort();
if (dirs.length === 0) {
throw new Error("No Rust evaluator found in ~/.quint/. Run `quint run` once with --backend rust to download it.");
}
const preferred = preferredVersion
? dirs.find((dir) => dir.includes(preferredVersion))
: undefined;
const latest = preferred ?? dirs[dirs.length - 1];
const exePath = join(quintDir, latest, processBoundary.executableName("quint_evaluator"));
if (!existsSync(exePath)) {
throw new Error(`Rust evaluator binary not found: ${exePath}`);
}
return exePath;
};
export const makeRunEvaluatorProcess = (spawnProcess = (evaluatorPath, args, options) => {
const proc = spawn(evaluatorPath, [...args], options);
if (proc.stdin === null || proc.stdout === null || proc.stderr === null) {
throw new Error("Rust evaluator was spawned without piped stdio");
}
return {
pid: proc.pid,
stdin: proc.stdin,
stdout: proc.stdout,
stderr: proc.stderr,
on: proc.on.bind(proc)
};
}, processBoundary = platformProcess) => (evaluatorPath, inputStr) => runManagedProcess({
processBoundary,
spawn: () => spawnProcess(evaluatorPath, ["simulate-from-stdin"], {
stdio: ["pipe", "pipe", "pipe"],
detached: processBoundary.detached
}),
captureResult: (proc) => {
let stdout = "";
let stderr = "";
proc.stdin.write(inputStr);
proc.stdin.end();
proc.stdout.on("data", (chunk) => {
stdout += chunk.toString();
});
proc.stderr.on("data", (chunk) => {
stderr += chunk.toString();
});
return (exitCode) => ({ stdout, exitCode, stderr });
}
}).pipe(Effect.mapError((error) => new QuintNotFoundError({ message: `Failed to start Rust evaluator: ${error.message}` })));
const runEvaluatorDirect = makeRunEvaluatorProcess();
const defaultDeps = {
compiledInputExists: existsSync,
cpuCount: () => cpus().length,
getEvaluatorPath: getRustEvaluatorPath,
randomSeedHex: makeRandomSeedHex,
readCompiledInput: (compiledInputPath) => Effect.tryPromise({
try: () => readFile(compiledInputPath, "utf-8"),
catch: (e) => new QuintError({ message: `Failed to read compiled input: ${e}` })
}),
runEvaluator: runEvaluatorDirect
};
export const makeCompiledEvaluatorTraceAdapter = (deps = defaultDeps) => ({
canGenerate: (opts) => {
const policy = resolveTraceGenerationPolicy(opts);
return isCompiledEvaluatorPolicy(policy) && deps.compiledInputExists(policy.options.compiledInput);
},
generate: (opts, outDir) => Effect.gen(function* () {
const policy = resolveTraceGenerationPolicy(opts);
if (!isCompiledEvaluatorPolicy(policy)) {
return yield* new QuintError({ message: "Compiled input path is required for compiled evaluator generation" });
}
yield* validateTraceGenerationConfiguration(policy.options);
const rawInput = yield* deps.readCompiledInput(policy.options.compiledInput);
const compiledInput = yield* decodeCompiledEvaluatorInput(rawInput);
const { input, seedHex } = patchCompiledEvaluatorInput(compiledInput, policy.options, deps.cpuCount(), deps.randomSeedHex());
console.error(`[quint-connect] seed: ${seedHex} (compiled-input path)`);
const evaluatorPath = yield* Effect.try({
try: deps.getEvaluatorPath,
catch: (e) => new QuintError({ message: `Failed to locate Rust evaluator: ${e}` })
});
const result = yield* deps.runEvaluator(evaluatorPath, input);
if (result.exitCode !== 0) {
return yield* new QuintError({
message: `Rust evaluator failed with exit code ${result.exitCode}:\n${result.stderr}`,
stderr: result.stderr,
exitCode: result.exitCode
});
}
const traces = yield* normalizeEvaluatorOutput(result.stdout);
yield* writeTraceFiles(outDir, traces);
return yield* readTraceFiles(outDir);
})
});
export const compiledEvaluatorTraceAdapter = makeCompiledEvaluatorTraceAdapter();
export { decodeCompiledEvaluatorInput, patchCompiledEvaluatorInput } from "./compiled-evaluator-input.js";
export { normalizeEvaluatorOutput } from "./compiled-evaluator-output.js";
//# sourceMappingURL=compiled-evaluator-adapter.js.map