Skip to content

Feature/mtp test adapter 2803 - #3229

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

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

Copy link
Copy Markdown
Contributor Author

@filzrev does the latest change suffice

@timcassell

timcassell commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator

Thanks for this — the adapter itself works. Built Debug and Release (0 warnings), unit suite 1057 passed, and the new MTP project discovers and runs 9/9 with filters behaving, including through a real packed .nupkg following testadapter.md steps 1–5. The design is sound; what blocks merge is the MSBuild layer.

1. The IsTestingPlatformApplication opt-outs never fire for package consumers (build/BenchmarkDotNet.TestAdapter.targets:16-17). Both conditions require the property to be empty, but MTP's buildTransitive targets are imported first and already default it to true (confirmed via -pp ordering and -getProperty). Against the packed nupkg: dotnet test hard-fails for the documented VSTest opt-in on .NET 10+ SDKs; warning CS8892 fires for every non-default configuration, which is an error under TreatWarningsAsErrors; and testadapter.md:157's GenerateProgramFile=false claim is false. Making the opt-out authoritative fixes all three:

<IsTestingPlatformApplication Condition="'$(BenchmarkDotNetUseVSTest)' == 'true' or '$(GenerateProgramFile)' == 'false'">false</IsTestingPlatformApplication>

The repo can't catch this because the samples and the new project <Import> the props/targets by file path from the csproj body — before nuget.g.targets, the opposite order from a real consumer. Same trap as #3186.

2. Existing VSTest users silently lose their entry point (build/BenchmarkDotNet.TestAdapter.props:16). GenerateBDNEntryPoint now requires BenchmarkDotNetUseVSTest=true, so the setup documented before this PR no longer gets EntryPoint.cs. Reproduced: Microsoft.NET.Test.Sdk's stub Main wins and the exe prints nothing and exits 0, where it used to run BenchmarkSwitcher.

Both are invisible to this repo's own build, and the new project has no assertions and is only ever built — coverage that consumes the built .nupkg would catch them.

3. The global.json has no effect where it sits

tests/BenchmarkDotNet.IntegrationTests.TestingPlatform/global.json sets test.runner: Microsoft.Testing.Platform, which is what makes dotnet test drive an MTP app instead of VSTest on .NET 10+ SDKs. It's resolved from the working directory upward, not relative to the project, so it only applies when dotnet test is invoked from that folder — and nothing in the repo does that. UnitTestRunner passes full project paths and runs from the repo root; the coverage workflow names project directories; and run-tests-selected.yaml, which does set working-directory to the project, takes a fixed type: choice list that doesn't include this project (and this PR doesn't add it).

So either delete the file, or add tests/BenchmarkDotNet.IntegrationTests.TestingPlatform to that workflow's project options — which is what would make it load-bearing. Two notes if you wire the project into CI:

  • dotnet test from the repo root fails, on the project or the solution: error : Testing with VSTest target is no longer supported by Microsoft.Testing.Platform on .NET 10 SDK and later. Not a CI path today, but a normal thing for a contributor or IDE to do.
  • Adding it to UnitTestRunner will fail as-is: that runner passes a full project path with no WorkingDirectory, so it executes from the repo root and won't find the file. Either set WorkingDirectory on the Cake DotNetTestSettings for this project, or skip dotnet test and exec the built exe directly — the latter works today, filters included.

Two fixes that look obvious but don't work, so nobody burns time on them:

  • Moving global.json to the repo root. It wouldn't pin the SDK (no sdk section), but test.runner is repo-wide, and BDN's suites are xunit 2.9.3 + Microsoft.NET.Test.Sdk. With it in place, dotnet test refuses them outright: "The following test projects are using VSTest test runner", exit 1. A repo picks one runner, and BDN's is VSTest.
  • Opting out per project. Neither IsTestProject=false nor TestingPlatformDotnetTestSupport=true suppresses it, because the error comes from MTP's own target, gated only on IsTestingPlatformApplication and the SDK version:
<Target Name="_MTPBeforeVSTest" BeforeTargets="VSTest">
  <Error Text="Testing with VSTest target is no longer supported…"
         Condition="'$(IsTestingPlatformApplication)'=='true' AND '$(TargetFramework)'!='' AND '$(_SupportsGlobalJsonTestRunner)'=='true'" />

It fires even when the VSTest target would be a complete no-op for that project, which is arguably worth an upstream issue against Microsoft.Testing.Platform.MSBuild.

Smaller

  • BenchmarkTestFramework.cs:174 — cancelled runs leave InProgress nodes with no terminal state, so IDEs pin them at "running".
  • BenchmarkEventProcessor.cs:57OnBuildComplete runs concurrently per build partition and mutates plain Dictionary/HashSet on the failure path.
  • BenchmarkTestNode.BuildPath escapes / but not the [/] it inserts around the job, so an exact-path tree filter can't match a node; three comments say --filter for what is --treenode-filter; AssemblyInfo.cs's only diff is a stray trailing space.

Core changes are behaviour-neutral — FullNameProvider only adds a method, so summaries, exports and baselines are unchanged, and UIDs are stable across discovery and execution.

The description mentions making MonoBenchmarks/SharedDiagnosers Debug-only — that's not in the diff; worth correcting.

Reviewed by Claude (Opus 5), posted by @timcassell.

Added tests/BenchmarkDotNet.IntegrationTests.TestingPlatform to run-tests-selected.yaml. Included a comment clarifying that this project is a Microsoft.Testing.Platform application, relies on global.json for dotnet test routing, and requires the workflow to set the working directory for correct resolution.
No functional changes; removed and immediately re-added the InternalsVisibleTo attribute line for formatting consistency.
Update logic for IsTestingPlatformApplication and GenerateProgramFile to ensure correct opt-out handling for Microsoft.Testing.Platform. Explicitly set IsTestingPlatformApplication to false when BenchmarkDotNetUseVSTest is true or GenerateProgramFile is false, and default to true otherwise. Set GenerateProgramFile to false when IsTestingPlatformApplication is true to prevent entry point conflicts. Add comments to clarify the changes.
Updated the XML documentation remark to specify that the tree node filter is controlled by the <c>--treenode-filter</c> option, replacing the outdated reference to <c>--filter</c>. This enhances documentation accuracy.
Introduced a lock (buildCompleteGate) to synchronize build failure handling in BenchmarkEventProcessor. This prevents race conditions when multiple threads report build completion concurrently, ensuring safe execution in parallel build scenarios. The failure handling logic remains unchanged.
Updated the XML documentation for GetFilterableProperties to specify --treenode-filter as the correct command-line argument, replacing the inaccurate --filter reference. This improves the accuracy of usage instructions.
Updated the comment describing the tree node filter to reference the correct command-line option, `--treenode-filter`, instead of the outdated `--filter`. No functional changes were made; this is a documentation clarification.
@sheddy123

sheddy123 commented Aug 30, 2026

Copy link
Copy Markdown
Contributor Author

@timcassell / @filzrev the last 4 issues raised have been reviewed and addressed:

  1. Mutator wiping categories - fixed in ImmutableConfigBuilder: the job's own categories are captured before Apply and merged back after. I didn't use IgnoreOnApply as suggested, UnfreezeCopyCore is built on ApplyCore, so it would drop categories on every WithXxx call, not just mutator application (which is why IdCharacteristic is special-cased there). I tried it; it broke TheSameCategoryIsNotAddedTwice. There's now a test pinning that invariant.
  2. Meta.Categories = null guard moved into MetaMode, the funnel all three entry points share, so the property setter and AddCategories are covered too.
  3. WithCategory(null) null categories now rejected instead of stored.
  4. Missing attribute and CLI option added [JobCategoryFilter] and --jobCategories, both tested end to end.

@timcassell

Copy link
Copy Markdown
Collaborator

Reviewed b6d217780..eb40140d5.

The adapter code holds up. The concurrency reasoning checks out against the runner rather than just the comments: OnBuildComplete really is raised from the parallel build tasks (BenchmarkRunnerClean.cs:436) and really is the only concurrent callback, since BDN swaps in NullLogger whenever there is more than one partition (BenchmarkRunnerClean.cs:421) — so buildCompleteGate covers the right surface and the unsynchronised StringBuilder in OutputDeviceLogger is safe. The channel drain, linked cancellation and TryComplete in the inner finally are all correct. The histogram block in AppendMeasurementSummary is verbatim from the existing VSTestEventProcessor, so no new risk there.

Two things worth raising.

1. The new integration project asserts nothing, and never builds a benchmark.

All three probes pin Job.Dry.WithToolchain(InProcessEmitToolchain.Default), so the build path is never taken — which leaves OnBuildComplete, the one callback that needed the new locking and that maps build failures onto failed tests, with zero coverage. The project has no assertions at all; CI runs dotnet test against it and the only thing proven is that a run does not crash. Nothing pins uid stability across the discover/run process boundary, the Description-over-method-name display naming, --treenode-filter matching, or the collision report — which is the actual new logic. A single out-of-process job would cover the build path.

2. Neither in-repo consumer exercises the import order BenchmarkDotNet.TestAdapter.targets is built around.

The IsTestingPlatformApplication overwrite is justified in-comment by "a real package consumer imports [Microsoft.Testing.Platform.MSBuild's targets] before this file". But the samples project and the new test project both <Import> inside the csproj body, which MSBuild evaluates before nuget.g.targets (imported at the end via Sdk.targets). In-repo, BDN's targets therefore land before MTP.MSBuild's — the reverse of the packaged order. It probably still works either way if MTP's default is conditioned on empty, but the ordering the comment reasons about is never actually tested. Same delivery-path split as #3186; a pack-and-restore smoke test would close it.

Minor: BenchmarkTestNode.Escape replaces / with \/ but leaves existing backslashes alone, so a parameter whose ToString() contains a literal \/ is indistinguishable from an escaped separator in the tree path.

Confirmed fixed since the last round: global.json is scoped to the test project rather than the repo root, and GetUniqueId now comes from the core public extension added in #3227, with the adapter's duplicate removed.

Reviewed by Claude Opus 5 via Claude Code.

Added a new step in run-tests.yaml to run a PowerShell smoke test on the packed BenchmarkDotNet.TestAdapter after the 'pack' task. This ensures correct NuGet build file import order, since nothing in the solution currently consumes the package directly.
Added ConsumedBenchmark.cs with a simple benchmark using BenchmarkDotNet and a custom fast config. Introduced TestAdapterConsumer.csproj targeting .NET 10.0, referencing BenchmarkDotNet.TestAdapter from a local artifacts source for smoke testing real-world usage.
A new BenchmarkDotNet.IntegrationTests.TestingPlatform.Failures.csproj was added targeting .NET 10.0 as an executable. The project includes assembly metadata, enforces code optimization for consistent benchmarks, and references BenchmarkDotNet.TestAdapter with manual imports of its .props and .targets files. Common build property and target files are also imported to ensure correct MSBuild behavior.
Added SeparatorProbe class in BenchmarkDotNet.IntegrationTests.TestingPlatform. This benchmark uses a parameter with '/' as a tree separator, includes a Length() method, and applies a custom FastConfig with a dry job and InProcessEmitToolchain for faster execution.
Introduce BuildFailureProbe in BenchmarkDotNet.IntegrationTests.TestingPlatform.Failures. This class uses a custom toolchain (FailingBuildConfig) with a NoopGenerator, FailingBuilder, and UnreachableExecutor to reliably simulate build failures for adapter testing, without relying on uncompilable code.
Added CollisionProbe class to test BenchmarkDotNet's behavior when benchmark parameters have identical string representations, using a custom Ambiguous type. Ensures the adapter reports collisions instead of running ambiguous benchmarks.
Added OutOfProcessProbe class in BenchmarkDotNet.IntegrationTests.TestingPlatform with an Add() benchmark method. Configured to run out-of-process using a custom OutOfProcessConfig and Job.Dry to ensure a real build/execute cycle for adapter testing, unlike in-process probes.
Added conditional project references to BenchmarkDotNet.IntegrationTests.TestingPlatform and .Failures in BenchmarkDotNet.IntegrationTests.csproj. These are included only for .NETCoreApp targets with ReferenceOutputAssembly set to false, ensuring correct build order for probe apps used in TestingPlatformAdapterTests.
Added TestingPlatformAdapterTests (under #if NETCOREAPP) using BenchmarkDotNet to perform integration tests on Microsoft.Testing.Platform probe apps. Tests cover benchmark discovery, UID consistency, filtering, build/run behavior, and error reporting by running probe apps as separate processes and asserting on their output. Introduced helper methods for process execution, output parsing, and result summarization.
Updated the Escape method in BenchmarkTestNode.cs to percent-encode '/' as '%2F' and '%' as '%25'. This prevents path segmentation issues in Microsoft.Testing.Platform, ensuring correct tree structure and benchmark addressability. Filters must now use '%2F' instead of '/'.
Updated documentation to clarify that benchmark parameter values containing slashes (/) or percent signs (%) are percent-encoded in the tree node filter path (e.g., a/b as a%2Fb, % as %25). This encoding applies only to the filter, not to the displayed benchmark name.
Added test-adapter-consumer.ps1 to perform smoke tests on the packed BenchmarkDotNet.TestAdapter NuGet package. The script restores and builds a consumer project, checks MSBuild property resolutions, and verifies benchmark discovery to ensure correct adapter behavior when used as a package. Includes detailed comments, parameter handling, and error checking.
Add <Optimize>true</Optimize> to ensure the assembly is always built with optimizations, preventing BenchmarkEnumerator from hiding out-of-process benchmarks in non-Release builds. Expand comments to clarify manual build file imports and MSBuild processing order.
Added a section to README.md describing the BenchmarkDotNet.IntegrationTests.TestingPlatform.Failures project. The documentation explains its purpose as a collection of intentionally failing benchmarks for testing BenchmarkDotNet.TestAdapter error handling, including UID collision and build failure mapping. It also clarifies the relationship with TestingPlatformAdapterTests and the location of passing benchmarks.
Added BenchmarkDotNet.IntegrationTests.TestingPlatform.Failures to the solution file, ensuring it is included with other integration test projects.
@sheddy123

Copy link
Copy Markdown
Contributor Author

@timcassell the issues have been addressed

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