Uh oh!
There was an error while loading. Please reload this page.
Add MSTest reflection source generator (issue #1837) - #8586
Add MSTest reflection source generator (issue #1837)#8586Amaury Levé (Evangelink) merged 18 commits into
Conversation
There was a problem hiding this comment.
Pull request overview
This PR introduces an opt-in MSTest reflection metadata source generator (MSTestAdapter.PlatformServices.SourceGeneration) plus new runtime infrastructure in MSTestAdapter.PlatformServices to swap IReflectionOperations/IFileOperations to source-gen-backed implementations via a generated [ModuleInitializer]. This is intended as an early building block toward NativeAOT support (#1837).
Changes:
- Add a new incremental generator project that discovers
[TestClass]/[TestMethod]and emits a module initializer registering metadata. - Add runtime “source-generated” reflection/file operations and a public hook (
ReflectionMetadataHook) for generated code to register metadata. - Add unit tests for the generator and wire new projects into
TestFx.slnxandMSTest.slnf, plus updatePublicAPI.Unshipped.txt.
Show a summary per file
| File | Description |
|---|---|
| TestFx.slnx | Adds the new generator project + its unit test project to the full solution. |
| MSTest.slnf | Adds the new generator project + its unit test project to the MSTest solution filter. |
| src/Adapter/MSTestAdapter.PlatformServices/PlatformServiceProvider.cs | Adds internal hook to swap IReflectionOperations/IFileOperations. |
| src/Adapter/MSTestAdapter.PlatformServices/PublicAPI/PublicAPI.Unshipped.txt | Declares new public API surface (ReflectionMetadataHook, SourceGeneratedReflectionDataProvider). |
| src/Adapter/MSTestAdapter.PlatformServices/SourceGeneration/*.cs | Adds runtime hook + source-gen-backed reflection/file operations + toggle. |
| src/Adapter/MSTestAdapter.PlatformServices.SourceGeneration/* | Adds generator models/helpers + incremental generator + emitter + banned symbols. |
| test/UnitTests/MSTestAdapter.PlatformServices.SourceGeneration.UnitTests/* | Adds generator unit tests + test runner program + test project. |
Copilot's findings
- Files reviewed: 19/19 changed files
- Comments generated: 14
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Amaury Levé (Evangelink)
commented
May 25, 2026
🔍 Build Failure AnalysisSummary — The build failed due to a missing interface method and enforced code style violations across multiple target frameworks. Root cause 1: Missing interface method |
| Code | Project | File:Line | Message |
|---|---|---|---|
CS1061 | MSTestAdapter.PlatformServices | ReflectionMetadataHook.cs:37 | 'IPlatformServiceProvider' does not contain a definition for 'SetSourceGeneratedOperations'... (net8.0) |
CS1061 | MSTestAdapter.PlatformServices | ReflectionMetadataHook.cs:37 | 'IPlatformServiceProvider' does not contain a definition for 'SetSourceGeneratedOperations'... (net9.0) |
IDE0032 | MSTestAdapter.PlatformServices | SourceGeneratorToggle.cs:13 | Use auto property (net8.0) |
IDE0032 | MSTestAdapter.PlatformServices | SourceGeneratorToggle.cs:13 | Use auto property (net9.0) |
IDE0032 | MSTestAdapter.PlatformServices | SourceGeneratedReflectionOperations.cs:15 | Use auto property (net8.0) |
IDE0032 | MSTestAdapter.PlatformServices | SourceGeneratedReflectionOperations.cs:15 | Use auto property (net9.0) |
IDE0032 | MSTestAdapter.PlatformServices | SourceGeneratedFileOperations.cs:16 | Use auto property (net8.0) |
IDE0032 | MSTestAdapter.PlatformServices | SourceGeneratedFileOperations.cs:16 | Use auto property (net9.0) |
IDE0046 | MSTestAdapter.PlatformServices | SourceGeneratedReflectionOperations.cs:26 | 'if' statement can be simplified (net8.0) |
IDE0046 | MSTestAdapter.PlatformServices | SourceGeneratedReflectionOperations.cs:26 | 'if' statement can be simplified (net9.0) |
IDE0046 | MSTestAdapter.PlatformServices | SourceGeneratedReflectionOperations.cs:257 | 'if' statement can be simplified (net8.0) |
IDE0046 | MSTestAdapter.PlatformServices | SourceGeneratedReflectionOperations.cs:257 | 'if' statement can be simplified (net9.0) |
IDE0046 | MSTestAdapter.PlatformServices | SourceGeneratedReflectionOperations.cs:291 | 'if' statement can be simplified (net8.0) |
IDE0046 | MSTestAdapter.PlatformServices | SourceGeneratedReflectionOperations.cs:291 | 'if' statement can be simplified (net9.0) |
🤖 Generated by the Build Failure Analysis workflow · commit 6887de2
Generated by Build Failure Analysis for issue #8586 · ● 1.3M · ◷
Amaury Levé (Evangelink)
left a comment
There was a problem hiding this comment.
Generated by Build Failure Analysis for issue #8586 · ● 1.3M
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
6887de2 to
73050eeCompareConsolidates the experimental MSTest.Engine package into the existing MSTest.SourceGeneration package, addresses PR review comments, and adds correctness fixes for ref/out/in parameter signatures, inaccessible nested test classes, file-local test classes, open generic test classes, overridden method deduplication, and composite-provider null handling. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
73050ee to
a670c54CompareUh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
- Delegate IReflectionOperations.GetType(string) to fallback so simple type names cannot bind to a same-named type from the wrong assembly via the composite TypesByName lookup (matches Type.GetType semantics). - Simplify HasByRefParameter to a single LINQ Any() expression. - Drop unused reassignment of GeneratorDriver in the generator test harness's RunGeneratorAndGetCompilation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Amaury Levé (Evangelink)
commented
Jun 1, 2026
Copilot resolve the merge conflicts in this pull request |
…gen-reflection-issue-1837 # Conflicts: # src/Adapter/MSTest.Engine/Engine/TestFrameworkEngine.cs # src/Adapter/MSTest.Engine/PublicAPI/PublicAPI.Unshipped.txt # src/Adapter/MSTest.Engine/TestNodes/InternalUnsafeActionParameterizedTestNode.cs # src/Adapter/MSTest.Engine/TestNodes/InternalUnsafeActionTaskParameterizedTestNode.cs # src/Adapter/MSTest.Engine/TestNodes/InternalUnsafeAsyncActionParameterizedTestNode.cs # src/Adapter/MSTest.Engine/TestNodes/InternalUnsafeAsyncActionTaskParameterizedTestNode.cs # test/UnitTests/MSTest.Engine.UnitTests/BFSTestNodeVisitorTests.cs Co-authored-by: Evangelink <11340282+Evangelink@users.noreply.github.com>
Merge conflicts resolved in 8578ce3. All conflicts were "deleted by us" — files in |
…flection - ReflectionMetadataEmitter: emit [DynamicDependency(All, typeof(T))] per test class on the [ModuleInitializer], so the trimmer keeps constructors and other reflected members alive (otherwise discovery fails with 'Cannot find a valid constructor for test class'). - ReflectionMetadataEmitter: annotate ResolveMethod's Type parameter with [DynamicallyAccessedMembers(PublicMethods | NonPublicMethods)] to satisfy IL2070 in the generated module initializer. - SourceGeneratedReflectionOperations: stop routing fallback through _fallback.GetCustomAttributesCached. ReflectionOperations.NotCachedReflectionAccessor reads PlatformServiceProvider.Instance.ReflectionOperations, which after SetMetadata is the source-gen wrapper itself -- causing infinite mutual recursion and a StackOverflowException at runtime. Use _fallback.GetCustomAttributes (direct reflection) instead. - MSTest.Sdk NativeAOT.targets: add MSTest.TestAdapter package reference and set EnableMSTestRunner/IsTestingPlatformApplication = true (mirroring ClassicEngine.targets) so MSTestAdapter.PlatformServices.dll (the source-generator runtime hook host) is available to NAOT-published apps. - NativeAotTests / SdkTests / TrimTests: tolerate upstream IL warnings from Microsoft.TestPlatform.ObjectModel and System.Private.DataContractSerialization (warnAsError: false) and assert via shared TrimAndAotAssertions.MSTestOwnedSourceFiles that MSTest-owned source files do not appear in publish output, mirroring the pattern established in PR #8686. Rename Publish_ShouldNotProduceTrimWarnings to Publish_WithSourceGeneration_DoesNotSurfaceMSTestOwnedTrimWarnings. - NativeAotTests: use AssertOutputContainsSummary helper (current MTP output format). - samples/NativeAotRunner/TestProject1: convert to MSTest.Sdk shape and drop the pinned MSTest.SourceGeneration 2.0.0-alpha.26228.3 reference (which emitted now- removed Microsoft.Testing.Framework.TestNode types and broke the WindowsSamples CI legs). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Uh oh!
There was an error while loading. Please reload this page.
A [TestMethod] declared as Test<T>(T value) would have its parameter types collected as ypeof(T), but the generated module initializer is non-generic, so T does not bind there and the compilation fails. Reflection mode handles generic test methods at runtime, so opting into the generator must not turn a valid program into a build error. Add an IsGenericMethod guard alongside the existing open-generic-class and by-ref-parameter guards, and add a unit test covering both the generic-with-params and generic-without-params shapes. Addresses PR review feedback: #8586 (comment) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…gen-reflection-issue-1837
…ver)] The single public Register entry point exists only because the source generator's [ModuleInitializer] needs to call across the assembly boundary into MSTestAdapter.PlatformServices. Hand-written code should never use it. Marking the type and method as EditorBrowsable.Never plus strengthening the XML docs makes that intent obvious to anyone browsing the API. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Returning IEnumerator<T> from a struct enumerator goes through an IEnumerable<T> cast and allocates on every foreach. The source-generator pipeline iterates metadata.Classes, cls.Methods, and method.ParameterTypes on every incremental tick, so this was a real allocation hot spot. Return ImmutableArray<T>.Enumerator (a struct) by value so the foreach binds to the duck-typed pattern with no boxing. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The toggle's UseSourceGenerator getter was never read; Enable() was called once from ReflectionMetadataHook.Register but had no observable effect. The actual provider swap is done in the same code path via PlatformServiceProvider.SetSourceGeneratedOperations. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
…ress review feedback - Add docs/source-generator/design.md covering scope, emitter gaps, the three fallback categories (A/B/C), discovery limitations, trim/AOT story (warnings vs. runtime), recommended pairing with `TrimmerRootAssembly`, perf positioning vs. delegate-based generators, what wiring the AotReflection PoC unlocks beyond perf, and a sunset plan for the current alpha packages. - Emit `[DynamicDependency(All, typeof(BaseType))]` for every accessible non-generic base in a test class's inheritance chain so members declared on an abstract base (`[ClassInitialize]`, `[AssemblyInitialize]`, `TestContext` setter, ...) survive trimming under PublishAot / PublishTrimmed. - Categorize every fallback in SourceGeneratedReflectionOperations (A = generator-gap, closable; B = contract-mismatch, by design; C = cross-assembly, unavoidable) and label each call site so future contributors do not silently fall through. - Address PR review threads: - Simplify GetRuntimeMethod to delegate to the reflection fallback. The previous custom loop duplicated GetRuntimeMethods' scan and risked diverging from Type.GetMethod binder semantics (overload resolution, generic / by-ref handling). - Route GetType(Assembly, string) through a new per-provider TryGetTypeByName virtual so two assemblies with the same fully-qualified type name no longer shadow each other in the composite's merged snapshot. TypesByName is no longer merged centrally; the per-provider lookup is consulted directly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Amaury Levé (Evangelink)
commented
Jun 4, 2026
Pushed c896beb covering the remaining review feedback plus the design documentation we've been discussing. Review threads addressed (both now resolved)
Documentation
Abstract base
Verification
|
- SourceGeneratedReflectionOperations.GetRuntimeMethod: drop redundant manual loop and delegate directly to the fallback provider (which already handles partial source-gen data). - InheritedTestClassAttributeWithSourceGeneratorAnalyzer.HasDirectAttribute: simplify foreach to LINQ Any() per code-quality bot suggestion. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Uh oh!
There was an error while loading. Please reload this page.
markdownlint MD033 forbids inline HTML except for <a> tags. Escape the placeholder text '<link>' so it renders as literal angle brackets. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Uh oh!
There was an error while loading. Please reload this page.
Azat Mukhametshin (azat-msft)
left a comment
There was a problem hiding this comment.
Approved based on Amaury Levé (@Evangelink) 's request. Did not review, only approved to enable further work on this issue.
Uh oh!
There was an error while loading. Please reload this page.
Fixes part of #1837.
What
Introduces a new Roslyn
IIncrementalGenerator(projectMSTest.SourceGeneration, undersrc/Analyzers/) that discovers[TestClass]types at compile time and emits a[ModuleInitializer]registering aSourceGeneratedReflectionDataProviderfor the user's assembly.It also adds the runtime infrastructure in
MSTestAdapter.PlatformServices(folderSourceGeneration/) to swap theIReflectionOperationsandIFileOperationsservices for source-gen-backed implementations once metadata is registered.This is the first step toward Native AOT support: when the generator runs, MSTest reads test metadata from compile-time-known data instead of doing reflection at runtime. The feature is opt-in — when no metadata is registered, the platform keeps using the existing reflection-based implementations.
Why
Issue #1837 tracks Native AOT support for MSTest. PR #8263 introduced the
IReflectionOperationsservice abstraction as a prerequisite. This PR delivers the next building block by adding the source generator that will populate that abstraction without reflection.Layout
src/Analyzers/MSTest.SourceGeneration/—ReflectionMetadataGeneratorand its emitters/models/helpers, packaged as analyzers/dotnet/cs.src/Adapter/MSTestAdapter.PlatformServices/SourceGeneration/—ReflectionMetadataHook,SourceGeneratedReflectionDataProvider,CompositeSourceGeneratedReflectionDataProvider,SourceGeneratedReflectionOperations,SourceGeneratedFileOperations,SourceGeneratorToggle.PlatformServiceProvider: a singleinternalSetSourceGeneratedOperationsmethod used byReflectionMetadataHookto swap providers.src/Adapter/MSTest.Engine/project and its unit tests, which the new design replaces. (This is what shows up in the solution-file diff.)test/UnitTests/MSTest.SourceGeneration.UnitTests/.PublicAPI.Unshipped.txtupdated.Scope (MVP)
[ModuleInitializer]that callsReflectionMetadataHook.SetMetadata.[TestClass]+[TestMethod]), static/abstract classes being skipped, empty-assembly emission, and a Roslyn-compiles-cleanly smoke test.Known follow-ups (out of scope for this PR)
These are intentionally deferred to keep this PR reviewable:
AssemblyAttributes,TypeAttributes,TypeProperties,TypeMethodAttributes,TypeConstructorsInvoker,TypeMethodLocations). The MVP only emits the assembly name, type list, and per-type method list.[ModuleInitializer]polyfill fornetstandard2.0/ older TFM consumers.EnableMSTestSourceGeneration).DataRow/DynamicData/ inherited attributes / base-class roll-up.AssemblyInitialize/ClassInitialize/TestInitializelifecycle.Validation
Built locally with
build.cmd -c Debug(0 warnings, 0 errors).Notes for reviewers
ReflectionMetadataHook.SetMetadataand theSourceGeneratedReflectionDataProvidershape are public, because the generated module initializer needs to call them.SourceGeneratorToggleusesInterlocked.Exchangeto ensure exactly-once swap and to make the toggle race-free if multiple assemblies' module initializers run.ReflectionMetadataGeneratorskipsstaticandabstracttypes — they cannot be discovered as test classes.IReflectionOperations.GetType(string)delegates straight to the fallback so callers cannot bind to a same-named type from the wrong assembly via the compositeTypesByNamelookup; the assembly-qualifiedGetType(Assembly, string)overload is the one that uses the source-generated data.