test(expr): build the runner's newer expression engine and compare it with the legacy one
gen/runner-new compiles src/Sdk/Expressions (GitHub.Actions.Expressions), the engine WorkflowTemplateEvaluator runs next to the legacy one. engines.mjs runs 46,148 expressions through both: the only difference is negative zero.
- commit
- d706e56f12ef6e237c5d9d272b03996d9b45f78b
- author
- brice · · 2026-10-12 18:34 UTC
- committer
- brice · · 2026-10-18 14:59 UTC
- parent
- a970bd2
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
4 files · +231 -0
A expr/gen/runner-new/.gitignore +2 -0
| @@ -0,0 +1,2 @@ | |||
| 1 | + | bin/ | |
| 2 | + | obj/ | |
| @@ -0,0 +1,2 @@ | |||||
| 1 | + | bin/ | |||
| 2 | + | obj/ | |||
A expr/gen/runner-new/Program.cs +132 -0
| @@ -0,0 +1,132 @@ | |||
| 1 | + | // SPDX-License-Identifier: MIT | |
| 2 | + | ||
| 3 | + | using System; | |
| 4 | + | using System.Collections.Generic; | |
| 5 | + | using System.IO; | |
| 6 | + | using System.Linq; | |
| 7 | + | using System.Text; | |
| 8 | + | using GitHub.Actions.Expressions; | |
| 9 | + | using GitHub.Actions.Expressions.Data; | |
| 10 | + | using GitHub.Actions.Expressions.Sdk; | |
| 11 | + | using GitHub.Actions.Expressions.Sdk.Functions; | |
| 12 | + | using Newtonsoft.Json; | |
| 13 | + | using Newtonsoft.Json.Linq; | |
| 14 | + | ||
| 15 | + | // Differential harness: runs expressions through the runner's own | |
| 16 | + | // Expressions2 code (compiled from the runner repository) and prints one JSON | |
| 17 | + | // line per case. | |
| 18 | + | // | |
| 19 | + | // Input (argv[0]): JSON {"contexts": {name: json}, "cases": [{"e": expr, "ctx": {name: json} | null}]} | |
| 20 | + | // Output: JSON lines {"k": kind, "s": string form, "j": toJson, "err": {"t": "parse"|"eval", "m": message}} | |
| 21 | + | ||
| 22 | + | internal sealed class CtxNode : NamedValue | |
| 23 | + | { | |
| 24 | + | protected override Object EvaluateCore(EvaluationContext context, out ResultMemory resultMemory) | |
| 25 | + | { | |
| 26 | + | resultMemory = null; | |
| 27 | + | return (context.State as DictionaryExpressionData)[Name]; | |
| 28 | + | } | |
| 29 | + | } | |
| 30 | + | ||
| 31 | + | internal sealed class Success : Function { protected override Object EvaluateCore(EvaluationContext c, out ResultMemory m) { m = null; return true; } } | |
| 32 | + | internal sealed class Always : Function { protected override Object EvaluateCore(EvaluationContext c, out ResultMemory m) { m = null; return true; } } | |
| 33 | + | internal sealed class HashFiles : Function | |
| 34 | + | { | |
| 35 | + | protected override Object EvaluateCore(EvaluationContext c, out ResultMemory m) | |
| 36 | + | { | |
| 37 | + | m = null; | |
| 38 | + | var s = "hash:"; | |
| 39 | + | for (var i = 0; i < Parameters.Count; i++) | |
| 40 | + | { | |
| 41 | + | if (i > 0) s += "|"; | |
| 42 | + | s += Parameters[i].Evaluate(c).ConvertToString(); | |
| 43 | + | } | |
| 44 | + | return s; | |
| 45 | + | } | |
| 46 | + | } | |
| 47 | + | ||
| 48 | + | internal static class P | |
| 49 | + | { | |
| 50 | + | static DictionaryExpressionData Load(JObject ctx) | |
| 51 | + | { | |
| 52 | + | var d = new DictionaryExpressionData(); | |
| 53 | + | foreach (var p in ctx.Properties()) | |
| 54 | + | { | |
| 55 | + | var token = p.Value; | |
| 56 | + | d[p.Name] = token.ToExpressionData(); | |
| 57 | + | } | |
| 58 | + | return d; | |
| 59 | + | } | |
| 60 | + | ||
| 61 | + | static int Main(string[] args) | |
| 62 | + | { | |
| 63 | + | JObject root; using (var jr0 = new JsonTextReader(new StringReader(File.ReadAllText(args[0]))) { DateParseHandling = DateParseHandling.None }) root = JObject.Load(jr0); | |
| 64 | + | var defaultCtx = (JObject)root["contexts"]; | |
| 65 | + | var funcs = new IFunctionInfo[] | |
| 66 | + | { | |
| 67 | + | new FunctionInfo<Success>("success", 0, 0), | |
| 68 | + | new FunctionInfo<Always>("always", 0, 0), | |
| 69 | + | new FunctionInfo<HashFiles>("hashFiles", 1, 255), | |
| 70 | + | }; | |
| 71 | + | var output = new StreamWriter(Console.OpenStandardOutput(), new UTF8Encoding(false)); | |
| 72 | + | var defaultData = Load(defaultCtx); | |
| 73 | + | foreach (var c in (JArray)root["cases"]) | |
| 74 | + | { | |
| 75 | + | var e = (string)c["e"]; | |
| 76 | + | var custom = c["ctx"] as JObject; | |
| 77 | + | var data = custom != null ? Load(custom) : defaultData; | |
| 78 | + | var names = (custom ?? defaultCtx).Properties().Select(p => p.Name); | |
| 79 | + | var res = new JObject(); | |
| 80 | + | IExpressionNode tree = null; | |
| 81 | + | try | |
| 82 | + | { | |
| 83 | + | tree = new ExpressionParser().CreateTree(e, null, names.Select(n => (INamedValueInfo)new NamedValueInfo<CtxNode>(n)).ToList(), funcs); | |
| 84 | + | } | |
| 85 | + | catch (Exception ex) | |
| 86 | + | { | |
| 87 | + | res["err"] = new JObject { ["t"] = "parse", ["m"] = ex.Message, ["x"] = ex.GetType().Name }; | |
| 88 | + | } | |
| 89 | + | if (res["err"] == null) | |
| 90 | + | { | |
| 91 | + | if (tree == null) | |
| 92 | + | { | |
| 93 | + | res["empty"] = true; | |
| 94 | + | } | |
| 95 | + | else | |
| 96 | + | { | |
| 97 | + | try | |
| 98 | + | { | |
| 99 | + | var r = tree.Evaluate(null, null, data, null); | |
| 100 | + | res["k"] = r.Kind.ToString(); | |
| 101 | + | res["s"] = r.ConvertToString(); | |
| 102 | + | // toJson of the result | |
| 103 | + | var tj = new ToJson(); | |
| 104 | + | tj.AddParameter(new Literal(r.Value)); | |
| 105 | + | try | |
| 106 | + | { | |
| 107 | + | var jr = ((IExpressionNode)tj).Evaluate(null, null, data, null); | |
| 108 | + | res["j"] = jr.ConvertToString(); | |
| 109 | + | } | |
| 110 | + | catch (Exception ex2) | |
| 111 | + | { | |
| 112 | + | res["jerr"] = ex2.Message; | |
| 113 | + | } | |
| 114 | + | } | |
| 115 | + | catch (Exception ex) | |
| 116 | + | { | |
| 117 | + | res["err"] = new JObject { ["t"] = "eval", ["m"] = ex.Message, ["x"] = ex.GetType().Name }; | |
| 118 | + | } | |
| 119 | + | } | |
| 120 | + | } | |
| 121 | + | // ASCII-escaped, so that a lone surrogate (which UTF-8 cannot carry) survives. | |
| 122 | + | var sw = new StringWriter(); | |
| 123 | + | using (var jw = new JsonTextWriter(sw) { StringEscapeHandling = StringEscapeHandling.EscapeNonAscii, Formatting = Formatting.None }) | |
| 124 | + | { | |
| 125 | + | res.WriteTo(jw); | |
| 126 | + | } | |
| 127 | + | output.WriteLine(sw.ToString()); | |
| 128 | + | } | |
| 129 | + | output.Flush(); | |
| 130 | + | return 0; | |
| 131 | + | } | |
| 132 | + | } | |
| @@ -0,0 +1,132 @@ | |||||
| 1 | + | // SPDX-License-Identifier: MIT | |||
| 2 | + | ||||
| 3 | + | using System; | |||
| 4 | + | using System.Collections.Generic; | |||
| 5 | + | using System.IO; | |||
| 6 | + | using System.Linq; | |||
| 7 | + | using System.Text; | |||
| 8 | + | using GitHub.Actions.Expressions; | |||
| 9 | + | using GitHub.Actions.Expressions.Data; | |||
| 10 | + | using GitHub.Actions.Expressions.Sdk; | |||
| 11 | + | using GitHub.Actions.Expressions.Sdk.Functions; | |||
| 12 | + | using Newtonsoft.Json; | |||
| 13 | + | using Newtonsoft.Json.Linq; | |||
| 14 | + | ||||
| 15 | + | // Differential harness: runs expressions through the runner's own | |||
| 16 | + | // Expressions2 code (compiled from the runner repository) and prints one JSON | |||
| 17 | + | // line per case. | |||
| 18 | + | // | |||
| 19 | + | // Input (argv[0]): JSON {"contexts": {name: json}, "cases": [{"e": expr, "ctx": {name: json} | null}]} | |||
| 20 | + | // Output: JSON lines {"k": kind, "s": string form, "j": toJson, "err": {"t": "parse"|"eval", "m": message}} | |||
| 21 | + | ||||
| 22 | + | internal sealed class CtxNode : NamedValue | |||
| 23 | + | { | |||
| 24 | + | protected override Object EvaluateCore(EvaluationContext context, out ResultMemory resultMemory) | |||
| 25 | + | { | |||
| 26 | + | resultMemory = null; | |||
| 27 | + | return (context.State as DictionaryExpressionData)[Name]; | |||
| 28 | + | } | |||
| 29 | + | } | |||
| 30 | + | ||||
| 31 | + | internal sealed class Success : Function { protected override Object EvaluateCore(EvaluationContext c, out ResultMemory m) { m = null; return true; } } | |||
| 32 | + | internal sealed class Always : Function { protected override Object EvaluateCore(EvaluationContext c, out ResultMemory m) { m = null; return true; } } | |||
| 33 | + | internal sealed class HashFiles : Function | |||
| 34 | + | { | |||
| 35 | + | protected override Object EvaluateCore(EvaluationContext c, out ResultMemory m) | |||
| 36 | + | { | |||
| 37 | + | m = null; | |||
| 38 | + | var s = "hash:"; | |||
| 39 | + | for (var i = 0; i < Parameters.Count; i++) | |||
| 40 | + | { | |||
| 41 | + | if (i > 0) s += "|"; | |||
| 42 | + | s += Parameters[i].Evaluate(c).ConvertToString(); | |||
| 43 | + | } | |||
| 44 | + | return s; | |||
| 45 | + | } | |||
| 46 | + | } | |||
| 47 | + | ||||
| 48 | + | internal static class P | |||
| 49 | + | { | |||
| 50 | + | static DictionaryExpressionData Load(JObject ctx) | |||
| 51 | + | { | |||
| 52 | + | var d = new DictionaryExpressionData(); | |||
| 53 | + | foreach (var p in ctx.Properties()) | |||
| 54 | + | { | |||
| 55 | + | var token = p.Value; | |||
| 56 | + | d[p.Name] = token.ToExpressionData(); | |||
| 57 | + | } | |||
| 58 | + | return d; | |||
| 59 | + | } | |||
| 60 | + | ||||
| 61 | + | static int Main(string[] args) | |||
| 62 | + | { | |||
| 63 | + | JObject root; using (var jr0 = new JsonTextReader(new StringReader(File.ReadAllText(args[0]))) { DateParseHandling = DateParseHandling.None }) root = JObject.Load(jr0); | |||
| 64 | + | var defaultCtx = (JObject)root["contexts"]; | |||
| 65 | + | var funcs = new IFunctionInfo[] | |||
| 66 | + | { | |||
| 67 | + | new FunctionInfo<Success>("success", 0, 0), | |||
| 68 | + | new FunctionInfo<Always>("always", 0, 0), | |||
| 69 | + | new FunctionInfo<HashFiles>("hashFiles", 1, 255), | |||
| 70 | + | }; | |||
| 71 | + | var output = new StreamWriter(Console.OpenStandardOutput(), new UTF8Encoding(false)); | |||
| 72 | + | var defaultData = Load(defaultCtx); | |||
| 73 | + | foreach (var c in (JArray)root["cases"]) | |||
| 74 | + | { | |||
| 75 | + | var e = (string)c["e"]; | |||
| 76 | + | var custom = c["ctx"] as JObject; | |||
| 77 | + | var data = custom != null ? Load(custom) : defaultData; | |||
| 78 | + | var names = (custom ?? defaultCtx).Properties().Select(p => p.Name); | |||
| 79 | + | var res = new JObject(); | |||
| 80 | + | IExpressionNode tree = null; | |||
| 81 | + | try | |||
| 82 | + | { | |||
| 83 | + | tree = new ExpressionParser().CreateTree(e, null, names.Select(n => (INamedValueInfo)new NamedValueInfo<CtxNode>(n)).ToList(), funcs); | |||
| 84 | + | } | |||
| 85 | + | catch (Exception ex) | |||
| 86 | + | { | |||
| 87 | + | res["err"] = new JObject { ["t"] = "parse", ["m"] = ex.Message, ["x"] = ex.GetType().Name }; | |||
| 88 | + | } | |||
| 89 | + | if (res["err"] == null) | |||
| 90 | + | { | |||
| 91 | + | if (tree == null) | |||
| 92 | + | { | |||
| 93 | + | res["empty"] = true; | |||
| 94 | + | } | |||
| 95 | + | else | |||
| 96 | + | { | |||
| 97 | + | try | |||
| 98 | + | { | |||
| 99 | + | var r = tree.Evaluate(null, null, data, null); | |||
| 100 | + | res["k"] = r.Kind.ToString(); | |||
| 101 | + | res["s"] = r.ConvertToString(); | |||
| 102 | + | // toJson of the result | |||
| 103 | + | var tj = new ToJson(); | |||
| 104 | + | tj.AddParameter(new Literal(r.Value)); | |||
| 105 | + | try | |||
| 106 | + | { | |||
| 107 | + | var jr = ((IExpressionNode)tj).Evaluate(null, null, data, null); | |||
| 108 | + | res["j"] = jr.ConvertToString(); | |||
| 109 | + | } | |||
| 110 | + | catch (Exception ex2) | |||
| 111 | + | { | |||
| 112 | + | res["jerr"] = ex2.Message; | |||
| 113 | + | } | |||
| 114 | + | } | |||
| 115 | + | catch (Exception ex) | |||
| 116 | + | { | |||
| 117 | + | res["err"] = new JObject { ["t"] = "eval", ["m"] = ex.Message, ["x"] = ex.GetType().Name }; | |||
| 118 | + | } | |||
| 119 | + | } | |||
| 120 | + | } | |||
| 121 | + | // ASCII-escaped, so that a lone surrogate (which UTF-8 cannot carry) survives. | |||
| 122 | + | var sw = new StringWriter(); | |||
| 123 | + | using (var jw = new JsonTextWriter(sw) { StringEscapeHandling = StringEscapeHandling.EscapeNonAscii, Formatting = Formatting.None }) | |||
| 124 | + | { | |||
| 125 | + | res.WriteTo(jw); | |||
| 126 | + | } | |||
| 127 | + | output.WriteLine(sw.ToString()); | |||
| 128 | + | } | |||
| 129 | + | output.Flush(); | |||
| 130 | + | return 0; | |||
| 131 | + | } | |||
| 132 | + | } | |||
A expr/gen/runner-new/engines.mjs +73 -0
| @@ -0,0 +1,73 @@ | |||
| 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 | + | ||
| 17 | + | import { execFileSync } from "node:child_process"; | |
| 18 | + | import { readFileSync, writeFileSync, mkdtempSync, rmSync } from "node:fs"; | |
| 19 | + | import { tmpdir } from "node:os"; | |
| 20 | + | import { join, dirname } from "node:path"; | |
| 21 | + | import { fileURLToPath } from "node:url"; | |
| 22 | + | import { gunzipSync } from "node:zlib"; | |
| 23 | + | import { checkEnvironment } from "../runner/env.mjs"; | |
| 24 | + | ||
| 25 | + | const here = dirname(fileURLToPath(import.meta.url)); | |
| 26 | + | const testdata = join(here, "../../testdata"); | |
| 27 | + | checkEnvironment(process.argv[2]); | |
| 28 | + | ||
| 29 | + | const dec = h => (h === "-" ? "" : h.split(".").map(u => String.fromCharCode(parseInt(u, 16))).join("")); | |
| 30 | + | const lit = s => "'" + s.replaceAll("'", "''") + "'"; | |
| 31 | + | const hasLone = s => /[\ud800-\udbff](?![\udc00-\udfff])|(?<![\ud800-\udbff])[\udc00-\udfff]/.test(s); | |
| 32 | + | ||
| 33 | + | const oracle = JSON.parse(gunzipSync(readFileSync(join(testdata, "oracle.json.gz")))); | |
| 34 | + | const corpora = { oracle: { contexts: oracle.contexts, cases: oracle.cases.map(c => c.e) } }; | |
| 35 | + | const exprs = []; | |
| 36 | + | for (const c of JSON.parse(readFileSync(join(testdata, "fromjson_cases.json"), "utf8"))) exprs.push(`toJSON(fromJSON(${lit(c.in)}))`); | |
| 37 | + | for (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 | + | } | |
| 42 | + | for (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 | + | } | |
| 48 | + | corpora.strings = { contexts: {}, cases: exprs }; | |
| 49 | + | ||
| 50 | + | const run = (dll, input) => execFileSync("dotnet", [dll, input], { maxBuffer: 1 << 30 }).toString("utf8").split("\n").filter(Boolean).map(l => JSON.parse(l)); | |
| 51 | + | const sig = r => (r.err ? JSON.stringify(["err", r.err.t, r.err.x === "JsonReaderException" ? r.err.x : r.err.m]) : JSON.stringify(r)); | |
| 52 | + | ||
| 53 | + | const tmp = mkdtempSync(join(tmpdir(), "engines-")); | |
| 54 | + | let unexpected = 0, total = 0; | |
| 55 | + | for (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 | + | } | |
| 71 | + | rmSync(tmp, { recursive: true }); | |
| 72 | + | console.log(`${total} expressions, ${unexpected} unexpected differences`); | |
| 73 | + | process.exit(unexpected ? 1 : 0); | |
| @@ -0,0 +1,73 @@ | |||||
| 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 | + | ||||
| 17 | + | import { execFileSync } from "node:child_process"; | |||
| 18 | + | import { readFileSync, writeFileSync, mkdtempSync, rmSync } from "node:fs"; | |||
| 19 | + | import { tmpdir } from "node:os"; | |||
| 20 | + | import { join, dirname } from "node:path"; | |||
| 21 | + | import { fileURLToPath } from "node:url"; | |||
| 22 | + | import { gunzipSync } from "node:zlib"; | |||
| 23 | + | import { checkEnvironment } from "../runner/env.mjs"; | |||
| 24 | + | ||||
| 25 | + | const here = dirname(fileURLToPath(import.meta.url)); | |||
| 26 | + | const testdata = join(here, "../../testdata"); | |||
| 27 | + | checkEnvironment(process.argv[2]); | |||
| 28 | + | ||||
| 29 | + | const dec = h => (h === "-" ? "" : h.split(".").map(u => String.fromCharCode(parseInt(u, 16))).join("")); | |||
| 30 | + | const lit = s => "'" + s.replaceAll("'", "''") + "'"; | |||
| 31 | + | const hasLone = s => /[\ud800-\udbff](?![\udc00-\udfff])|(?<![\ud800-\udbff])[\udc00-\udfff]/.test(s); | |||
| 32 | + | ||||
| 33 | + | const oracle = JSON.parse(gunzipSync(readFileSync(join(testdata, "oracle.json.gz")))); | |||
| 34 | + | const corpora = { oracle: { contexts: oracle.contexts, cases: oracle.cases.map(c => c.e) } }; | |||
| 35 | + | const exprs = []; | |||
| 36 | + | for (const c of JSON.parse(readFileSync(join(testdata, "fromjson_cases.json"), "utf8"))) exprs.push(`toJSON(fromJSON(${lit(c.in)}))`); | |||
| 37 | + | for (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 | + | } | |||
| 42 | + | for (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 | + | } | |||
| 48 | + | corpora.strings = { contexts: {}, cases: exprs }; | |||
| 49 | + | ||||
| 50 | + | const run = (dll, input) => execFileSync("dotnet", [dll, input], { maxBuffer: 1 << 30 }).toString("utf8").split("\n").filter(Boolean).map(l => JSON.parse(l)); | |||
| 51 | + | const sig = r => (r.err ? JSON.stringify(["err", r.err.t, r.err.x === "JsonReaderException" ? r.err.x : r.err.m]) : JSON.stringify(r)); | |||
| 52 | + | ||||
| 53 | + | const tmp = mkdtempSync(join(tmpdir(), "engines-")); | |||
| 54 | + | let unexpected = 0, total = 0; | |||
| 55 | + | for (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 | + | } | |||
| 71 | + | rmSync(tmp, { recursive: true }); | |||
| 72 | + | console.log(`${total} expressions, ${unexpected} unexpected differences`); | |||
| 73 | + | process.exit(unexpected ? 1 : 0); | |||
A expr/gen/runner-new/runner-new.csproj +24 -0
| @@ -0,0 +1,24 @@ | |||
| 1 | + | <Project Sdk="Microsoft.NET.Sdk"> | |
| 2 | + | <!-- Same driver as ../runner, built on the newer engine of actions/runner, | |
| 3 | + | src/Sdk/Expressions (namespace GitHub.Actions.Expressions), the one that | |
| 4 | + | WorkflowTemplateEvaluator runs next to the legacy engine to compare. | |
| 5 | + | Used by engines.mjs to find out where the two engines differ. --> | |
| 6 | + | <PropertyGroup> | |
| 7 | + | <OutputType>Exe</OutputType> | |
| 8 | + | <TargetFramework>net9.0</TargetFramework> | |
| 9 | + | <Nullable>disable</Nullable> | |
| 10 | + | <LangVersion>11.0</LangVersion> | |
| 11 | + | <NoWarn>NU1701;CS8632;CS0108;CS0114;CS1591;CS0618;SYSLIB0050</NoWarn> | |
| 12 | + | <EnableDefaultCompileItems>false</EnableDefaultCompileItems> | |
| 13 | + | <AllowUnsafeBlocks>true</AllowUnsafeBlocks> | |
| 14 | + | </PropertyGroup> | |
| 15 | + | <Target Name="RequireRunnerSrc" BeforeTargets="BeforeBuild;CoreCompile"> | |
| 16 | + | <Error Condition="'$(RunnerSrc)' == '' Or !Exists('$(RunnerSrc)/Expressions')" Text="Set RunnerSrc to the src/Sdk directory of a clone of actions/runner: dotnet build -c Release expr/gen/runner-new -p:RunnerSrc=<clone>/src/Sdk" /> | |
| 17 | + | </Target> | |
| 18 | + | <ItemGroup> | |
| 19 | + | <Compile Include="$(RunnerSrc)/Expressions/**/*.cs" /> | |
| 20 | + | <Compile Include="Program.cs" /> | |
| 21 | + | <EmbeddedResource Include="$(RunnerSrc)/Expressions/Resources/ExpressionResources.resx" LogicalName="GitHub.Actions.Expressions.ExpressionResources.resources" /> | |
| 22 | + | <PackageReference Include="Newtonsoft.Json" Version="13.0.3" /> | |
| 23 | + | </ItemGroup> | |
| 24 | + | </Project> | |
| @@ -0,0 +1,24 @@ | |||||
| 1 | + | <Project Sdk="Microsoft.NET.Sdk"> | |||
| 2 | + | <!-- Same driver as ../runner, built on the newer engine of actions/runner, | |||
| 3 | + | src/Sdk/Expressions (namespace GitHub.Actions.Expressions), the one that | |||
| 4 | + | WorkflowTemplateEvaluator runs next to the legacy engine to compare. | |||
| 5 | + | Used by engines.mjs to find out where the two engines differ. --> | |||
| 6 | + | <PropertyGroup> | |||
| 7 | + | <OutputType>Exe</OutputType> | |||
| 8 | + | <TargetFramework>net9.0</TargetFramework> | |||
| 9 | + | <Nullable>disable</Nullable> | |||
| 10 | + | <LangVersion>11.0</LangVersion> | |||
| 11 | + | <NoWarn>NU1701;CS8632;CS0108;CS0114;CS1591;CS0618;SYSLIB0050</NoWarn> | |||
| 12 | + | <EnableDefaultCompileItems>false</EnableDefaultCompileItems> | |||
| 13 | + | <AllowUnsafeBlocks>true</AllowUnsafeBlocks> | |||
| 14 | + | </PropertyGroup> | |||
| 15 | + | <Target Name="RequireRunnerSrc" BeforeTargets="BeforeBuild;CoreCompile"> | |||
| 16 | + | <Error Condition="'$(RunnerSrc)' == '' Or !Exists('$(RunnerSrc)/Expressions')" Text="Set RunnerSrc to the src/Sdk directory of a clone of actions/runner: dotnet build -c Release expr/gen/runner-new -p:RunnerSrc=<clone>/src/Sdk" /> | |||
| 17 | + | </Target> | |||
| 18 | + | <ItemGroup> | |||
| 19 | + | <Compile Include="$(RunnerSrc)/Expressions/**/*.cs" /> | |||
| 20 | + | <Compile Include="Program.cs" /> | |||
| 21 | + | <EmbeddedResource Include="$(RunnerSrc)/Expressions/Resources/ExpressionResources.resx" LogicalName="GitHub.Actions.Expressions.ExpressionResources.resources" /> | |||
| 22 | + | <PackageReference Include="Newtonsoft.Json" Version="13.0.3" /> | |||
| 23 | + | </ItemGroup> | |||
| 24 | + | </Project> | |||