diff --git a/tests/harness/differential.test.ts b/tests/harness/differential.test.ts index fbf9cb1ec..a2206fd97 100644 --- a/tests/harness/differential.test.ts +++ b/tests/harness/differential.test.ts @@ -22,6 +22,7 @@ import { describe, expect, test } from "vitest"; import ts5 from "typescript"; import { compile } from "@scriptc/compiler"; import { corpusSelect, shardSuffix } from "./shard.js"; +import { oracleEnvKey } from "./oracle-env.js"; const execFileAsync = promisify(execFile); const repoRoot = join(import.meta.dirname, "../.."); @@ -238,8 +239,8 @@ function programInputs(file: string): string[] { // Node's verdict for a corpus program is a pure function of the program bytes, // the shims, and the Node build (corpus stdout is deterministic by // construction — it must be identical to a non-Node native binary's output). So -// cache it, keyed by all of those plus the invocation shape (SCRIPTC_TEST_ENV -// and the cwd). Only the SPAWN is skipped: the native side always runs live +// cache it, keyed by all of those plus the invocation shape (the inherited +// environment, see oracle-env.ts, and the cwd). Only the SPAWN is skipped: the native side always runs live // and the comparison itself never changes. SCRIPTC_NO_CACHE=1 (or an unset // SCRIPTC_CACHE_DIR) disables the cache in both directions — no reads, no writes. // Storage shares the compile cache's root and its LRU sweep (see native-toolchain.ts). @@ -265,7 +266,7 @@ function oracleKeyBase(): Promise { .update(readFileSync(fileURLToPath(comptimeShim))) .update("\0") .update("\0") - .update(process.env["SCRIPTC_TEST_ENV"] ?? "") + .update(oracleEnvKey(process.env)) .update("\0") .update(process.cwd()) .update("\0") diff --git a/tests/harness/oracle-env.test.ts b/tests/harness/oracle-env.test.ts new file mode 100644 index 000000000..da09dde25 --- /dev/null +++ b/tests/harness/oracle-env.test.ts @@ -0,0 +1,25 @@ +import { expect, test } from "vitest"; +import { oracleEnvKey } from "./oracle-env.js"; + +test("the oracle key changes with any inherited variable", () => { + const base = { PATH: "/bin", SCRIPTC_TEST_ENV: "from-harness" }; + expect(oracleEnvKey({ ...base, NODE_ENV: "production" })).not.toBe( + oracleEnvKey({ ...base, NODE_ENV: "test" }), + ); + expect(oracleEnvKey({ ...base, NODE_ENV: "test" })).not.toBe(oracleEnvKey(base)); + expect(oracleEnvKey({ ...base, FOO: "a=b" })).not.toBe( + oracleEnvKey({ ...base, FOO: "a", B: "" }), + ); +}); + +test("the oracle key ignores the runner's worker numbers and variable order", () => { + expect(oracleEnvKey({ A: "1", B: "2", VITEST_WORKER_ID: "1", VITEST_POOL_ID: "1" })).toBe( + oracleEnvKey({ + VITEST_POOL_ID: "7", + B: "2", + TINYPOOL_WORKER_ID: "3", + A: "1", + VITEST_WORKER_ID: "9", + }), + ); +}); diff --git a/tests/harness/oracle-env.ts b/tests/harness/oracle-env.ts new file mode 100644 index 000000000..5f66f26bb --- /dev/null +++ b/tests/harness/oracle-env.ts @@ -0,0 +1,15 @@ +/* The environment part of the differential oracle cache key. The Node + * oracle runs with the harness's inherited environment, so a program whose + * output reads a variable (NODE_ENV, SCRIPTC_TEST_ENV, ...) must not reuse a + * verdict recorded under a different value. The variables that number the + * test runner's own workers differ between workers of one run and are left + * out, so every worker shares the cache. */ +const WORKER_VARIABLES = new Set(["VITEST_POOL_ID", "VITEST_WORKER_ID", "TINYPOOL_WORKER_ID"]); + +export function oracleEnvKey(env: NodeJS.ProcessEnv): string { + return Object.keys(env) + .filter((name) => !WORKER_VARIABLES.has(name) && env[name] !== undefined) + .sort() + .map((name) => `${name}=${env[name]}\0`) + .join(""); +}