Skip to content

Commit 7690906

Browse files
authored
Add support for Platform-specific TFMs introduced in .NET 5 (#1560)
* new versions of .NET Core should not use "netcoreapp" word in the target framework moniker example: .NET Core 6.0 is just .NET 6 and has a "net6.0" moniker * add tests that ensure that new Platform-specific TFMs are recognized by BenchmarkPartitioner * recognize Platform-specific apps using... reflection ;) * make sure that a platform-specific TFM creates corresponding toolchain that is equatable with other instances * extend console args support with the platform-specific monikers
1 parent c6d6fb2 commit 7690906

File tree

8 files changed

+125
-11
lines changed

8 files changed

+125
-11
lines changed

src/BenchmarkDotNet/ConsoleArguments/ConfigParser.cs

+12-3
Original file line numberDiff line numberDiff line change
@@ -106,12 +106,12 @@ private static bool Validate(CommandLineOptions options, ILogger logger)
106106

107107
foreach (string runtime in options.Runtimes)
108108
{
109-
if (!Enum.TryParse<RuntimeMoniker>(runtime.Replace(".", string.Empty), ignoreCase: true, out var parsed))
109+
if (!TryParse(runtime, out RuntimeMoniker runtimeMoniker))
110110
{
111111
logger.WriteLineError($"The provided runtime \"{runtime}\" is invalid. Available options are: {string.Join(", ", Enum.GetNames(typeof(RuntimeMoniker)).Select(name => name.ToLower()))}.");
112112
return false;
113113
}
114-
else if (parsed == RuntimeMoniker.Wasm && (options.WasmMainJs == null || options.WasmMainJs.IsNotNullButDoesNotExist()))
114+
else if (runtimeMoniker == RuntimeMoniker.Wasm && (options.WasmMainJs == null || options.WasmMainJs.IsNotNullButDoesNotExist()))
115115
{
116116
logger.WriteLineError($"The provided {nameof(options.WasmMainJs)} \"{options.WasmMainJs}\" does NOT exist. It MUST be provided.");
117117
return false;
@@ -319,7 +319,7 @@ private static Job CreateJobForGivenRuntime(Job baseJob, string runtimeId, Comma
319319
{
320320
TimeSpan? timeOut = options.TimeOutInSeconds.HasValue ? TimeSpan.FromSeconds(options.TimeOutInSeconds.Value) : default(TimeSpan?);
321321

322-
if (!Enum.TryParse(runtimeId.Replace(".", string.Empty), ignoreCase: true, out RuntimeMoniker runtimeMoniker))
322+
if (!TryParse(runtimeId, out RuntimeMoniker runtimeMoniker))
323323
{
324324
throw new InvalidOperationException("Impossible, already validated by the Validate method");
325325
}
@@ -481,5 +481,14 @@ private static string GetCoreRunToolchainDisplayName(IReadOnlyList<FileInfo> pat
481481

482482
return coreRunPath.FullName.Substring(lastCommonDirectorySeparatorIndex);
483483
}
484+
485+
private static bool TryParse(string runtime, out RuntimeMoniker runtimeMoniker)
486+
{
487+
int index = runtime.IndexOf('-');
488+
489+
return index < 0
490+
? Enum.TryParse<RuntimeMoniker>(runtime.Replace(".", string.Empty), ignoreCase: true, out runtimeMoniker)
491+
: Enum.TryParse<RuntimeMoniker>(runtime.Substring(0, index).Replace(".", string.Empty), ignoreCase: true, out runtimeMoniker);
492+
}
484493
}
485494
}

src/BenchmarkDotNet/Environments/Runtimes/CoreRuntime.cs

+32-2
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ private CoreRuntime(RuntimeMoniker runtimeMoniker, string msBuildMoniker, string
2424
{
2525
}
2626

27+
public bool IsPlatformSpecific => MsBuildMoniker.IndexOf('-') > 0;
28+
2729
/// <summary>
2830
/// use this method if you want to target .NET Core version not supported by current version of BenchmarkDotNet. Example: .NET Core 10
2931
/// </summary>
@@ -62,9 +64,10 @@ internal static CoreRuntime FromVersion(Version version)
6264
case Version v when v.Major == 2 && v.Minor == 2: return Core22;
6365
case Version v when v.Major == 3 && v.Minor == 0: return Core30;
6466
case Version v when v.Major == 3 && v.Minor == 1: return Core31;
65-
case Version v when v.Major == 5 && v.Minor == 0: return Core50;
67+
case Version v when v.Major == 5 && v.Minor == 0: return GetPlatformSpecific(Core50);
68+
case Version v when v.Major == 6 && v.Minor == 0: return GetPlatformSpecific(Core60);
6669
default:
67-
return CreateForNewVersion($"netcoreapp{version.Major}.{version.Minor}", $".NET Core {version.Major}.{version.Minor}");
70+
return CreateForNewVersion($"net{version.Major}.{version.Minor}", $".NET {version.Major}.{version.Minor}");
6871
}
6972
}
7073

@@ -172,5 +175,32 @@ internal static bool TryGetVersionFromFrameworkName(string frameworkName, out Ve
172175

173176
// Version.TryParse does not handle thing like 3.0.0-WORD
174177
private static string GetParsableVersionPart(string fullVersionName) => new string(fullVersionName.TakeWhile(c => char.IsDigit(c) || c == '.').ToArray());
178+
179+
private static CoreRuntime GetPlatformSpecific(CoreRuntime fallback)
180+
{
181+
// TargetPlatformAttribute is not part of .NET Standard 2.0 so as usuall we have to use some reflection hacks...
182+
var targetPlatformAttributeType = typeof(object).Assembly.GetType("System.Runtime.Versioning.TargetPlatformAttribute", throwOnError: false);
183+
if (targetPlatformAttributeType is null) // an old preview version of .NET 5
184+
return fallback;
185+
186+
var exe = Assembly.GetEntryAssembly();
187+
if (exe is null)
188+
return fallback;
189+
190+
var attributeInstance = exe.GetCustomAttribute(targetPlatformAttributeType);
191+
if (attributeInstance is null)
192+
return fallback;
193+
194+
var platformNameProperty = targetPlatformAttributeType.GetProperty("PlatformName");
195+
if (platformNameProperty is null)
196+
return fallback;
197+
198+
if (!(platformNameProperty.GetValue(attributeInstance) is string platformName))
199+
return fallback;
200+
201+
// it's something like "Windows7.0";
202+
var justName = new string(platformName.TakeWhile(char.IsLetter).ToArray());
203+
return new CoreRuntime(fallback.RuntimeMoniker, $"{fallback.MsBuildMoniker}-{justName}", fallback.Name);
204+
}
175205
}
176206
}

src/BenchmarkDotNet/Toolchains/CsProj/CsProjCoreToolchain.cs

+8-1
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,12 @@
77
using BenchmarkDotNet.Toolchains.DotNetCli;
88
using BenchmarkDotNet.Toolchains.InProcess.Emit;
99
using JetBrains.Annotations;
10+
using System;
1011

1112
namespace BenchmarkDotNet.Toolchains.CsProj
1213
{
1314
[PublicAPI]
14-
public class CsProjCoreToolchain : Toolchain
15+
public class CsProjCoreToolchain : Toolchain, IEquatable<CsProjCoreToolchain>
1516
{
1617
[PublicAPI] public static readonly IToolchain NetCoreApp20 = From(NetCoreAppSettings.NetCoreApp20);
1718
[PublicAPI] public static readonly IToolchain NetCoreApp21 = From(NetCoreAppSettings.NetCoreApp21);
@@ -70,5 +71,11 @@ public override bool IsSupported(BenchmarkCase benchmarkCase, ILogger logger, IR
7071

7172
return true;
7273
}
74+
75+
public override bool Equals(object obj) => obj is CsProjCoreToolchain typed && Equals(typed);
76+
77+
public bool Equals(CsProjCoreToolchain other) => Generator.Equals(other.Generator);
78+
79+
public override int GetHashCode() => Generator.GetHashCode();
7380
}
7481
}

src/BenchmarkDotNet/Toolchains/CsProj/CsProjGenerator.cs

+15-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
namespace BenchmarkDotNet.Toolchains.CsProj
1919
{
2020
[PublicAPI]
21-
public class CsProjGenerator : DotNetCliGenerator
21+
public class CsProjGenerator : DotNetCliGenerator, IEquatable<CsProjGenerator>
2222
{
2323
private const string DefaultSdkName = "Microsoft.NET.Sdk";
2424

@@ -169,5 +169,19 @@ protected virtual FileInfo GetProjectFilePath(Type benchmarkTarget, ILogger logg
169169
}
170170
return projectFile;
171171
}
172+
173+
public override bool Equals(object obj) => obj is CsProjGenerator other && Equals(other);
174+
175+
public bool Equals(CsProjGenerator other)
176+
=> TargetFrameworkMoniker == other.TargetFrameworkMoniker
177+
&& RuntimeFrameworkVersion == other.RuntimeFrameworkVersion
178+
&& CliPath == other.CliPath
179+
&& PackagesPath == other.PackagesPath;
180+
181+
public override int GetHashCode()
182+
=> TargetFrameworkMoniker.GetHashCode()
183+
^ (RuntimeFrameworkVersion?.GetHashCode() ?? 0)
184+
^ (CliPath?.GetHashCode() ?? 0)
185+
^ (PackagesPath?.GetHashCode() ?? 0);
172186
}
173187
}

src/BenchmarkDotNet/Toolchains/ToolchainExtensions.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ internal static IToolchain GetToolchain(this Runtime runtime, Descriptor descrip
5555
case CoreRuntime coreRuntime:
5656
if (descriptor != null && descriptor.Type.Assembly.IsLinqPad())
5757
return InProcessEmitToolchain.Instance;
58-
if (coreRuntime.RuntimeMoniker != RuntimeMoniker.NotRecognized)
58+
if (coreRuntime.RuntimeMoniker != RuntimeMoniker.NotRecognized && !coreRuntime.IsPlatformSpecific)
5959
return GetToolchain(coreRuntime.RuntimeMoniker);
6060

6161
return CsProjCoreToolchain.From(new NetCoreAppSettings(coreRuntime.MsBuildMoniker, null, coreRuntime.Name));

tests/BenchmarkDotNet.Tests/ConfigParserTests.cs

+13
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,19 @@ public void Net50AndNet60MonikersAreRecognizedAsNetCoreMonikers(string tfm)
319319
Assert.Equal(tfm, ((DotNetCliGenerator)toolchain.Generator).TargetFrameworkMoniker);
320320
}
321321

322+
[Theory]
323+
[InlineData("net5.0-windows")]
324+
[InlineData("net5.0-ios")]
325+
public void PlatformSpecificMonikersAreSupported(string msBuildMoniker)
326+
{
327+
var config = ConfigParser.Parse(new[] { "-r", msBuildMoniker }, new OutputLogger(Output)).config;
328+
329+
Assert.Single(config.GetJobs());
330+
CsProjCoreToolchain toolchain = config.GetJobs().Single().GetToolchain() as CsProjCoreToolchain;
331+
Assert.NotNull(toolchain);
332+
Assert.Equal(msBuildMoniker, ((DotNetCliGenerator)toolchain.Generator).TargetFrameworkMoniker);
333+
}
334+
322335
[Fact]
323336
public void CanCompareFewDifferentRuntimes()
324337
{

tests/BenchmarkDotNet.Tests/Running/JobRuntimePropertiesComparerTests.cs

+41
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using BenchmarkDotNet.Jobs;
66
using BenchmarkDotNet.Running;
77
using BenchmarkDotNet.Tests.XUnit;
8+
using BenchmarkDotNet.Toolchains.CsProj;
89
using Xunit;
910

1011
namespace BenchmarkDotNet.Tests.Running
@@ -39,6 +40,13 @@ [Benchmark] public void M2() { }
3940
[Benchmark] public void M3() { }
4041
}
4142

43+
public class Plain3
44+
{
45+
[Benchmark] public void M1() { }
46+
[Benchmark] public void M2() { }
47+
[Benchmark] public void M3() { }
48+
}
49+
4250
[Fact]
4351
public void BenchmarksAreGroupedByJob()
4452
{
@@ -128,5 +136,38 @@ public void CustomNuGetJobsAreGroupedByPackageVersion()
128136
foreach (var grouping in grouped)
129137
Assert.Equal(3 * 2, grouping.Count()); // (M1 + M2 + M3) * (Plain1 + Plain2)
130138
}
139+
140+
[Fact]
141+
public void CustomTargetPlatformJobsAreGroupedByTargetFrameworkMoniker()
142+
{
143+
var net5Config = ManualConfig.Create(DefaultConfig.Instance)
144+
.AddJob(Job.Default.WithToolchain(CsProjCoreToolchain.NetCoreApp50));
145+
var net5WindowsConfig1 = ManualConfig.Create(DefaultConfig.Instance)
146+
.AddJob(Job.Default.WithToolchain(CsProjCoreToolchain.From(new Toolchains.DotNetCli.NetCoreAppSettings(
147+
targetFrameworkMoniker: "net5.0-windows",
148+
runtimeFrameworkVersion: null,
149+
name: ".NET 5.0"))));
150+
// a different INSTANCE of CsProjCoreToolchain that also targets "net5.0-windows"
151+
var net5WindowsConfig2 = ManualConfig.Create(DefaultConfig.Instance)
152+
.AddJob(Job.Default.WithToolchain(CsProjCoreToolchain.From(new Toolchains.DotNetCli.NetCoreAppSettings(
153+
targetFrameworkMoniker: "net5.0-windows",
154+
runtimeFrameworkVersion: null,
155+
name: ".NET 5.0"))));
156+
157+
var benchmarksNet5 = BenchmarkConverter.TypeToBenchmarks(typeof(Plain1), net5Config);
158+
var benchmarksNet5Windows1 = BenchmarkConverter.TypeToBenchmarks(typeof(Plain2), net5WindowsConfig1);
159+
var benchmarksNet5Windows2 = BenchmarkConverter.TypeToBenchmarks(typeof(Plain3), net5WindowsConfig2);
160+
161+
var grouped = benchmarksNet5.BenchmarksCases
162+
.Union(benchmarksNet5Windows1.BenchmarksCases)
163+
.Union(benchmarksNet5Windows2.BenchmarksCases)
164+
.GroupBy(benchmark => benchmark, new BenchmarkPartitioner.BenchmarkRuntimePropertiesComparer())
165+
.ToArray();
166+
167+
Assert.Equal(2, grouped.Length);
168+
169+
Assert.Single(grouped, group => group.Count() == 3); // Plain1 (3 methods) runing against "net5.0"
170+
Assert.Single(grouped, group => group.Count() == 6); // Plain2 (3 methods) and Plain3 (3 methods) runing against "net5.0-windows"
171+
}
131172
}
132173
}

tests/BenchmarkDotNet.Tests/RuntimeVersionDetectionTests.cs

+3-3
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ public class RuntimeVersionDetectionTests
1717
[InlineData(".NETCoreApp,Version=v3.0", RuntimeMoniker.NetCoreApp30, "netcoreapp3.0")]
1818
[InlineData(".NETCoreApp,Version=v3.1", RuntimeMoniker.NetCoreApp31, "netcoreapp3.1")]
1919
[InlineData(".NETCoreApp,Version=v5.0", RuntimeMoniker.Net50, "net5.0")]
20-
[InlineData(".NETCoreApp,Version=v123.0", RuntimeMoniker.NotRecognized, "netcoreapp123.0")]
20+
[InlineData(".NETCoreApp,Version=v123.0", RuntimeMoniker.NotRecognized, "net123.0")]
2121
public void TryGetVersionFromFrameworkNameHandlesValidInput(string frameworkName, RuntimeMoniker expectedTfm, string expectedMsBuildMoniker)
2222
{
2323
Assert.True(CoreRuntime.TryGetVersionFromFrameworkName(frameworkName, out Version version));
@@ -44,7 +44,7 @@ public void TryGetVersionFromFrameworkNameHandlesInvalidInput(string frameworkNa
4444
[InlineData(RuntimeMoniker.NetCoreApp30, "netcoreapp3.0", "Microsoft .NET Core", "3.0.0-preview8-28379-12")]
4545
[InlineData(RuntimeMoniker.NetCoreApp31, "netcoreapp3.1", "Microsoft .NET Core", "3.1.0-something")]
4646
[InlineData(RuntimeMoniker.Net50, "net5.0", "Microsoft .NET Core", "5.0.0-alpha1.19415.3")]
47-
[InlineData(RuntimeMoniker.NotRecognized, "netcoreapp123.0", "Microsoft .NET Core", "123.0.0-future")]
47+
[InlineData(RuntimeMoniker.NotRecognized, "net123.0", "Microsoft .NET Core", "123.0.0-future")]
4848
public void TryGetVersionFromProductInfoHandlesValidInput(RuntimeMoniker expectedTfm, string expectedMsBuildMoniker, string productName, string productVersion)
4949
{
5050
Assert.True(CoreRuntime.TryGetVersionFromProductInfo(productVersion, productName, out Version version));
@@ -74,7 +74,7 @@ public static IEnumerable<object[]> FromNetCoreAppVersionHandlesValidInputArgume
7474
yield return new object[] { Path.Combine(directoryPrefix, "2.2.6") + Path.DirectorySeparatorChar, RuntimeMoniker.NetCoreApp22, "netcoreapp2.2" };
7575
yield return new object[] { Path.Combine(directoryPrefix, "3.0.0-preview8-28379-12") + Path.DirectorySeparatorChar, RuntimeMoniker.NetCoreApp30, "netcoreapp3.0" };
7676
yield return new object[] { Path.Combine(directoryPrefix, "5.0.0-alpha1.19422.13") + Path.DirectorySeparatorChar, RuntimeMoniker.Net50, "net5.0" };
77-
yield return new object[] { Path.Combine(directoryPrefix, "123.0.0") + Path.DirectorySeparatorChar, RuntimeMoniker.NotRecognized, "netcoreapp123.0" };
77+
yield return new object[] { Path.Combine(directoryPrefix, "123.0.0") + Path.DirectorySeparatorChar, RuntimeMoniker.NotRecognized, "net123.0" };
7878
}
7979

8080
[Theory]

0 commit comments

Comments
 (0)