// 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 Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.SourceGeneration.Generators;
namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.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
{
[TestMethod]
public void CollectInheritedPropertyAttributes_PreservesOrderMultipleAndNearestSingleApplications()
{
CSharpCompilation compilation = CreateCompilation(
"""
using System;
[AttributeUsage(AttributeTargets.All, AllowMultiple = true)]
public sealed class MultiAttribute(string value) : Attribute { }
[AttributeUsage(AttributeTargets.All)]
public sealed class SingleAttribute(string value) : Attribute { }
[AttributeUsage(AttributeTargets.All, Inherited = false)]
public class LocalAttribute : Attribute { }
public sealed class DerivedLocalAttribute : LocalAttribute { }
public class Grand
{
[Multi("grand"), Single("grand"), DerivedLocal]
public virtual int Value { get; set; }
}
public class Middle : Grand
{
[Multi("middle"), Single("middle")]
public override int Value { get; set; }
}
public sealed class Leaf : Middle
{
[Multi("leaf")]
public override int Value { get; set; }
}
""");
compilation.GetDiagnostics().Where(diagnostic => diagnostic.Severity == DiagnosticSeverity.Error).Should().BeEmpty();
IPropertySymbol property = compilation.GetTypeByMetadataName("Leaf")!.GetMembers("Value").OfType().Single();
System.Collections.Immutable.ImmutableArray attributes = AttributeMaterializationHelper.CollectInheritedAttributes(property);
attributes.Select(attribute => (attribute.AttributeClass!.Name, (string)attribute.ConstructorArguments[0].Value!))
.Should().Equal(("MultiAttribute", "leaf"), ("MultiAttribute", "middle"), ("SingleAttribute", "middle"), ("MultiAttribute", "grand"));
}
[TestMethod]
public void CollectInheritedPropertyAttributes_NoOverrideKeepsOwnNonInheritedAttributeAndEmptyIsEmpty()
{
CSharpCompilation compilation = CreateCompilation(
"""
using System;
[AttributeUsage(AttributeTargets.All, Inherited = false)]
public sealed class LocalAttribute : Attribute { }
public sealed class Sample
{
[Local]
public int Own { get; set; }
public int Empty { get; set; }
}
""");
compilation.GetDiagnostics().Where(diagnostic => diagnostic.Severity == DiagnosticSeverity.Error).Should().BeEmpty();
INamedTypeSymbol type = compilation.GetTypeByMetadataName("Sample")!;
IPropertySymbol own = type.GetMembers("Own").OfType().Single();
IPropertySymbol empty = type.GetMembers("Empty").OfType().Single();
AttributeMaterializationHelper.CollectInheritedAttributes(own).Select(attribute => attribute.AttributeClass!.Name)
.Should().Equal("LocalAttribute");
AttributeMaterializationHelper.CollectInheritedAttributes(empty).Should().BeEmpty();
}
///
/// 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, Inherited = false)]
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, Inherited = 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 { }
[System.AttributeUsage(System.AttributeTargets.Method)]
public class TestInitializeAttribute : System.Attribute { }
[System.AttributeUsage(System.AttributeTargets.Method)]
public class TestCleanupAttribute : System.Attribute { }
[System.AttributeUsage(System.AttributeTargets.Assembly, AllowMultiple = true)]
public class ParallelizeAttribute : System.Attribute
{
public int Workers { get; set; }
public string? Scope { get; set; }
}
[System.AttributeUsage(System.AttributeTargets.Method, AllowMultiple = true, Inherited = false)]
public class DataRowAttribute : System.Attribute
{
public DataRowAttribute() : this(System.Array.Empty()) { }
public DataRowAttribute(object? data) { Data = new object?[] { data }; }
public DataRowAttribute(string?[]? stringArrayData) : this(new object?[] { stringArrayData }) { }
public DataRowAttribute(params object?[]? data) { Data = data ?? new object?[] { null }; }
public object?[] Data { get; }
}
public enum DynamicDataSourceType { Property = 0, Method = 1, AutoDetect = 2, Field = 3 }
[System.AttributeUsage(System.AttributeTargets.Method, AllowMultiple = true, Inherited = false)]
public class DynamicDataAttribute : System.Attribute
{
public DynamicDataAttribute(string sourceName) { }
public DynamicDataAttribute(string sourceName, DynamicDataSourceType sourceType) { }
public DynamicDataAttribute(string sourceName, System.Type declaringType) { }
public DynamicDataAttribute(string sourceName, System.Type declaringType, DynamicDataSourceType sourceType) { }
public DynamicDataAttribute(string sourceName, params object?[] arguments) { }
public DynamicDataAttribute(string sourceName, System.Type declaringType, params object?[] arguments) { }
public string? DynamicDataDisplayName { get; set; }
public System.Type? DynamicDataDisplayNameDeclaringType { get; set; }
}
public static class DynamicDataSourceResolver
{
public static void RegisterDataProvider(System.Type declaringType, string sourceName, DynamicDataSourceType sourceType, System.Func dataProvider) { }
public static void RegisterDisplayNameProvider(System.Type declaringType, string methodName, System.Func displayNameProvider) { }
}
}
""";
///
/// Minimal stub of the adapter's runtime hook so the emitted [ModuleInitializer] wiring
/// (shared from MSTest.SourceGeneration) compiles in the Roslyn test compilation without
/// referencing MSTestAdapter.PlatformServices.
///
private const string RuntimeHookStub = """
namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.SourceGeneration
{
public readonly struct ConstructorInvokerInfo
{
public ConstructorInvokerInfo(System.Type[] parameterTypes, System.Func invoker) { ParameterTypes = parameterTypes; Invoker = invoker; }
public System.Type[] ParameterTypes { get; }
public System.Func Invoker { get; }
}
public static class ReflectionMetadataHook
{
public static System.Collections.Generic.IReadOnlyDictionary? RegisteredTestMethods { get; private set; }
public static void Register(System.Reflection.Assembly assembly, System.Type[] types, System.Collections.Generic.IReadOnlyDictionary testMethods) { }
public static void Register(System.Reflection.Assembly assembly, System.Type[] types, System.Collections.Generic.IReadOnlyDictionary testMethods, System.Collections.Generic.IReadOnlyDictionary typeAttributes, object[] assemblyAttributes) { }
public static void Register(System.Reflection.Assembly assembly, System.Type[] types, System.Collections.Generic.IReadOnlyDictionary testMethods, System.Collections.Generic.IReadOnlyDictionary typeAttributes, object[] assemblyAttributes, System.Collections.Generic.IReadOnlyDictionary> methodInvokers, System.Collections.Generic.IReadOnlyDictionary constructorInvokers, System.Collections.Generic.IReadOnlyDictionary> propertySetters) { }
public static void Register(System.Reflection.Assembly assembly, System.Type[] types, System.Collections.Generic.IReadOnlyDictionary testMethods, System.Collections.Generic.IReadOnlyDictionary typeAttributes, object[] assemblyAttributes, System.Collections.Generic.IReadOnlyDictionary methodAttributes, System.Collections.Generic.IReadOnlyDictionary> methodInvokers, System.Collections.Generic.IReadOnlyDictionary constructorInvokers, System.Collections.Generic.IReadOnlyDictionary> propertySetters) { }
public static void Register(System.Reflection.Assembly assembly, System.Type[] types, System.Collections.Generic.IReadOnlyDictionary testMethods, System.Collections.Generic.IReadOnlyDictionary typeAttributes, object[] assemblyAttributes, System.Collections.Generic.IReadOnlyDictionary methodAttributes, System.Collections.Generic.IReadOnlyDictionary> methodInvokers, System.Collections.Generic.IReadOnlyDictionary constructorInvokers, System.Collections.Generic.IReadOnlyDictionary> propertySetters, System.Collections.Generic.IReadOnlyDictionary descriptorTestMethods, System.Type[] descriptorCompleteTypes) { RegisteredTestMethods = testMethods; }
}
}
""";
[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 Task.CompletedTask; },");
registry.Should().Contain("SupportsGeneratedDescriptors = true");
registry.Should().Contain("AreGeneratedDescriptorsComplete = true");
registry.Should().Contain("IsDescriptorSupported = true");
}
[TestMethod]
public void Generator_DeclaresDescriptorSupportOnlyForBoundedSynchronousSubset()
{
const string userCode = """
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Sample
{
[TestClass]
public class MyTests
{
[TestMethod]
[DataRow(1)]
public void Supported(int value) { }
[TestMethod]
[TestCategory("fallback")]
public void AttributeFallback() { }
[TestMethod]
public async System.Threading.Tasks.Task AsyncFallback()
{
await System.Threading.Tasks.Task.Yield();
}
}
}
""";
GeneratorRunResult result = RunGenerator(MinimalMSTestStub, userCode);
result.Diagnostics.Should().BeEmpty();
string registry = GetRegistry(result);
string registration = result.GeneratedSources
.Single(source => source.HintName == "MSTestReflectionMetadata.Registration.g.cs")
.SourceText.ToString();
registry.Should().Contain("AreGeneratedDescriptorsComplete = false");
int supportedIndex = registry.IndexOf("Name = \"Supported\"", System.StringComparison.Ordinal);
int attributeFallbackIndex = registry.IndexOf("Name = \"AttributeFallback\"", System.StringComparison.Ordinal);
int asyncFallbackIndex = registry.IndexOf("Name = \"AsyncFallback\"", System.StringComparison.Ordinal);
supportedIndex.Should().BeGreaterThan(-1);
attributeFallbackIndex.Should().BeGreaterThan(supportedIndex);
asyncFallbackIndex.Should().BeGreaterThan(attributeFallbackIndex);
registry.Substring(supportedIndex, attributeFallbackIndex - supportedIndex)
.Should().Contain("IsDescriptorSupported = true");
registry.Substring(attributeFallbackIndex, asyncFallbackIndex - attributeFallbackIndex)
.Should().Contain("IsDescriptorSupported = false");
registry[asyncFallbackIndex..].Should().Contain("IsDescriptorSupported = false");
registration.Should().Contain("descriptorTestMethods[type] = descriptorMethodRoots.ToArray();");
registration.Should().Contain("areDescriptorMethodsResolved = false;");
registration.Should().Contain("testClass.AreGeneratedDescriptorsComplete && areDescriptorMethodsResolved");
registration.Should().Contain("descriptorCompleteTypes.Add(type);");
registration.Should().Contain("descriptorTestMethods, descriptorCompleteTypes.ToArray()");
}
[TestMethod]
public void Generator_InaccessibleTestMethodRetainsLegacyDiscoveryFallback()
{
const string userCode = """
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Sample
{
[TestClass]
public class MyTests
{
[TestMethod]
public void Supported() { }
[TestMethod]
private void InaccessibleFallback() { }
}
}
""";
GeneratorRunResult result = RunGenerator(MinimalMSTestStub, userCode);
result.Diagnostics.Should().BeEmpty();
string registry = GetRegistry(result);
registry.Should().Contain("Name = \"Supported\"");
registry.Should().Contain("IsDescriptorSupported = true");
registry.Should().Contain("AreGeneratedDescriptorsComplete = false");
registry.Should().NotContain("Name = \"InaccessibleFallback\"");
}
[TestMethod]
public void Generator_UnsupportedExecutionShapesRetainLegacyDiscoveryFallback()
{
const string userCode = """
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Sample
{
[TestClass]
public class MyTests
{
[TestMethod]
public static void StaticTest() { }
[TestMethod]
public System.Threading.Tasks.Task SynchronousTaskTest()
=> System.Threading.Tasks.Task.CompletedTask;
[TestMethod]
public System.Threading.Tasks.ValueTask SynchronousValueTaskTest()
=> default;
[TestMethod]
public async void AsyncVoidTest()
{
await System.Threading.Tasks.Task.Yield();
}
}
}
""";
GeneratorRunResult result = RunGenerator(MinimalMSTestStub, userCode);
result.Diagnostics.Should().BeEmpty();
string registry = GetRegistry(result);
registry.Should().Contain("AreGeneratedDescriptorsComplete = false");
foreach (string methodName in new[] { "StaticTest", "SynchronousTaskTest", "SynchronousValueTaskTest", "AsyncVoidTest" })
{
int methodIndex = registry.IndexOf($"Name = \"{methodName}\"", System.StringComparison.Ordinal);
methodIndex.Should().BeGreaterThan(-1);
int nextMethodIndex = registry.IndexOf("Name = \"", methodIndex + 1, System.StringComparison.Ordinal);
string methodEntry = nextMethodIndex < 0
? registry[methodIndex..]
: registry.Substring(methodIndex, nextMethodIndex - methodIndex);
methodEntry.Should().Contain("IsDescriptorSupported = false");
}
}
[TestMethod]
public void Generator_PartialTestClassRetainsLegacyDiscoveryFallback()
{
const string userCode = """
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Sample
{
[TestClass]
public partial class PartialTests
{
[TestMethod]
public void Test1() { }
}
public partial class PartialTests
{
[TestMethod]
public void Test2() { }
}
}
""";
GeneratorRunResult result = RunGenerator(MinimalMSTestStub, userCode);
result.Diagnostics.Should().BeEmpty();
string registry = GetRegistry(result);
int test1Index = registry.IndexOf("Name = \"Test1\"", System.StringComparison.Ordinal);
int test2Index = registry.IndexOf("Name = \"Test2\"", System.StringComparison.Ordinal);
int propertiesIndex = registry.IndexOf("Properties = ", test2Index, System.StringComparison.Ordinal);
test1Index.Should().BeGreaterThan(-1);
test2Index.Should().BeGreaterThan(test1Index);
propertiesIndex.Should().BeGreaterThan(test2Index);
registry.Should().Contain("Type = typeof(global::Sample.PartialTests)");
registry.Substring(test1Index, test2Index - test1Index)
.Should().Contain("IsDescriptorSupported = true");
registry.Substring(test2Index, propertiesIndex - test2Index)
.Should().Contain("IsDescriptorSupported = true");
registry.Should().Contain("AreGeneratedDescriptorsComplete = false");
}
[TestMethod]
public void Generator_InheritedCustomTestMethodOverrideRetainsLegacyDiscoveryFallback()
{
const string userCode = """
using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Sample
{
[AttributeUsage(AttributeTargets.Method, Inherited = true)]
public sealed class InheritedTestMethodAttribute : TestMethodAttribute
{
}
public class BaseTests
{
[InheritedTestMethod]
public virtual void Run() { }
}
[TestClass]
public class DerivedTests : BaseTests
{
public override void Run() { }
[TestMethod]
public void Supported() { }
}
}
""";
GeneratorRunResult result = RunGenerator(MinimalMSTestStub, userCode);
result.Diagnostics.Should().BeEmpty();
string registry = GetRegistry(result);
int runIndex = registry.IndexOf("Name = \"Run\"", System.StringComparison.Ordinal);
int supportedIndex = registry.IndexOf("Name = \"Supported\"", runIndex, System.StringComparison.Ordinal);
runIndex.Should().BeGreaterThan(-1);
supportedIndex.Should().BeGreaterThan(runIndex);
string runEntry = registry.Substring(runIndex, supportedIndex - runIndex);
runEntry.Should().Contain("IsTestMethod = true");
runEntry.Should().Contain("IsDescriptorSupported = false");
registry.Should().Contain("AreGeneratedDescriptorsComplete = false");
}
[TestMethod]
public void Generator_TestMethodAccessorRetainsLegacyDiscoveryFallback()
{
const string userCode = """
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Sample
{
[TestClass]
public class AccessorTests
{
public int Value
{
get => 0;
[TestMethod]
[DataRow(1)]
set { }
}
[TestMethod]
public void Supported() { }
}
}
""";
GeneratorRunResult result = RunGenerator(MinimalMSTestStub, userCode);
result.Diagnostics.Should().BeEmpty();
string registry = GetRegistry(result);
registry.Should().Contain("Name = \"Supported\"");
registry.Should().Contain("IsDescriptorSupported = true");
registry.Should().Contain("AreGeneratedDescriptorsComplete = false");
registry.Should().NotContain("Name = \"set_Value\"");
}
[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().ContainSingle(d => d.Id == "AOTSG0001");
string registry = GetRegistry(result);
// Static classes are excluded from the registry (cannot be instantiated) and reported via AOTSG0001.
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().ContainSingle(d => d.Id == "AOTSG0002");
string registry = GetRegistry(result);
// Open-generic test classes are out of scope for this PoC and reported via AOTSG0002.
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_RegistersOnlySupportedConstructorShapes()
{
// MSTest only instantiates a test class via a parameterless or single-TestContext ctor. An
// additional ctor(object) (or any other shape) must NOT be registered: it is never selected by
// the adapter, and registering it could let the runtime's argument-type matching pick it over
// the intended ctor.
const string userCode = """
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Microsoft.VisualStudio.TestTools.UnitTesting
{
public class TestContext { }
}
namespace Sample
{
[TestClass]
public class CtorTests
{
public CtorTests() { }
public CtorTests(TestContext context) { }
public CtorTests(object anything) { }
[TestMethod]
public void Test1() { }
}
}
""";
GeneratorRunResult result = RunGenerator(MinimalMSTestStub, userCode);
result.Diagnostics.Should().BeEmpty();
string registry = GetRegistry(result);
// Parameterless and single-TestContext ctors are registered.
registry.Should().Contain("Invoke = static args => new global::Sample.CtorTests(),");
registry.Should().Contain("Invoke = static args => new global::Sample.CtorTests((global::Microsoft.VisualStudio.TestTools.UnitTesting.TestContext)args![0]!),");
// The unsupported ctor(object) is not registered.
registry.Should().NotContain("new global::Sample.CtorTests((object)args![0]!)");
}
[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().NotContain("ParameterNames");
}
[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_OmitsInaccessibleAttribute_AndStillCompiles()
{
// [Mark] resolves to Outer.MarkAttribute, which is private to Outer. It is validly applied to
// Inner (nested in Outer, so it can see Outer's private members), but the generated registry —
// a top-level type in a different namespace — cannot reference it. Emitting
// `new global::Sample.Outer.MarkAttribute()` would fail with CS0122, so the safener must omit
// it while still materializing the accessible [TestCategory] on the same class.
const string userCode = """
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Sample
{
public class Outer
{
private sealed class MarkAttribute : System.Attribute { }
[TestClass]
[Mark]
[TestCategory("Smoke")]
public class Inner
{
[TestMethod]
public void Test1() { }
}
}
}
""";
Compilation outputCompilation = RunGeneratorAndGetCompilation(MinimalMSTestStub, userCode);
// The emitted registry must compile — no CS0122 from referencing the private attribute.
outputCompilation.GetDiagnostics()
.Where(d => d.Severity == DiagnosticSeverity.Error)
.Should().BeEmpty("the safener must omit attributes the generated code cannot reference");
GeneratorRunResult result = RunGenerator(MinimalMSTestStub, userCode);
string registry = GetRegistry(result);
// Accessible attribute is still materialized; the inaccessible one is omitted.
registry.Should().Contain("global::Microsoft.VisualStudio.TestTools.UnitTesting.TestCategoryAttribute");
registry.Should().NotContain("MarkAttribute");
}
[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_NonPublicSetter_IsNotPubliclySettable_AndNotRegistered()
{
const string userCode = """
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Sample
{
public class TestContext { }
[TestClass]
public class PropTests
{
[TestContext]
public TestContext? Context { get; private set; }
[TestMethod]
public void Test1() { }
}
}
""";
GeneratorRunResult result = RunGenerator(MinimalMSTestStub, userCode);
result.Diagnostics.Should().BeEmpty();
string registry = GetRegistry(result);
// A non-public setter is not callable from the generated code, so the registry emits a
// throwing setter (never a direct assignment) and reports HasPublicSetter = false.
registry.Should().Contain("HasPublicSetter = false");
registry.Should().Contain("Property 'Context' has no public setter.");
registry.Should().NotContain(".Context = (global::Sample.TestContext)value!");
// The module initializer gates setter registration on HasPublicSetter, so this property's
// (throwing) setter is never published; the adapter falls back to PropertyInfo.SetValue.
string registration = result.GeneratedSources
.Single(s => s.HintName == "MSTestReflectionMetadata.Registration.g.cs")
.SourceText.ToString();
registration.Should().Contain("if (property.HasPublicSetter)");
}
[TestMethod]
public void Generator_InitOnlySetter_IsNotPubliclySettable_AndDoesNotEmitAssignment()
{
// An init-only setter has public accessibility but cannot be assigned outside an object
// initializer; emitting `instance.Prop = value` would not compile (CS8852). The generator
// must treat it as non-settable.
const string userCode = """
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Sample
{
public class TestContext { }
[TestClass]
public class PropTests
{
[TestContext]
public TestContext? Context { get; init; }
[TestMethod]
public void Test1() { }
}
}
""";
GeneratorRunResult result = RunGenerator(MinimalMSTestStub, userCode);
result.Diagnostics.Should().BeEmpty();
string registry = GetRegistry(result);
registry.Should().Contain("HasPublicSetter = false");
registry.Should().Contain("Property 'Context' has no public setter.");
registry.Should().NotContain(".Context = (global::Sample.TestContext)value!");
}
[TestMethod]
public void Generator_EmitsStaticPropertyAccess_WithoutInstanceCast()
{
const string userCode = """
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Sample
{
[TestClass]
public class StaticProps
{
public static int Value { get; set; }
[TestMethod]
public void Test1() { }
}
}
""";
GeneratorRunResult result = RunGenerator(MinimalMSTestStub, userCode);
result.Diagnostics.Should().BeEmpty();
string registry = GetRegistry(result);
registry.Should().Contain("Name = \"Value\"");
// A static member must be accessed through the type, never through an instance cast,
// otherwise the generated code fails to compile with CS0176.
registry.Should().Contain("Get = static instance => (object?)global::Sample.StaticProps.Value,");
registry.Should().Contain("Set = static (instance, value) => global::Sample.StaticProps.Value = (int)value!,");
registry.Should().NotContain("((global::Sample.StaticProps)instance).Value");
}
[TestMethod]
public void Generator_SkipsIndexerProperty()
{
const string userCode = """
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Sample
{
[TestClass]
public class WithIndexer
{
public int this[int index] => index;
[TestMethod]
public void Test1() { }
}
}
""";
Compilation outputCompilation = RunGeneratorAndGetCompilation(MinimalMSTestStub, userCode);
string registry = outputCompilation
.SyntaxTrees
.Single(t => t.FilePath.EndsWith("MSTestReflectionMetadata.Registry.g.cs", System.StringComparison.Ordinal))
.ToString();
// Indexers cannot be represented by the name-based Get/Set delegate shape, so they are
// dropped entirely instead of emitting non-compiling `((T)instance).this[]` code.
registry.Should().NotContain("this[");
registry.Should().NotContain("Name = \"Item\"");
IEnumerable errors = outputCompilation
.GetDiagnostics()
.Where(d => d.Severity == DiagnosticSeverity.Error);
errors.Should().BeEmpty("indexers must be skipped so the emitted registry still compiles");
}
[TestMethod]
public void Generator_EmitsThrowingGetter_ForSetOnlyProperty()
{
const string userCode = """
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Sample
{
[TestClass]
public class WriteOnly
{
private int _value;
public int Value { set { _value = value; } }
[TestMethod]
public void Test1() { }
}
}
""";
Compilation outputCompilation = RunGeneratorAndGetCompilation(MinimalMSTestStub, userCode);
string registry = outputCompilation
.SyntaxTrees
.Single(t => t.FilePath.EndsWith("MSTestReflectionMetadata.Registry.g.cs", System.StringComparison.Ordinal))
.ToString();
registry.Should().Contain("Name = \"Value\"");
// A set-only property has no readable getter; emitting `instance.Value` would not compile,
// so the Get delegate throws instead.
registry.Should().Contain("Get = static instance => throw new InvalidOperationException(\"Property 'Value' has no accessible getter.\"),");
registry.Should().Contain("((global::Sample.WriteOnly)instance!).Value = (int)value!");
IEnumerable errors = outputCompilation
.GetDiagnostics()
.Where(d => d.Severity == DiagnosticSeverity.Error);
errors.Should().BeEmpty("a set-only property must still produce a compiling registry");
}
[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_SkipsProtectedMembers()
{
const string userCode = """
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Sample
{
public class TestContext { }
[TestClass]
public class ProtectedShapes
{
[TestContext]
protected TestContext? Context { get; set; }
[TestMethod]
protected void ProtectedTest() { }
[TestMethod]
private protected void PrivateProtectedTest() { }
[TestMethod]
protected internal void ProtectedInternalTest() { }
}
}
""";
Compilation outputCompilation = RunGeneratorAndGetCompilation(MinimalMSTestStub, userCode);
string registry = outputCompilation
.SyntaxTrees
.Single(t => t.FilePath.EndsWith("MSTestReflectionMetadata.Registry.g.cs", System.StringComparison.Ordinal))
.ToString();
registry.Should().NotContain("ProtectedTest");
registry.Should().NotContain("PrivateProtectedTest");
registry.Should().NotContain("Context");
registry.Should().Contain("ProtectedInternalTest");
IEnumerable errors = outputCompilation
.GetDiagnostics()
.Where(d => d.Severity == DiagnosticSeverity.Error);
errors.Should().BeEmpty("the registry can only call members accessible from a non-derived type in the same assembly");
}
[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_ProducesStableOutputAndCachesSteps_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(
generators: new ISourceGenerator[] { new MSTestReflectionMetadataGenerator().AsSourceGenerator() },
additionalTexts: null,
parseOptions: (CSharpParseOptions)compilation.SyntaxTrees.First().Options,
optionsProvider: ReflectionFreeOptionsProvider.Instance,
driverOptions: new GeneratorDriverOptions(IncrementalGeneratorOutputKind.None, trackIncrementalGeneratorSteps: true));
GeneratorDriver firstDriver = driver.RunGenerators(compilation);
GeneratorDriverRunResult firstResult = firstDriver.GetRunResult();
GeneratorDriver secondDriver = firstDriver.RunGenerators(compilation);
GeneratorDriverRunResult secondResult = secondDriver.GetRunResult();
firstResult.Diagnostics.Should().BeEmpty();
secondResult.Diagnostics.Should().BeEmpty();
firstResult.Results.Should().ContainSingle();
secondResult.Results.Should().ContainSingle();
// Two passes against the same compilation MUST produce identical sources (deterministic output).
secondResult.Results[0].GeneratedSources
.Select(s => (s.HintName, Text: s.SourceText.ToString()))
.Should()
.BeEquivalentTo(firstResult.Results[0].GeneratedSources
.Select(s => (s.HintName, Text: s.SourceText.ToString())));
// The implementation-source-output pipeline MUST report cached steps on the second run
// (proves the generator's incremental graph actually short-circuits when inputs are unchanged).
secondResult.Results[0].TrackedOutputSteps
.Should()
.NotBeEmpty("the generator registers RegisterImplementationSourceOutput which produces a tracked output step");
secondResult.Results[0].TrackedOutputSteps
.SelectMany(static kv => kv.Value)
.SelectMany(static step => step.Outputs)
.Should()
.OnlyContain(
static o => o.Reason == IncrementalStepRunReason.Cached || o.Reason == IncrementalStepRunReason.Unchanged,
"output steps for unchanged inputs MUST be Cached or Unchanged on subsequent runs");
}
[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 = CreateDriver(compilation);
// 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:
// support types + registry + module-initializer registration.
result.Results[0].GeneratedSources.Should().HaveCount(3);
}
[TestMethod]
public void Generator_IncludesMethodsFromBaseType()
{
const string userCode = """
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Sample
{
public class BaseTests
{
[TestInitialize]
public void Setup() { }
[TestMethod]
public void InheritedTest() { }
}
[TestClass]
public class DerivedTests : BaseTests
{
[TestMethod]
public void DerivedTest() { }
}
}
""";
string registry = GetRegistry(RunGenerator(MinimalMSTestStub, userCode));
registry.Should().Contain("Name = \"InheritedTest\"");
registry.Should().Contain("Name = \"Setup\"");
registry.Should().Contain("Name = \"DerivedTest\"");
// The TestInitialize attribute applied on the base method must propagate too.
registry.Should().Contain("global::Microsoft.VisualStudio.TestTools.UnitTesting.TestInitializeAttribute");
}
[TestMethod]
public void Generator_ExcludesProtectedAndPrivateProtectedMembersFromBaseType()
{
const string userCode = """
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Sample
{
public class TestContext { }
public class BaseTests
{
[TestMethod]
protected void ProtectedTest() { }
[TestMethod]
private protected void PrivateProtectedTest() { }
[TestMethod]
protected internal void ProtectedInternalTest() { }
[TestContext]
protected TestContext ProtectedContext { get; set; } = new();
[TestContext]
private protected TestContext PrivateProtectedContext { get; set; } = new();
[TestContext]
protected internal TestContext ProtectedInternalContext { get; set; } = new();
}
[TestClass]
public class DerivedTests : BaseTests
{
[TestMethod]
public void Test() { }
}
}
""";
GeneratorRunResult result = RunGenerator(MinimalMSTestStub, userCode);
result.Diagnostics.Should().BeEmpty();
string registry = GetRegistry(result);
registry.Should().NotContain("Name = \"ProtectedTest\"");
registry.Should().NotContain("Name = \"PrivateProtectedTest\"");
registry.Should().NotContain("Name = \"ProtectedContext\"");
registry.Should().NotContain("Name = \"PrivateProtectedContext\"");
registry.Should().Contain("Name = \"ProtectedInternalTest\"");
registry.Should().Contain("Name = \"ProtectedInternalContext\"");
}
[TestMethod]
public void Generator_ExcludesInaccessibleMembersFromBaseTypeInAnotherAssembly()
{
const string baseCode = """
using Microsoft.VisualStudio.TestTools.UnitTesting;
public class GrandparentTests
{
[TestMethod]
public void HiddenTest() { }
[TestMethod]
public void PropertyHidesMethod() { }
[TestMethod]
public void StaticHidesInstance() { }
[TestMethod]
public void InaccessibleOverloadHidesMethodGroup(int value) { }
[TestContext]
public int HiddenContext { get; set; }
public int MethodHidesProperty { [TestMethod] get; set; }
}
public class BaseTests : GrandparentTests
{
[TestMethod]
public void PublicInheritedTest() { }
[TestMethod]
protected internal void InheritedTest() { }
[TestMethod]
internal void InternalTest() { }
protected internal new void HiddenTest() { }
protected internal int PropertyHidesMethod { get; set; }
protected internal static new void StaticHidesInstance() { }
protected internal void InaccessibleOverloadHidesMethodGroup(string value) { }
[TestContext]
public int PublicContext { get; set; }
[TestContext]
protected internal int InaccessibleContext { get; set; }
[TestContext]
internal int InternalContext { get; set; }
protected internal new int HiddenContext { get; set; }
protected internal void MethodHidesProperty() { }
public int ContextWithInaccessibleGetter { protected internal get; set; }
public int ContextWithInternalGetter { internal get; set; }
}
""";
CSharpCompilation baseCompilation = CreateCompilation(MinimalMSTestStub, baseCode)
.WithAssemblyName("BaseAssembly");
using var stream = new MemoryStream();
baseCompilation.Emit(stream).Success.Should().BeTrue();
MetadataReference baseReference = MetadataReference.CreateFromImage(stream.ToArray());
const string consumerCode = """
using Microsoft.VisualStudio.TestTools.UnitTesting;
[TestClass]
public class DerivedTests : BaseTests { }
""";
CSharpCompilation consumerCompilation = CreateCompilation(consumerCode);
consumerCompilation = consumerCompilation
.WithOptions(consumerCompilation.Options.WithMetadataImportOptions(MetadataImportOptions.All))
.AddReferences(baseReference);
GeneratorDriver driver = CreateDriver(consumerCompilation);
driver.RunGeneratorsAndUpdateCompilation(consumerCompilation, out Compilation outputCompilation, out _);
string registry = outputCompilation.SyntaxTrees
.Single(t => t.FilePath.EndsWith("MSTestReflectionMetadata.Registry.g.cs", StringComparison.Ordinal))
.ToString();
registry.Should().NotContain("Name = \"InheritedTest\"");
registry.Should().NotContain("Name = \"InternalTest\"");
registry.Should().Contain("Name = \"PublicInheritedTest\"");
registry.Should().NotContain("Name = \"InaccessibleContext\"");
registry.Should().NotContain("Name = \"InternalContext\"");
registry.Should().Contain("Name = \"PublicContext\"");
registry.Should().NotContain("Name = \"HiddenTest\"");
registry.Should().NotContain("Name = \"HiddenContext\"");
registry.Should().NotContain("Name = \"PropertyHidesMethod\"");
registry.Should().NotContain("Name = \"StaticHidesInstance\"");
registry.Should().NotContain("Name = \"InaccessibleOverloadHidesMethodGroup\"");
registry.Should().NotContain("Name = \"MethodHidesProperty\"");
registry.Should().Contain("Property 'ContextWithInaccessibleGetter' has no accessible getter.");
registry.Should().Contain("Property 'ContextWithInternalGetter' has no accessible getter.");
outputCompilation.GetDiagnostics()
.Where(d => d.Severity == DiagnosticSeverity.Error)
.Should().BeEmpty();
}
[TestMethod]
public void Generator_PropertyHidingTestMethod_MarksDescriptorsIncomplete()
{
const string userCode = """
using Microsoft.VisualStudio.TestTools.UnitTesting;
public class BaseTests
{
[TestMethod]
public void Hidden() { }
}
[TestClass]
public class DerivedTests : BaseTests
{
public new int Hidden { get; set; }
}
""";
string registry = GetRegistry(RunGenerator(MinimalMSTestStub, userCode));
registry.Should().Contain("AreGeneratedDescriptorsComplete = false");
registry.Should().NotContain("((global::DerivedTests)instance!).Hidden();");
}
[TestMethod]
public void Generator_NonMethodMembersHideBaseMethods()
{
const string userCode = """
using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
public class BaseTests
{
[TestMethod]
public void FieldHidden() { }
[TestMethod]
public void EventHidden() { }
[TestMethod]
public void TypeHidden() { }
}
[TestClass]
public class DerivedTests : BaseTests
{
public int FieldHidden;
public event Action? EventHidden;
public class TypeHidden { }
}
""";
Compilation outputCompilation = RunGeneratorAndGetCompilation(MinimalMSTestStub, userCode);
string registry = outputCompilation.SyntaxTrees
.Single(t => t.FilePath.EndsWith("MSTestReflectionMetadata.Registry.g.cs", StringComparison.Ordinal))
.ToString();
registry.Should().Contain("AreGeneratedDescriptorsComplete = false");
registry.Should().NotContain("((global::DerivedTests)instance!).FieldHidden();");
registry.Should().NotContain("((global::DerivedTests)instance!).EventHidden();");
registry.Should().NotContain("((global::DerivedTests)instance!).TypeHidden();");
outputCompilation.GetDiagnostics()
.Where(d => d.Severity == DiagnosticSeverity.Error)
.Should().BeEmpty();
}
[TestMethod]
public void Generator_MethodHidingTestAttributedPropertyAccessor_MarksDescriptorsIncomplete()
{
const string userCode = """
using Microsoft.VisualStudio.TestTools.UnitTesting;
public class BaseTests
{
public int Hidden { [TestMethod] get; set; }
}
[TestClass]
public class DerivedTests : BaseTests
{
public new void Hidden() { }
}
""";
string registry = GetRegistry(RunGenerator(MinimalMSTestStub, userCode));
registry.Should().Contain("AreGeneratedDescriptorsComplete = false");
registry.Should().NotContain("PropertyType = typeof(int)");
}
[TestMethod]
public void Generator_HiddenUnsupportedInheritedMethods_ReportDiagnostics()
{
const string userCode = """
using Microsoft.VisualStudio.TestTools.UnitTesting;
public class BaseTests
{
[TestMethod]
public void GenericHidden() { }
[TestMethod]
public void ByRefHidden(ref int value) { }
}
[TestClass]
public class DerivedTests : BaseTests
{
public int GenericHidden { get; set; }
public int ByRefHidden { get; set; }
}
""";
GeneratorRunResult result = RunGenerator(MinimalMSTestStub, userCode);
result.Diagnostics.Should().ContainSingle(d => d.Id == "AOTSG0004");
result.Diagnostics.Should().ContainSingle(d => d.Id == "AOTSG0005");
}
[TestMethod]
public void Generator_IncludesMethodsFromMultiLevelInheritance()
{
const string userCode = """
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Sample
{
public class GrandparentTests
{
[TestMethod]
public void GrandparentTest() { }
}
public class ParentTests : GrandparentTests
{
[TestMethod]
public void ParentTest() { }
}
[TestClass]
public class LeafTests : ParentTests
{
[TestMethod]
public void LeafTest() { }
}
}
""";
string registry = GetRegistry(RunGenerator(MinimalMSTestStub, userCode));
registry.Should().Contain("Name = \"GrandparentTest\"");
registry.Should().Contain("Name = \"ParentTest\"");
registry.Should().Contain("Name = \"LeafTest\"");
}
[TestMethod]
public void Generator_OverriddenVirtualMethod_KeepsOnlyDerivedImplementation()
{
const string userCode = """
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Sample
{
public class BaseTests
{
[TestMethod]
public virtual void Run() { }
}
[TestClass]
public class DerivedTests : BaseTests
{
public override void Run() { }
}
}
""";
string registry = GetRegistry(RunGenerator(MinimalMSTestStub, userCode));
// Only one entry for Run should be emitted, and the invoker must dispatch on the derived type.
int runEntries = registry.Split(["Name = \"Run\""], System.StringSplitOptions.None).Length - 1;
runEntries.Should().Be(1, "the derived override must replace the base entry (not duplicate it)");
registry.Should().Contain("((global::Sample.DerivedTests)instance!).Run();");
registry.Should().NotContain("((global::Sample.BaseTests)instance!).Run();");
registry.Should().Contain("AreGeneratedDescriptorsComplete = true");
// TestMethodAttribute is not inherited, so the override should not pick up the base attribute.
registry.Should().NotContain("global::Microsoft.VisualStudio.TestTools.UnitTesting.TestMethodAttribute");
}
[TestMethod]
public void Generator_OverriddenVirtualMethod_HonorsInheritedAttributeUsage()
{
const string userCode = """
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Sample
{
public class BaseTests
{
[TestMethod]
[TestCategory("Base")]
[DataRow(1)]
public virtual void Run(int value) { }
}
[TestClass]
public class DerivedTests : BaseTests
{
[TestMethod]
[TestCategory("Derived")]
public override void Run(int value) { }
}
}
""";
string registry = GetRegistry(RunGenerator(MinimalMSTestStub, userCode));
registry.Should().Contain("\"Base\"");
registry.Should().Contain("\"Derived\"");
registry.Should().NotContain("DataRows");
registry.Should().NotContain("new object?[] { 1 }");
}
[TestMethod]
public void Generator_NewKeywordHiddenMethod_DedupsBySignature()
{
const string userCode = """
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Sample
{
public class BaseTests
{
[TestMethod]
public void Hidden() { }
}
[TestClass]
public class DerivedTests : BaseTests
{
[TestMethod]
public new void Hidden() { }
}
}
""";
string registry = GetRegistry(RunGenerator(MinimalMSTestStub, userCode));
int hiddenEntries = registry.Split(["Name = \"Hidden\""], System.StringSplitOptions.None).Length - 1;
hiddenEntries.Should().Be(1, "members with the same name and signature must be de-duplicated; derived wins");
registry.Should().Contain("((global::Sample.DerivedTests)instance!).Hidden();");
registry.Should().Contain("AreGeneratedDescriptorsComplete = true");
}
[TestMethod]
public void Generator_PrivateSameSignatureMethod_DoesNotReplaceInheritedTest()
{
const string userCode = """
using Microsoft.VisualStudio.TestTools.UnitTesting;
public class BaseTests
{
[TestMethod]
public void Hidden() { }
}
[TestClass]
public class DerivedTests : BaseTests
{
private new void Hidden() { }
}
""";
string registry = GetRegistry(RunGenerator(MinimalMSTestStub, userCode));
registry.Should().Contain("AreGeneratedDescriptorsComplete = false");
registry.Should().NotContain("((global::DerivedTests)instance!).Hidden();");
}
[TestMethod]
public void Generator_PublicNonTestSameSignatureMethod_DoesNotReplaceInheritedTest()
{
const string userCode = """
using Microsoft.VisualStudio.TestTools.UnitTesting;
public class BaseTests
{
[TestMethod]
public void Hidden() { }
}
[TestClass]
public class DerivedTests : BaseTests
{
public new void Hidden() { }
}
""";
string registry = GetRegistry(RunGenerator(MinimalMSTestStub, userCode));
registry.Should().Contain("AreGeneratedDescriptorsComplete = false");
registry.Should().Contain("((global::DerivedTests)instance!).Hidden();");
}
[TestMethod]
public void Generator_InstanceTestMethod_ReplacesStaticAncestor()
{
const string userCode = """
using Microsoft.VisualStudio.TestTools.UnitTesting;
public class BaseTests
{
[TestMethod]
public static void Hidden() { }
}
[TestClass]
public class DerivedTests : BaseTests
{
[TestMethod]
public new void Hidden() { }
}
""";
string registry = GetRegistry(RunGenerator(MinimalMSTestStub, userCode));
registry.Should().Contain("AreGeneratedDescriptorsComplete = true");
registry.Should().Contain("((global::DerivedTests)instance!).Hidden();");
}
[TestMethod]
public void Generator_DynamicTestMethod_ReplacesObjectAncestor()
{
const string userCode = """
using Microsoft.VisualStudio.TestTools.UnitTesting;
public class BaseTests
{
[TestMethod]
public void Hidden(object value) { }
}
[TestClass]
public class DerivedTests : BaseTests
{
[TestMethod]
public new void Hidden(dynamic value) { }
}
""";
string registry = GetRegistry(RunGenerator(MinimalMSTestStub, userCode));
registry.Should().Contain("AreGeneratedDescriptorsComplete = true");
registry.Should().Contain("((global::DerivedTests)instance!).Hidden((dynamic)args![0]!);");
}
[TestMethod]
public void RuntimeSignature_DynamicAndObjectReturnTypes_AreEquivalent()
{
const string userCode = """
public class DynamicReturn
{
public dynamic Method() => new object();
}
public class ObjectReturn
{
public object Method() => new object();
}
""";
CSharpCompilation compilation = CreateCompilation(userCode);
var dynamicMethod = (IMethodSymbol)compilation.GetTypeByMetadataName("DynamicReturn")!.GetMembers("Method").Single();
var objectMethod = (IMethodSymbol)compilation.GetTypeByMetadataName("ObjectReturn")!.GetMembers("Method").Single();
TestMemberValidationHelper.HaveSameRuntimeDiscoverySignature(dynamicMethod, objectMethod).Should().BeTrue();
}
[TestMethod]
public void RuntimeSignature_DifferentReturnTypes_AreNotEquivalent()
{
const string userCode = """
using Microsoft.VisualStudio.TestTools.UnitTesting;
public class BaseTests
{
[TestMethod]
public void Hidden() { }
}
[TestClass]
public class DerivedTests : BaseTests
{
[TestMethod]
public new int Hidden() => 1;
}
""";
CSharpCompilation compilation = CreateCompilation(userCode);
var baseMethod = (IMethodSymbol)compilation.GetTypeByMetadataName("BaseTests")!.GetMembers("Hidden").Single();
var derivedMethod = (IMethodSymbol)compilation.GetTypeByMetadataName("DerivedTests")!.GetMembers("Hidden").Single();
TestMemberValidationHelper.HaveSameRuntimeDiscoverySignature(derivedMethod, baseMethod).Should().BeFalse();
GetRegistry(RunGenerator(MinimalMSTestStub, userCode))
.Should().Contain("AreGeneratedDescriptorsComplete = false");
}
[TestMethod]
public void RuntimeSignature_DifferentlyNamedMethodTypeParameters_AreNotEquivalent()
{
const string userCode = """
public class First
{
public void Method(T value) { }
}
public class Second
{
public void Method(U value) { }
}
""";
CSharpCompilation compilation = CreateCompilation(userCode);
var firstMethod = (IMethodSymbol)compilation.GetTypeByMetadataName("First")!.GetMembers("Method").Single();
var secondMethod = (IMethodSymbol)compilation.GetTypeByMetadataName("Second")!.GetMembers("Method").Single();
TestMemberValidationHelper.HaveSameRuntimeDiscoverySignature(firstMethod, secondMethod).Should().BeFalse();
}
[TestMethod]
public void Generator_NestedGenericContainingTypeSubstitutions_HaveDistinctRuntimeSignatures()
{
const string userCode = """
using Microsoft.VisualStudio.TestTools.UnitTesting;
public class Outer
{
public class Middle
{
public class Inner { }
}
}
public class BaseTests
{
[TestMethod]
public void Hidden(Outer.Middle.Inner value) { }
}
[TestClass]
public class DerivedTests : BaseTests
{
[TestMethod]
public new void Hidden(Outer.Middle.Inner value) { }
}
""";
string registry = GetRegistry(RunGenerator(MinimalMSTestStub, userCode));
registry.Should().Contain("AreGeneratedDescriptorsComplete = false");
registry.Should().Contain("typeof(global::Outer.Middle.Inner)");
registry.Should().NotContain("typeof(global::Outer.Middle.Inner)");
}
[TestMethod]
public void Generator_DerivedMethodGroup_HidesBaseOverloads()
{
const string userCode = """
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Sample
{
public class BaseTests
{
[TestMethod]
public void Op(int x) { }
}
[TestClass]
public class DerivedTests : BaseTests
{
[TestMethod]
public void Op(string x) { }
}
}
""";
Compilation outputCompilation = RunGeneratorAndGetCompilation(MinimalMSTestStub, userCode);
string registry = outputCompilation.SyntaxTrees
.Single(t => t.FilePath.EndsWith("MSTestReflectionMetadata.Registry.g.cs", StringComparison.Ordinal))
.ToString();
int opEntries = registry.Split(["Name = \"Op\""], System.StringSplitOptions.None).Length - 1;
opEntries.Should().Be(1);
registry.Should().Contain("AreGeneratedDescriptorsComplete = false");
registry.Should().Contain("typeof(string)");
outputCompilation.GetDiagnostics()
.Where(d => d.Severity == DiagnosticSeverity.Error)
.Should().BeEmpty();
}
[TestMethod]
public void Generator_DerivedIndexer_DoesNotHideBaseItemMethod()
{
const string userCode = """
using Microsoft.VisualStudio.TestTools.UnitTesting;
public class BaseTests
{
[TestMethod]
public void Item() { }
}
[TestClass]
public class DerivedTests : BaseTests
{
public int this[int index] => index;
}
""";
Compilation outputCompilation = RunGeneratorAndGetCompilation(MinimalMSTestStub, userCode);
string registry = outputCompilation.SyntaxTrees
.Single(t => t.FilePath.EndsWith("MSTestReflectionMetadata.Registry.g.cs", StringComparison.Ordinal))
.ToString();
registry.Should().Contain("AreGeneratedDescriptorsComplete = true");
registry.Should().Contain("((global::DerivedTests)instance!).Item();");
outputCompilation.GetDiagnostics()
.Where(d => d.Severity == DiagnosticSeverity.Error)
.Should().BeEmpty();
}
[TestMethod]
public void Generator_DerivedPropertyAccessor_DoesNotHideBaseAccessorNamedMethod()
{
const string userCode = """
using Microsoft.VisualStudio.TestTools.UnitTesting;
public class BaseTests
{
[TestMethod]
public void get_Value() { }
}
[TestClass]
public class DerivedTests : BaseTests
{
public int Value => 1;
}
""";
Compilation outputCompilation = RunGeneratorAndGetCompilation(MinimalMSTestStub, userCode);
string registry = outputCompilation.SyntaxTrees
.Single(t => t.FilePath.EndsWith("MSTestReflectionMetadata.Registry.g.cs", StringComparison.Ordinal))
.ToString();
registry.Should().Contain("AreGeneratedDescriptorsComplete = true");
registry.Should().Contain("((global::DerivedTests)instance!).get_Value();");
outputCompilation.GetDiagnostics()
.Where(d => d.Severity == DiagnosticSeverity.Error)
.Should().BeEmpty();
using var assemblyStream = new MemoryStream();
outputCompilation.Emit(assemblyStream).Success.Should().BeTrue();
var assembly = System.Reflection.Assembly.Load(assemblyStream.ToArray());
Type hookType = assembly.GetType("Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.SourceGeneration.ReflectionMetadataHook")!;
var registeredTestMethods = (IReadOnlyDictionary)hookType
.GetProperty("RegisteredTestMethods")!
.GetValue(null)!;
System.Reflection.MethodInfo registeredMethod = registeredTestMethods.Values.SelectMany(static methods => methods).Single();
registeredMethod.DeclaringType!.Name.Should().Be("BaseTests");
registeredMethod.ReturnType.Should().Be(typeof(void));
}
[TestMethod]
public void Generator_IncludesPropertiesFromBaseType()
{
const string userCode = """
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Sample
{
public class TestContext { }
public class BaseTests
{
[TestContext]
public virtual TestContext Context { get; set; } = new();
}
[TestClass]
public class DerivedTests : BaseTests
{
[TestMethod]
public void Test() { }
}
}
""";
string registry = GetRegistry(RunGenerator(MinimalMSTestStub, userCode));
registry.Should().Contain("Name = \"Context\"");
registry.Should().Contain("global::Microsoft.VisualStudio.TestTools.UnitTesting.TestContextAttribute");
}
[TestMethod]
public void Generator_ExcludesProtectedBaseMembers()
{
const string userCode = """
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Sample
{
public abstract class BaseTests
{
[TestInitialize]
protected void ProtectedSetup() { }
[TestCleanup]
private protected void PrivateProtectedCleanup() { }
[TestMethod]
protected void ProtectedTest() { }
}
[TestClass]
public class DerivedTests : BaseTests
{
[TestMethod]
public void PublicTest() { }
}
}
""";
Compilation outputCompilation = RunGeneratorAndGetCompilation(MinimalMSTestStub, userCode);
string registry = outputCompilation
.SyntaxTrees
.Single(t => t.FilePath.EndsWith("MSTestReflectionMetadata.Registry.g.cs", System.StringComparison.Ordinal))
.ToString();
registry.Should().Contain("PublicTest");
registry.Should().NotContain("ProtectedSetup");
registry.Should().NotContain("PrivateProtectedCleanup");
registry.Should().NotContain("ProtectedTest");
IEnumerable errors = outputCompilation
.GetDiagnostics()
.Where(d => d.Severity == DiagnosticSeverity.Error);
errors.Should().BeEmpty("generated invokers are emitted from a non-derived type and cannot call protected base members");
}
[TestMethod]
public void Generator_DoesNotInheritConstructors()
{
const string userCode = """
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Sample
{
public class BaseTests
{
public BaseTests(int x) { }
}
[TestClass]
public class DerivedTests : BaseTests
{
public DerivedTests() : base(1) { }
[TestMethod]
public void Test() { }
}
}
""";
string registry = GetRegistry(RunGenerator(MinimalMSTestStub, userCode));
// Only the derived ctor (parameterless) should be emitted — base ctor is never inherited.
registry.Should().Contain("Invoke = static args => new global::Sample.DerivedTests(),");
registry.Should().NotContain("Invoke = static args => new global::Sample.BaseTests(");
// No int parameter from the base constructor leaks into the constructor list.
registry.Should().NotContain("ParameterTypes = new Type[] { typeof(int) },");
}
[TestMethod]
public void Generator_AbstractBaseWithConcreteDerived_FoldsBaseMembers()
{
const string userCode = """
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Sample
{
public class TestContext { }
public abstract class AbstractBase
{
[TestInitialize]
public void Setup() { }
[TestContext]
public TestContext Ctx { get; set; } = new();
}
[TestClass]
public class Concrete : AbstractBase
{
[TestMethod]
public void Test() { }
}
}
""";
string registry = GetRegistry(RunGenerator(MinimalMSTestStub, userCode));
registry.Should().Contain("Name = \"Setup\"");
registry.Should().Contain("Name = \"Ctx\"");
registry.Should().Contain("Name = \"Test\"");
// The base class was abstract but the concrete derived type is the one emitted.
registry.Should().Contain("Type = typeof(global::Sample.Concrete)");
}
[TestMethod]
public void Generator_DoesNotWalkPastSystemObject()
{
const string userCode = """
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Sample
{
[TestClass]
public class SimpleTests
{
[TestMethod]
public void Test() { }
}
}
""";
string registry = GetRegistry(RunGenerator(MinimalMSTestStub, userCode));
// Members of System.Object (ToString, Equals, GetHashCode, GetType) must NOT be emitted.
registry.Should().NotContain("Name = \"ToString\"");
registry.Should().NotContain("Name = \"Equals\"");
registry.Should().NotContain("Name = \"GetHashCode\"");
registry.Should().NotContain("Name = \"GetType\"");
}
[TestMethod]
public void Generator_MaterializesSchedulingAttributes_OnMethodAndClass()
{
// '[DependsOn]' and '[ResourceLock]' are not part of the generator's small set of specially
// understood attributes; they ride the generic attribute-materialization path, which is what
// makes them work under Native AOT. This pins that so the generic path is not narrowed to an
// allow-list by accident.
const string userCode = """
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Microsoft.VisualStudio.TestTools.UnitTesting
{
[System.AttributeUsage(System.AttributeTargets.Class | System.AttributeTargets.Method, AllowMultiple = true, Inherited = false)]
public sealed class DependsOnAttribute : System.Attribute
{
public DependsOnAttribute(string testMethodName) { }
public DependsOnAttribute(System.Type testClass) { }
public bool ProceedOnFailure { get; set; }
}
[System.AttributeUsage(System.AttributeTargets.Class | System.AttributeTargets.Method, AllowMultiple = true, Inherited = false)]
public sealed class ResourceLockAttribute : System.Attribute
{
public ResourceLockAttribute(string resource) { }
}
}
namespace Sample
{
[TestClass]
[DependsOn(typeof(Tests))]
public class Tests
{
[TestMethod]
public void CreateCart() { }
[TestMethod]
[DependsOn(nameof(CreateCart), ProceedOnFailure = true)]
[ResourceLock("database")]
public void AddItem() { }
}
}
""";
GeneratorRunResult result = RunGenerator(MinimalMSTestStub, userCode);
result.Diagnostics.Should().BeEmpty();
string registry = GetRegistry(result);
registry.Should().Contain("new global::Microsoft.VisualStudio.TestTools.UnitTesting.DependsOnAttribute(\"CreateCart\")");
registry.Should().Contain("ProceedOnFailure = true");
registry.Should().Contain("new global::Microsoft.VisualStudio.TestTools.UnitTesting.DependsOnAttribute(typeof(global::Sample.Tests))");
registry.Should().Contain("new global::Microsoft.VisualStudio.TestTools.UnitTesting.ResourceLockAttribute(\"database\")");
}
[TestMethod]
public void Generator_CapturesAssemblyLevelAttribute()
{
const string userCode = """
using Microsoft.VisualStudio.TestTools.UnitTesting;
[assembly: Parallelize(Workers = 4, Scope = "Method")]
namespace Sample
{
[TestClass]
public class Tests
{
[TestMethod]
public void Test() { }
}
}
""";
GeneratorRunResult result = RunGenerator(MinimalMSTestStub, userCode);
result.Diagnostics.Should().BeEmpty();
string registry = GetRegistry(result);
registry.Should().Contain("#pragma warning disable MSTESTEXP");
registry.Should().Contain("public static object[] AssemblyAttributes { get; } = new object[]");
registry.Should().Contain("(Attribute)new global::Microsoft.VisualStudio.TestTools.UnitTesting.ParallelizeAttribute()");
registry.Should().Contain("Workers = 4");
registry.Should().Contain("Scope = \"Method\"");
}
[TestMethod]
public void Generator_AssemblyAttributes_IsEmptyArray_WhenNoneApplied()
{
const string userCode = """
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Sample
{
[TestClass]
public class Tests
{
[TestMethod]
public void Test() { }
}
}
""";
GeneratorRunResult result = RunGenerator(MinimalMSTestStub, userCode);
result.Diagnostics.Should().BeEmpty();
string registry = GetRegistry(result);
registry.Should().Contain("public static object[] AssemblyAttributes { get; } = Array.Empty();");
registry.Should().NotContain("public static object[] AssemblyAttributes { get; } = new object[]");
}
[TestMethod]
public void Generator_CapturesMultipleAssemblyAttributes_InDeclarationOrder()
{
const string userCode = """
using Microsoft.VisualStudio.TestTools.UnitTesting;
[assembly: Parallelize(Workers = 1)]
[assembly: Parallelize(Workers = 2)]
[assembly: Parallelize(Workers = 3)]
namespace Sample
{
[TestClass]
public class Tests
{
[TestMethod]
public void Test() { }
}
}
""";
GeneratorRunResult result = RunGenerator(MinimalMSTestStub, userCode);
result.Diagnostics.Should().BeEmpty();
string registry = GetRegistry(result);
int idx1 = registry.IndexOf("Workers = 1", StringComparison.Ordinal);
int idx2 = registry.IndexOf("Workers = 2", StringComparison.Ordinal);
int idx3 = registry.IndexOf("Workers = 3", StringComparison.Ordinal);
idx1.Should().BeGreaterThan(-1);
idx2.Should().BeGreaterThan(idx1);
idx3.Should().BeGreaterThan(idx2);
}
[TestMethod]
public void Generator_AssemblyAttributes_AreEmittedEvenWithNoTestClasses()
{
const string userCode = """
using Microsoft.VisualStudio.TestTools.UnitTesting;
[assembly: Parallelize(Workers = 8)]
namespace Sample
{
public class NotATest { }
}
""";
GeneratorRunResult result = RunGenerator(MinimalMSTestStub, userCode);
result.Diagnostics.Should().BeEmpty();
string registry = GetRegistry(result);
registry.Should().Contain("new global::Microsoft.VisualStudio.TestTools.UnitTesting.ParallelizeAttribute()");
registry.Should().Contain("Workers = 8");
registry.Should().NotContain("new TestClassReflectionInfo(");
}
[TestMethod]
public void Generator_DoesNotEmitRedundantDataRowsDescriptor()
{
const string userCode = """
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Sample
{
[TestClass]
public class Tests
{
[TestMethod]
public void NoData() { }
}
}
""";
GeneratorRunResult result = RunGenerator(MinimalMSTestStub, userCode);
result.Diagnostics.Should().BeEmpty();
string support = result.GeneratedSources
.Single(s => s.HintName == "MSTestReflectionMetadata.SupportTypes.g.cs")
.SourceText.ToString();
string registry = GetRegistry(result);
support.Should().NotContain("DataRows");
registry.Should().NotContain("DataRows");
}
[TestMethod]
public void Generator_EmitsDataRowAttributeWithoutDuplicateArgumentArray()
{
const string userCode = """
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Sample
{
[TestClass]
public class Tests
{
[TestMethod]
[DataRow(1, "x")]
public void Test(int a, string b) { }
}
}
""";
GeneratorRunResult result = RunGenerator(MinimalMSTestStub, userCode);
result.Diagnostics.Should().BeEmpty();
string registry = GetRegistry(result);
registry.Should().Contain("new global::Microsoft.VisualStudio.TestTools.UnitTesting.DataRowAttribute(new object[] { 1, \"x\" })");
registry.Should().NotContain("DataRows");
}
[TestMethod]
public void Generator_EmitsMultipleDataRowAttributes_InDeclarationOrder()
{
const string userCode = """
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Sample
{
[TestClass]
public class Tests
{
[TestMethod]
[DataRow(1, "a")]
[DataRow(2, "b")]
[DataRow(3, "c")]
public void Test(int a, string b) { }
}
}
""";
GeneratorRunResult result = RunGenerator(MinimalMSTestStub, userCode);
result.Diagnostics.Should().BeEmpty();
string registry = GetRegistry(result);
int idx1 = registry.IndexOf("DataRowAttribute(new object[] { 1, \"a\" })", StringComparison.Ordinal);
int idx2 = registry.IndexOf("DataRowAttribute(new object[] { 2, \"b\" })", StringComparison.Ordinal);
int idx3 = registry.IndexOf("DataRowAttribute(new object[] { 3, \"c\" })", StringComparison.Ordinal);
idx1.Should().BeGreaterThan(-1);
idx2.Should().BeGreaterThan(idx1);
idx3.Should().BeGreaterThan(idx2);
}
[TestMethod]
public void Generator_LeavesDataRowParamsMaterializationToAttribute()
{
const string userCode = """
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Sample
{
[TestClass]
public class Tests
{
[TestMethod]
[DataRow(1, 2, 3, 4)]
public void Test(int a, int b, int c, int d) { }
}
}
""";
GeneratorRunResult result = RunGenerator(MinimalMSTestStub, userCode);
result.Diagnostics.Should().BeEmpty();
string registry = GetRegistry(result);
registry.Should().Contain("new global::Microsoft.VisualStudio.TestTools.UnitTesting.DataRowAttribute(new object[] { 1, 2, 3, 4 })");
registry.Should().NotContain("DataRows");
}
[TestMethod]
public void Generator_PreservesDataRowConstantTypes()
{
const string userCode = """
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Sample
{
internal enum ByteEnum : byte { Max = byte.MaxValue }
internal enum SByteEnum : sbyte { Min = sbyte.MinValue }
internal enum ShortEnum : short { Min = short.MinValue }
internal enum UShortEnum : ushort { Max = ushort.MaxValue }
internal enum IntEnum : int { Min = int.MinValue }
internal enum UIntEnum : uint { Max = uint.MaxValue }
internal enum LongEnum : long { Min = long.MinValue }
internal enum ULongEnum : ulong { Max = ulong.MaxValue }
[TestClass]
public class Tests
{
[TestMethod]
[DataRow(false, (byte)0x4D, (sbyte)-1, (short)-2, (ushort)3, uint.MaxValue, ulong.MaxValue, '\n')]
[DataRow(ByteEnum.Max, SByteEnum.Min, ShortEnum.Min, UShortEnum.Max, IntEnum.Min, UIntEnum.Max, LongEnum.Min, ULongEnum.Max)]
public void Test(params object?[] values) { }
}
}
""";
Compilation outputCompilation = RunGeneratorAndGetCompilation(MinimalMSTestStub, userCode);
string registry = outputCompilation.SyntaxTrees
.Single(tree => tree.FilePath.EndsWith("MSTestReflectionMetadata.Registry.g.cs", StringComparison.Ordinal))
.ToString();
registry.Should().Contain(
"DataRowAttribute(new object[] { false, (byte)77, (sbyte)(-1), (short)(-2), (ushort)3, 4294967295U, 18446744073709551615UL, '\\n' })");
registry.Should().Contain("(global::Sample.ByteEnum)(255)");
registry.Should().Contain("(global::Sample.SByteEnum)(-128)");
registry.Should().Contain("(global::Sample.ShortEnum)(-32768)");
registry.Should().Contain("(global::Sample.UShortEnum)(65535)");
registry.Should().Contain("(global::Sample.IntEnum)(-2147483648)");
registry.Should().Contain("(global::Sample.UIntEnum)(4294967295U)");
registry.Should().Contain("(global::Sample.LongEnum)(-9223372036854775808L)");
registry.Should().Contain("(global::Sample.ULongEnum)(18446744073709551615UL)");
Diagnostic[] errors = outputCompilation.GetDiagnostics()
.Where(diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)
.ToArray();
errors.Should().BeEmpty(
"the typed-constant corpus should compile. Diagnostics: {0}",
string.Join(Environment.NewLine, errors.Select(diagnostic => diagnostic.ToString())));
object[][] rows = GetMaterializedDataRows(outputCompilation);
rows[0][1].Should().BeOfType().Which.Should().Be(0x4D);
rows[0][2].Should().BeOfType().Which.Should().Be(-1);
rows[0][3].Should().BeOfType().Which.Should().Be(-2);
rows[0][4].Should().BeOfType().Which.Should().Be(3);
rows[0][5].Should().BeOfType().Which.Should().Be(uint.MaxValue);
rows[0][6].Should().BeOfType().Which.Should().Be(ulong.MaxValue);
rows[0][7].Should().BeOfType().Which.Should().Be('\n');
rows[1].Select(value => value.GetType().FullName).Should().Equal(
"Sample.ByteEnum",
"Sample.SByteEnum",
"Sample.ShortEnum",
"Sample.UShortEnum",
"Sample.IntEnum",
"Sample.UIntEnum",
"Sample.LongEnum",
"Sample.ULongEnum");
}
[TestMethod]
public void Generator_EscapesDataRowStringAndCharacterLiterals()
{
const string userCode = """
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Sample
{
[TestClass]
public class Tests
{
[TestMethod]
[DataRow("\tMZXW6\r\n", "\"\\\0\u2028\u2029", '\'', '\\', '\0', '\u2028')]
public void Test(params object?[] values) { }
}
}
""";
Compilation outputCompilation = RunGeneratorAndGetCompilation(MinimalMSTestStub, userCode);
string registry = outputCompilation.SyntaxTrees
.Single(tree => tree.FilePath.EndsWith("MSTestReflectionMetadata.Registry.g.cs", StringComparison.Ordinal))
.ToString();
registry.Should().Contain("\"\\tMZXW6\\r\\n\"");
registry.Should().Contain("\"\\\"\\\\\\0\\u2028\\u2029\"");
registry.Should().Contain("'\\''");
registry.Should().Contain("'\\\\'");
registry.Should().Contain("'\\0'");
registry.Should().Contain("'\\u2028'");
outputCompilation.GetDiagnostics().Where(diagnostic => diagnostic.Severity == DiagnosticSeverity.Error).Should().BeEmpty();
object[][] rows = GetMaterializedDataRows(outputCompilation);
rows[0][0].Should().Be("\tMZXW6\r\n");
rows[0][1].Should().Be("\"\\\0\u2028\u2029");
rows[0][2].Should().Be('\'');
rows[0][3].Should().Be('\\');
rows[0][4].Should().Be('\0');
rows[0][5].Should().Be('\u2028');
}
[TestMethod]
public void Generator_DeterministicLiteralCorpus_CompilesWithoutGeneratedDiagnostics()
{
const int seed = 0x5EED;
var random = new Random(seed);
string[] values =
[
"\uD800",
"\uDFFF",
"\uD83D\uDE00",
.. Enumerable.Range(0, 64)
.Select(_ => new string(Enumerable.Range(0, random.Next(0, 24))
.Select(_ => (char)random.Next(char.MinValue, char.MaxValue + 1))
.ToArray()))
];
string dataRows = string.Join(
Environment.NewLine,
values.Select(value => $" [DataRow({Microsoft.CodeAnalysis.CSharp.SymbolDisplay.FormatLiteral(value, quote: true)})]"));
string userCode = $$"""
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Sample
{
[TestClass]
public class Tests
{
{{dataRows}}
[DataRow(float.NaN)]
[DataRow(float.PositiveInfinity)]
[DataRow(float.NegativeInfinity)]
[DataRow(double.NaN)]
[DataRow(double.PositiveInfinity)]
[DataRow(double.NegativeInfinity)]
[DataRow(float.MinValue)]
[DataRow(float.MaxValue)]
[DataRow(-0.0F)]
[DataRow(float.Epsilon)]
[DataRow(double.MinValue)]
[DataRow(double.MaxValue)]
[DataRow(-0.0D)]
[DataRow(double.Epsilon)]
[DataRow(new int[] { int.MinValue, 0, int.MaxValue })]
[TestMethod]
public void Test(object? value) { }
}
}
""";
var parseOptions = new CSharpParseOptions(documentationMode: DocumentationMode.Diagnose);
CSharpCompilation compilation = CreateCompilation(parseOptions, MinimalMSTestStub, userCode);
GeneratorDriver driver = CreateDriver(compilation);
driver.RunGeneratorsAndUpdateCompilation(compilation, out Compilation outputCompilation, out _);
Diagnostic[] generatedDiagnostics = outputCompilation.GetDiagnostics()
.Where(diagnostic => diagnostic.Severity >= DiagnosticSeverity.Warning
&& diagnostic.Location.SourceTree?.FilePath.EndsWith(".g.cs", StringComparison.Ordinal) is true)
.ToArray();
generatedDiagnostics.Should().BeEmpty($"the deterministic literal corpus with seed {seed} should produce clean generated source");
string registry = outputCompilation.SyntaxTrees
.Single(tree => tree.FilePath.EndsWith("MSTestReflectionMetadata.Registry.g.cs", StringComparison.Ordinal))
.ToString();
registry.Should().Contain("global::System.Single.NaN");
registry.Should().Contain("global::System.Single.PositiveInfinity");
registry.Should().Contain("global::System.Single.NegativeInfinity");
registry.Should().Contain("global::System.Double.NaN");
registry.Should().Contain("global::System.Double.PositiveInfinity");
registry.Should().Contain("global::System.Double.NegativeInfinity");
object[][] rows = GetMaterializedDataRows(outputCompilation);
string[] actualValues = rows.Take(values.Length).Select(row => (string)row[0]).ToArray();
actualValues.Should().Equal(values, $"the deterministic literal corpus with seed {seed} should round-trip");
int rowIndex = values.Length;
float.IsNaN((float)rows[rowIndex++][0]).Should().BeTrue();
float.IsPositiveInfinity((float)rows[rowIndex++][0]).Should().BeTrue();
float.IsNegativeInfinity((float)rows[rowIndex++][0]).Should().BeTrue();
double.IsNaN((double)rows[rowIndex++][0]).Should().BeTrue();
double.IsPositiveInfinity((double)rows[rowIndex++][0]).Should().BeTrue();
double.IsNegativeInfinity((double)rows[rowIndex++][0]).Should().BeTrue();
rows[rowIndex++][0].Should().Be(float.MinValue);
rows[rowIndex++][0].Should().Be(float.MaxValue);
BitConverter.SingleToInt32Bits((float)rows[rowIndex++][0]).Should().Be(BitConverter.SingleToInt32Bits(-0.0F));
rows[rowIndex++][0].Should().Be(float.Epsilon);
rows[rowIndex++][0].Should().Be(double.MinValue);
rows[rowIndex++][0].Should().Be(double.MaxValue);
BitConverter.DoubleToInt64Bits((double)rows[rowIndex++][0]).Should().Be(BitConverter.DoubleToInt64Bits(-0.0D));
rows[rowIndex++][0].Should().Be(double.Epsilon);
rows[rowIndex++][0].Should().BeOfType().Which.Should().Equal(int.MinValue, 0, int.MaxValue);
rowIndex.Should().Be(rows.Length);
}
[TestMethod]
[DataRow("null", "string[]")]
[DataRow("(object)null", "object")]
[DataRow("(object[])null", "object[]")]
[DataRow("(string[])null", "string[]")]
[DataRow("(int[])null", "object")]
public void Generator_HandlesNullValueInDataRow(string argument, string emittedType)
{
string userCode = $$"""
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Sample
{
[TestClass]
public class Tests
{
[TestMethod]
[DataRow({{argument}})]
public void Test(string? value) { }
}
}
""";
Compilation outputCompilation = RunGeneratorAndGetCompilation(MinimalMSTestStub, userCode);
outputCompilation.GetDiagnostics()
.Where(diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)
.Should().BeEmpty();
string registry = outputCompilation.SyntaxTrees
.Single(tree => tree.FilePath.EndsWith("MSTestReflectionMetadata.Registry.g.cs", StringComparison.Ordinal))
.ToString();
registry.Should().Contain($"new global::Microsoft.VisualStudio.TestTools.UnitTesting.DataRowAttribute(({emittedType})null!)");
registry.Should().NotContain("DataRows");
GetMaterializedDataRows(outputCompilation).Should().ContainSingle()
.Which.Should().ContainSingle()
.Which.Should().BeNull();
}
[TestMethod]
public void Generator_HandlesNestedNullArraysInDataRow()
{
const string userCode = """
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Sample
{
[TestClass]
public class Tests
{
[TestMethod]
[DataRow(new object[] { (int[])null, new object[] { (string[])null } })]
public void Test(int[]? value, object[] nested) { }
}
}
""";
Compilation outputCompilation = RunGeneratorAndGetCompilation(MinimalMSTestStub, userCode);
outputCompilation.GetDiagnostics()
.Where(diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)
.Should().BeEmpty();
object[] row = GetMaterializedDataRows(outputCompilation).Should().ContainSingle().Which;
row.Should().HaveCount(2);
row[0].Should().BeNull();
row[1].Should().BeOfType().Which.Should().ContainSingle().Which.Should().BeNull();
}
[TestMethod]
[DataRow("null", "((global::Sample.Value[])null!)")]
[DataRow("Property = null", "() { Property = (global::Sample.Value[])null! }")]
[DataRow("Field = null", "() { Field = (global::Sample.Value[])null! }")]
[DataRow("new Value[0]", "(Array.Empty())")]
[DataRow("new[] { Value.First }", "(new global::Sample.Value[] { (global::Sample.Value)(0) })")]
public void Generator_MaterializesArrayAttributeArguments(string arguments, string expectedArguments)
{
string userCode = $$"""
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Sample
{
internal enum Value { First }
internal sealed class ArrayMarkerAttribute : System.Attribute
{
public ArrayMarkerAttribute() { }
public ArrayMarkerAttribute(Value[]? values) { }
public Value[]? Property { get; set; }
public Value[]? Field;
}
[TestClass]
[ArrayMarker({{arguments}})]
public class Tests
{
[TestMethod]
public void Test() { }
}
}
""";
Compilation outputCompilation = RunGeneratorAndGetCompilation(MinimalMSTestStub, userCode);
outputCompilation.GetDiagnostics()
.Where(diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)
.Should().BeEmpty();
string registry = outputCompilation.SyntaxTrees
.Single(tree => tree.FilePath.EndsWith("MSTestReflectionMetadata.Registry.g.cs", StringComparison.Ordinal))
.ToString();
registry.Should().Contain($"new global::Sample.ArrayMarkerAttribute{expectedArguments}");
registry.Should().NotContain("AreAttributesComplete = false");
}
[TestMethod]
[DataRow("new Hidden[0]", null)]
[DataRow("new[] { Hidden.First }", null)]
[DataRow("new object[] { new Hidden[0] }", null)]
[DataRow("Value = new Hidden[0]", null)]
[DataRow("Value = new[] { Hidden.First }", null)]
[DataRow("Value = new object[] { new Hidden[0] }", null)]
// Roslyn types boxed null arrays as object, so they do not require an inaccessible Hidden[] cast.
[DataRow("(Hidden[])null", "((object)null!)")]
[DataRow("new object[] { (Hidden[])null }", "(new object[] { (object)null! })")]
[DataRow("Value = (Hidden[])null", "() { Value = (object)null! }")]
[DataRow("Value = new object[] { (Hidden[])null }", "() { Value = new object[] { (object)null! } }")]
public void Generator_HandlesArrayArgumentsWithInaccessibleElementTypes(string arguments, string? expectedArguments)
{
string userCode = $$"""
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Sample
{
public sealed class ArrayMarkerAttribute : System.Attribute
{
public ArrayMarkerAttribute() { }
public ArrayMarkerAttribute(object? value) { }
public object? Value { get; set; }
}
[TestClass]
public class Tests
{
private enum Hidden { First }
[TestMethod]
[ArrayMarker({{arguments}})]
[TestCategory("retained")]
public void Test() { }
}
}
""";
Compilation outputCompilation = RunGeneratorAndGetCompilation(MinimalMSTestStub, userCode);
outputCompilation.GetDiagnostics()
.Where(diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)
.Should().BeEmpty();
string registry = outputCompilation.SyntaxTrees
.Single(tree => tree.FilePath.EndsWith("MSTestReflectionMetadata.Registry.g.cs", StringComparison.Ordinal))
.ToString();
if (expectedArguments is null)
{
registry.Should().NotContain("ArrayMarkerAttribute");
registry.Should().Contain("AreAttributesComplete = false");
}
else
{
registry.Should().Contain($"new global::Sample.ArrayMarkerAttribute{expectedArguments}");
registry.Should().NotContain("AreAttributesComplete = false");
}
registry.Should().NotContain("global::Sample.Tests.Hidden");
registry.Should().Contain("new global::Microsoft.VisualStudio.TestTools.UnitTesting.TestCategoryAttribute(\"retained\")");
}
[TestMethod]
public void Generator_SkipsProtectedAndPrivateProtectedMembers()
{
// Generated invokers live in a separate static helper class (not a derived type),
// so 'protected' and 'private protected' members are not callable from the emitted
// code. They MUST be excluded from the registry to keep the generated source compiling.
const string userCode = """
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Sample
{
public class BaseTests
{
[TestMethod]
protected void ProtectedTest() { }
[TestMethod]
private protected void PrivateProtectedTest() { }
}
[TestClass]
public class DerivedTests : BaseTests
{
[TestMethod]
public void PublicTest() { }
}
}
""";
Compilation outputCompilation = RunGeneratorAndGetCompilation(MinimalMSTestStub, userCode);
IEnumerable errors = outputCompilation
.GetDiagnostics()
.Where(d => d.Severity == DiagnosticSeverity.Error);
errors.Should().BeEmpty("the generated registry MUST NOT reference protected / private protected members");
string registry = outputCompilation
.SyntaxTrees
.Single(t => t.FilePath.EndsWith("MSTestReflectionMetadata.Registry.g.cs", System.StringComparison.Ordinal))
.ToString();
registry.Should().Contain("Name = \"PublicTest\"");
registry.Should().NotContain("Name = \"ProtectedTest\"");
registry.Should().NotContain("Name = \"PrivateProtectedTest\"");
}
[TestMethod]
public void Generator_KeepsAllowMultipleAttributes_AcrossOverrideChain()
{
// [TestCategory] is AllowMultiple=true. Collecting attributes across the override chain
// MUST keep every instance instead of collapsing them by type, otherwise the inherited
// categories disappear from the registry.
const string userCode = """
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Sample
{
public class BaseTests
{
[TestMethod]
[TestCategory("BaseCat")]
public virtual void Run() { }
}
[TestClass]
public class DerivedTests : BaseTests
{
[TestCategory("DerivedCat")]
public override void Run() { }
}
}
""";
string registry = GetRegistry(RunGenerator(MinimalMSTestStub, userCode));
registry.Should().Contain("\"BaseCat\"");
registry.Should().Contain("\"DerivedCat\"");
}
[TestMethod]
public void Generator_HonorsAttributeUsage_DeclaredOnBaseAttributeType()
{
// GetAttributeUsage MUST walk the attribute type's base-type chain. AllowMultiple is
// inherited from a base attribute that declares [AttributeUsage(AllowMultiple = true)]
// even when the derived attribute does not redeclare its own [AttributeUsage]. If the
// walk were skipped, the fallback default AllowMultiple=false would silently drop the
// second occurrence below.
const string userCode = """
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
namespace Sample
{
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
public class BaseTagAttribute : Attribute
{
public BaseTagAttribute(string value) { Value = value; }
public string Value { get; }
}
// No [AttributeUsage] here on purpose — must inherit AllowMultiple=true from BaseTagAttribute.
public class DerivedTagAttribute : BaseTagAttribute
{
public DerivedTagAttribute(string value) : base(value) { }
}
[TestClass]
public class Tests
{
[TestMethod]
[DerivedTag("first")]
[DerivedTag("second")]
public void Run() { }
}
}
""";
string registry = GetRegistry(RunGenerator(MinimalMSTestStub, userCode));
registry.Should().Contain("\"first\"");
registry.Should().Contain("\"second\"");
}
[TestMethod]
public void Generator_ReportsAndSkipsGenericMethods_WithSameName()
{
// Methods that differ only in generic arity (e.g. M() vs M()) MUST be treated as
// distinct in the per-class dedup key, otherwise the generator might drop the
// non-generic overload alongside the generic ones.
//
// Generic test methods themselves are unsupported (AOTSG0004), so they are excluded
// from the registry — but the non-generic overload must still be emitted independently.
const string userCode = """
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Sample
{
[TestClass]
public class GenericArityTests
{
[TestMethod]
public void Run() { }
[TestMethod]
public void Run() { }
[TestMethod]
public void Run() { }
}
}
""";
GeneratorRunResult result = RunGenerator(MinimalMSTestStub, userCode);
Compilation outputCompilation = RunGeneratorAndGetCompilation(MinimalMSTestStub, userCode);
IEnumerable errors = outputCompilation
.GetDiagnostics()
.Where(d => d.Severity == DiagnosticSeverity.Error);
errors.Should().BeEmpty();
result.Diagnostics.Where(d => d.Id == "AOTSG0004").Should().HaveCount(2);
string registry = GetRegistry(result);
// Only the non-generic Run must be emitted. The generic overloads are excluded by AOTSG0004
// but must not cause the non-generic sibling to be dropped through key collision.
int occurrences = System.Text.RegularExpressions.Regex
.Matches(registry, "Name = \"Run\"")
.Count;
occurrences.Should().Be(1);
}
[TestMethod]
public void Generator_DoesNotInherit_TestMethodOrDataRow_OntoOverride()
{
// [TestMethod] and [DataRow] are declared with AttributeUsage(Inherited = false) in
// the real MSTest assembly. An override that does NOT re-apply [TestMethod] is not a
// test, and inherited [DataRow]s must not leak from the base method onto the override.
const string userCode = """
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Sample
{
public class BaseTests
{
[TestMethod]
[DataRow(1)]
[DataRow(2)]
public virtual void Run(int x) { }
}
[TestClass]
public class DerivedTests : BaseTests
{
// Override deliberately does not re-apply [TestMethod] or any [DataRow].
public override void Run(int x) { }
}
}
""";
string registry = GetRegistry(RunGenerator(MinimalMSTestStub, userCode));
// Neither the [TestMethod] nor the inherited [DataRow]s should propagate onto the override.
registry.Should().NotContain("Microsoft.VisualStudio.TestTools.UnitTesting.TestMethodAttribute()");
registry.Should().NotContain("Microsoft.VisualStudio.TestTools.UnitTesting.DataRowAttribute(");
registry.Should().NotContain("new DataRowModel(");
}
[TestMethod]
public void Generator_SupportType_DeclaresInvokeAsTaskReturning()
{
const string userCode = """
// Empty consumer — we only care about the post-init support types.
""";
GeneratorRunResult result = RunGenerator(MinimalMSTestStub, userCode);
result.Diagnostics.Should().BeEmpty();
string support = result.GeneratedSources
.Single(s => s.HintName == "MSTestReflectionMetadata.SupportTypes.g.cs")
.SourceText.ToString();
support.Should().Contain("using System.Threading.Tasks;");
// Invoke must be Task-returning so consumers can await without type-testing the result.
// Uses `set` (not `init`) per the repo guideline that new public-shaped API must not use init accessors.
support.Should().Contain("public Func Invoke { get; set; } = static (_, _) => Task.CompletedTask;");
}
[TestMethod]
public void Generator_InvokerForVoidMethod_ReturnsCompletedTask()
{
const string userCode = """
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Sample
{
[TestClass]
public class Tests
{
[TestMethod]
public void SyncVoid() { }
}
}
""";
string registry = GetRegistry(RunGenerator(MinimalMSTestStub, userCode));
registry.Should().Contain("Invoke = static (instance, args) => { ((global::Sample.Tests)instance!).SyncVoid(); return Task.CompletedTask; },");
}
[TestMethod]
public void Generator_InvokerForTaskMethod_ForwardsTask()
{
const string userCode = """
using System.Threading.Tasks;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Sample
{
[TestClass]
public class Tests
{
[TestMethod]
public Task AsyncTask() => Task.CompletedTask;
}
}
""";
string registry = GetRegistry(RunGenerator(MinimalMSTestStub, userCode));
// Task and Task both forward via the same `Task? __t = …` path; null is tolerated so
// the invoker contract (non-null Task) holds even for a misbehaving test method.
registry.Should().Contain("Invoke = static (instance, args) => { Task? __t = ((global::Sample.Tests)instance!).AsyncTask(); return __t ?? Task.CompletedTask; },");
}
[TestMethod]
public void Generator_InvokerForTaskOfTMethod_ForwardsTask()
{
const string userCode = """
using System.Threading.Tasks;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Sample
{
[TestClass]
public class Tests
{
[TestMethod]
public Task AsyncTaskOfInt() => Task.FromResult(42);
}
}
""";
string registry = GetRegistry(RunGenerator(MinimalMSTestStub, userCode));
registry.Should().Contain("Invoke = static (instance, args) => { Task? __t = ((global::Sample.Tests)instance!).AsyncTaskOfInt(); return __t ?? Task.CompletedTask; },");
}
[TestMethod]
public void Generator_InvokerForValueTaskMethod_ConsumesCompletedValueTask()
{
const string userCode = """
using System.Threading.Tasks;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Sample
{
[TestClass]
public class Tests
{
[TestMethod]
public ValueTask AsyncValueTask() => default;
}
}
""";
string registry = GetRegistry(RunGenerator(MinimalMSTestStub, userCode));
// ValueTask unwrap uses IsCompletedSuccessfully so the synchronous-completion fast path
// skips the Task allocation while still consuming the ValueTask source.
registry.Should().Contain("Invoke = static (instance, args) => { var __vt = ((global::Sample.Tests)instance!).AsyncValueTask(); if (__vt.IsCompletedSuccessfully) { __vt.GetAwaiter().GetResult(); return Task.CompletedTask; } return __vt.AsTask(); },");
}
[TestMethod]
public void Generator_InvokerForValueTaskOfTMethod_ConsumesCompletedValueTask()
{
const string userCode = """
using System.Threading.Tasks;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Sample
{
[TestClass]
public class Tests
{
[TestMethod]
public ValueTask AsyncValueTaskOfString() => new ValueTask("ok");
}
}
""";
string registry = GetRegistry(RunGenerator(MinimalMSTestStub, userCode));
registry.Should().Contain("Invoke = static (instance, args) => { var __vt = ((global::Sample.Tests)instance!).AsyncValueTaskOfString(); if (__vt.IsCompletedSuccessfully) { __vt.GetAwaiter().GetResult(); return Task.CompletedTask; } return __vt.AsTask(); },");
}
[TestMethod]
public void Generator_InvokerForNonVoidSyncMethod_DiscardsResultAndReturnsCompletedTask()
{
const string userCode = """
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Sample
{
[TestClass]
public class Tests
{
[TestMethod]
public int SyncInt() => 42;
}
}
""";
string registry = GetRegistry(RunGenerator(MinimalMSTestStub, userCode));
// For a sync non-void test the returned value is discarded but the call must still execute
// (its side-effects ARE the test). A plain invocation statement discards the value while also
// supporting `ref`-returning methods, where assigning a byref return to `_` would not compile.
registry.Should().Contain("Invoke = static (instance, args) => { ((global::Sample.Tests)instance!).SyncInt(); return Task.CompletedTask; },");
}
[TestMethod]
public void Generator_EmittedRegistry_ImportsSystemThreadingTasks()
{
const string userCode = """
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Sample
{
[TestClass]
public class Tests
{
[TestMethod]
public void Test() { }
}
}
""";
string registry = GetRegistry(RunGenerator(MinimalMSTestStub, userCode));
// The registry file references Task.CompletedTask directly in every invoker, so it must
// bring System.Threading.Tasks into scope.
registry.Should().Contain("using System.Threading.Tasks;");
}
[TestMethod]
public void Generator_DoesNotInheritAttribute_WhenAttributeUsageInheritedFalse()
{
// Custom attribute declared with [AttributeUsage(Inherited = false)] must not leak
// from the base method onto the derived override (matches MemberInfo.GetCustomAttributes(inherit: true)).
const string userCode = """
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Sample
{
[System.AttributeUsage(System.AttributeTargets.Method, Inherited = false)]
public class NonInheritedMarkerAttribute : System.Attribute { }
public class BaseTests
{
[TestMethod]
[NonInheritedMarker]
public virtual void Run() { }
}
[TestClass]
public class DerivedTests : BaseTests
{
public override void Run() { }
}
}
""";
string registry = GetRegistry(RunGenerator(MinimalMSTestStub, userCode));
registry.Should().NotContain("NonInheritedMarker");
}
[TestMethod]
public void Diagnostic_AOTSG0002_ReportedForNestedClassInsideGenericOuter()
{
const string userCode = """
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Sample
{
public class Outer
{
[TestClass]
public class InnerTests
{
[TestMethod]
public void Test1() { }
}
}
}
""";
GeneratorRunResult result = RunGenerator(MinimalMSTestStub, userCode);
result.Diagnostics.Should().ContainSingle(d => d.Id == "AOTSG0002");
string registry = GetRegistry(result);
registry.Should().NotContain("InnerTests");
}
[TestMethod]
public void Diagnostic_AOTSG0003_ReportedForFileLocalClass()
{
const string userCode = """
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Sample;
[TestClass]
file class FileLocalTests
{
[TestMethod]
public void Test1() { }
}
""";
GeneratorRunResult result = RunGenerator(MinimalMSTestStub, userCode);
result.Diagnostics.Should().ContainSingle(d => d.Id == "AOTSG0003");
string registry = GetRegistry(result);
registry.Should().NotContain("FileLocalTests");
}
[TestMethod]
public void Diagnostic_AOTSG0003_ReportedForPrivateNestedClass()
{
const string userCode = """
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Sample
{
public class Outer
{
[TestClass]
private class HiddenTests
{
[TestMethod]
public void Test1() { }
}
}
}
""";
GeneratorRunResult result = RunGenerator(MinimalMSTestStub, userCode);
result.Diagnostics.Should().ContainSingle(d => d.Id == "AOTSG0003");
string registry = GetRegistry(result);
registry.Should().NotContain("HiddenTests");
}
[TestMethod]
public void Diagnostic_AOTSG0003_NotReportedForInternalNestedInPublicOuter()
{
const string userCode = """
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Sample
{
public class Outer
{
[TestClass]
internal class VisibleTests
{
[TestMethod]
public void Test1() { }
}
}
}
""";
GeneratorRunResult result = RunGenerator(MinimalMSTestStub, userCode);
result.Diagnostics.Should().BeEmpty();
string registry = GetRegistry(result);
registry.Should().Contain("typeof(global::Sample.Outer.VisibleTests)");
}
[TestMethod]
public void Diagnostic_AOTSG0003_ReportedWhenOuterIsPrivateNested()
{
const string userCode = """
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Sample
{
public class Outer
{
private class HiddenOuter
{
[TestClass]
public class Tests
{
[TestMethod]
public void Test1() { }
}
}
}
}
""";
GeneratorRunResult result = RunGenerator(MinimalMSTestStub, userCode);
result.Diagnostics.Should().ContainSingle(d => d.Id == "AOTSG0003");
string registry = GetRegistry(result);
registry.Should().NotContain("typeof(global::Sample.Outer.HiddenOuter.Tests)");
}
[TestMethod]
public void Diagnostic_AOTSG0004_ReportedForGenericTestMethod_OtherMethodsStillEmitted()
{
const string userCode = """
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Sample
{
[TestClass]
public class Tests
{
[TestMethod]
public void GenericMethod() { }
[TestMethod]
public void NormalMethod() { }
}
}
""";
GeneratorRunResult result = RunGenerator(MinimalMSTestStub, userCode);
result.Diagnostics.Should().ContainSingle(d => d.Id == "AOTSG0004");
string registry = GetRegistry(result);
// The class itself is still emitted because at least one supported member remains.
registry.Should().Contain("typeof(global::Sample.Tests)");
// The generic method is excluded from the registry; the normal one is present.
registry.Should().NotContain("\"GenericMethod\"");
registry.Should().Contain("\"NormalMethod\"");
}
[TestMethod]
public void Diagnostic_AOTSG0005_ReportedForByRefParameter()
{
const string userCode = """
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Sample
{
[TestClass]
public class Tests
{
[TestMethod]
public void RefParam(ref int x) { }
[TestMethod]
public void NormalMethod() { }
}
}
""";
GeneratorRunResult result = RunGenerator(MinimalMSTestStub, userCode);
result.Diagnostics.Should().ContainSingle(d => d.Id == "AOTSG0005");
string registry = GetRegistry(result);
registry.Should().Contain("typeof(global::Sample.Tests)");
registry.Should().NotContain("\"RefParam\"");
registry.Should().Contain("\"NormalMethod\"");
}
[TestMethod]
public void Diagnostic_AOTSG0005_ReportedForOutAndInParameters()
{
const string userCode = """
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Sample
{
[TestClass]
public class Tests
{
[TestMethod]
public void OutParam(out int x) { x = 0; }
[TestMethod]
public void InParam(in int x) { }
}
}
""";
GeneratorRunResult result = RunGenerator(MinimalMSTestStub, userCode);
result.Diagnostics.Where(d => d.Id == "AOTSG0005").Should().HaveCount(2);
string registry = GetRegistry(result);
registry.Should().NotContain("\"OutParam\"");
registry.Should().NotContain("\"InParam\"");
}
[TestMethod]
public void Diagnostic_AOTSG0005_ReportedForByRefConstructorParameter()
{
const string userCode = """
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Sample
{
[TestClass]
public class Tests
{
public Tests() { }
public Tests(ref int x) { }
[TestMethod]
public void Test1() { }
}
}
""";
GeneratorRunResult result = RunGenerator(MinimalMSTestStub, userCode);
result.Diagnostics.Should().ContainSingle(d => d.Id == "AOTSG0005");
// Constructor display name in the diagnostic must not produce a double-dot ("Tests..ctor"); we emit "Tests.ctor".
Diagnostic ctorDiagnostic = result.Diagnostics.Single(d => d.Id == "AOTSG0005");
string message = ctorDiagnostic.GetMessage(System.Globalization.CultureInfo.InvariantCulture);
message.Should().NotContain("..ctor");
message.Should().Contain("Sample.Tests.ctor");
string registry = GetRegistry(result);
// The valid parameterless constructor is still emitted.
registry.Should().Contain("typeof(global::Sample.Tests)");
}
[TestMethod]
public void Diagnostic_NoneReportedForWellFormedTestClass()
{
const string userCode = """
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Sample
{
[TestClass]
public class Tests
{
[TestMethod]
public void Test1() { }
[TestMethod]
public void Test2(int x, string y) { }
}
}
""";
GeneratorRunResult result = RunGenerator(MinimalMSTestStub, userCode);
result.Diagnostics.Should().BeEmpty();
}
[TestMethod]
public void Generator_SupportTypes_DoNotUseInitAccessors()
{
const string userCode = """
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Sample
{
[TestClass]
public class Tests
{
[TestMethod]
public void Test() { }
}
}
""";
GeneratorRunResult result = RunGenerator(MinimalMSTestStub, userCode);
string support = result.GeneratedSources
.Single(s => s.HintName == "MSTestReflectionMetadata.SupportTypes.g.cs")
.SourceText.ToString();
// Repo guideline: newly introduced public-shaped API (even when emitted as `internal sealed`
// into the consumer assembly) MUST NOT use `init` accessors. Guard against accidental
// reintroduction.
support.Should().NotContain("{ get; init; }");
}
[TestMethod]
public void Generator_DynamicDependenciesDoNotRootNestedTypes()
{
const string userCode = """
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Sample
{
public class BaseTests
{
private sealed class NestedException : System.Exception { }
[TestMethod]
public void InheritedTest() { }
}
[TestClass]
public class DerivedTests : BaseTests
{
private sealed class NestedStream : System.IO.MemoryStream { }
private enum ScenarioState { Ready }
[TestMethod]
public void Test() { }
}
}
""";
GeneratorRunResult result = RunGenerator(MinimalMSTestStub, userCode);
result.Diagnostics.Should().BeEmpty();
string registration = result.GeneratedSources
.Single(s => s.HintName == "MSTestReflectionMetadata.Registration.g.cs")
.SourceText.ToString();
registration.Should().Contain("[DynamicDependency(TestClassMemberTypes, typeof(global::Sample.DerivedTests))]");
registration.Should().Contain("[DynamicDependency(TestClassMemberTypes, typeof(global::Sample.BaseTests))]");
foreach (string memberType in new[]
{
"PublicConstructors",
"NonPublicConstructors",
"PublicMethods",
"NonPublicMethods",
"PublicFields",
"NonPublicFields",
"PublicProperties",
"NonPublicProperties",
})
{
registration.Should().Contain($"DynamicallyAccessedMemberTypes.{memberType}");
}
registration.Should().NotContain("DynamicallyAccessedMemberTypes.All");
registration.Should().NotContain("DynamicallyAccessedMemberTypes.PublicNestedTypes");
registration.Should().NotContain("DynamicallyAccessedMemberTypes.NonPublicNestedTypes");
}
[TestMethod]
public void Generator_RootsClosedGenericBaseForDynamicDataReflectionFallback()
{
const string userCode = """
using System.Collections.Generic;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Sample
{
public abstract class GenericBase
{
protected static IEnumerable Data => new[] { new object[] { 1 } };
}
[TestClass]
public class DerivedTests : GenericBase
{
[TestMethod]
[DynamicData(nameof(Data))]
public void Test(int value) { }
}
}
""";
GeneratorRunResult result = RunGenerator(MinimalMSTestStub, userCode);
result.Diagnostics.Should().BeEmpty();
string registration = result.GeneratedSources
.Single(s => s.HintName == "MSTestReflectionMetadata.Registration.g.cs")
.SourceText.ToString();
string registry = GetRegistry(result);
registration.Should().Contain(
"[DynamicDependency(TestClassMemberTypes, typeof(global::Sample.GenericBase))]");
registry.Should().Contain("DynamicDataSources = Array.Empty()");
}
[TestMethod]
public void Generator_PreservesNullableEnumTestMethodParametersForNativeAot()
{
const string userCode = """
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Sample
{
public enum CustomValue
{
First,
Second,
}
[TestClass]
public class MyTests
{
[TestMethod]
public void Test1(CustomValue? value) { }
}
}
""";
GeneratorRunResult result = RunGenerator(MinimalMSTestStub, userCode);
string registration = result.GeneratedSources
.Single(s => s.HintName == "MSTestReflectionMetadata.Registration.g.cs")
.SourceText.ToString();
registration.Should().Contain(
"[DynamicDependency(DynamicallyAccessedMemberTypes.PublicFields, typeof(global::Sample.CustomValue))]");
}
[TestMethod]
public void Generator_EmitsModuleInitializer_RegisteringAssemblyWithAttributes_AndCompilesAgainstHook()
{
const string userCode = """
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Sample
{
[TestClass]
[TestCategory("Smoke")]
public class MyTests
{
[TestMethod]
public void Test1() { }
public void NotATest() { }
}
}
""";
// The AOT generator is self-contained: it emits a single [ModuleInitializer] that registers
// the assembly with the adapter (so referencing only this package makes tests runnable),
// and the registration also publishes pre-materialized type-level attributes so the adapter
// serves them without runtime reflection.
CSharpCompilation compilation = CreateCompilation(MinimalMSTestStub, userCode);
GeneratorDriver driver = CreateDriver(compilation);
driver = driver.RunGeneratorsAndUpdateCompilation(compilation, out Compilation outputCompilation, out _);
GeneratorRunResult result = driver.GetRunResult().Results[0];
string registration = result.GeneratedSources
.Single(s => s.HintName == "MSTestReflectionMetadata.Registration.g.cs")
.SourceText.ToString();
registration.Should().Contain("[ModuleInitializer]");
registration.Should().Contain("[DynamicDependency(TestClassMemberTypes, typeof(global::Sample.MyTests))]");
// The initializer consumes the MSTestReflectionMetadata registry (no more dead code) and
// publishes the delegate-based invokers via the richer Register overload so the adapter
// executes tests without runtime reflection.
registration.Should().Contain("global::MSTest.SourceGenerated.MSTestReflectionMetadata.TestClasses");
registration.Should().Contain("var methodInvokers = new Dictionary>(");
registration.Should().Contain("var propertySetters = new Dictionary>(0);");
registration.Should().Contain("methodInvokers[methodInfo] = method.Invoke;");
registration.Should().Contain("constructorInvokers[type] = constructors;");
registration.Should().Contain("propertySetters[propertyInfo] = property.Set;");
registration.Should().Contain(".ReflectionMetadataHook.Register(assembly, types, testMethods, typeAttributes, assemblyAttributes, methodAttributes, methodInvokers, constructorInvokers, propertySetters, descriptorTestMethods, descriptorCompleteTypes.ToArray());");
// Only [TestMethod]-annotated methods become test roots; the registry's IsTestMethod flag
// drives that filtering at module-load time.
registration.Should().Contain("if (method.IsTestMethod)");
registration.Should().Contain("testMethodRoots.Add(methodInfo);");
// Enumerate reflection members once per class, then preserve the existing exact-signature
// matching and unresolved-member fallback over those cached arrays.
registration.Should().Contain("MethodInfo[]? availableMethods = null;");
registration.Should().Contain("availableMethods ??= type.GetMethods(memberFlags);");
registration.Should().Contain("ResolveMethod(availableMethods, method.DeclaringType, method.Name, method.ParameterTypes)");
registration.Should().Contain("private static MethodInfo? ResolveMethod(MethodInfo[] availableMethods, Type declaringType");
registration.Should().Contain("availableProperties ??= type.GetProperties(memberFlags);");
registration.Should().Contain("private static PropertyInfo? ResolveProperty(PropertyInfo[] availableProperties");
registration.Should().NotContain("type.GetMethods(flags)");
registration.Should().Contain("object[] assemblyAttributes = global::MSTest.SourceGenerated.MSTestReflectionMetadata.AssemblyAttributes;");
registration.Should().NotContain("assemblyAttributeList");
// The generator run itself should produce no diagnostics. This guards against future
// generator changes that emit warnings/errors going unnoticed because the emitted code
// still happens to compile.
result.Diagnostics.Should().BeEmpty();
IEnumerable errors = outputCompilation
.GetDiagnostics()
.Where(d => d.Severity == DiagnosticSeverity.Error);
errors.Should().BeEmpty("the emitted module initializer must compile against the adapter's ReflectionMetadataHook");
}
[TestMethod]
public void Generator_EmitsNothing_InRootingMode()
{
// The reflection-free generator must stay completely silent unless the consumer opts in via
// ReflectionFree . When the property is unset (no
// options provider below) the generator falls back to rooting, where the separate rooting
// generator owns emission, so this generator emits no sources at all — otherwise the assembly
// would be registered twice. (The shipped product default is ReflectionFree, supplied by
// MSTest.TestAdapter.targets; this test exercises the generator's unset fallback directly.)
const string userCode = """
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Sample
{
[TestClass]
public class MyTests
{
[TestMethod]
public void Test1() { }
}
}
""";
CSharpCompilation compilation = CreateCompilation(MinimalMSTestStub, userCode);
// No options provider => MSTestSourceGenMode is unset => generator falls back to rooting
// (independent of the shipped ReflectionFree product default, which comes from MSBuild targets).
GeneratorDriver driver = CSharpGeneratorDriver.Create(
generators: new ISourceGenerator[] { new MSTestReflectionMetadataGenerator().AsSourceGenerator() },
additionalTexts: null,
parseOptions: (CSharpParseOptions)compilation.SyntaxTrees.First().Options,
optionsProvider: null,
driverOptions: default);
driver = driver.RunGenerators(compilation);
GeneratorRunResult result = driver.GetRunResult().Results[0];
result.Diagnostics.Should().BeEmpty();
result.GeneratedSources.Should().BeEmpty("the reflection-free generator must not emit in rooting mode");
}
[TestMethod]
public void Generator_EmitsDynamicDataAccessor_ForPropertySource()
{
const string userCode = """
using System.Collections.Generic;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Sample
{
[TestClass]
public class MyTests
{
public static IEnumerable Data => new[] { new object[] { 1 } };
[TestMethod]
[DynamicData(nameof(Data))]
public void Test1(int a) { }
}
}
""";
string registry = GetRegistry(RunGenerator(MinimalMSTestStub, userCode));
registry.Should().Contain("DynamicDataSources = new DynamicDataSourceReflectionInfo[]");
registry.Should().Contain("SourceName = \"Data\"");
registry.Should().Contain("DeclaringType = typeof(global::Sample.MyTests)");
registry.Should().Contain("return (object?)global::Sample.MyTests.Data;");
// Property reads run user code, so the reflection fallback (PropertyInfo.GetValue) wraps a thrown
// exception in TargetInvocationException; the generated accessor must preserve that.
registry.Should().Contain("throw new global::System.Reflection.TargetInvocationException(ex);");
}
[TestMethod]
public void Generator_EmitsDynamicDataAccessor_ForParameterlessMethodSource()
{
const string userCode = """
using System.Collections.Generic;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Sample
{
[TestClass]
public class MyTests
{
public static IEnumerable GetData() => new[] { new object[] { 1 } };
[TestMethod]
[DynamicData(nameof(GetData))]
public void Test1(int a) { }
}
}
""";
string registry = GetRegistry(RunGenerator(MinimalMSTestStub, userCode));
registry.Should().Contain("SourceName = \"GetData\"");
registry.Should().Contain("return (object?)global::Sample.MyTests.GetData();");
}
[TestMethod]
public void Generator_DoesNotEmitDynamicDataAccessor_ForMethodSourceWithArguments()
{
// A source method that takes arguments is invoked via MethodInfo.Invoke at runtime, whose binder
// applies primitive widening / conversions (e.g. int 3 -> long) that a direct unbox cast cannot
// replicate. Such sources must keep the (DAM-safe) reflection fallback instead of a generated
// accessor.
const string userCode = """
using System.Collections.Generic;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Sample
{
[TestClass]
public class MyTests
{
public static IEnumerable GetData(int count, string label) => new[] { new object[] { count, label } };
[TestMethod]
[DynamicData(nameof(GetData), 3, "x")]
public void Test1(int a, string b) { }
}
}
""";
string registry = GetRegistry(RunGenerator(MinimalMSTestStub, userCode));
registry.Should().NotContain("GetData = static args =>");
registry.Should().Contain("DynamicDataSources = Array.Empty()");
}
[TestMethod]
public void Generator_DoesNotEmitDynamicDataAccessor_ForVoidMethodSource()
{
// A void source method cannot be cast to object ((object?)Type.Source() would not compile), so the
// generator must skip it and keep the reflection fallback (which fails at runtime, as it did before).
const string userCode = """
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Sample
{
[TestClass]
public class MyTests
{
public static void GetData() { }
[TestMethod]
[DynamicData(nameof(GetData))]
public void Test1(int a) { }
}
}
""";
string registry = GetRegistry(RunGenerator(MinimalMSTestStub, userCode));
registry.Should().NotContain("GetData = static args =>");
registry.Should().Contain("DynamicDataSources = Array.Empty()");
}
[TestMethod]
public void Generator_DoesNotEmitDynamicDataAccessor_WhenSourceKindMismatchesExplicitSourceType()
{
// 'Data' is a property, but the attribute explicitly requests a Method source. Reflection would look
// for a method named Data (and throw), so the generator must not register a property accessor that
// would silently satisfy the (invalid) method request.
const string userCode = """
using System.Collections.Generic;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Sample
{
[TestClass]
public class MyTests
{
public static IEnumerable Data => new[] { new object[] { 1 } };
[TestMethod]
[DynamicData(nameof(Data), DynamicDataSourceType.Method)]
public void Test1(int a) { }
}
}
""";
string registry = GetRegistry(RunGenerator(MinimalMSTestStub, userCode));
registry.Should().NotContain("GetData = static args =>");
registry.Should().Contain("DynamicDataSources = Array.Empty()");
}
[TestMethod]
public void Generator_EmitsDynamicDataAccessor_ForFieldSource()
{
const string userCode = """
using System.Collections.Generic;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Sample
{
[TestClass]
public class MyTests
{
public static readonly IEnumerable DataField = new[] { new object[] { 1 } };
[TestMethod]
[DynamicData(nameof(DataField), DynamicDataSourceType.Field)]
public void Test1(int a) { }
}
}
""";
string registry = GetRegistry(RunGenerator(MinimalMSTestStub, userCode));
// A field read cannot run user code, so it stays a plain expression-bodied accessor with no
// TargetInvocationException wrapping (reflection reads fields via FieldInfo.GetValue without wrapping).
registry.Should().Contain("GetData = static args => (object?)global::Sample.MyTests.DataField");
registry.Should().NotContain("TargetInvocationException");
}
[TestMethod]
public void Generator_DoesNotEmitDynamicDataAccessor_ForUndefinedSourceType()
{
// (DynamicDataSourceType)99 is a legal cast in an attribute but not a defined enum member. Emitting
// DynamicDataSourceType.99 would not compile, so the generator must skip it and keep the reflection
// fallback.
const string userCode = """
using System.Collections.Generic;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Sample
{
[TestClass]
public class MyTests
{
public static IEnumerable Data => new[] { new object[] { 1 } };
[TestMethod]
[DynamicData(nameof(Data), (DynamicDataSourceType)99)]
public void Test1(int a) { }
}
}
""";
string registry = GetRegistry(RunGenerator(MinimalMSTestStub, userCode));
registry.Should().NotContain("GetData = static args =>");
registry.Should().Contain("DynamicDataSources = Array.Empty()");
}
[TestMethod]
public void Generator_EmitsDynamicDataAccessor_ForCrossTypeSource()
{
const string userCode = """
using System.Collections.Generic;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Sample
{
public static class DataHolder
{
public static IEnumerable Data => new[] { new object[] { 1 } };
}
[TestClass]
public class MyTests
{
[TestMethod]
[DynamicData(nameof(DataHolder.Data), typeof(DataHolder))]
public void Test1(int a) { }
}
}
""";
string registry = GetRegistry(RunGenerator(MinimalMSTestStub, userCode));
registry.Should().Contain("DeclaringType = typeof(global::Sample.DataHolder)");
registry.Should().Contain("return (object?)global::Sample.DataHolder.Data;");
}
[TestMethod]
public void Generator_EmitsDisplayNameAccessor_WhenCustomDisplayNameSpecified()
{
const string userCode = """
using System.Collections.Generic;
using System.Reflection;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Sample
{
[TestClass]
public class MyTests
{
public static IEnumerable Data => new[] { new object[] { 1 } };
public static string GetName(MethodInfo methodInfo, object[] data) => "custom";
[TestMethod]
[DynamicData(nameof(Data), DynamicDataDisplayName = nameof(GetName))]
public void Test1(int a) { }
}
}
""";
string registry = GetRegistry(RunGenerator(MinimalMSTestStub, userCode));
registry.Should().Contain("DisplayNameMethodName = \"GetName\"");
registry.Should().Contain("return global::Sample.MyTests.GetName(methodInfo, data!);");
}
[TestMethod]
public void Generator_SkipsUnresolvableDynamicDataSource()
{
// The source name does not match any member, so the generator must not emit an accessor
// (the runtime falls back to reflection for it).
const string userCode = """
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Sample
{
[TestClass]
public class MyTests
{
[TestMethod]
[DynamicData("DoesNotExist")]
public void Test1(int a) { }
}
}
""";
string registry = GetRegistry(RunGenerator(MinimalMSTestStub, userCode));
registry.Should().Contain("DynamicDataSources = Array.Empty()");
}
[TestMethod]
public void Generator_DynamicDataRegistration_Compiles()
{
const string userCode = """
using System.Collections.Generic;
using System.Reflection;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Sample
{
[TestClass]
public class MyTests
{
public static IEnumerable Data => new[] { new object[] { 1 } };
public static string GetName(MethodInfo methodInfo, object[] data) => "custom";
[TestMethod]
[DynamicData(nameof(Data), DynamicDataDisplayName = nameof(GetName))]
public void Test1(int a) { }
}
}
""";
Compilation outputCompilation = RunGeneratorAndGetCompilation(MinimalMSTestStub, userCode);
outputCompilation.GetDiagnostics()
.Where(d => d.Severity == DiagnosticSeverity.Error)
.Should().BeEmpty();
}
[TestMethod]
public void GeneratedTypeAttributes_IncludeInheritedAttributes_WhenInheritedIsTrue()
{
const string userCode = """
using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Sample
{
[AttributeUsage(AttributeTargets.Class, Inherited = true)]
public sealed class InheritedMarkerAttribute : Attribute { }
[InheritedMarker]
public abstract class BaseTests { }
[TestClass]
public sealed class DerivedTests : BaseTests
{
[TestMethod]
public void Test1() { }
}
}
""";
GeneratorRunResult result = RunGenerator(MinimalMSTestStub, userCode);
result.Diagnostics.Should().BeEmpty();
string registry = GetRegistry(result);
registry.Should().Contain("new global::Sample.InheritedMarkerAttribute()");
registry.Should().Contain("AreAttributesComplete = true");
}
[TestMethod]
public void GeneratedTypeAttributes_ExcludeBaseAttributes_WhenInheritedIsFalse()
{
const string userCode = """
using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Sample
{
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
public sealed class BaseOnlyMarkerAttribute : Attribute { }
[BaseOnlyMarker]
public abstract class BaseTests { }
[TestClass]
public sealed class DerivedTests : BaseTests
{
[TestMethod]
public void Test1() { }
}
}
""";
GeneratorRunResult result = RunGenerator(MinimalMSTestStub, userCode);
result.Diagnostics.Should().BeEmpty();
GetRegistry(result).Should().NotContain("global::Sample.BaseOnlyMarkerAttribute");
}
[TestMethod]
public void GeneratedTypeAttributes_KeepMostDerivedAttribute_WhenAllowMultipleIsFalse()
{
const string userCode = """
using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Sample
{
[AttributeUsage(AttributeTargets.Class, Inherited = true, AllowMultiple = false)]
public sealed class MarkerAttribute : Attribute
{
public MarkerAttribute(string value) { }
}
[Marker("base")]
public abstract class BaseTests { }
[TestClass]
[Marker("derived")]
public sealed class DerivedTests : BaseTests
{
[TestMethod]
public void Test1() { }
}
}
""";
string registry = GetRegistry(RunGenerator(MinimalMSTestStub, userCode));
registry.Should().Contain("new global::Sample.MarkerAttribute(\"derived\")");
registry.Should().NotContain("new global::Sample.MarkerAttribute(\"base\")");
}
[TestMethod]
public void GeneratedTypeAttributes_KeepAllDerivedAndBaseAttributes_WhenAllowMultipleIsTrue()
{
const string userCode = """
using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Sample
{
[AttributeUsage(AttributeTargets.Class, Inherited = true, AllowMultiple = true)]
public sealed class MarkerAttribute : Attribute
{
public MarkerAttribute(string value) { }
}
[Marker("base")]
public abstract class BaseTests { }
[TestClass]
[Marker("derived")]
public sealed class DerivedTests : BaseTests
{
[TestMethod]
public void Test1() { }
}
}
""";
string registry = GetRegistry(RunGenerator(MinimalMSTestStub, userCode));
int derivedIndex = registry.IndexOf("new global::Sample.MarkerAttribute(\"derived\")", System.StringComparison.Ordinal);
int baseIndex = registry.IndexOf("new global::Sample.MarkerAttribute(\"base\")", System.StringComparison.Ordinal);
derivedIndex.Should().BeGreaterThan(-1);
baseIndex.Should().BeGreaterThan(derivedIndex, "inherited attributes should be emitted in derived-to-base order");
}
[TestMethod]
public void GeneratedTypeAttributes_MarkMetadataIncomplete_WhenAttributeCannotBeMaterialized()
{
const string userCode = """
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Sample
{
public class Outer
{
private sealed class HiddenMarkerAttribute : System.Attribute { }
[TestClass]
[HiddenMarker]
public sealed class Tests
{
[TestMethod]
public void Test1() { }
}
}
}
""";
Compilation outputCompilation = RunGeneratorAndGetCompilation(MinimalMSTestStub, userCode);
outputCompilation.GetDiagnostics()
.Where(d => d.Severity == DiagnosticSeverity.Error)
.Should().BeEmpty();
string registry = outputCompilation.SyntaxTrees
.Single(t => t.FilePath.EndsWith("MSTestReflectionMetadata.Registry.g.cs", System.StringComparison.Ordinal))
.ToString();
registry.Should().NotContain("HiddenMarkerAttribute");
registry.Should().Contain("AreAttributesComplete = false");
string registration = outputCompilation.SyntaxTrees
.Single(t => t.FilePath.EndsWith("MSTestReflectionMetadata.Registration.g.cs", System.StringComparison.Ordinal))
.ToString();
int completenessGuardIndex = registration.IndexOf("if (testClass.AreAttributesComplete)", System.StringComparison.Ordinal);
int registrationIndex = registration.IndexOf("typeAttributes[type] = testClass.Attributes;", completenessGuardIndex, System.StringComparison.Ordinal);
completenessGuardIndex.Should().BeGreaterThan(-1);
registrationIndex.Should().BeGreaterThan(completenessGuardIndex);
}
[TestMethod]
public void GeneratedMethodAttributes_MarkEmptyMetadataComplete()
{
const string userCode = """
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Sample
{
[TestClass]
public sealed class Tests
{
public void Helper() { }
}
}
""";
GeneratorRunResult result = RunGenerator(MinimalMSTestStub, userCode);
result.Diagnostics.Should().BeEmpty();
string registry = GetRegistry(result);
int helperIndex = registry.IndexOf("Name = \"Helper\"", System.StringComparison.Ordinal);
int emptyAttributesIndex = registry.IndexOf("Attributes = Array.Empty()", helperIndex, System.StringComparison.Ordinal);
int completeIndex = registry.IndexOf("AreAttributesComplete = true", emptyAttributesIndex, System.StringComparison.Ordinal);
helperIndex.Should().BeGreaterThan(-1);
emptyAttributesIndex.Should().BeGreaterThan(helperIndex);
completeIndex.Should().BeGreaterThan(emptyAttributesIndex);
}
[TestMethod]
public void GeneratedMethodAttributes_KeepAsyncDeclaredMetadataComplete()
{
const string userCode = """
using System.Threading.Tasks;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Sample
{
[TestClass]
public sealed class Tests
{
[TestMethod]
[TestCategory("Async")]
[DataRow(1, "small", true)]
public async Task AsyncTaskTest(int size, string category, bool enabled)
=> await Task.Yield();
[TestMethod]
public async void AsyncVoidTest()
=> await Task.Yield();
}
}
""";
GeneratorRunResult result = RunGenerator(MinimalMSTestStub, userCode);
result.Diagnostics.Should().BeEmpty();
string registry = GetRegistry(result);
int asyncTaskIndex = registry.IndexOf("Name = \"AsyncTaskTest\"", System.StringComparison.Ordinal);
int asyncVoidIndex = registry.IndexOf("Name = \"AsyncVoidTest\"", System.StringComparison.Ordinal);
int asyncTaskCompleteIndex = registry.IndexOf("AreAttributesComplete = true", asyncTaskIndex, System.StringComparison.Ordinal);
int asyncVoidCompleteIndex = registry.IndexOf("AreAttributesComplete = true", asyncVoidIndex, System.StringComparison.Ordinal);
asyncTaskIndex.Should().BeGreaterThan(-1);
asyncVoidIndex.Should().BeGreaterThan(asyncTaskIndex);
asyncTaskCompleteIndex.Should().BeGreaterThan(asyncTaskIndex).And.BeLessThan(asyncVoidIndex);
asyncVoidCompleteIndex.Should().BeGreaterThan(asyncVoidIndex);
registry.Should().Contain("IsAsync = true");
registry.Should().Contain("new global::Microsoft.VisualStudio.TestTools.UnitTesting.TestCategoryAttribute(\"Async\")");
registry.Should().Contain("new global::Microsoft.VisualStudio.TestTools.UnitTesting.DataRowAttribute(new object[] { 1, \"small\", true })");
string registration = result.GeneratedSources
.Single(s => s.HintName == "MSTestReflectionMetadata.Registration.g.cs")
.SourceText.ToString();
registration.Should().Contain("if (method.IsAsync)");
registration.Should().Contain("methodInfo.GetCustomAttributes(typeof(AsyncStateMachineAttribute), inherit: false)");
registration.Should().Contain("methodInfo.GetCustomAttributes(typeof(DebuggerStepThroughAttribute), inherit: false)");
registration.Should().Contain("attributesWithCompilerMetadata");
}
[TestMethod]
public void GeneratedMethodAttributes_LeaveExplicitCompilerSpecialAsyncMetadataToTargetedReflection()
{
const string userCode = """
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Sample
{
[TestClass]
public sealed class Tests
{
[TestMethod]
[AsyncStateMachine(typeof(Tests))]
[DebuggerStepThrough]
public async Task AsyncTaskTest()
=> await Task.Yield();
}
}
""";
Compilation outputCompilation = RunGeneratorAndGetCompilation(MinimalMSTestStub, userCode);
outputCompilation.GetDiagnostics()
.Where(d => d.Severity == DiagnosticSeverity.Error)
.Should().BeEmpty();
string registry = outputCompilation.SyntaxTrees
.Single(t => t.FilePath.EndsWith("MSTestReflectionMetadata.Registry.g.cs", System.StringComparison.Ordinal))
.ToString();
registry.Should().NotContain("new global::System.Runtime.CompilerServices.AsyncStateMachineAttribute");
registry.Should().NotContain("new global::System.Diagnostics.DebuggerStepThroughAttribute");
registry.Should().Contain("AreAttributesComplete = true");
}
[TestMethod]
public void RuntimeRegistration_RegistersCompleteMethodAttributes()
{
const string userCode = """
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Sample
{
[TestClass]
public sealed class Tests
{
[TestMethod]
[TestCategory("Generated")]
public void Test1() { }
}
}
""";
Compilation outputCompilation = RunGeneratorAndGetCompilation(MinimalMSTestStub, userCode);
string registration = outputCompilation.SyntaxTrees
.Single(t => t.FilePath.EndsWith("MSTestReflectionMetadata.Registration.g.cs", System.StringComparison.Ordinal))
.ToString();
registration.Should().Contain("var methodAttributes = new Dictionary(");
int resolvedGuard = registration.IndexOf("if (methodInfo is not null)", System.StringComparison.Ordinal);
int completenessGuard = registration.IndexOf("if (method.AreAttributesComplete)", resolvedGuard, System.StringComparison.Ordinal);
int assignment = registration.IndexOf("methodAttributes[methodInfo] = attributes;", completenessGuard, System.StringComparison.Ordinal);
resolvedGuard.Should().BeGreaterThan(-1);
completenessGuard.Should().BeGreaterThan(resolvedGuard);
assignment.Should().BeGreaterThan(completenessGuard);
registration.Should().Contain(".ReflectionMetadataHook.Register(assembly, types, testMethods, typeAttributes, assemblyAttributes, methodAttributes, methodInvokers, constructorInvokers, propertySetters, descriptorTestMethods, descriptorCompleteTypes.ToArray());");
outputCompilation.GetDiagnostics()
.Where(d => d.Severity >= DiagnosticSeverity.Warning
&& d.Location.SourceTree?.FilePath.EndsWith(".g.cs", System.StringComparison.Ordinal) is true)
.Should().BeEmpty();
}
[TestMethod]
public void RuntimeRegistration_DoesNotRegisterIncompleteMethodAttributes()
{
const string userCode = """
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Sample
{
public class Outer
{
private sealed class HiddenAttribute : System.Attribute { }
[TestClass]
public sealed class Tests
{
[TestMethod]
[Hidden]
public void Test1() { }
}
}
}
""";
Compilation outputCompilation = RunGeneratorAndGetCompilation(MinimalMSTestStub, userCode);
string registry = outputCompilation.SyntaxTrees
.Single(t => t.FilePath.EndsWith("MSTestReflectionMetadata.Registry.g.cs", System.StringComparison.Ordinal))
.ToString();
string registration = outputCompilation.SyntaxTrees
.Single(t => t.FilePath.EndsWith("MSTestReflectionMetadata.Registration.g.cs", System.StringComparison.Ordinal))
.ToString();
registry.Should().Contain("AreAttributesComplete = false");
registry.Should().NotContain("new global::Sample.Outer.HiddenAttribute");
registration.Should().Contain("if (method.AreAttributesComplete)");
outputCompilation.GetDiagnostics().Where(d => d.Severity == DiagnosticSeverity.Error).Should().BeEmpty();
}
[TestMethod]
public void RuntimeRegistration_RegistersCompleteEmptyMethodAttributeArray()
{
const string userCode = """
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Sample
{
[TestClass]
public sealed class Tests
{
public void Helper() { }
}
}
""";
GeneratorRunResult result = RunGenerator(MinimalMSTestStub, userCode);
string registry = GetRegistry(result);
string registration = result.GeneratedSources
.Single(s => s.HintName == "MSTestReflectionMetadata.Registration.g.cs")
.SourceText.ToString();
registry.Should().Contain("Attributes = Array.Empty()");
registry.Should().Contain("AreAttributesComplete = true");
registration.Should().Contain("methodAttributes[methodInfo] = attributes;");
}
[TestMethod]
public void RuntimeRegistration_DoesNotPopulatePropertyOrLocationDictionaries()
{
const string userCode = """
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Sample
{
[TestClass]
public sealed class Tests
{
[TestMethod]
public void Test1() { }
}
}
""";
string registration = RunGenerator(MinimalMSTestStub, userCode).GeneratedSources
.Single(s => s.HintName == "MSTestReflectionMetadata.Registration.g.cs")
.SourceText.ToString();
registration.Should().NotContain("TypeProperties");
registration.Should().NotContain("TypePropertiesByName");
registration.Should().NotContain("TypeMethodLocations");
registration.Should().NotContain("typeof(global::Sample.Tests).GetProperty(");
}
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 = CreateDriver(compilation);
driver = driver.RunGeneratorsAndUpdateCompilation(compilation, out _, out _);
return driver.GetRunResult().Results[0];
}
private static Compilation RunGeneratorAndGetCompilation(params string[] sources)
{
CSharpCompilation compilation = CreateCompilation(sources);
GeneratorDriver driver = CreateDriver(compilation);
driver.RunGeneratorsAndUpdateCompilation(compilation, out Compilation outputCompilation, out _);
return outputCompilation;
}
private static object[][] GetMaterializedDataRows(Compilation outputCompilation)
{
using var assemblyStream = new MemoryStream();
outputCompilation.Emit(assemblyStream).Success.Should().BeTrue();
var assembly = System.Reflection.Assembly.Load(assemblyStream.ToArray());
Type registryType = assembly.GetType("MSTest.SourceGenerated.MSTestReflectionMetadata")!;
var testClasses = (System.Collections.IEnumerable)registryType.GetProperty("TestClasses")!.GetValue(null)!;
object testClass = testClasses.Cast().Single();
var methods = (System.Collections.IEnumerable)testClass.GetType().GetProperty("Methods")!.GetValue(testClass)!;
object method = methods.Cast().Single();
var attributes = (Attribute[])method.GetType().GetProperty("Attributes")!.GetValue(method)!;
return attributes
.Where(attribute => attribute.GetType().Name == "DataRowAttribute")
.Select(attribute => (object[])attribute.GetType().GetProperty("Data")!.GetValue(attribute)!)
.ToArray();
}
// Drives the generator in ReflectionFree mode (the mode under test). Without the
// MSTestSourceGenMode=ReflectionFree option the generator stays silent (rooting is the default),
// so the test harness always opts in.
private static GeneratorDriver CreateDriver(CSharpCompilation compilation, bool trackIncrementalGeneratorSteps = false)
=> CSharpGeneratorDriver.Create(
generators: new ISourceGenerator[] { new MSTestReflectionMetadataGenerator().AsSourceGenerator() },
additionalTexts: null,
parseOptions: (CSharpParseOptions)compilation.SyntaxTrees.First().Options,
optionsProvider: ReflectionFreeOptionsProvider.Instance,
driverOptions: new GeneratorDriverOptions(IncrementalGeneratorOutputKind.None, trackIncrementalGeneratorSteps));
private static CSharpCompilation CreateCompilation(params string[] sources)
=> CreateCompilation(CSharpParseOptions.Default, sources);
private static CSharpCompilation CreateCompilation(CSharpParseOptions parseOptions, params string[] sources)
{
// Always include the adapter hook stub so the emitted [ModuleInitializer] registration
// (which calls ReflectionMetadataHook.Register) compiles in the Roslyn test compilation
// without referencing MSTestAdapter.PlatformServices.
IEnumerable trees = sources.Append(RuntimeHookStub).Select(source => CSharpSyntaxTree.ParseText(source, parseOptions));
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));
}
///
/// An whose global options report
/// build_property.MSTestSourceGenMode = ReflectionFree , so the generator under test emits
/// its reflection-free output instead of staying silent (rooting is the default mode).
///
private sealed class ReflectionFreeOptionsProvider : AnalyzerConfigOptionsProvider
{
public static readonly ReflectionFreeOptionsProvider Instance = new();
public override AnalyzerConfigOptions GlobalOptions { get; } = new ReflectionFreeOptions();
public override AnalyzerConfigOptions GetOptions(SyntaxTree tree) => GlobalOptions;
public override AnalyzerConfigOptions GetOptions(AdditionalText textFile) => GlobalOptions;
private sealed class ReflectionFreeOptions : AnalyzerConfigOptions
{
public override bool TryGetValue(string key, [NotNullWhen(true)] out string? value)
{
if (key == "build_property.MSTestSourceGenMode")
{
value = "ReflectionFree";
return true;
}
value = null;
return false;
}
}
}
}