Skip to content

Commit 4a036cf

Browse files
authored
Merge pull request #1785 from nunit/issue-1779a
Handle prerelease dotnet runtimes correctly
2 parents 7200e37 + e5329d9 commit 4a036cf

3 files changed

Lines changed: 309 additions & 141 deletions

File tree

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
// Copyright (c) Charlie Poole, Rob Prouse and Contributors. MIT License - see LICENSE.txt
2+
3+
using System;
4+
using System.IO;
5+
using System.Collections.Generic;
6+
using NUnit.Framework;
7+
using System.Linq;
8+
9+
namespace NUnit.Engine
10+
{
11+
public static class DotNetHelperTests
12+
{
13+
[Test]
14+
public static void CanGetInstallDirectory([Values] bool x86)
15+
{
16+
string path = DotNet.GetInstallDirectory(x86);
17+
Assert.That(Directory.Exists(path));
18+
Assert.That(File.Exists(Path.Combine(path, OS.IsWindows ? "dotnet.exe" : "dotnet")));
19+
}
20+
21+
[Test]
22+
public static void CanGetExecutable([Values] bool x86)
23+
{
24+
string path = DotNet.GetDotnetExecutable(x86);
25+
Assert.That(File.Exists(path));
26+
Assert.That(Path.GetFileName(path), Is.EqualTo(OS.IsWindows ? "dotnet.exe" : "dotnet"));
27+
}
28+
29+
[Test]
30+
public static void CanIssueDotNetCommand([Values] bool x86)
31+
{
32+
var output = DotNet.DotnetCommand("--help", x86);
33+
Assert.That(output.Count(), Is.GreaterThan(0));
34+
}
35+
36+
[TestCaseSource(nameof(RuntimeCases))]
37+
public static void CanParseInputLine(string line, string name, string packageVersion, string path,
38+
bool isPreRelease, Version version, string suffix)
39+
{
40+
DotNet.RuntimeInfo runtime = DotNet.RuntimeInfo.Parse(line);
41+
Assert.That(runtime.Name, Is.EqualTo(name));
42+
Assert.That(runtime.PackageVersion, Is.EqualTo(packageVersion));
43+
Assert.That(runtime.Path, Is.EqualTo(path));
44+
Assert.That(runtime.IsPreRelease, Is.EqualTo(isPreRelease));
45+
Assert.That(runtime.Version, Is.EqualTo(version));
46+
Assert.That(runtime.PreReleaseSuffix, Is.EqualTo(suffix));
47+
}
48+
49+
static TestCaseData[] RuntimeCases = [
50+
new TestCaseData("Microsoft.NETCore.App 8.0.22 [C:\\Program Files\\dotnet\\shared\\Microsoft.NETCore.App]",
51+
"Microsoft.NETCore.App", "8.0.22", "C:\\Program Files\\dotnet\\shared\\Microsoft.NETCore.App",
52+
false, new Version(8,0,22), null),
53+
new TestCaseData("Microsoft.WindowsDesktop.App 9.0.11 [C:\\Program Files\\dotnet\\shared\\Microsoft.WindowsDesktop.App]",
54+
"Microsoft.WindowsDesktop.App", "9.0.11", "C:\\Program Files\\dotnet\\shared\\Microsoft.WindowsDesktop.App",
55+
false, new Version(9,0,11), null),
56+
new TestCaseData("Microsoft.AspNetCore.App 7.0.20 [C:\\Program Files\\dotnet\\shared\\Microsoft.AspNetCore.App]",
57+
"Microsoft.AspNetCore.App", "7.0.20", "C:\\Program Files\\dotnet\\shared\\Microsoft.AspNetCore.App",
58+
false, new Version(7,0,20), null),
59+
new TestCaseData("Microsoft.AspNetCore.App 9.0.0-rc.2.24474.3 [C:\\Program Files\\dotnet\\shared\\Microsoft.AspNetCore.App]",
60+
"Microsoft.AspNetCore.App", "9.0.0-rc.2.24474.3", "C:\\Program Files\\dotnet\\shared\\Microsoft.AspNetCore.App",
61+
true, new Version(9,0,0), "rc.2.24474.3")];
62+
63+
[TestCase("Microsoft.NETCore.App", "8.0.0", "8.0.22")]
64+
[TestCase("Microsoft.NETCore.App", "8.0.0.0", "8.0.22")]
65+
[TestCase("Microsoft.NETCore.App", "8.0.0.100", "8.0.22")]
66+
[TestCase("Microsoft.NETCore.App", "8.0.100", "9.0.11")]
67+
[TestCase("Microsoft.AspNetCore.App", "5.0.0", "8.0.22")] // Rather than 8.0.2
68+
[TestCase("Microsoft.AspNetCore.App", "7.0.0", "8.0.22")] // Rather than 8.0.2
69+
[TestCase("Microsoft.AspNetCore.App", "8.0.0", "8.0.22")] // Rather than 8.0.2
70+
[TestCase("Microsoft.WindowsDesktop.App", "9.0.0", "9.0.11")] // Rather than the pre-release version
71+
[TestCase("Microsoft.WindowsDesktop.App", "10.0.0", "10.0.0-rc.2.25502.107")]
72+
public static void FindBestRuntimeTests(string runtimeName, string targetVersion, string expectedVersion)
73+
{
74+
var availableRuntimes = SimulatedListRuntimesOutput.Where(r => r.Name == runtimeName);
75+
Assert.That(DotNet.FindBestRuntime(new Version(targetVersion), availableRuntimes, out DotNet.RuntimeInfo bestRuntime));
76+
Assert.That(bestRuntime, Is.Not.Null);
77+
Assert.That(bestRuntime.PackageVersion, Is.EqualTo(expectedVersion));
78+
}
79+
80+
static DotNet.RuntimeInfo[] SimulatedListRuntimesOutput = [
81+
DotNet.RuntimeInfo.Parse(@"Microsoft.AspNetCore.App 8.0.2 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]"),
82+
DotNet.RuntimeInfo.Parse(@"Microsoft.AspNetCore.App 8.0.22 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]"),
83+
DotNet.RuntimeInfo.Parse(@"Microsoft.AspNetCore.App 9.0.0-rc.2.24474.3 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]"),
84+
DotNet.RuntimeInfo.Parse(@"Microsoft.AspNetCore.App 9.0.11 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]"),
85+
DotNet.RuntimeInfo.Parse(@"Microsoft.AspNetCore.App 10.0.0-rc.2.25502.107 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]"),
86+
DotNet.RuntimeInfo.Parse(@"Microsoft.AspNetCore.App 10.0.0 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]"),
87+
DotNet.RuntimeInfo.Parse(@"Microsoft.NETCore.App 8.0.22 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]"),
88+
DotNet.RuntimeInfo.Parse(@"Microsoft.NETCore.App 9.0.0-rc.2.24473.5 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]"),
89+
DotNet.RuntimeInfo.Parse(@"Microsoft.NETCore.App 9.0.11 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]"),
90+
DotNet.RuntimeInfo.Parse(@"Microsoft.NETCore.App 10.0.0-rc.2.25502.107 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]"),
91+
DotNet.RuntimeInfo.Parse(@"Microsoft.NETCore.App 10.0.0 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]"),
92+
DotNet.RuntimeInfo.Parse(@"Microsoft.WindowsDesktop.App 8.0.22 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]"),
93+
DotNet.RuntimeInfo.Parse(@"Microsoft.WindowsDesktop.App 9.0.0-rc.2.24474.4 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]"),
94+
DotNet.RuntimeInfo.Parse(@"Microsoft.WindowsDesktop.App 9.0.11 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]"),
95+
DotNet.RuntimeInfo.Parse(@"Microsoft.WindowsDesktop.App 10.0.0-rc.2.25502.107 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]") ];
96+
//DotNet.RuntimeInfo.Parse(@"Microsoft.WindowsDesktop.App 10.0.0 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]") ];
97+
}
98+
}

src/NUnitEngine/nunit.engine.core/DotNetHelper.cs

Lines changed: 101 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
using System.Diagnostics;
77
using System.IO;
88
using System.Linq;
9+
using System.Reflection;
10+
using System.Xml.Linq;
911

1012
namespace NUnit.Engine
1113
{
@@ -34,54 +36,138 @@ public static class DotNet
3436
private static Lazy<List<RuntimeInfo>> _x64Runtimes = new Lazy<List<RuntimeInfo>>(() => [.. GetAllRuntimes(x86: false)]);
3537
private static Lazy<List<RuntimeInfo>> _x86Runtimes = new Lazy<List<RuntimeInfo>>(() => [.. GetAllRuntimes(x86: true)]);
3638

39+
/// <summary>
40+
/// DotNet.RuntimeInfo holds information about a single installed runtime.
41+
/// </summary>
3742
public class RuntimeInfo
3843
{
39-
public string Name;
40-
public Version Version;
41-
public string Path;
44+
/// <summary>
45+
/// Gets the runtime name, e.g. Microsoft.NetCore.App, Microsoft.AspNetCore.App or Microsoft.WindowsDesktop.App.
46+
/// </summary>
47+
public string Name { get; }
48+
49+
/// <summary>
50+
/// Gets the package version as a string, possibly including a pre-release suffix.
51+
/// </summary>
52+
public string PackageVersion { get; }
53+
54+
/// <summary>
55+
/// Gets the path to the directory containing assemblies for this runtime
56+
/// </summary>
57+
public string Path { get; }
58+
59+
/// <summary>
60+
/// Gets a flag, which is true if this runtime is a pre-release, otherwise fales.
61+
/// </summary>
62+
public bool IsPreRelease { get; }
4263

43-
public RuntimeInfo(string name, string version, string path)
44-
: this(name, new Version(version), path) { }
64+
/// <summary>
65+
/// Gets the three-part version of this runtime.
66+
/// </summary>
67+
public Version Version { get; }
4568

46-
public RuntimeInfo(string name, Version version, string path)
69+
/// <summary>
70+
/// Gets the pre-release suffix if IsPreRelease is true, otherwise null
71+
/// </summary>
72+
public string PreReleaseSuffix { get; }
73+
74+
75+
/// <summary>
76+
/// Constructs a Runtime instance.
77+
/// </summary>
78+
public RuntimeInfo(string name, string packageVersion, string path)
4779
{
4880
Name = name;
49-
Version = version;
81+
PackageVersion = packageVersion;
5082
Path = path;
83+
84+
int dash = PackageVersion.IndexOf('-');
85+
IsPreRelease = dash > 0;
86+
87+
if (IsPreRelease)
88+
{
89+
Version = new Version(packageVersion.Substring(0, dash));
90+
PreReleaseSuffix = packageVersion.Substring(dash + 1);
91+
}
92+
else
93+
Version = new Version(packageVersion);
94+
}
95+
96+
/// <summary>
97+
/// Parses a single line from the --list-runtimes display to create
98+
/// an instance of DotNet.RuntimeInfo.
99+
/// </summary>
100+
/// <param name="line">Line from execution of dotnet --list-runtimes</param>
101+
/// <returns>A DotNet.RuntimeInfo</returns>
102+
public static RuntimeInfo Parse(string line)
103+
{
104+
string[] parts = line.Trim().Split([' '], 3);
105+
return new RuntimeInfo(parts[0], parts[1], parts[2].Trim(['[', ']']));
51106
}
52107
}
53108

54109
/// <summary>
55110
/// Get the correct install directory, depending on whether we need X86 or X64 architecture.
56111
/// </summary>
57112
/// <param name="x86">Flag indicating whether the X86 architecture is needed</param>
58-
/// <returns></returns>
59113
public static string GetInstallDirectory(bool x86) => x86
60114
? _x86InstallDirectory.Value : _x64InstallDirectory.Value;
61115

62116
/// <summary>
63117
/// Get the correct dotnet.exe, depending on whether we need X86 or X64 architecture.
64118
/// </summary>
65119
/// <param name="x86">Flag indicating whether the X86 architecture is needed</param>
66-
/// <returns></returns>
67-
public static string GetDotnetExecutable(bool x86) => Path.Combine(GetInstallDirectory(x86), OS.IsWindows ? "dotnet.exe" : "dotnet");
120+
public static string GetDotnetExecutable(bool x86) =>
121+
Path.Combine(GetInstallDirectory(x86), OS.IsWindows ? "dotnet.exe" : "dotnet");
68122

123+
/// <summary>
124+
/// Gets an enumeration of all installed runtimes matching the specified name and x86 flag.
125+
/// </summary>
126+
/// <param name="name">Name of the requested runtime</param>
127+
/// <param name="x86">Flag indicating whether the X86 architecture is needed</param>
69128
public static IEnumerable<RuntimeInfo> GetRuntimes(string name, bool x86)
70129
{
71130
var runtimes = x86 ? _x86Runtimes.Value : _x64Runtimes.Value;
72131
return runtimes.Where(r => r.Name == name);
73132
}
74133

75-
private static IEnumerable<RuntimeInfo> GetAllRuntimes(bool x86)
134+
/// <summary>
135+
/// Finds the "best" runtime for a particular asssembly version among those installed.
136+
/// May return null, if no suitable runtime is available.
137+
/// </summary>
138+
/// <param name="targetVersion">The version of assembly sought.</param>
139+
/// <param name="name">Name of the requested runtime</param>
140+
/// <param name="x86">Flag indicating whether the X86 architecture is needed</param>
141+
/// <param name="bestRuntime">Output variable set to the runtime that was found or null</param>
142+
/// <returns>True if a runtime was found, otherwise false</returns>
143+
public static bool FindBestRuntime(Version targetVersion, string name, bool x86, out RuntimeInfo bestRuntime) =>
144+
FindBestRuntime(targetVersion, GetRuntimes(name, x86), out bestRuntime);
145+
146+
// Internal method used to facilitate testing
147+
internal static bool FindBestRuntime(Version targetVersion, IEnumerable<RuntimeInfo> availableRuntimes, out RuntimeInfo bestRuntime)
76148
{
77-
foreach (string line in DotnetCommand("--list-runtimes", x86: x86))
149+
bestRuntime = null;
150+
151+
if (targetVersion is null)
152+
return false;
153+
154+
foreach (var candidate in availableRuntimes)
78155
{
79-
string[] parts = line.Trim().Split([' '], 3);
80-
yield return new RuntimeInfo(parts[0], parts[1], parts[2].Trim(['[', ']']));
156+
if (candidate.Version >= targetVersion)
157+
if (bestRuntime is null || candidate.Version.Major == bestRuntime.Version.Major)
158+
bestRuntime = candidate;
81159
}
160+
161+
return bestRuntime is not null;
162+
}
163+
164+
private static IEnumerable<RuntimeInfo> GetAllRuntimes(bool x86)
165+
{
166+
foreach (string line in DotnetCommand("--list-runtimes", x86: x86))
167+
yield return RuntimeInfo.Parse(line);
82168
}
83169

84-
private static IEnumerable<string> DotnetCommand(string arguments, bool x86 = false)
170+
internal static IEnumerable<string> DotnetCommand(string arguments, bool x86 = false)
85171
{
86172
var process = new Process
87173
{

0 commit comments

Comments
 (0)