Skip to content

Feature/mtp test adapter 2803 - #3229

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

Feature/mtp test adapter 2803#3229
sheddy123 wants to merge 44 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

- 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.
Simplified project definitions in BenchmarkDotNet.slnx by removing custom build entries and updating project paths, including renaming the TestingPlatform project.
- Add "testingplatform" to cSpell.json to suppress spelling warnings.
- Refactor .props packaging in csproj to use a single entry with multiple paths, ensuring cross-platform compatibility and resolving NU5129.
@sheddy123

sheddy123 commented Aug 15, 2026

Copy link
Copy Markdown
Contributor Author

@filzrev , @timcassell, this is up for review. Thanks

@timcassell

timcassell commented Aug 16, 2026

Copy link
Copy Markdown
Collaborator

Reviewed the MTP adapter glue. The shared-code extraction (GetUnrandomizedJobDisplayInfoBenchmarkCaseIdentityExtensions, the BenchmarkEnumerator split, FullNameProvider.GetBenchmarkUid) looks clean. Two correctness issues in BenchmarkTestFramework, plus a couple of minor notes.

1. A faulting queued work item orphans the benchmark run and disposes the semaphore under it

src/BenchmarkDotNet.TestingPlatform/BenchmarkTestFramework.cs:108–157

The background runTask (BenchmarkRunner.Run) keeps calling workQueue.Enqueue from the event-processor/logger callbacks for the whole run, while the main thread awaits workQueue.DrainAsync(). DrainAsync (AsyncWorkQueue.cs:52) awaits each item and does not catch exceptions. If any queued item throws — e.g. MessageBus.PublishAsync or outputDevice.DisplayAsync faulting as the host bus tears down — the exception propagates out of RunAsync, so await runTask (line 157) is never reached and using var workQueue (line 108) disposes the SemaphoreSlim. The still-running runTask then hits Enqueue → available.Release() on the disposed semaphore, throwing ObjectDisposedException on BenchmarkDotNet's run thread, while the benchmark run itself continues orphaned and its Task fault goes unobserved.

Suggest wrapping the drain so runTask is driven to completion (or cancelled) before the workQueue is disposed.

2. A duplicate UID aborts the entire run request instead of failing one test

src/BenchmarkDotNet.TestingPlatform/BenchmarkTestFramework.cs:104

matches.ToDictionary(match => match.Node.Uid, match => match.Node) throws ArgumentException ("An item with the same key has already been added") on a duplicate UID. UIDs come from FullNameProvider.GetBenchmarkUid = class.method(params) [job], where the params are user-stringified values. Two distinct [Arguments]/[ParamsSource] values whose ToString() collapses to the same text (or the same value under two jobs with identical unrandomized DisplayInfo) will abort the whole run request with an unhandled exception rather than reporting the tests. Note the discovery path (DiscoverAsync, lines 84–95) has no equivalent guard and would instead publish two nodes with the same TestNodeUid.

Minor

  • Cancellation drain (BenchmarkTestFramework.cs:118 / OutputDeviceLogger.cs:62): the already-cancelled cancellationToken is passed to outputDevice.DisplayAsync, so queued log writes throw OperationCanceledException inside DrainAsync before the remaining items drain. Close to the intended "OCE on cancel" behavior, but combined with finding 1 the resulting runTask fault is unobserved.
  • Cosmetic (FullNameProvider.cs:76, 82–83): trailing whitespace on [PublicAPI] and a double blank line.

Review by Claude (Claude Code), posted on Tim Cassell's behalf.

Introduced GenericProbe<T> benchmark class in a new BenchmarkDotNet.IntegrationTests.TestingPlatform namespace to evaluate how test runners handle closed generic types. The benchmark tests instance creation for int, char, and List<string> type arguments. Added GenericProbeConfig to configure the benchmark with InProcessEmit toolchain and a dry job.
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.
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