test(expr): build the runner's Expressions2 engine and record its answers on the oracle corpus
- commit
- 3251a5503d62c80a8bc8bd645c35a99768118545
- author
- brice · · 2026-10-12 17:21 UTC
- parent
- 3a9395e
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
7 files · +234 -0
A expr/gen/runner/.gitignore +2 -0
| @@ -0,0 +1,2 @@ | |||
| 1 | + | bin/ | |
| 2 | + | obj/ | |
| @@ -0,0 +1,2 @@ | |||||
| 1 | + | bin/ | |||
| 2 | + | obj/ | |||
A expr/gen/runner/Program.cs +126 -0
| @@ -0,0 +1,126 @@ | |||
| 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.DistributedTask.Expressions2; | |
| 9 | + | using GitHub.DistributedTask.Expressions2.Sdk; | |
| 10 | + | using GitHub.DistributedTask.Expressions2.Sdk.Functions; | |
| 11 | + | using GitHub.DistributedTask.Pipelines.ContextData; | |
| 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 DictionaryContextData)[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 DictionaryContextData Load(JObject ctx) | |
| 51 | + | { | |
| 52 | + | var d = new DictionaryContextData(); | |
| 53 | + | foreach (var p in ctx.Properties()) | |
| 54 | + | { | |
| 55 | + | var token = p.Value; | |
| 56 | + | d[p.Name] = token.ToPipelineContextData(); | |
| 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 | + | output.WriteLine(res.ToString(Formatting.None)); | |
| 122 | + | } | |
| 123 | + | output.Flush(); | |
| 124 | + | return 0; | |
| 125 | + | } | |
| 126 | + | } | |
| @@ -0,0 +1,126 @@ | |||||
| 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.DistributedTask.Expressions2; | |||
| 9 | + | using GitHub.DistributedTask.Expressions2.Sdk; | |||
| 10 | + | using GitHub.DistributedTask.Expressions2.Sdk.Functions; | |||
| 11 | + | using GitHub.DistributedTask.Pipelines.ContextData; | |||
| 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 DictionaryContextData)[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 DictionaryContextData Load(JObject ctx) | |||
| 51 | + | { | |||
| 52 | + | var d = new DictionaryContextData(); | |||
| 53 | + | foreach (var p in ctx.Properties()) | |||
| 54 | + | { | |||
| 55 | + | var token = p.Value; | |||
| 56 | + | d[p.Name] = token.ToPipelineContextData(); | |||
| 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 | + | output.WriteLine(res.ToString(Formatting.None)); | |||
| 122 | + | } | |||
| 123 | + | output.Flush(); | |||
| 124 | + | return 0; | |||
| 125 | + | } | |||
| 126 | + | } | |||
A expr/gen/runner/README.md +18 -0
| @@ -0,0 +1,18 @@ | |||
| 1 | + | # Running the runner's expression engine | |
| 2 | + | ||
| 3 | + | `runner.csproj` compiles `src/Sdk/DTExpressions2` (the Expressions2 engine the | |
| 4 | + | runner uses on github.com), its `ContextData` types and `DTLogging` from a clone | |
| 5 | + | of https://github.com/actions/runner, replacing the few assemblies they need by | |
| 6 | + | the stubs in `Stubs.cs`. `Program.cs` reads | |
| 7 | + | `{"contexts": {...}, "cases": [{"e": "<expression>"}]}` and prints one JSON line | |
| 8 | + | per case: `{"k": kind, "s": string form, "j": toJSON of the result}`, or | |
| 9 | + | `{"err": {"t": "parse"|"eval", "m": message}}`. | |
| 10 | + | ||
| 11 | + | dotnet build -c Release expr/gen/runner -p:RunnerSrc=/path/to/runner/src/Sdk | |
| 12 | + | dotnet expr/gen/runner/bin/Release/net9.0/runner.dll input.json | |
| 13 | + | ||
| 14 | + | It is how the C# column of `expr/PORTING.md` was established: the 34 765 | |
| 15 | + | expressions of `testdata/oracle.json.gz` were run through it and compared with | |
| 16 | + | this package. Requires the .NET SDK; nothing in `go test` depends on it. | |
| 17 | + | The stubs are not the runner's code: `JsonUtility.ToString` is | |
| 18 | + | `JsonConvert.SerializeObject` and the error resources are copied texts. | |
| @@ -0,0 +1,18 @@ | |||||
| 1 | + | # Running the runner's expression engine | |||
| 2 | + | ||||
| 3 | + | `runner.csproj` compiles `src/Sdk/DTExpressions2` (the Expressions2 engine the | |||
| 4 | + | runner uses on github.com), its `ContextData` types and `DTLogging` from a clone | |||
| 5 | + | of https://github.com/actions/runner, replacing the few assemblies they need by | |||
| 6 | + | the stubs in `Stubs.cs`. `Program.cs` reads | |||
| 7 | + | `{"contexts": {...}, "cases": [{"e": "<expression>"}]}` and prints one JSON line | |||
| 8 | + | per case: `{"k": kind, "s": string form, "j": toJSON of the result}`, or | |||
| 9 | + | `{"err": {"t": "parse"|"eval", "m": message}}`. | |||
| 10 | + | ||||
| 11 | + | dotnet build -c Release expr/gen/runner -p:RunnerSrc=/path/to/runner/src/Sdk | |||
| 12 | + | dotnet expr/gen/runner/bin/Release/net9.0/runner.dll input.json | |||
| 13 | + | ||||
| 14 | + | It is how the C# column of `expr/PORTING.md` was established: the 34 765 | |||
| 15 | + | expressions of `testdata/oracle.json.gz` were run through it and compared with | |||
| 16 | + | this package. Requires the .NET SDK; nothing in `go test` depends on it. | |||
| 17 | + | The stubs are not the runner's code: `JsonUtility.ToString` is | |||
| 18 | + | `JsonConvert.SerializeObject` and the error resources are copied texts. | |||
A expr/gen/runner/Stubs.cs +28 -0
| @@ -0,0 +1,28 @@ | |||
| 1 | + | // SPDX-License-Identifier: MIT | |
| 2 | + | ||
| 3 | + | using System; | |
| 4 | + | namespace GitHub.Services.Common { | |
| 5 | + | public static class ArgumentUtility { | |
| 6 | + | public static void CheckForNull(Object v, String name) { if (v == null) throw new ArgumentNullException(name); } | |
| 7 | + | public static void CheckStringForNullOrEmpty(String v, String name, bool trim = false) { if (String.IsNullOrEmpty(v)) throw new ArgumentException(name); } | |
| 8 | + | } | |
| 9 | + | } | |
| 10 | + | namespace GitHub.Services.WebApi.Internal { | |
| 11 | + | [AttributeUsage(AttributeTargets.All)] public sealed class ClientIgnoreAttribute : Attribute {} | |
| 12 | + | } | |
| 13 | + | namespace GitHub.Services.WebApi { | |
| 14 | + | public abstract class VssSecureJsonConverter : Newtonsoft.Json.JsonConverter { public override void WriteJson(Newtonsoft.Json.JsonWriter w, object v, Newtonsoft.Json.JsonSerializer s) {} } | |
| 15 | + | public static class JsonUtility { | |
| 16 | + | // Same as the runner's JsonUtility.ToString for a string: default JsonTextWriter settings. | |
| 17 | + | public static String ToString(Object o) { return Newtonsoft.Json.JsonConvert.SerializeObject(o); } | |
| 18 | + | } | |
| 19 | + | } | |
| 20 | + | namespace GitHub.DistributedTask.Expressions { | |
| 21 | + | internal static class ExpressionResources { | |
| 22 | + | public static String ExceededAllowedMemory(object e) { return "The maximum allowed memory size was exceeded while evaluating the following expression: " + e; } | |
| 23 | + | public static String InvalidFormatArgIndex(object f) { return "The following format string references more arguments than were supplied: " + f; } | |
| 24 | + | public static String InvalidFormatSpecifiers(object s, object k) { return "The format specifiers '" + s + "' are not valid for objects of type '" + k + "'"; } | |
| 25 | + | public static String InvalidFormatString(object f) { return "The following format string is invalid: " + f; } | |
| 26 | + | } | |
| 27 | + | } | |
| 28 | + | namespace GitHub.Actions.Expressions.Data { internal class Dummy {} } | |
| @@ -0,0 +1,28 @@ | |||||
| 1 | + | // SPDX-License-Identifier: MIT | |||
| 2 | + | ||||
| 3 | + | using System; | |||
| 4 | + | namespace GitHub.Services.Common { | |||
| 5 | + | public static class ArgumentUtility { | |||
| 6 | + | public static void CheckForNull(Object v, String name) { if (v == null) throw new ArgumentNullException(name); } | |||
| 7 | + | public static void CheckStringForNullOrEmpty(String v, String name, bool trim = false) { if (String.IsNullOrEmpty(v)) throw new ArgumentException(name); } | |||
| 8 | + | } | |||
| 9 | + | } | |||
| 10 | + | namespace GitHub.Services.WebApi.Internal { | |||
| 11 | + | [AttributeUsage(AttributeTargets.All)] public sealed class ClientIgnoreAttribute : Attribute {} | |||
| 12 | + | } | |||
| 13 | + | namespace GitHub.Services.WebApi { | |||
| 14 | + | public abstract class VssSecureJsonConverter : Newtonsoft.Json.JsonConverter { public override void WriteJson(Newtonsoft.Json.JsonWriter w, object v, Newtonsoft.Json.JsonSerializer s) {} } | |||
| 15 | + | public static class JsonUtility { | |||
| 16 | + | // Same as the runner's JsonUtility.ToString for a string: default JsonTextWriter settings. | |||
| 17 | + | public static String ToString(Object o) { return Newtonsoft.Json.JsonConvert.SerializeObject(o); } | |||
| 18 | + | } | |||
| 19 | + | } | |||
| 20 | + | namespace GitHub.DistributedTask.Expressions { | |||
| 21 | + | internal static class ExpressionResources { | |||
| 22 | + | public static String ExceededAllowedMemory(object e) { return "The maximum allowed memory size was exceeded while evaluating the following expression: " + e; } | |||
| 23 | + | public static String InvalidFormatArgIndex(object f) { return "The following format string references more arguments than were supplied: " + f; } | |||
| 24 | + | public static String InvalidFormatSpecifiers(object s, object k) { return "The format specifiers '" + s + "' are not valid for objects of type '" + k + "'"; } | |||
| 25 | + | public static String InvalidFormatString(object f) { return "The following format string is invalid: " + f; } | |||
| 26 | + | } | |||
| 27 | + | } | |||
| 28 | + | namespace GitHub.Actions.Expressions.Data { internal class Dummy {} } | |||
A expr/gen/runner/answers.mjs +37 -0
| @@ -0,0 +1,37 @@ | |||
| 1 | + | // SPDX-License-Identifier: MIT | |
| 2 | + | // | |
| 3 | + | // Generates testdata/runner.json.gz: what the C# runner's expression engine | |
| 4 | + | // (actions/runner, src/Sdk/DTExpressions2) answers on the expressions of | |
| 5 | + | // testdata/oracle.json.gz. Needs the .NET SDK and a clone of actions/runner: | |
| 6 | + | // | |
| 7 | + | // dotnet build -c Release expr/gen/runner -p:RunnerSrc=<runner>/src/Sdk | |
| 8 | + | // node expr/gen/runner/answers.mjs <runner clone> | gzip -9 -n > expr/testdata/runner.json.gz | |
| 9 | + | ||
| 10 | + | import { execFileSync } from "node:child_process"; | |
| 11 | + | import { readFileSync, writeFileSync, mkdtempSync } from "node:fs"; | |
| 12 | + | import { tmpdir } from "node:os"; | |
| 13 | + | import { join, dirname } from "node:path"; | |
| 14 | + | import { fileURLToPath } from "node:url"; | |
| 15 | + | import { gunzipSync } from "node:zlib"; | |
| 16 | + | ||
| 17 | + | const here = dirname(fileURLToPath(import.meta.url)); | |
| 18 | + | const runnerClone = process.argv[2]; | |
| 19 | + | if (!runnerClone) throw new Error("usage: answers.mjs <actions/runner clone>"); | |
| 20 | + | ||
| 21 | + | const oracle = JSON.parse(gunzipSync(readFileSync(join(here, "../../testdata/oracle.json.gz")))); | |
| 22 | + | const dir = mkdtempSync(join(tmpdir(), "runner-answers-")); | |
| 23 | + | const input = join(dir, "in.json"); | |
| 24 | + | writeFileSync(input, JSON.stringify({ contexts: oracle.contexts, cases: oracle.cases.map(c => ({ e: c.e })) })); | |
| 25 | + | const out = execFileSync("dotnet", [join(here, "bin/Release/net9.0/runner.dll"), input], { maxBuffer: 1 << 30 }).toString("utf8").split("\n").filter(Boolean); | |
| 26 | + | if (out.length !== oracle.cases.length) throw new Error(`got ${out.length} answers for ${oracle.cases.length} cases`); | |
| 27 | + | const git = (...a) => execFileSync("git", ["-C", runnerClone, ...a]).toString().trim(); | |
| 28 | + | const dotnet = execFileSync("dotnet", ["--list-runtimes"]).toString().split("\n").find(l => l.includes("Microsoft.NETCore.App 9")) ?? ""; | |
| 29 | + | const cases = out.map((l, i) => { | |
| 30 | + | const r = JSON.parse(l); | |
| 31 | + | if (r.err) delete r.err.x; // .NET exception type name | |
| 32 | + | return { e: oracle.cases[i].e, ...r }; | |
| 33 | + | }); | |
| 34 | + | process.stdout.write(JSON.stringify({ | |
| 35 | + | reference: { source: "actions/runner src/Sdk/DTExpressions2 (Expressions2)", commit: git("rev-parse", "HEAD"), runtime: dotnet.trim() }, | |
| 36 | + | cases, | |
| 37 | + | })); | |
| @@ -0,0 +1,37 @@ | |||||
| 1 | + | // SPDX-License-Identifier: MIT | |||
| 2 | + | // | |||
| 3 | + | // Generates testdata/runner.json.gz: what the C# runner's expression engine | |||
| 4 | + | // (actions/runner, src/Sdk/DTExpressions2) answers on the expressions of | |||
| 5 | + | // testdata/oracle.json.gz. Needs the .NET SDK and a clone of actions/runner: | |||
| 6 | + | // | |||
| 7 | + | // dotnet build -c Release expr/gen/runner -p:RunnerSrc=<runner>/src/Sdk | |||
| 8 | + | // node expr/gen/runner/answers.mjs <runner clone> | gzip -9 -n > expr/testdata/runner.json.gz | |||
| 9 | + | ||||
| 10 | + | import { execFileSync } from "node:child_process"; | |||
| 11 | + | import { readFileSync, writeFileSync, mkdtempSync } from "node:fs"; | |||
| 12 | + | import { tmpdir } from "node:os"; | |||
| 13 | + | import { join, dirname } from "node:path"; | |||
| 14 | + | import { fileURLToPath } from "node:url"; | |||
| 15 | + | import { gunzipSync } from "node:zlib"; | |||
| 16 | + | ||||
| 17 | + | const here = dirname(fileURLToPath(import.meta.url)); | |||
| 18 | + | const runnerClone = process.argv[2]; | |||
| 19 | + | if (!runnerClone) throw new Error("usage: answers.mjs <actions/runner clone>"); | |||
| 20 | + | ||||
| 21 | + | const oracle = JSON.parse(gunzipSync(readFileSync(join(here, "../../testdata/oracle.json.gz")))); | |||
| 22 | + | const dir = mkdtempSync(join(tmpdir(), "runner-answers-")); | |||
| 23 | + | const input = join(dir, "in.json"); | |||
| 24 | + | writeFileSync(input, JSON.stringify({ contexts: oracle.contexts, cases: oracle.cases.map(c => ({ e: c.e })) })); | |||
| 25 | + | const out = execFileSync("dotnet", [join(here, "bin/Release/net9.0/runner.dll"), input], { maxBuffer: 1 << 30 }).toString("utf8").split("\n").filter(Boolean); | |||
| 26 | + | if (out.length !== oracle.cases.length) throw new Error(`got ${out.length} answers for ${oracle.cases.length} cases`); | |||
| 27 | + | const git = (...a) => execFileSync("git", ["-C", runnerClone, ...a]).toString().trim(); | |||
| 28 | + | const dotnet = execFileSync("dotnet", ["--list-runtimes"]).toString().split("\n").find(l => l.includes("Microsoft.NETCore.App 9")) ?? ""; | |||
| 29 | + | const cases = out.map((l, i) => { | |||
| 30 | + | const r = JSON.parse(l); | |||
| 31 | + | if (r.err) delete r.err.x; // .NET exception type name | |||
| 32 | + | return { e: oracle.cases[i].e, ...r }; | |||
| 33 | + | }); | |||
| 34 | + | process.stdout.write(JSON.stringify({ | |||
| 35 | + | reference: { source: "actions/runner src/Sdk/DTExpressions2 (Expressions2)", commit: git("rev-parse", "HEAD"), runtime: dotnet.trim() }, | |||
| 36 | + | cases, | |||
| 37 | + | })); | |||
A expr/gen/runner/runner.csproj +23 -0
| @@ -0,0 +1,23 @@ | |||
| 1 | + | <Project Sdk="Microsoft.NET.Sdk"> | |
| 2 | + | <!-- Builds the runner's own Expressions2 sources (actions/runner, MIT) with | |
| 3 | + | small stubs for the assemblies they depend on, and a driver that | |
| 4 | + | evaluates expressions with them. See README.md. --> | |
| 5 | + | <PropertyGroup> | |
| 6 | + | <OutputType>Exe</OutputType> | |
| 7 | + | <TargetFramework>net9.0</TargetFramework> | |
| 8 | + | <Nullable>disable</Nullable> | |
| 9 | + | <LangVersion>11.0</LangVersion> | |
| 10 | + | <NoWarn>NU1701;CS8632;CS0108;CS0114;CS1591;CS0618;SYSLIB0050</NoWarn> | |
| 11 | + | <EnableDefaultCompileItems>false</EnableDefaultCompileItems> | |
| 12 | + | <AllowUnsafeBlocks>true</AllowUnsafeBlocks> | |
| 13 | + | <RunnerSrc Condition="'$(RunnerSrc)' == ''">$(HOME)/dev/clones/runner/src/Sdk</RunnerSrc> | |
| 14 | + | </PropertyGroup> | |
| 15 | + | <ItemGroup> | |
| 16 | + | <Compile Include="$(RunnerSrc)/DTExpressions2/**/*.cs" /> | |
| 17 | + | <Compile Include="$(RunnerSrc)/DTLogging/**/*.cs" /> | |
| 18 | + | <Compile Include="$(RunnerSrc)/DTPipelines/Pipelines/ContextData/*.cs" Exclude="$(RunnerSrc)/DTPipelines/Pipelines/ContextData/PipelineContextDataExtensions.cs;$(RunnerSrc)/DTPipelines/Pipelines/ContextData/TemplateMemoryExtensions.cs;$(RunnerSrc)/DTPipelines/Pipelines/ContextData/TemplateTokenExtensions.cs" /> | |
| 19 | + | <Compile Include="Stubs.cs" /> | |
| 20 | + | <Compile Include="Program.cs" /> | |
| 21 | + | <PackageReference Include="Newtonsoft.Json" Version="13.0.3" /> | |
| 22 | + | </ItemGroup> | |
| 23 | + | </Project> | |
| @@ -0,0 +1,23 @@ | |||||
| 1 | + | <Project Sdk="Microsoft.NET.Sdk"> | |||
| 2 | + | <!-- Builds the runner's own Expressions2 sources (actions/runner, MIT) with | |||
| 3 | + | small stubs for the assemblies they depend on, and a driver that | |||
| 4 | + | evaluates expressions with them. See README.md. --> | |||
| 5 | + | <PropertyGroup> | |||
| 6 | + | <OutputType>Exe</OutputType> | |||
| 7 | + | <TargetFramework>net9.0</TargetFramework> | |||
| 8 | + | <Nullable>disable</Nullable> | |||
| 9 | + | <LangVersion>11.0</LangVersion> | |||
| 10 | + | <NoWarn>NU1701;CS8632;CS0108;CS0114;CS1591;CS0618;SYSLIB0050</NoWarn> | |||
| 11 | + | <EnableDefaultCompileItems>false</EnableDefaultCompileItems> | |||
| 12 | + | <AllowUnsafeBlocks>true</AllowUnsafeBlocks> | |||
| 13 | + | <RunnerSrc Condition="'$(RunnerSrc)' == ''">$(HOME)/dev/clones/runner/src/Sdk</RunnerSrc> | |||
| 14 | + | </PropertyGroup> | |||
| 15 | + | <ItemGroup> | |||
| 16 | + | <Compile Include="$(RunnerSrc)/DTExpressions2/**/*.cs" /> | |||
| 17 | + | <Compile Include="$(RunnerSrc)/DTLogging/**/*.cs" /> | |||
| 18 | + | <Compile Include="$(RunnerSrc)/DTPipelines/Pipelines/ContextData/*.cs" Exclude="$(RunnerSrc)/DTPipelines/Pipelines/ContextData/PipelineContextDataExtensions.cs;$(RunnerSrc)/DTPipelines/Pipelines/ContextData/TemplateMemoryExtensions.cs;$(RunnerSrc)/DTPipelines/Pipelines/ContextData/TemplateTokenExtensions.cs" /> | |||
| 19 | + | <Compile Include="Stubs.cs" /> | |||
| 20 | + | <Compile Include="Program.cs" /> | |||
| 21 | + | <PackageReference Include="Newtonsoft.Json" Version="13.0.3" /> | |||
| 22 | + | </ItemGroup> | |||
| 23 | + | </Project> | |||
A expr/testdata/runner.json.gz +0 -0 binary file: no text diff
binary file: no text diff