Skip to content

Commit 91f3f7e

Browse files
authored
Add a VSTest Adapter (#2438)
1 parent b3b2d91 commit 91f3f7e

24 files changed

+1036
-3
lines changed

BenchmarkDotNet.sln

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11

22
Microsoft Visual Studio Solution File, Format Version 12.00
3-
# Visual Studio 15
4-
VisualStudioVersion = 15.0.27130.2027
3+
# Visual Studio Version 17
4+
VisualStudioVersion = 17.8.34004.107
55
MinimumVisualStudioVersion = 10.0.40219.1
66
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{D6597E3A-6892-4A68-8E14-042FC941FDA2}"
77
EndProject
@@ -51,6 +51,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BenchmarkDotNet.Diagnostics
5151
EndProject
5252
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BenchmarkDotNet.IntegrationTests.ManualRunning.MultipleFrameworks", "tests\BenchmarkDotNet.IntegrationTests.ManualRunning.MultipleFrameworks\BenchmarkDotNet.IntegrationTests.ManualRunning.MultipleFrameworks.csproj", "{AACA2C63-A85B-47AB-99FC-72C3FF408B14}"
5353
EndProject
54+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BenchmarkDotNet.TestAdapter", "src\BenchmarkDotNet.TestAdapter\BenchmarkDotNet.TestAdapter.csproj", "{4C9C89B8-7C4E-4ECF-B3C9-324C8772EDAC}"
55+
EndProject
5456
Global
5557
GlobalSection(SolutionConfigurationPlatforms) = preSolution
5658
Debug|Any CPU = Debug|Any CPU
@@ -137,6 +139,10 @@ Global
137139
{AACA2C63-A85B-47AB-99FC-72C3FF408B14}.Debug|Any CPU.Build.0 = Debug|Any CPU
138140
{AACA2C63-A85B-47AB-99FC-72C3FF408B14}.Release|Any CPU.ActiveCfg = Release|Any CPU
139141
{AACA2C63-A85B-47AB-99FC-72C3FF408B14}.Release|Any CPU.Build.0 = Release|Any CPU
142+
{4C9C89B8-7C4E-4ECF-B3C9-324C8772EDAC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
143+
{4C9C89B8-7C4E-4ECF-B3C9-324C8772EDAC}.Debug|Any CPU.Build.0 = Debug|Any CPU
144+
{4C9C89B8-7C4E-4ECF-B3C9-324C8772EDAC}.Release|Any CPU.ActiveCfg = Release|Any CPU
145+
{4C9C89B8-7C4E-4ECF-B3C9-324C8772EDAC}.Release|Any CPU.Build.0 = Release|Any CPU
140146
EndGlobalSection
141147
GlobalSection(SolutionProperties) = preSolution
142148
HideSolutionNode = FALSE
@@ -162,6 +168,7 @@ Global
162168
{B620D10A-CD8E-4A34-8B27-FD6257E63AD0} = {63B94FD6-3F3D-4E04-9727-48E86AC4384C}
163169
{C5BDA61F-3A56-4B59-901D-0A17E78F4076} = {D6597E3A-6892-4A68-8E14-042FC941FDA2}
164170
{AACA2C63-A85B-47AB-99FC-72C3FF408B14} = {14195214-591A-45B7-851A-19D3BA2413F9}
171+
{4C9C89B8-7C4E-4ECF-B3C9-324C8772EDAC} = {D6597E3A-6892-4A68-8E14-042FC941FDA2}
165172
EndGlobalSection
166173
GlobalSection(ExtensibilityGlobals) = postSolution
167174
SolutionGuid = {4D9AF12B-1F7F-45A7-9E8C-E4E46ADCBD1F}

docs/articles/features/toc.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,6 @@
1111
- name: EtwProfiler
1212
href: etwprofiler.md
1313
- name: EventPipeProfiler
14-
href: event-pipe-profiler.md
14+
href: event-pipe-profiler.md
15+
- name: VSTest
16+
href: vstest.md

docs/articles/features/vstest.md

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
---
2+
uid: docs.baselines
3+
name: Running with VSTest
4+
---
5+
6+
# Running with VSTest
7+
BenchmarkDotNet has support for discovering and executing benchmarks through VSTest. This provides an alternative user experience to running benchmarks with the CLI and may be preferable for those who like their IDE's VSTest integrations that they may have used when running unit tests.
8+
9+
Below is an example of running some benchmarks from the BenchmarkDotNet samples project in Visual Studio's Test Explorer.
10+
11+
![](../../images/vs-testexplorer-demo.png)
12+
13+
## About VSTest
14+
15+
VSTest is one of the most popular test platforms in use in the .NET ecosystem, with test frameworks such as MSTest, xUnit, and NUnit providing support for it. Many IDEs, including Visual Studio and Rider, provide UIs for running tests through VSTest which some users may find more accessible than running them through the command line.
16+
17+
It may seem counterintuitive to run performance tests on a platform that is designed for unit tests that expect a boolean outcome of "Passed" or "Failed", however VSTest provides good value as a protocol for discovering and executing tests. In addition, we can still make use of this boolean output to indicate if the benchmark had validation errors that caused them to fail to run.
18+
19+
## Caveats and things to know
20+
- The VSTest adapter will not call your application's entry point.
21+
- If you use the entry point to customize how your benchmarks are run, you will need to do this through other means such as an assembly-level `IConfigSource`.
22+
- For more about this, please read: [Setting a default configuration](#setting-a-default-configuration).
23+
- The benchmark measurements may be affected by the VSTest host and your IDE
24+
- If you want to have more accurate performance results, it is recommended to run benchmarks through the CLI instead without other processes on the machine impacting performance.
25+
- This does not mean that the measurements are useless though, it will still be able to provide useful measurements during development when comparing different approaches.
26+
- The test adapter will not display or execute benchmarks if optimizations are disabled.
27+
- Please ensure you are compiling in Release mode or with `Optimize` set to true.
28+
- Using an `InProcess` toolchain will let you run your benchmarks with optimizations disabled and will let you attach the debugger as well.
29+
- The test adapter will generate an entry point for you automatically
30+
- The generated entry point will pass the command line arguments and the current assembly into `BenchmarkSwitcher`, so you can still use it in your CLI as well as in VSTest.
31+
- This means you can delete your entry point and only need to define your benchmarks.
32+
- If you want to use a custom entry point, you can still do so by setting `GenerateProgramFile` to `false` in your project file.
33+
34+
## How to use it
35+
36+
You need to install two packages into your benchmark project:
37+
38+
- `BenchmarkDotNet.TestAdapter`: Implements the VSTest protocol for BenchmarkDotNet
39+
- `Microsoft.NET.Test.Sdk`: Includes all the pieces needed for the VSTest host to run and load the VSTest adapter.
40+
41+
As mentioned in the caveats section, `BenchmarkDotNet.TestAdapter` will generate an entry point for you automatically, so if you have an entry point already you will either need to delete it or set `GenerateProgramFile` to `false` in your project file to continue using your existing one.
42+
43+
After doing this, you can set your build configuration to `Release`, run a build, and you should be able to see the benchmarks in your IDE's VSTest integration.
44+
45+
## Setting a default configuration
46+
47+
Previously, it was common for the default configuration to be defined inside the entry point. Since the entry point is not used when running benchmarks through VSTest, the default configuration must be specified using a `Config` attribute instead that is set on the assembly.
48+
49+
First, create a class that extends `ManualConfig` or `IConfig` which sets the default configuration you want:
50+
51+
```csharp
52+
class MyDefaultConfig : ManualConfig
53+
{
54+
public MyDefaultConfig()
55+
{
56+
AddJob(Job.Dry);
57+
AddLogger(Loggers.ConsoleLogger.Default);
58+
AddValidator(JitOptimizationsValidator.DontFailOnError);
59+
}
60+
}
61+
```
62+
63+
Then, set an assembly attribute with the following.
64+
65+
```csharp
66+
[assembly: Config(typeof(MyDefaultConfig))]
67+
```
68+
69+
By convention, assembly attributes are usually defined inside `AssemblyInfo.cs` in a directory called `Properties`.
70+
71+
## Viewing the results
72+
The full output from BenchmarkDotNet that you would have been used to seeing in the past will be sent to the "Tests" output of your IDE. Use this view if you want to see the tabular view that compares multiple benchmarks with each other, or if you want to see the results for each individual iteration.
73+
74+
One more place where you can view the results is in each individual test's output messages. In Visual Studio this can be viewed by clicking on the test in the Test Explorer after running it, and looking at the Test Detail Summary. Since this only displays statistics for a single benchmark case, it does not show the tabulated view that compares multiple benchmark cases, but instead displays a histogram and various other useful statistics. Not all IDEs support displaying these output messages, so you may only be able to view the results using the "Tests" output.

docs/images/vs-testexplorer-demo.png

179 KB
Loading

samples/BenchmarkDotNet.Samples.FSharp/BenchmarkDotNet.Samples.FSharp.fsproj

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@
66
<PropertyGroup>
77
<OutputType>Exe</OutputType>
88
<TargetFrameworks>net462;net8.0</TargetFrameworks>
9+
<GenerateProgramFile>false</GenerateProgramFile>
910
</PropertyGroup>
1011
<ItemGroup>
1112
<Compile Include="Program.fs" />
1213
</ItemGroup>
1314
<ItemGroup>
1415
<ProjectReference Include="..\..\src\BenchmarkDotNet\BenchmarkDotNet.csproj" />
16+
<ProjectReference Include="..\..\src\BenchmarkDotNet.TestAdapter\BenchmarkDotNet.TestAdapter.csproj"/>
1517
</ItemGroup>
1618

1719
<ItemGroup Condition=" '$(TargetFrameworkIdentifier)' == '.NETFramework' ">
@@ -21,5 +23,6 @@
2123
<ItemGroup>
2224
<PackageReference Update="FSharp.Core" Version="4.6.0" />
2325
<PackageReference Update="System.ValueTuple" Version="4.5.0" />
26+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
2427
</ItemGroup>
2528
</Project>

samples/BenchmarkDotNet.Samples/BenchmarkDotNet.Samples.csproj

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
<PlatformTarget>AnyCPU</PlatformTarget>
1212
<DebugSymbols>true</DebugSymbols>
1313
<NoWarn>$(NoWarn);CA1018;CA5351;CA1825</NoWarn>
14+
<!-- Disable entry point generation as this project has it's own entry point -->
15+
<GenerateProgramFile>false</GenerateProgramFile>
1416
</PropertyGroup>
1517
<ItemGroup Condition=" '$(TargetFrameworkIdentifier)' == '.NETFramework' ">
1618
<Reference Include="System.Reflection" />
@@ -19,10 +21,13 @@
1921
<ItemGroup>
2022
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
2123
<PackageReference Include="System.Drawing.Common" Version="4.7.2" />
24+
<!-- The Test SDK is required only for the VSTest Adapter to work -->
25+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
2226
</ItemGroup>
2327
<ItemGroup>
2428
<ProjectReference Include="..\..\src\BenchmarkDotNet.Diagnostics.dotTrace\BenchmarkDotNet.Diagnostics.dotTrace.csproj" />
2529
<ProjectReference Include="..\..\src\BenchmarkDotNet\BenchmarkDotNet.csproj" />
2630
<ProjectReference Include="..\..\src\BenchmarkDotNet.Diagnostics.Windows\BenchmarkDotNet.Diagnostics.Windows.csproj" />
31+
<ProjectReference Include="..\..\src\BenchmarkDotNet.TestAdapter\BenchmarkDotNet.TestAdapter.csproj" />
2732
</ItemGroup>
2833
</Project>
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
using BenchmarkDotNet.Attributes;
2+
using BenchmarkDotNet.Characteristics;
3+
using BenchmarkDotNet.Exporters;
4+
using BenchmarkDotNet.Extensions;
5+
using BenchmarkDotNet.Running;
6+
using Microsoft.TestPlatform.AdapterUtilities;
7+
using Microsoft.VisualStudio.TestPlatform.ObjectModel;
8+
using System;
9+
10+
namespace BenchmarkDotNet.TestAdapter
11+
{
12+
/// <summary>
13+
/// A set of extensions for BenchmarkCase to support converting to VSTest TestCase objects.
14+
/// </summary>
15+
internal static class BenchmarkCaseExtensions
16+
{
17+
/// <summary>
18+
/// Converts a BDN BenchmarkCase to a VSTest TestCase.
19+
/// </summary>
20+
/// <param name="benchmarkCase">The BenchmarkCase to convert.</param>
21+
/// <param name="assemblyPath">The dll or exe of the benchmark project.</param>
22+
/// <param name="includeJobInName">Whether or not the display name should include the job name.</param>
23+
/// <returns>The VSTest TestCase.</returns>
24+
internal static TestCase ToVsTestCase(this BenchmarkCase benchmarkCase, string assemblyPath, bool includeJobInName = false)
25+
{
26+
var benchmarkMethod = benchmarkCase.Descriptor.WorkloadMethod;
27+
var fullClassName = benchmarkCase.Descriptor.Type.GetCorrectCSharpTypeName();
28+
var benchmarkMethodName = benchmarkCase.Descriptor.WorkloadMethod.Name;
29+
var benchmarkFullMethodName = $"{fullClassName}.{benchmarkMethodName}";
30+
31+
// Display name has arguments as well.
32+
var displayMethodName = FullNameProvider.GetMethodName(benchmarkCase);
33+
if (includeJobInName)
34+
displayMethodName += $" [{benchmarkCase.GetUnrandomizedJobDisplayInfo()}]";
35+
36+
var displayName = $"{fullClassName}.{displayMethodName}";
37+
38+
var vsTestCase = new TestCase(benchmarkFullMethodName, VsTestAdapter.ExecutorUri, assemblyPath)
39+
{
40+
DisplayName = displayName,
41+
Id = GetTestCaseId(benchmarkCase)
42+
};
43+
44+
var benchmarkAttribute = benchmarkMethod.ResolveAttribute<BenchmarkAttribute>();
45+
if (benchmarkAttribute != null)
46+
{
47+
vsTestCase.CodeFilePath = benchmarkAttribute.SourceCodeFile;
48+
vsTestCase.LineNumber = benchmarkAttribute.SourceCodeLineNumber;
49+
}
50+
51+
var categories = DefaultCategoryDiscoverer.Instance.GetCategories(benchmarkMethod);
52+
foreach (var category in categories)
53+
vsTestCase.Traits.Add("Category", category);
54+
55+
vsTestCase.Traits.Add("", "BenchmarkDotNet");
56+
57+
return vsTestCase;
58+
}
59+
60+
/// <summary>
61+
/// If an ID is not provided, a random string is used for the ID. This method will identify if randomness was
62+
/// used for the ID and return the Job's DisplayInfo with that randomness removed so that the same benchmark
63+
/// can be referenced across multiple processes.
64+
/// </summary>
65+
/// <param name="benchmarkCase">The benchmark case.</param>
66+
/// <returns>The benchmark case' job's DisplayInfo without randomness.</returns>
67+
internal static string GetUnrandomizedJobDisplayInfo(this BenchmarkCase benchmarkCase)
68+
{
69+
var jobDisplayInfo = benchmarkCase.Job.DisplayInfo;
70+
if (!benchmarkCase.Job.HasValue(CharacteristicObject.IdCharacteristic) && benchmarkCase.Job.ResolvedId.StartsWith("Job-", StringComparison.OrdinalIgnoreCase))
71+
{
72+
// Replace Job-ABCDEF with Job
73+
jobDisplayInfo = "Job" + jobDisplayInfo.Substring(benchmarkCase.Job.ResolvedId.Length);
74+
}
75+
76+
return jobDisplayInfo;
77+
}
78+
79+
/// <summary>
80+
/// Gets an ID for a given BenchmarkCase that is uniquely identifiable from discovery to execution phase.
81+
/// </summary>
82+
/// <param name="benchmarkCase">The benchmark case.</param>
83+
/// <returns>The test case ID.</returns>
84+
internal static Guid GetTestCaseId(this BenchmarkCase benchmarkCase)
85+
{
86+
var testIdProvider = new TestIdProvider();
87+
testIdProvider.AppendString(VsTestAdapter.ExecutorUriString);
88+
testIdProvider.AppendString(benchmarkCase.Descriptor.DisplayInfo);
89+
testIdProvider.AppendString(benchmarkCase.GetUnrandomizedJobDisplayInfo());
90+
testIdProvider.AppendString(benchmarkCase.Parameters.DisplayInfo);
91+
return testIdProvider.GetId();
92+
}
93+
}
94+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
<Import Project="..\..\build\common.props" />
3+
<PropertyGroup>
4+
<TargetFrameworks>netstandard2.0;net462</TargetFrameworks>
5+
<AssemblyTitle>BenchmarkDotNet.TestAdapter</AssemblyTitle>
6+
<AssemblyName>BenchmarkDotNet.TestAdapter</AssemblyName>
7+
<PackageId>BenchmarkDotNet.TestAdapter</PackageId>
8+
<ProduceReferenceAssembly>True</ProduceReferenceAssembly>
9+
<Nullable>enable</Nullable>
10+
</PropertyGroup>
11+
12+
<ItemGroup>
13+
<PackageReference Include="Microsoft.TestPlatform.AdapterUtilities" Version="17.7.2" />
14+
<PackageReference Include="Microsoft.TestPlatform.ObjectModel" Version="17.7.2" />
15+
<PackageReference Include="Microsoft.TestPlatform.TranslationLayer" Version="17.7.2" />
16+
</ItemGroup>
17+
18+
<ItemGroup>
19+
<ProjectReference Include="..\BenchmarkDotNet\BenchmarkDotNet.csproj" />
20+
</ItemGroup>
21+
22+
<!-- Include files in nuget package for generating entry point -->
23+
<ItemGroup>
24+
<Compile Remove="Package\EntryPoint.*" />
25+
<None Include="Package\EntryPoint.*" Pack="true" PackagePath="entrypoints\" />
26+
<None Include="Package\BenchmarkDotNet.TestAdapter.props" Pack="true" PackagePath="build\" />
27+
</ItemGroup>
28+
</Project>
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
using BenchmarkDotNet.Extensions;
2+
using BenchmarkDotNet.Helpers;
3+
using BenchmarkDotNet.Running;
4+
using BenchmarkDotNet.Toolchains;
5+
using System;
6+
using System.Collections.Generic;
7+
using System.Linq;
8+
using System.Reflection;
9+
10+
namespace BenchmarkDotNet.TestAdapter
11+
{
12+
/// <summary>
13+
/// A class used for enumerating all the benchmarks in an assembly.
14+
/// </summary>
15+
internal static class BenchmarkEnumerator
16+
{
17+
/// <summary>
18+
/// Returns all the BenchmarkRunInfo objects from a given assembly.
19+
/// </summary>
20+
/// <param name="assemblyPath">The dll or exe of the benchmark project.</param>
21+
/// <returns>The benchmarks inside the assembly.</returns>
22+
public static BenchmarkRunInfo[] GetBenchmarksFromAssemblyPath(string assemblyPath)
23+
{
24+
var assembly = Assembly.LoadFrom(assemblyPath);
25+
26+
var isDebugAssembly = assembly.IsJitOptimizationDisabled() ?? false;
27+
28+
return GenericBenchmarksBuilder.GetRunnableBenchmarks(assembly.GetRunnableBenchmarks())
29+
.Select(type =>
30+
{
31+
var benchmarkRunInfo = BenchmarkConverter.TypeToBenchmarks(type);
32+
if (isDebugAssembly)
33+
{
34+
// If the assembly is a debug assembly, then only display them if they will run in-process
35+
// This will allow people to debug their benchmarks using VSTest if they wish.
36+
benchmarkRunInfo = new BenchmarkRunInfo(
37+
benchmarkRunInfo.BenchmarksCases.Where(c => c.GetToolchain().IsInProcess).ToArray(),
38+
benchmarkRunInfo.Type,
39+
benchmarkRunInfo.Config);
40+
}
41+
42+
return benchmarkRunInfo;
43+
})
44+
.Where(runInfo => runInfo.BenchmarksCases.Length > 0)
45+
.ToArray();
46+
}
47+
}
48+
}

0 commit comments

Comments
 (0)