diff --git a/MSTest.slnf b/MSTest.slnf
index 3c3f4c624a..f8858c6031 100644
--- a/MSTest.slnf
+++ b/MSTest.slnf
@@ -11,6 +11,7 @@
"src\\Analyzers\\MSTest.Analyzers.CodeFixes\\MSTest.Analyzers.CodeFixes.csproj",
"src\\Analyzers\\MSTest.Analyzers.Package\\MSTest.Analyzers.Package.csproj",
"src\\Analyzers\\MSTest.Analyzers\\MSTest.Analyzers.csproj",
+ "src\\Analyzers\\MSTest.AotReflection.SourceGeneration\\MSTest.AotReflection.SourceGeneration.csproj",
"src\\Analyzers\\MSTest.GlobalConfigsGenerator\\MSTest.GlobalConfigsGenerator.csproj",
"src\\Analyzers\\MSTest.SourceGeneration\\MSTest.SourceGeneration.csproj",
"src\\Package\\MSTest.Sdk\\MSTest.Sdk.csproj",
diff --git a/TestFx.slnx b/TestFx.slnx
index 991e9dc8db..4f9fc147da 100644
--- a/TestFx.slnx
+++ b/TestFx.slnx
@@ -69,6 +69,7 @@
+
@@ -133,6 +134,7 @@
+
diff --git a/src/Analyzers/MSTest.AotReflection.SourceGeneration/Generators/TestClassModelBuilder.cs b/src/Analyzers/MSTest.AotReflection.SourceGeneration/Generators/TestClassModelBuilder.cs
index 9188cf64c0..48d0f3209a 100644
--- a/src/Analyzers/MSTest.AotReflection.SourceGeneration/Generators/TestClassModelBuilder.cs
+++ b/src/Analyzers/MSTest.AotReflection.SourceGeneration/Generators/TestClassModelBuilder.cs
@@ -19,8 +19,7 @@ internal static class TestClassModelBuilder
{
private static readonly SymbolDisplayFormat FullyQualifiedFormat =
SymbolDisplayFormat.FullyQualifiedFormat.WithMiscellaneousOptions(
- SymbolDisplayMiscellaneousOptions.IncludeNullableReferenceTypeModifier
- | SymbolDisplayMiscellaneousOptions.UseSpecialTypes);
+ SymbolDisplayMiscellaneousOptions.UseSpecialTypes);
public static TestClassModel Build(INamedTypeSymbol typeSymbol)
{
diff --git a/src/Analyzers/MSTest.AotReflection.SourceGeneration/MSTest.AotReflection.SourceGeneration.csproj b/src/Analyzers/MSTest.AotReflection.SourceGeneration/MSTest.AotReflection.SourceGeneration.csproj
index 1369428997..dc9bbade05 100644
--- a/src/Analyzers/MSTest.AotReflection.SourceGeneration/MSTest.AotReflection.SourceGeneration.csproj
+++ b/src/Analyzers/MSTest.AotReflection.SourceGeneration/MSTest.AotReflection.SourceGeneration.csproj
@@ -29,4 +29,8 @@
+
+
+
+
diff --git a/test/UnitTests/MSTest.AotReflection.SourceGeneration.UnitTests/MSTest.AotReflection.SourceGeneration.UnitTests.csproj b/test/UnitTests/MSTest.AotReflection.SourceGeneration.UnitTests/MSTest.AotReflection.SourceGeneration.UnitTests.csproj
new file mode 100644
index 0000000000..b91def2e3a
--- /dev/null
+++ b/test/UnitTests/MSTest.AotReflection.SourceGeneration.UnitTests/MSTest.AotReflection.SourceGeneration.UnitTests.csproj
@@ -0,0 +1,24 @@
+
+
+
+ net8.0
+ MSTest.AotReflection.SourceGeneration.UnitTests
+ true
+ true
+ Exe
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/UnitTests/MSTest.AotReflection.SourceGeneration.UnitTests/MSTestReflectionMetadataGeneratorTests.cs b/test/UnitTests/MSTest.AotReflection.SourceGeneration.UnitTests/MSTestReflectionMetadataGeneratorTests.cs
new file mode 100644
index 0000000000..9363460a93
--- /dev/null
+++ b/test/UnitTests/MSTest.AotReflection.SourceGeneration.UnitTests/MSTestReflectionMetadataGeneratorTests.cs
@@ -0,0 +1,508 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using AwesomeAssertions;
+
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+
+using MSTest.AotReflection.SourceGeneration.Generators;
+
+namespace MSTest.AotReflection.SourceGeneration.UnitTests;
+
+///
+/// Behavior tests for .
+/// These pin the current PoC output so the upcoming follow-up PRs (#1837) can extend it safely.
+///
+[TestClass]
+public sealed class MSTestReflectionMetadataGeneratorTests
+{
+ ///
+ /// Minimal MSTest attribute stubs so the generator can locate [TestClass] /
+ /// [TestMethod] in test fixtures without dragging the real TestFramework
+ /// assemblies into the Roslyn compilation.
+ ///
+ private const string MinimalMSTestStub = """
+ namespace Microsoft.VisualStudio.TestTools.UnitTesting
+ {
+ [System.AttributeUsage(System.AttributeTargets.Class)]
+ public class TestClassAttribute : System.Attribute { }
+
+ [System.AttributeUsage(System.AttributeTargets.Method)]
+ public class TestMethodAttribute : System.Attribute
+ {
+ public TestMethodAttribute() { }
+ public TestMethodAttribute(string displayName) { DisplayName = displayName; }
+ public string? DisplayName { get; set; }
+ }
+
+ [System.AttributeUsage(System.AttributeTargets.Class | System.AttributeTargets.Method, AllowMultiple = true)]
+ public class TestCategoryAttribute : System.Attribute
+ {
+ public TestCategoryAttribute(string category) { Category = category; }
+ public string Category { get; }
+ }
+
+ [System.AttributeUsage(System.AttributeTargets.Property)]
+ public class TestContextAttribute : System.Attribute { }
+ }
+ """;
+
+ [TestMethod]
+ public void Generator_EmitsSupportTypes_OnAnyCompilation()
+ {
+ const string userCode = """
+ // Intentionally empty — no [TestClass] in the consumer.
+ """;
+
+ GeneratorRunResult result = RunGenerator(MinimalMSTestStub, userCode);
+
+ result.Diagnostics.Should().BeEmpty();
+ // Support types are emitted via RegisterPostInitializationOutput → always present.
+ string support = result.GeneratedSources
+ .Single(s => s.HintName == "MSTestReflectionMetadata.SupportTypes.g.cs")
+ .SourceText.ToString();
+
+ support.Should().Contain("namespace MSTest.SourceGenerated");
+ support.Should().Contain("internal sealed class TestClassReflectionInfo");
+ support.Should().Contain("internal sealed class TestMethodReflectionInfo");
+ support.Should().Contain("internal sealed class TestPropertyReflectionInfo");
+ support.Should().Contain("internal sealed class TestConstructorReflectionInfo");
+ }
+
+ [TestMethod]
+ public void Generator_EmitsRegistry_WithDiscoveredTestClass()
+ {
+ const string userCode = """
+ using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+ namespace Sample
+ {
+ [TestClass]
+ public class MyTests
+ {
+ [TestMethod]
+ public void Test1() { }
+ }
+ }
+ """;
+
+ GeneratorRunResult result = RunGenerator(MinimalMSTestStub, userCode);
+
+ result.Diagnostics.Should().BeEmpty();
+ string registry = GetRegistry(result);
+
+ registry.Should().Contain("internal static class MSTestReflectionMetadata");
+ registry.Should().Contain("public const string AssemblyName = \"TestSample\";");
+ registry.Should().Contain("Type = typeof(global::Sample.MyTests)");
+ registry.Should().Contain("Name = \"Test1\"");
+ registry.Should().Contain("Invoke = static (instance, args) => { ((global::Sample.MyTests)instance!).Test1(); return null; },");
+ }
+
+ [TestMethod]
+ public void Generator_EmitsEmptyRegistry_WhenNoTestClasses()
+ {
+ const string userCode = """
+ namespace Sample
+ {
+ // No [TestClass] anywhere.
+ public class NotATest { public void Foo() { } }
+ }
+ """;
+
+ GeneratorRunResult result = RunGenerator(MinimalMSTestStub, userCode);
+
+ result.Diagnostics.Should().BeEmpty();
+ string registry = GetRegistry(result);
+ registry.Should().Contain("public static IReadOnlyList TestClasses { get; } = new TestClassReflectionInfo[]");
+ // No concrete TestClassReflectionInfo instance is emitted (note the open paren).
+ registry.Should().NotContain("new TestClassReflectionInfo(");
+ }
+
+ [TestMethod]
+ public void Generator_SkipsStaticTestClass()
+ {
+ const string userCode = """
+ using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+ namespace Sample
+ {
+ [TestClass]
+ public static class StaticTests
+ {
+ [TestMethod]
+ public static void Test1() { }
+ }
+ }
+ """;
+
+ GeneratorRunResult result = RunGenerator(MinimalMSTestStub, userCode);
+
+ result.Diagnostics.Should().BeEmpty();
+ string registry = GetRegistry(result);
+ // Static classes are excluded by the predicate in the generator (cannot be instantiated).
+ registry.Should().NotContain("StaticTests");
+ }
+
+ [TestMethod]
+ public void Generator_SkipsAbstractTestClass()
+ {
+ const string userCode = """
+ using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+ namespace Sample
+ {
+ [TestClass]
+ public abstract class AbstractTests
+ {
+ [TestMethod]
+ public void Test1() { }
+ }
+
+ [TestClass]
+ public class ConcreteTests
+ {
+ [TestMethod]
+ public void Test2() { }
+ }
+ }
+ """;
+
+ GeneratorRunResult result = RunGenerator(MinimalMSTestStub, userCode);
+
+ result.Diagnostics.Should().BeEmpty();
+ string registry = GetRegistry(result);
+ // Abstract classes are filtered in BuildModel — they cannot be instantiated.
+ registry.Should().NotContain("AbstractTests");
+ registry.Should().Contain("typeof(global::Sample.ConcreteTests)");
+ }
+
+ [TestMethod]
+ public void Generator_SkipsGenericTestClass()
+ {
+ const string userCode = """
+ using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+ namespace Sample
+ {
+ [TestClass]
+ public class GenericTests
+ {
+ [TestMethod]
+ public void Test1() { }
+ }
+ }
+ """;
+
+ GeneratorRunResult result = RunGenerator(MinimalMSTestStub, userCode);
+
+ result.Diagnostics.Should().BeEmpty();
+ string registry = GetRegistry(result);
+ // Open-generic test classes are out of scope for this PoC.
+ registry.Should().NotContain("GenericTests");
+ }
+
+ [TestMethod]
+ public void Generator_EmitsConstructorInvoker()
+ {
+ const string userCode = """
+ using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+ namespace Sample
+ {
+ [TestClass]
+ public class CtorTests
+ {
+ public CtorTests() { }
+
+ [TestMethod]
+ public void Test1() { }
+ }
+ }
+ """;
+
+ GeneratorRunResult result = RunGenerator(MinimalMSTestStub, userCode);
+
+ result.Diagnostics.Should().BeEmpty();
+ string registry = GetRegistry(result);
+ registry.Should().Contain("Constructors = new TestConstructorReflectionInfo[]");
+ registry.Should().Contain("Invoke = static args => new global::Sample.CtorTests(),");
+ }
+
+ [TestMethod]
+ public void Generator_EmitsParameterTypes_ForMethodWithParameters()
+ {
+ const string userCode = """
+ using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+ namespace Sample
+ {
+ [TestClass]
+ public class ParamTests
+ {
+ [TestMethod]
+ public void Test1(int x, string y) { }
+ }
+ }
+ """;
+
+ GeneratorRunResult result = RunGenerator(MinimalMSTestStub, userCode);
+
+ result.Diagnostics.Should().BeEmpty();
+ string registry = GetRegistry(result);
+ registry.Should().Contain("ParameterTypes = new Type[] { typeof(int), typeof(string) }");
+ registry.Should().Contain("ParameterNames = new string[] { \"x\", \"y\" }");
+ }
+
+ [TestMethod]
+ public void Generator_FlagsAsyncReturnTypes()
+ {
+ const string userCode = """
+ using System.Threading.Tasks;
+ using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+ namespace Sample
+ {
+ [TestClass]
+ public class AsyncTests
+ {
+ [TestMethod]
+ public Task Test1() => Task.CompletedTask;
+
+ [TestMethod]
+ public ValueTask Test2() => default;
+ }
+ }
+ """;
+
+ GeneratorRunResult result = RunGenerator(MinimalMSTestStub, userCode);
+
+ result.Diagnostics.Should().BeEmpty();
+ string registry = GetRegistry(result);
+ registry.Should().Contain("Name = \"Test1\"");
+ registry.Should().Contain("ReturnsTask = true");
+ registry.Should().Contain("Name = \"Test2\"");
+ registry.Should().Contain("ReturnsValueTask = true");
+ }
+
+ [TestMethod]
+ public void Generator_CapturesClassLevelAttributes()
+ {
+ const string userCode = """
+ using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+ namespace Sample
+ {
+ [TestClass]
+ [TestCategory("Smoke")]
+ public class TaggedTests
+ {
+ [TestMethod]
+ public void Test1() { }
+ }
+ }
+ """;
+
+ GeneratorRunResult result = RunGenerator(MinimalMSTestStub, userCode);
+
+ result.Diagnostics.Should().BeEmpty();
+ string registry = GetRegistry(result);
+ registry.Should().Contain("global::Microsoft.VisualStudio.TestTools.UnitTesting.TestClassAttribute");
+ registry.Should().Contain("global::Microsoft.VisualStudio.TestTools.UnitTesting.TestCategoryAttribute");
+ registry.Should().Contain("\"Smoke\"");
+ }
+
+ [TestMethod]
+ public void Generator_EmitsPropertyGetterAndSetter()
+ {
+ const string userCode = """
+ using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+ namespace Sample
+ {
+ public class TestContext { }
+
+ [TestClass]
+ public class PropTests
+ {
+ [TestContext]
+ public TestContext? Context { get; set; }
+
+ [TestMethod]
+ public void Test1() { }
+ }
+ }
+ """;
+
+ GeneratorRunResult result = RunGenerator(MinimalMSTestStub, userCode);
+
+ result.Diagnostics.Should().BeEmpty();
+ string registry = GetRegistry(result);
+ registry.Should().Contain("Name = \"Context\"");
+ registry.Should().Contain("HasPublicSetter = true");
+ registry.Should().Contain("Get = static instance => instance is null ? null : (object?)((global::Sample.PropTests)instance).Context,");
+ registry.Should().Contain("Set = static (instance, value) => ((global::Sample.PropTests)instance!).Context = (global::Sample.TestContext)value!,");
+ }
+
+ [TestMethod]
+ public void Generator_EmittedSource_CompilesCleanly()
+ {
+ const string userCode = """
+ using System.Threading.Tasks;
+ using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+ namespace Sample
+ {
+ public class TestContext { }
+
+ [TestClass]
+ [TestCategory("Smoke")]
+ public class FullShape
+ {
+ [TestContext]
+ public TestContext? Context { get; set; }
+
+ public FullShape() { }
+
+ [TestMethod("alias")]
+ public void Sync(int x) { }
+
+ [TestMethod]
+ public Task Asynchronous() => Task.CompletedTask;
+ }
+ }
+ """;
+
+ Compilation outputCompilation = RunGeneratorAndGetCompilation(MinimalMSTestStub, userCode);
+
+ IEnumerable diagnostics = outputCompilation
+ .GetDiagnostics()
+ .Where(d => d.Severity == DiagnosticSeverity.Error);
+
+ diagnostics.Should().BeEmpty(
+ "the generated source MUST compile cleanly when consumed in the same compilation as the user code");
+ }
+
+ [TestMethod]
+ public void Generator_StripsNullableAnnotation_FromTypeofExpressions()
+ {
+ const string userCode = """
+ using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+ namespace Sample
+ {
+ public class TestContext { }
+
+ [TestClass]
+ public class NullableShapes
+ {
+ [TestContext]
+ public TestContext? Context { get; set; }
+
+ [TestMethod]
+ public void TakesNullableRef(string? value) { }
+
+ [TestMethod]
+ public void TakesNullableValueType(int? n) { }
+ }
+ }
+ """;
+
+ Compilation outputCompilation = RunGeneratorAndGetCompilation(MinimalMSTestStub, userCode);
+ string registry = outputCompilation
+ .SyntaxTrees
+ .Single(t => t.FilePath.EndsWith("MSTestReflectionMetadata.Registry.g.cs", System.StringComparison.Ordinal))
+ .ToString();
+
+ // typeof(...) MUST NOT carry nullable reference type annotation (CS8639).
+ registry.Should().NotContain("typeof(global::Sample.TestContext?)");
+ registry.Should().NotContain("typeof(string?)");
+ // Reference types in typeof drop the annotation entirely.
+ registry.Should().Contain("typeof(global::Sample.TestContext)");
+ registry.Should().Contain("typeof(string)");
+ // Nullable value types are still distinct from their underlying type and must be preserved as Nullable.
+ registry.Should().Contain("typeof(int?)");
+
+ // The whole compilation must be free of CS errors.
+ IEnumerable errors = outputCompilation
+ .GetDiagnostics()
+ .Where(d => d.Severity == DiagnosticSeverity.Error);
+ errors.Should().BeEmpty("typeof(T?) on a reference type is invalid C# (CS8639)");
+ }
+
+ [TestMethod]
+ public void Generator_IsIncremental_SupportTypesAreCached_WhenInputUnchanged()
+ {
+ const string userCode = """
+ using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+ namespace Sample
+ {
+ [TestClass]
+ public class IncTests
+ {
+ [TestMethod]
+ public void Test1() { }
+ }
+ }
+ """;
+
+ CSharpCompilation compilation = CreateCompilation(MinimalMSTestStub, userCode);
+ GeneratorDriver driver = CSharpGeneratorDriver
+ .Create(new MSTestReflectionMetadataGenerator())
+ .WithUpdatedParseOptions((CSharpParseOptions)compilation.SyntaxTrees.First().Options);
+
+ // Track step output cache reasons.
+ driver = driver.RunGenerators(compilation);
+ driver = driver.RunGenerators(compilation);
+
+ GeneratorDriverRunResult result = driver.GetRunResult();
+ result.Diagnostics.Should().BeEmpty();
+ result.Results.Should().ContainSingle();
+ // Two passes against the same compilation must produce identical sources.
+ result.Results[0].GeneratedSources.Should().HaveCount(2);
+ }
+
+ private static string GetRegistry(GeneratorRunResult result)
+ => result.GeneratedSources
+ .Single(s => s.HintName == "MSTestReflectionMetadata.Registry.g.cs")
+ .SourceText.ToString()
+ .Replace("\r\n", "\n");
+
+ private static GeneratorRunResult RunGenerator(params string[] sources)
+ {
+ CSharpCompilation compilation = CreateCompilation(sources);
+ GeneratorDriver driver = CSharpGeneratorDriver.Create(new MSTestReflectionMetadataGenerator());
+ driver = driver.RunGeneratorsAndUpdateCompilation(compilation, out _, out _);
+ return driver.GetRunResult().Results[0];
+ }
+
+ private static Compilation RunGeneratorAndGetCompilation(params string[] sources)
+ {
+ CSharpCompilation compilation = CreateCompilation(sources);
+ GeneratorDriver driver = CSharpGeneratorDriver.Create(new MSTestReflectionMetadataGenerator());
+ driver.RunGeneratorsAndUpdateCompilation(compilation, out Compilation outputCompilation, out _);
+ return outputCompilation;
+ }
+
+ private static CSharpCompilation CreateCompilation(params string[] sources)
+ {
+ IEnumerable trees = sources.Select(s => CSharpSyntaxTree.ParseText(s));
+ MetadataReference[] references = new[]
+ {
+ MetadataReference.CreateFromFile(typeof(object).Assembly.Location),
+ MetadataReference.CreateFromFile(typeof(System.Runtime.CompilerServices.ModuleInitializerAttribute).Assembly.Location),
+ MetadataReference.CreateFromFile(typeof(System.Reflection.Assembly).Assembly.Location),
+ MetadataReference.CreateFromFile(typeof(System.Collections.Generic.Dictionary<,>).Assembly.Location),
+ MetadataReference.CreateFromFile(typeof(System.Reflection.MethodInfo).Assembly.Location),
+ MetadataReference.CreateFromFile(typeof(System.Linq.Enumerable).Assembly.Location),
+ MetadataReference.CreateFromFile(typeof(System.Threading.Tasks.Task).Assembly.Location),
+ };
+
+ return CSharpCompilation.Create(
+ "TestSample",
+ trees,
+ references,
+ new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
+ }
+}
diff --git a/test/UnitTests/MSTest.AotReflection.SourceGeneration.UnitTests/Program.cs b/test/UnitTests/MSTest.AotReflection.SourceGeneration.UnitTests/Program.cs
new file mode 100644
index 0000000000..9fbdc73473
--- /dev/null
+++ b/test/UnitTests/MSTest.AotReflection.SourceGeneration.UnitTests/Program.cs
@@ -0,0 +1,19 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Microsoft.Testing.Extensions;
+
+ITestApplicationBuilder builder = await TestApplication.CreateBuilderAsync(args);
+builder.AddMSTest(() => [Assembly.GetEntryAssembly()!]);
+
+#if ENABLE_CODECOVERAGE
+builder.AddCodeCoverageProvider();
+#endif
+builder.AddHangDumpProvider();
+builder.AddCrashDumpProvider(ignoreIfNotSupported: true);
+builder.AddTrxReportProvider();
+builder.AddAppInsightsTelemetryProvider();
+builder.AddAzureDevOpsProvider();
+
+using ITestApplication app = await builder.BuildAsync();
+return await app.RunAsync();