Skip to content

Feature/mtp test adapter 2803 - #3229

Open
sheddy123 wants to merge 48 commits into
dotnet:masterfrom
sheddy123:feature/mtp-test-adapter-2803
Open

Feature/mtp test adapter 2803#3229
sheddy123 wants to merge 48 commits into
dotnet:masterfrom
sheddy123:feature/mtp-test-adapter-2803

Conversation

@sheddy123

Copy link
Copy Markdown
Contributor

#2803
@timcassell

Introduce BenchmarkTestFramework to integrate BenchmarkDotNet with Microsoft.Testing.Platform, enabling benchmark discovery and execution. Implements session management, filtering, event processing, and output routing. Handles test node updates and cancellation, with support for experimental platform features.
Added BenchmarkDotNet.TestAdapter.TestingPlatform and BenchmarkDotNet.IntegrationTests.TestingPlatform projects to the solution. Updated MonoBenchmarks and SharedDiagnosers integration test projects to only build for the solution in Debug configuration.

Added a new guide for running benchmarks with Microsoft.Testing.Platform (MTP), covering setup, usage, and caveats. Updated the table of contents to include the new page and added a note to the VSTest docs about the MTP adapter option.
Added InternalsVisibleTo attribute in AssemblyInfo.cs to expose internal members to the BenchmarkDotNet.TestAdapter.TestingPlatform assembly, ensuring it uses the same public key as other related assemblies.
Deleted the internal static method GetUnrandomizedJobDisplayInfo from BenchmarkCaseExtensions.cs. This method handled normalization of job display info by removing randomness from job IDs for consistent benchmark referencing. No other code changes were made.
Introduced BenchmarkCaseIdentityExtensions with GetUnrandomizedJobDisplayInfo to normalize Job DisplayInfo by removing random ID components. This ensures consistent benchmark identification across processes for test adapters.
Introduce GetBenchmarksFromAssembly to extract benchmarks from an already loaded Assembly. Refactor existing logic to use this method, improving code reuse and enabling benchmark retrieval from both loaded assemblies and file paths.
Add MSBuild props to enable TestingPlatform integration, set defaults for `dotnet test` compatibility, disable parallel TFM runs, and auto-register BenchmarkDotNet builder hook.
Introduced AsyncWorkQueue, an internal sealed class in BenchmarkDotNet.TestAdapter.TestingPlatform. It enables ordered, thread-safe queuing of asynchronous work items, allowing synchronous producers and asynchronous consumers. Utilizes ConcurrentQueue and SemaphoreSlim, supports completion signaling, and implements IDisposable for resource cleanup.
Created a new .csproj targeting netstandard2.0 for the TestingPlatform adapter. Configured project metadata, packaging, and references. Integrated Microsoft.Testing.Platform.MSBuild and BenchmarkDotNet, and linked shared source files for benchmark enumeration. Set IsTestingPlatformApplication to false to avoid test app behavior.
Introduced BenchmarkDotNetExtension class implementing IExtension to provide extension metadata and enablement status for Microsoft.Testing.Platform integration.
Introduced BenchmarkEventProcessor to process BenchmarkDotNet events and translate them into test node updates for the testing platform. Handles validation errors, build results, benchmark execution, and ensures all benchmarks have published results. Includes logic for error aggregation, output formatting, and timing information.
Introduce BenchmarkTestFramework to integrate BenchmarkDotNet with Microsoft.Testing.Platform, enabling benchmark discovery and execution. Implements session management, filtering, event processing, and output routing. Handles test node updates and cancellation, with support for experimental platform features.
Introduced the internal sealed class BenchmarkTestNode to encapsulate immutable BenchmarkCase data for Microsoft.Testing.Platform integration. This includes stable UID generation, display name and path construction, property management, and support for test filtering and message bus conversion.
Introduced OutputDeviceLogger class implementing ILogger to forward BenchmarkDotNet logs to the platform output device. Handles log kinds, buffers lines, and asynchronously displays output to ensure build progress and results are visible in test run output.
Introduce TestApplicationBuilderExtensions with AddBenchmarkDotNet methods for integrating BenchmarkDotNet benchmarks into Microsoft.Testing.Platform. Includes overloads for entry assembly and specific assemblies, null checks, test framework registration, and tree node filter service support.
Introduced a static TestingPlatformBuilderHook class in the BenchmarkDotNet.TestAdapter.TestingPlatform namespace. This class provides an AddExtensions method to register BenchmarkDotNet with the test application builder, intended for use by generated code and hidden from IntelliSense.
Added a "test" section to global.json to specify "Microsoft.Testing.Platform" as the test runner. This configures the project to use the designated testing platform.
Introduce BenchmarkDotNet.IntegrationTests.TestingPlatform.csproj targeting net10.0 as an executable. The project includes assembly metadata, references BenchmarkDotNet.TestAdapter.TestingPlatform, manually imports its build props, and uses shared common.props and common.targets for build configuration.
Introduced SampleBenchmarks class in BenchmarkDotNet.IntegrationTests.TestingPlatform. Defines Add and Multiply benchmarks with parameterized Size, categorized as "Fast" and "Slow". Uses a custom FastConfig to run benchmarks in-process with a single dry iteration for quick end-to-end testing.
Added BenchmarkDotNet.TestAdapter.TestingPlatform and BenchmarkDotNet.IntegrationTests.TestingPlatform projects to the solution. Updated MonoBenchmarks and SharedDiagnosers integration test projects to only build for the solution in Debug configuration.
Explicitly set BenchmarkDotNet.TestAdapter.TestingPlatform and BenchmarkDotNet.IntegrationTests.TestingPlatform to not build in the Debug configuration by adding <Build Solution="Debug|*" Project="false" /> in BenchmarkDotNet.slnx. No other changes made.
@timcassell

Copy link
Copy Markdown
Collaborator

Let's name it BenchmarkDotNet.TestingPlatform.

Updated MonoBenchmarks and SharedDiagnosers integration test projects to only build for the solution in Debug configuration.

Why? We run tests in Release configuration.

/// The job is always part of the uid, otherwise two cases of the same benchmark that only differ by job would
/// collide. The parameters are already part of the method name.
/// </remarks>
public static string GetUid(BenchmarkCase benchmarkCase)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I though GetUid logics should be implemented on BenchmarkDotNet core project side.
Because --filter-uid option is useful for normal benchmark exe project without MTP.

I've implemented MSTest based UID generation logics on #3227.
Is it able to confirm these logics can be shared with TestAdapter?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This has been noted and taken into consideration. I have done the fix

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#3227 is merged to master.
So GUID based UID generator is available.

public static string FromBenchmarkCase(BenchmarkCase benchmarkCase)


var properties = new List<IProperty>
{
new TestMethodIdentifierProperty(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Following generic benchmarks are not shown correctly on VS Test Explorer.

    [InProcess]
    [GenericTypeArguments(typeof(int))]
    [GenericTypeArguments(typeof(int?))]
    [GenericTypeArguments(typeof(int[]))]
    [GenericTypeArguments(typeof(int?[]))]
    [GenericTypeArguments(typeof(int[,]))]
    [GenericTypeArguments(typeof(int?[,]))]
    public class GenericTypeBenchmarks<T>
    {
        [Benchmark]
        public void Benchmark() { }
    }
Image

I though TestMethodIdentifier's property require ECMA-335 compliant type names.
https://learn.microsoft.com/en/dotnet/api/microsoft.testing.platform.extensions.messages.testmethodidentifierproperty

xUnit.net example.
https://github.com/xunit/xunit/blob/rel/4.0.0/src/xunit.v3.common/Extensions/ReflectionExtensions.cs#L171

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like generics are still not displayed as expected.

Image

- Correct NuGet package and namespace in documentation
- Add GetBenchmarkUid for stable benchmark identification
- Change namespace in BenchmarkCaseIdentityExtensions
- Update InternalsVisibleTo for TestingPlatform assembly
Deleted all source, project, and props files from BenchmarkDotNet.TestAdapter.TestingPlatform. This removes all implementation and integration for running benchmarks as tests via Microsoft.Testing.Platform, including test discovery, execution, and result processing logic.
Add BenchmarkDotNet.TestingPlatform.props to enable seamless integration with Microsoft.Testing.Platform. This includes setting required properties for Testing Platform application behavior, ensuring `dotnet test` compatibility on older SDKs, disabling parallel test execution for multi-targeted projects by default, and registering BenchmarkDotNet as a builder hook.
Introduced AsyncWorkQueue in BenchmarkDotNet.TestingPlatform to enable thread-safe, ordered queuing of asynchronous work items. Supports synchronous enqueuing, asynchronous draining, completion signaling, and resource disposal using ConcurrentQueue and SemaphoreSlim.
Introduce a new project to integrate BenchmarkDotNet with Microsoft.Testing.Platform, enabling benchmarks to be discovered and executed as tests. Implements extension identification, test framework, event processing, test node representation, and output logging. Provides builder extensions for easy registration and an MSBuild hook for automatic integration. Updates project configuration for packaging and dependencies.
Updated BenchmarkDotNet.IntegrationTests.TestingPlatform.csproj to reference BenchmarkDotNet.TestingPlatform instead of BenchmarkDotNet.TestAdapter.TestingPlatform. Adjusted both the ProjectReference and Import paths accordingly.
Benchmarks are now grouped by UID to detect collisions. When multiple benchmarks share a UID, `PublishCollisionAsync` reports the issue as a failed test node, allowing other benchmarks to proceed. Only benchmarks with unique UIDs are executed. The refactor introduces a `Match` class, improves cancellation and exception handling, and ensures proper resource cleanup during async operations.
Only whitespace was changed above the GetBenchmarkUid method; no functional or logical modifications were made.
Comment thread src/BenchmarkDotNet.TestingPlatform/AsyncWorkQueue.cs Outdated
Deleted AsyncWorkQueue.cs, removing the AsyncWorkQueue class and all associated methods for managing and draining asynchronous work items. This eliminates the custom ordered async work queue implementation.
Replaces custom AsyncWorkQueue with ChannelWriter<Func<Task>> for queuing log display tasks. Updates constructor and field types, and switches from Enqueue to TryWrite for task scheduling. This enhances integration with .NET's built-in concurrency primitives.
Switch to System.Threading.Channels for the benchmark event work queue to improve thread safety and prevent deadlocks. Update event processor and logger to use the channel writer, and add a DrainAsync method to process queued work items sequentially until completion.
@filzrev

filzrev commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

Is it required to keep VSTest based TestAdapter?

I think it's better just update the test adapter to use MTP, rather than creating a separate package.
as commented at #2803 (comment)

It might be cause following limitations by dropping VSTest-based TestAdapter.

  • It can't mix VSTest-based test projects with MTP based benchmark project in single solution.
  • It's not works on old platform that don't support MTP (e.g. Visual Studio 2019)

Though, It can reduce maintenance cost by dropping VSTest based TestAdapter.
Because TestAdapter feature is mainly for test/debugging purpose.

Standardized benchmark case UID generation by replacing all usages of FullNameProvider.GetBenchmarkUid with BenchmarkCase.GetUniqueId. Removed the obsolete GetBenchmarkUid method. Updated comments for clarity. Also adjusted .slnx to control build for TestingPlatform projects.
@sheddy123

Copy link
Copy Markdown
Contributor Author

@filzrev you're right that this is what @timcassell asked for in #2803, so I should settle it before going further.

One thing worth separating, though, is that "one package" and "drop VSTest" aren't the same decision. A single package can serve both protocols; that's what MSTest does (EnableMSTestRunner flips the same adapter package into MTP mode), and MS even ships Microsoft.Testing.Extensions.VSTestBridge for frameworks that want one implementation to cover both. BDN wouldn't need the bridge since both implementations already exist here; they'd just ship together, gated by a property. That keeps VS2019 and mixed VSTest/MTP solutions working while still being one package.

Merging into BenchmarkDotNet.TestAdapter is mostly repackaging on my side the framework/node/event-processor code moves over intact; what changes is the csproj, the build props, and choosing which entry point to generate.

I will be happy to do it. @timcassell @filzrev , which do you want: 1. two packages or 2. one package supporting both protocols? I'll rework to whichever you pick.

@timcassell

Copy link
Copy Markdown
Collaborator

"Drop vstest" was implied by my original comment, however if we can have both without too much hassle, I think it's fine. The vstest adapter is considered feature complete afaik, so maintenance should only be a matter of keeping up with any core API changes. I'm not too concerned about 1 vs 2 packages, whatever seems better for user consumption for minimal confusion. If you go with 1 package, the new platform should be the default.

@sheddy123

Copy link
Copy Markdown
Contributor Author

Thanks for the response @timcassell
I'll go with 1 package, MTP as the default, since making people choose between two packages is exactly the confusion worth avoiding, and VSTest stays available behind an opt-in property for VS2019 and mixed VSTest/MTP solutions.
A user adding BenchmarkDotNet.TestAdapter shouldn't have to first learn what VSTest and MTP are to choose between two packages

However, one migration detail I want to flag before I do it: the package currently generates a BenchmarkSwitcher.FromAssembly(...).Run(args) entry point (entrypoints/EntryPoint.cs), and in MTP mode Microsoft.Testing.Platform.MSBuild generates its own instead. So on upgrade, dotnet run on an existing benchmark project changes behaviour. I am thinking of accepting it and document it in the changelog since it is simpler and matches "new platform is the default"

@timcassell

Copy link
Copy Markdown
Collaborator

However, one migration detail I want to flag before I do it: the package currently generates a BenchmarkSwitcher.FromAssembly(...).Run(args) entry point (entrypoints/EntryPoint.cs), and in MTP mode Microsoft.Testing.Platform.MSBuild generates its own instead.

Right, that's why the current vstest adapter disables that (see BenchmarkDotNet.TestAdapter.props);

So on upgrade, dotnet run on an existing benchmark project changes behaviour. I am thinking of accepting it and document it in the changelog since it is simpler and matches "new platform is the default"

We're already shipping lots of breaking changes in 0.16, so it's fine.

Refactor BenchmarkDotNet.TestingPlatform integration by moving configuration logic from .props to BenchmarkDotNet.TestAdapter.targets. Remove obsolete .props and .csproj files. Update cSpell dictionary to use "testadapter" instead of "testingplatform".
Rewrote and reorganized documentation to focus on the new BenchmarkDotNet.TestAdapter package and its integration with Microsoft.Testing.Platform (MTP) and VSTest. Clarified default behaviors, entry point handling, and configuration steps. Updated code samples and project file snippets. Revised table of contents to reflect the new structure and clarified the relationship between MTP and VSTest. Added notes on IDE support and caveats.
Refactor namespaces from BenchmarkDotNet.TestingPlatform to BenchmarkDotNet.TestAdapter.TestingPlatform throughout the codebase. Update the extension UID and related comments to match the new adapter naming convention and integration targets.
Added explicit imports for BenchmarkDotNet.TestAdapter .props and .targets files in both F# and C# sample projects to ensure adapter build logic is applied. Also imported common.targets. This preserves custom entry points and prevents conversion to Microsoft.Testing.Platform applications.
Switched project reference and build file imports from BenchmarkDotNet.TestingPlatform to BenchmarkDotNet.TestAdapter, including .props and .targets files.
Add NuGet description and set IsTestingPlatformApplication to false to avoid treating the adapter as a test app. Add Microsoft.Testing.Platform.MSBuild as a dependency for downstream projects. Update .props and .targets packaging for cross-platform compatibility. Only generate entry point for VSTest scenarios; clarify comments.
Removed BenchmarkDotNet.TestingPlatform from the solution and deleted its InternalsVisibleTo entry from AssemblyInfo.cs, as it no longer requires access to internal members. No other InternalsVisibleTo changes were made.
@sheddy123

Copy link
Copy Markdown
Contributor Author

@timcassell / @filzrev any update on this latest changes for this PR?

/// <param name="assemblyPath">The dll or exe of the benchmark project.</param>
/// <param name="includeJobInName">Whether or not the display name should include the job name.</param>
/// <returns>The VSTest TestCase.</returns>
internal static TestCase ToVsTestCase(this BenchmarkCase benchmarkCase, string assemblyPath, bool includeJobInName = false)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it able to move VSTest specific codes to dedicated directory same as TestingPlatform?

@sheddy123 sheddy123 Aug 22, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes.
The VSTest code can be moved into a dedicated VSTest/ directory alongside TestingPlatform/. Only BenchmarkEnumerator.cs is shared between the two adapters every other file is VSTest-only and can move.

// keeps the same identity across processes and across tools. The job is only part of the display name
// when it actually adds information.
var uid = benchmarkCase.GetUniqueId();
var displayName = $"{fullClassName}.{parametrizedMethodName}" + (includeJobInName ? $" [{jobDisplayInfo}]" : "");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It need to handle Description property of BenchmarkAttribute here to keep compatibility with VSTest-based test adapter.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @filzrev good catch on the display name. The uid is already covered GetUniqueId hashes Descriptor.DisplayInfo, which includes WorkloadMethodDisplayInfo (the formatted Description), the same input the VSTest adapter's GetTestCaseId uses. The display name isn't: it goes through FullNameProvider.GetMethodName, which is WorkloadMethod.Name + parameters. I'll build the name (and the tree-node path segment) from Descriptor.WorkloadMethodDisplayInfo instead.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One thing to confirm: TestCase.DisplayName in the VSTest adapter uses GetMethodName too, so it doesn't show Description today either. Do you want me to change both here so they stay in sync, or leave the VSTest side alone in this PR?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I though TestAdapter is using Description for display name before.
And it's changed to FQDN by PR #2978 to workaround TestExplorer issue.

On MTP mode.
It can separately define DisplayName and TestIdentifierProperty(It must be unique).
So it might be better to use Description property for display name.

Comment thread BenchmarkDotNet.slnx Outdated
<Project Path="tests/BenchmarkDotNet.IntegrationTests.Static/BenchmarkDotNet.IntegrationTests.Static.csproj" />
<Project Path="tests/BenchmarkDotNet.IntegrationTests.TestingPlatform/BenchmarkDotNet.IntegrationTests.TestingPlatform.csproj">
<Build Solution="Release|*" Project="false" />
</Project>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the purpose of this setting?

As far as I knows.
TestAdapter can execute benchmark with Debug configuration if benchmark using InProcess toolchain.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@filzrev This is a leftover from an earlier iteration it shouldn't be there. I've verified the project builds cleanly in Release and that --list-tests and a full run both work against the Release output, and you're right about Debug: BenchmarkEnumerator filters to in-process toolchains when the assembly is unoptimized, and the sample uses InProcessEmitToolchain, so Debug works either way. Removing the element so the project is built in all configurations and is actually covered by CI's Release build.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Debug: BenchmarkEnumerator filters to in-process toolchains when the assembly is unoptimized,

Although this isn't directly related to this PR,
I'd like to make it possible to disable this behavior via textconfig.json.

Because TestExplorer is expected to be used for debugging and testing.
(It need to Debugger.Break() for csproj-based toolchan though)

public FastConfig() => AddJob(Job.Dry.WithToolchain(InProcessEmitToolchain.Default));
}
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it able to confirm following invalid config test cases?
On my environment, Test discovery is interrupted by exception.

// Specify `abstract` config.
// This benchmark should be silently ignored. (Discovery/Execution should not be stopped by exception)
[Config(typeof(DebugConfig))]
public class Benchmarks_WithAbstractConfigAttribute
{
    [Benchmark]
    public void Benchmark01()
    {
    }
}

[Config(typeof(NoPublicConstructorConfig))]
public class Benchmarks_WithInvalidConfig
{
    [Benchmark]
    public void Benchmark01()
    {
    }

    private class NoPublicConstructorConfig : ManualConfig
    {
        private NoPublicConstructorConfig() { }
    }
}

@sheddy123 sheddy123 Aug 22, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes confirmed. I reproduced it earlier in this session. Adding both classes to the integration test project and running --list-tests crashes discovery:

Unhandled exception. System.MissingMethodException: Cannot dynamically create an instance of type
'BenchmarkDotNet.Configs.DebugConfig'. Reason: Cannot create an abstract class.
at BenchmarkDotNet.Attributes.ConfigAttribute..ctor(Type type)
at System.Reflection.CustomAttribute.GetCustomAttributes(...)
at BenchmarkDotNet.Helpers.GenericBenchmarksBuilder.BuildGenericsIfNeeded(Type type)
at BenchmarkDotNet.TestAdapter.BenchmarkEnumerator.GetBenchmarksFromAssembly(Assembly assembly)
at BenchmarkDotNet.TestAdapter.TestingPlatform.BenchmarkTestFramework.GetMatchingBenchmarks(...)

The NoPublicConstructorConfig case fails the same way with No parameterless constructor defined for type ....

Two things worth knowing beyond the confirmation:

  1. It isn't MTP-specific. The throw is in GenericBenchmarksBuilder.BuildGenericsIfNeeded, reached from BenchmarkEnumerator.GetBenchmarksFromAssembly shared with the VSTest adapter. Discovery on master breaks the same way.
  2. It throws earlier than the obvious fix would catch. It happens while the type list is being built, not in BenchmarkConverter.TypeToBenchmarks, so a try/catch around the conversion wouldn't help. The isolation has to wrap the per-type attribute read.

Replaced the explicit <Build> configuration for BenchmarkDotNet.IntegrationTests.TestingPlatform with a standard <Project> entry to align with the format used for other projects in the solution.
@@ -20,6 +28,13 @@
<PackageReference Include="Microsoft.TestPlatform.TranslationLayer" Version="17.8.0" />

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Following comment is leaved as not and it's not be handled on this PR


These VSTest specific packages should be referenced when using VSTest mode.
If's better remove these PackageReference on MTP mode.

Expected behavior

  1. By default, It should use MTP mode and VSTest related package references are removed by custom MSBuild targets.
  2. If VSTest mode. It should raise error that instruct explicitly specify VSTestMode on PackageReference.

Replaced default introduction with a personalized message identifying as GitHub Copilot and offering software development assistance.
Refactored the logic for generating method display names in BenchmarkDotNet. Improved separation of concerns by extracting display name generation to a dedicated provider. Updated the display name formatting to include job information conditionally. Enhanced maintainability and clarity in the test adapter's method identification process.
Introduced DescribedProbe to test benchmarks with and without custom descriptions. Includes FastConfig for quick in-process execution and a configurable Size parameter.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants