Skip to content
Merged
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
2 changes: 1 addition & 1 deletion ct-runner/deno.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"./run": "./src/run-suite.ts"
},
"tasks": {
"test": "deno test --allow-read=..",
"test": "deno test --allow-read=..,/tmp --allow-write=/tmp --allow-run",
"check": "deno check src tests"
}
}
55 changes: 50 additions & 5 deletions ct-runner/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,21 @@
// ct-runner CLI entry.
//
// deno run -A ct-runner/src/main.ts <suite.wasm> --out results.jsonl \
// [--imports <module.ts>] [--target NAME] [--suite-name NAME] \
// [--translator <translator_shim.wasm>] [--imports <module.ts>] \
// [--target NAME] [--suite-name NAME] \
// [--only SUBSTRING] [--case-timeout-ms N] [--no-fresh-cases] [--jspi]
//
// `--imports <module.ts>` convention (contracts/embedder-api.md §"Module
// wiring and instantiation"): a TS module whose default export is either
// the imports record directly, or a factory (sync or async) producing one.
// Never test-context — the runner supplies that itself.
//
// `--translator` (or DELTIC_TRANSLATOR in the environment) names the
// translator-shim wasm explicitly — required when this CLI runs outside a
// deltic checkout (e.g. imported by URL at a release tag, with the wasm
// taken from that release's `deltic-translator-shim.wasm` asset; see
// docs/consumers.md and issue #16's interim release scheme). Inside a
// checkout it defaults to the local release build under `target/`.

import { Translator } from "../../runtime/src/shim/mod.ts";
import type { ComponentArtifacts } from "../../runtime/src/embedder/mod.ts";
Expand All @@ -20,7 +28,8 @@ function usageError(msg: string): never {
console.error(`error: ${msg}`);
console.error(
"usage: deno run -A ct-runner/src/main.ts <suite.wasm> --out <results.jsonl> " +
"[--imports <module.ts>] [--target NAME] [--suite-name NAME] " +
"[--translator <translator_shim.wasm>] [--imports <module.ts>] " +
"[--target NAME] [--suite-name NAME] " +
"[--only SUBSTRING] [--case-timeout-ms N] [--no-fresh-cases] [--jspi]",
);
Deno.exit(2);
Expand All @@ -29,6 +38,7 @@ function usageError(msg: string): never {
interface Cli {
suitePath: string;
out: string;
translator?: string;
importsModule?: string;
target: string;
suiteName?: string;
Expand All @@ -41,6 +51,7 @@ interface Cli {
function parseArgs(argv: string[]): Cli {
const positional: string[] = [];
let out: string | undefined;
let translator: string | undefined;
let importsModule: string | undefined;
let target = "deltic/host";
let suiteName: string | undefined;
Expand All @@ -55,6 +66,9 @@ function parseArgs(argv: string[]): Cli {
case "--out":
out = argv[++i];
break;
case "--translator":
translator = argv[++i];
break;
case "--imports":
importsModule = argv[++i];
break;
Expand Down Expand Up @@ -86,6 +100,7 @@ function parseArgs(argv: string[]): Cli {
return {
suitePath: positional[0],
out,
translator,
importsModule,
target,
suiteName,
Expand All @@ -109,15 +124,45 @@ async function loadImportsModule(path: string): Promise<Record<string, unknown>>
return (def ?? {}) as Record<string, unknown>;
}

async function loadTranslator(): Promise<Translator> {
/** Resolve the translator-shim wasm: explicit `--translator`, then
* `DELTIC_TRANSLATOR`, then the checkout-local release build. The explicit
* paths exist for consumers running this CLI outside a deltic checkout
* (URL-imported at a release tag): `import.meta.url` is then remote, so the
* repo-relative default cannot work — they point at the release's
* `deltic-translator-shim.wasm` asset instead. */
async function loadTranslator(explicit?: string): Promise<Translator> {
const fromEnv = Deno.env.get("DELTIC_TRANSLATOR");
const path = explicit ?? (fromEnv !== undefined && fromEnv !== "" ? fromEnv : undefined);
if (path !== undefined) {
let bytes: Uint8Array;
try {
bytes = await Deno.readFile(path);
} catch (e) {
console.error(
`error: cannot read translator wasm at ${path}` +
` (${explicit !== undefined ? "--translator" : "DELTIC_TRANSLATOR"}): ${e}`,
);
Deno.exit(1);
}
return await Translator.create(bytes);
}

if (REPO_ROOT.protocol !== "file:") {
console.error(
"error: running outside a deltic checkout — pass --translator " +
"<translator_shim.wasm> (or set DELTIC_TRANSLATOR); the wasm ships as " +
"a release asset (deltic-translator-shim.wasm).",
);
Deno.exit(1);
}
const rel = "target/wasm32-unknown-unknown/release/translator_shim.wasm";
let bytes: Uint8Array;
try {
bytes = await Deno.readFile(new URL(rel, REPO_ROOT));
} catch {
console.error(
`error: missing ${rel} — run: cargo build -p translator-shim --release ` +
`--target wasm32-unknown-unknown`,
`--target wasm32-unknown-unknown (or pass --translator)`,
);
Deno.exit(1);
}
Expand All @@ -132,7 +177,7 @@ function suiteNameFrom(path: string): string {
async function main() {
const cli = parseArgs(Deno.args);
const componentBytes = await Deno.readFile(cli.suitePath);
const translator = await loadTranslator();
const translator = await loadTranslator(cli.translator);
const { plan, adapters } = translator.translate(componentBytes);
const artifacts: ComponentArtifacts = { plan, componentBytes, adapters };

Expand Down
97 changes: 97 additions & 0 deletions ct-runner/tests/cli_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
// CLI-level e2e: main.ts as a consumer runs it — a subprocess, with the
// translator named EXPLICITLY (`--translator` / DELTIC_TRANSLATOR) rather
// than found in the checkout. This is the remote-consumption contract
// (docs/consumers.md; polymorph-test's deltic lane pins a release tag and
// passes the release's translator asset), so the repo-relative fallback
// must never be the only path that works.

import { assertEq } from "../../runtime/tests/support/asserts.ts";
import { haveFixture, TEST_SUITE_WASM } from "./support.ts";

const root = new URL("../../", import.meta.url);
const MAIN = new URL("../src/main.ts", import.meta.url).pathname;
const TRANSLATOR = new URL(
"target/wasm32-unknown-unknown/release/translator_shim.wasm",
root,
).pathname;
const SUITE = new URL(TEST_SUITE_WASM, root).pathname;

const ready = await haveFixture(TEST_SUITE_WASM);

interface CliRun {
code: number;
stderr: string;
lines: string[] | null;
}

async function runCli(
args: string[],
env: Record<string, string> = {},
): Promise<CliRun> {
const out = await Deno.makeTempFile({ suffix: ".jsonl" });
try {
const cmd = new Deno.Command(Deno.execPath(), {
args: ["run", "-A", MAIN, SUITE, "--out", out, ...args],
env,
stdout: "inherit",
stderr: "piped",
});
const res = await cmd.output();
let lines: string[] | null = null;
try {
const text = await Deno.readTextFile(out);
// makeTempFile pre-creates the (empty) file; an early CLI exit leaves
// it empty, which callers treat the same as absent.
lines = text === "" ? null : text.trimEnd().split("\n");
} catch {
// CLI exited before writing — callers assert on code/stderr.
}
return {
code: res.code,
stderr: new TextDecoder().decode(res.stderr),
lines,
};
} finally {
await Deno.remove(out).catch(() => {});
}
}

Deno.test({
name: "cli: --translator <path> runs the suite (no checkout fallback used)",
ignore: !ready,
fn: async () => {
const { code, lines } = await runCli(["--translator", TRANSLATOR]);
// The fixture suite contains a deliberately failing case, so the CLI's
// contract is exit 1 (same discipline as polymorph-test's verify legs).
assertEq(code, 1);
assertEq(lines !== null, true);
assertEq(lines!.length, 1 + 6 + 1); // envelope + 6 cases + terminator
const envelope = JSON.parse(lines![0]);
assertEq(envelope.target, "deltic/host");
},
});

Deno.test({
name: "cli: DELTIC_TRANSLATOR env is honored",
ignore: !ready,
fn: async () => {
const { code, lines } = await runCli([], { DELTIC_TRANSLATOR: TRANSLATOR });
assertEq(code, 1);
assertEq(lines!.length, 1 + 6 + 1);
},
});

Deno.test({
name: "cli: unreadable --translator fails loud, names the flag",
ignore: !ready,
fn: async () => {
const { code, stderr, lines } = await runCli([
"--translator",
"/nonexistent/translator_shim.wasm",
]);
assertEq(code, 1);
assertEq(lines, null);
assertEq(stderr.includes("--translator"), true);
assertEq(stderr.includes("/nonexistent/translator_shim.wasm"), true);
},
});
Loading