From 4c2045c3eca58fa35c5d748f3ef8d35562ac825a Mon Sep 17 00:00:00 2001 From: Patrick Stevens <3138005+Smaug123@users.noreply.github.com> Date: Fri, 7 Jun 2024 20:22:43 +0100 Subject: [PATCH] Obtain a compatible runtime from runconfig (#35) --- .gitignore | 1 + TestRunner/Program.fs | 89 +++++++++++++++++++++++++++++++++++- TestRunner/RuntimeConfig.fs | 50 ++++++++++++++++++++ TestRunner/Seq.fs | 22 +++++++++ TestRunner/TestRunner.fsproj | 13 ++++++ TestRunner/myriad.toml | 0 nix/deps.nix | 20 ++++++++ 7 files changed, 194 insertions(+), 1 deletion(-) create mode 100644 TestRunner/RuntimeConfig.fs create mode 100644 TestRunner/Seq.fs create mode 100644 TestRunner/myriad.toml diff --git a/.gitignore b/.gitignore index 38fb0e1..1766714 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ result .analyzerpackages/ analysis.sarif .direnv/ +Generated*.fs diff --git a/TestRunner/Program.fs b/TestRunner/Program.fs index dcdb721..aa21fce 100644 --- a/TestRunner/Program.fs +++ b/TestRunner/Program.fs @@ -1,8 +1,10 @@ namespace TestRunner open System +open WoofWare.DotnetRuntimeLocator open System.IO open System.Reflection +open System.Runtime.Loader // Fix for https://github.com/Smaug123/unofficial-nunit-runner/issues/8 // Set AppContext.BaseDirectory to where the test DLL is. @@ -15,7 +17,91 @@ type SetBaseDir (testDll : FileInfo) = member _.Dispose () = AppContext.SetData ("APP_CONTEXT_BASE_DIRECTORY", oldBaseDir) + +type Ctx (dll : FileInfo, runtimes : DirectoryInfo list) = + inherit AssemblyLoadContext () + + override this.Load (target : AssemblyName) : Assembly = + let path = Path.Combine (dll.Directory.FullName, $"%s{target.Name}.dll") + + if File.Exists path then + this.LoadFromAssemblyPath path + else + + runtimes + |> List.tryPick (fun di -> + let path = Path.Combine (di.FullName, $"%s{target.Name}.dll") + + if File.Exists path then + this.LoadFromAssemblyPath path |> Some + else + None + ) + |> Option.defaultValue null + + module Program = + let selectRuntime + (config : RuntimeOptions) + (f : DotnetEnvironmentInfo) + : Choice option + = + let rollForward = + match Environment.GetEnvironmentVariable "DOTNET_ROLL_FORWARD" with + | null -> + config.RollForward + |> Option.map RollForward.Parse + |> Option.defaultValue RollForward.Minor + | s -> RollForward.Parse s + + let desired = Version config.Framework.Version + + match rollForward with + | RollForward.Minor -> + let available = + f.Frameworks + |> Seq.choose (fun fi -> + if fi.Name = config.Framework.Name then + Some (fi, Version fi.Version) + else + None + ) + |> Seq.filter (fun (_, version) -> version.Major = desired.Major && version.Minor >= desired.Minor) + |> Seq.tryMinBy (fun (_, version) -> version.Minor, version.Build) + + match available with + | Some (f, _) -> Some (Choice1Of2 f) + | None -> + // TODO: maybe we can ask the SDK. But we keep on trucking: maybe we're self-contained, + // and we'll actually find all the runtime next to the DLL. + None + | _ -> failwith "non-minor RollForward not supported yet; please shout if you want it" + + let locateRuntimes (dll : FileInfo) : DirectoryInfo list = + let runtimeConfig = + let name = + if not (dll.Name.EndsWith (".dll", StringComparison.OrdinalIgnoreCase)) then + failwith $"Expected DLL %s{dll.FullName} to end in .dll" + + dll.Name.Substring (0, dll.Name.Length - 4) + + Path.Combine (dll.Directory.FullName, $"%s{name}.runtimeconfig.json") + |> File.ReadAllText + |> System.Text.Json.Nodes.JsonNode.Parse + |> RuntimeConfig.jsonParse + |> fun f -> f.RuntimeOptions + + let availableRuntimes = DotnetEnvironmentInfo.Get () + + let runtime = selectRuntime runtimeConfig availableRuntimes + + match runtime with + | None -> + // Keep on trucking: let's be optimistic and hope that we're self-contained. + [ dll.Directory ] + | Some (Choice1Of2 runtime) -> [ dll.Directory ; DirectoryInfo runtime.Path ] + | Some (Choice2Of2 sdk) -> [ dll.Directory ; DirectoryInfo sdk.Path ] + let main argv = let testDll, filter = match argv |> List.ofSeq with @@ -32,7 +118,8 @@ module Program = use _ = new SetBaseDir (testDll) - let assy = Assembly.LoadFrom testDll.FullName + let ctx = Ctx (testDll, locateRuntimes testDll) + let assy = ctx.LoadFromAssemblyPath testDll.FullName let anyFailures = assy.ExportedTypes diff --git a/TestRunner/RuntimeConfig.fs b/TestRunner/RuntimeConfig.fs new file mode 100644 index 0000000..5f1517f --- /dev/null +++ b/TestRunner/RuntimeConfig.fs @@ -0,0 +1,50 @@ +namespace TestRunner + +open System +open WoofWare.Myriad.Plugins + +[] +type FrameworkDescription = + { + Name : string + Version : string + } + +[] +type RuntimeOptions = + { + Tfm : string + Framework : FrameworkDescription + RollForward : string option + } + +[] +type RuntimeConfig = + { + RuntimeOptions : RuntimeOptions + } + +[] +type RollForward = + | Minor + | Major + | LatestPatch + | LatestMinor + | LatestMajor + | Disable + + static member Parse (s : string) : RollForward = + if s.Equals ("minor", StringComparison.OrdinalIgnoreCase) then + RollForward.Minor + elif s.Equals ("major", StringComparison.OrdinalIgnoreCase) then + RollForward.Major + elif s.Equals ("latestpatch", StringComparison.OrdinalIgnoreCase) then + RollForward.LatestPatch + elif s.Equals ("latestminor", StringComparison.OrdinalIgnoreCase) then + RollForward.LatestMinor + elif s.Equals ("latestmajor", StringComparison.OrdinalIgnoreCase) then + RollForward.LatestMajor + elif s.Equals ("disable", StringComparison.OrdinalIgnoreCase) then + RollForward.Disable + else + failwith $"Could not interpret '%s{s}' as a RollForward" diff --git a/TestRunner/Seq.fs b/TestRunner/Seq.fs new file mode 100644 index 0000000..e2ddd44 --- /dev/null +++ b/TestRunner/Seq.fs @@ -0,0 +1,22 @@ +namespace TestRunner + +[] +module internal Seq = + + let tryMinBy (f : 'a -> 'b) (s : 'a seq) : 'a option = + use enum = s.GetEnumerator () + + if not (enum.MoveNext ()) then + None + else + + let mutable answer = enum.Current + let mutable fAnswer = f enum.Current + + while enum.MoveNext () do + let fNext = f enum.Current + + if fNext < fAnswer then + answer <- enum.Current + + Some answer diff --git a/TestRunner/TestRunner.fsproj b/TestRunner/TestRunner.fsproj index 5a37b34..a62f3a9 100644 --- a/TestRunner/TestRunner.fsproj +++ b/TestRunner/TestRunner.fsproj @@ -15,9 +15,15 @@ nunit;test;runner true FS3559 + 2.1.40 + + + RuntimeConfig.fs + + @@ -32,6 +38,13 @@ + + + + + + + diff --git a/TestRunner/myriad.toml b/TestRunner/myriad.toml new file mode 100644 index 0000000..e69de29 diff --git a/nix/deps.nix b/nix/deps.nix index b1db0b8..2ff18a9 100644 --- a/nix/deps.nix +++ b/nix/deps.nix @@ -61,6 +61,11 @@ version = "17.10.0"; sha256 = "1bl471s7fx9jycr0cc8rylwf34mrvlg9qn1an6l86nisavfcyb7v"; }) + (fetchNuGet { + pname = "Myriad.Sdk"; + version = "0.8.3"; + sha256 = "0qv78c5s5m04xb8h17nnn2ig26zcyya91k2dpj745cm1cbnzvvgc"; + }) (fetchNuGet { pname = "Nerdbank.GitVersioning"; version = "3.6.139"; @@ -176,6 +181,21 @@ version = "7.0.3"; sha256 = "0zjrnc9lshagm6kdb9bdh45dmlnkpwcpyssa896sda93ngbmj8k9"; }) + (fetchNuGet { + pname = "WoofWare.DotnetRuntimeLocator"; + version = "0.1.2"; + sha256 = "0kwkq28ddzc0bpr22jmgcl8dhnhg776gf6l054rsxw8lrvpwhmv9"; + }) + (fetchNuGet { + pname = "WoofWare.Myriad.Plugins"; + version = "2.1.40"; + sha256 = "025lv42zjvqpr2di0iaqhqpricqary3l2a3cxgjjl0zxzflfbmx2"; + }) + (fetchNuGet { + pname = "WoofWare.Myriad.Plugins.Attributes"; + version = "3.1.4"; + sha256 = "06yw013f2qs2r8bxvja2c5kzbqc5knd3sc3pf6w5gaz4fbzwc2c3"; + }) (fetchNuGet { pname = "WoofWare.PrattParser"; version = "0.1.2";