1// SPDX-License-Identifier: MIT
2//
3// Runs the oracle corpus, the fromJSON corpus, the number corpus and the string
4// comparison corpus through both expression engines of actions/runner and lists
5// where they differ:
6// legacy src/Sdk/DTExpressions2 (driver ../runner)
7// newer src/Sdk/Expressions (driver ./runner-new)
8//
9// dotnet build -c Release expr/gen/runner -p:RunnerSrc=$RUNNER_CLONE/src/Sdk
10// dotnet build -c Release expr/gen/runner-new -p:RunnerSrc=$RUNNER_CLONE/src/Sdk
11// RUNNER_CLONE=<clone> node expr/gen/runner-new/engines.mjs
12//
13// The exception message of a fromJSON failure is not compared (the engines word
14// it differently); everything else must be identical. Exit code 1 if anything
15// differs beyond the known difference: negative zero (the newer engine prints 0).
16
17import { execFileSync } from "node:child_process";
18import { readFileSync, writeFileSync, mkdtempSync, rmSync } from "node:fs";
19import { tmpdir } from "node:os";
20import { join, dirname } from "node:path";
21import { fileURLToPath } from "node:url";
22import { gunzipSync } from "node:zlib";
23import { checkEnvironment } from "../runner/env.mjs";
24
25const here = dirname(fileURLToPath(import.meta.url));
26const testdata = join(here, "../../testdata");
27checkEnvironment(process.argv[2]);
28
29const dec = h => (h === "-" ? "" : h.split(".").map(u => String.fromCharCode(parseInt(u, 16))).join(""));
30const lit = s => "'" + s.replaceAll("'", "''") + "'";
31const hasLone = s => /[\ud800-\udbff](?![\udc00-\udfff])|(?<![\ud800-\udbff])[\udc00-\udfff]/.test(s);
32
33const oracle = JSON.parse(gunzipSync(readFileSync(join(testdata, "oracle.json.gz"))));
34const corpora = { oracle: { contexts: oracle.contexts, cases: oracle.cases.map(c => c.e) } };
35const exprs = [];
36for (const c of JSON.parse(readFileSync(join(testdata, "fromjson_cases.json"), "utf8"))) exprs.push(`toJSON(fromJSON(${lit(c.in)}))`);
37for (const l of readFileSync(join(testdata, "number_cases.txt"), "utf8").trim().split("\n")) {
38 const s = dec(l.split(" ")[0]);
39 if (hasLone(s)) continue;
40 for (const op of ["== 0", "== 1", "> 255", "< 0"]) exprs.push(`${lit(s)} ${op}`);
41}
42for (const l of readFileSync(join(testdata, "ordinal_cases.txt"), "utf8").trim().split("\n")) {
43 const [a, b] = l.split(" ");
44 const A = dec(a), B = dec(b);
45 if (hasLone(A + B)) continue;
46 exprs.push(`${lit(A)} == ${lit(B)}`, `${lit(A)} < ${lit(B)}`, `contains(${lit(A)}, ${lit(B)})`, `startsWith(${lit(A)}, ${lit(B)})`, `endsWith(${lit(A)}, ${lit(B)})`);
47}
48corpora.strings = { contexts: {}, cases: exprs };
49
50const run = (dll, input) => execFileSync("dotnet", [dll, input], { maxBuffer: 1 << 30 }).toString("utf8").split("\n").filter(Boolean).map(l => JSON.parse(l));
51const sig = r => (r.err ? JSON.stringify(["err", r.err.t, r.err.x === "JsonReaderException" ? r.err.x : r.err.m]) : JSON.stringify(r));
52
53const tmp = mkdtempSync(join(tmpdir(), "engines-"));
54let unexpected = 0, total = 0;
55for (const [name, c] of Object.entries(corpora)) {
56 const input = join(tmp, name + ".json");
57 writeFileSync(input, JSON.stringify({ contexts: c.contexts, cases: c.cases.map(e => ({ e })) }));
58 const a = run(join(here, "../runner/bin/Release/net9.0/runner.dll"), input);
59 const b = run(join(here, "bin/Release/net9.0/runner-new.dll"), input);
60 let diffs = 0;
61 a.forEach((r, i) => {
62 total++;
63 if (sig(r) === sig(b[i])) return;
64 diffs++;
65 const known = /^(toJSON\(-0\)|-0|toJSON\(fromJSON\('-\.0'\)\))$/.test(c.cases[i]);
66 if (!known) unexpected++;
67 console.log(`${known ? "known " : "DIFF "} ${name}: ${JSON.stringify(c.cases[i].slice(0, 80))}\n legacy ${sig(r).slice(0, 100)}\n newer ${sig(b[i]).slice(0, 100)}`);
68 });
69 console.log(`${name}: ${a.length} expressions, ${diffs} differences`);
70}
71rmSync(tmp, { recursive: true });
72console.log(`${total} expressions, ${unexpected} unexpected differences`);
73process.exit(unexpected ? 1 : 0);