diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Discovery/AssemblyEnumerator.cs b/src/Adapter/MSTestAdapter.PlatformServices/Discovery/AssemblyEnumerator.cs
index 0c875a34bb..cabcc54cc9 100644
--- a/src/Adapter/MSTestAdapter.PlatformServices/Discovery/AssemblyEnumerator.cs
+++ b/src/Adapter/MSTestAdapter.PlatformServices/Discovery/AssemblyEnumerator.cs
@@ -70,6 +70,16 @@ public AssemblyEnumerator(MSTestSettings settings) =>
/// Flag set to true when parameterized test data must be serialized.
/// A collection of Test Elements.
internal AssemblyEnumerationResult EnumerateAssembly(string assemblyFileName, bool mustSerialize)
+ => EnumerateAssembly(assemblyFileName, mustSerialize, useGeneratedDescriptors: false);
+
+ ///
+ /// Enumerates through all types in the assembly in search of valid test methods.
+ ///
+ /// The assembly file name.
+ /// Flag set to true when parameterized test data must be serialized.
+ /// Whether native MTP discovery may consume complete source-generated descriptors.
+ /// A collection of Test Elements.
+ internal AssemblyEnumerationResult EnumerateAssembly(string assemblyFileName, bool mustSerialize, bool useGeneratedDescriptors)
{
List warnings = [];
DebugEx.Assert(!StringEx.IsNullOrWhiteSpace(assemblyFileName), "Invalid assembly file name.");
@@ -97,7 +107,7 @@ internal AssemblyEnumerationResult EnumerateAssembly(string assemblyFileName, bo
foreach (Type type in types)
{
List testsInType = DiscoverTestsInType(assemblyFileName, type, warnings, discoverInternals,
- dataSourcesUnfoldingStrategy, mustSerialize);
+ dataSourcesUnfoldingStrategy, mustSerialize, useGeneratedDescriptors);
tests.AddRange(testsInType);
}
@@ -161,7 +171,8 @@ private List DiscoverTestsInType(
List warningMessages,
bool discoverInternals,
TestDataSourceUnfoldingStrategy dataSourcesUnfoldingStrategy,
- bool mustSerialize)
+ bool mustSerialize,
+ bool useGeneratedDescriptors)
{
string? typeFullName = null;
var tests = new List();
@@ -170,7 +181,9 @@ private List DiscoverTestsInType(
{
typeFullName = type.FullName;
TypeEnumerator testTypeEnumerator = GetTypeEnumerator(type, assemblyFileName, discoverInternals);
- List? unitTestCases = testTypeEnumerator.Enumerate(warningMessages);
+ List? unitTestCases = useGeneratedDescriptors
+ ? testTypeEnumerator.Enumerate(warningMessages, useGeneratedDescriptors: true)
+ : testTypeEnumerator.Enumerate(warningMessages);
if (unitTestCases != null)
{
diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Discovery/AssemblyEnumeratorWrapper.cs b/src/Adapter/MSTestAdapter.PlatformServices/Discovery/AssemblyEnumeratorWrapper.cs
index 454373159d..96cd11906f 100644
--- a/src/Adapter/MSTestAdapter.PlatformServices/Discovery/AssemblyEnumeratorWrapper.cs
+++ b/src/Adapter/MSTestAdapter.PlatformServices/Discovery/AssemblyEnumeratorWrapper.cs
@@ -97,7 +97,7 @@ private static AssemblyEnumerationResult GetTestsInIsolation(string fullFilePath
// of strings normally (by reference), and we were mutating that collection in the appdomain.
// But this does not mutate the collection outside of appdomain, so we lost all warnings that happened inside.
bool mustSerialize = !isMTP || isolationHost is TestSourceHost { UsesAppDomain: true };
- return assemblyEnumerator.EnumerateAssembly(fullFilePath, mustSerialize);
+ return assemblyEnumerator.EnumerateAssembly(fullFilePath, mustSerialize, useGeneratedDescriptors: isMTP);
}
private static bool IsManagedAssembly(string fileName)
diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Discovery/TypeEnumerator.cs b/src/Adapter/MSTestAdapter.PlatformServices/Discovery/TypeEnumerator.cs
index 8cab727c67..5d821c64b4 100644
--- a/src/Adapter/MSTestAdapter.PlatformServices/Discovery/TypeEnumerator.cs
+++ b/src/Adapter/MSTestAdapter.PlatformServices/Discovery/TypeEnumerator.cs
@@ -48,6 +48,12 @@ internal TypeEnumerator(Type type, string assemblyFilePath, ReflectHelper reflec
/// Contains warnings if any, that need to be passed back to the caller.
/// list of test cases.
internal virtual List? Enumerate(List warnings)
+ => EnumerateCore(warnings, useGeneratedDescriptors: false);
+
+ internal virtual List? Enumerate(List warnings, bool useGeneratedDescriptors)
+ => EnumerateCore(warnings, useGeneratedDescriptors);
+
+ private List? EnumerateCore(List warnings, bool useGeneratedDescriptors)
{
if (!_typeValidator.IsValidTestClass(_type, warnings))
{
@@ -66,7 +72,13 @@ internal TypeEnumerator(Type type, string assemblyFilePath, ReflectHelper reflec
#endif
// If test class is valid, then get the tests
- return GetTests(warnings);
+ return useGeneratedDescriptors
+ && PlatformServiceProvider.Instance.ReflectionOperations.TryGetTestMethodDescriptors(
+ _type,
+ out MethodInfo[]? descriptorMethods,
+ out bool areAllTestMethodsSupported)
+ ? GetTests(warnings, descriptorMethods, areAllTestMethodsSupported)
+ : GetTests(warnings);
}
///
@@ -75,10 +87,16 @@ internal TypeEnumerator(Type type, string assemblyFilePath, ReflectHelper reflec
/// Contains warnings if any, that need to be passed back to the caller.
/// List of Valid Tests.
internal List GetTests(List warnings)
+ => GetTests(warnings, [], areAllTestMethodsSupported: false);
+
+ private List GetTests(List warnings, MethodInfo[] descriptorMethods, bool areAllTestMethodsSupported)
{
bool foundDuplicateTests = false;
var foundTests = new HashSet();
- var tests = new List();
+ var tests = new List(descriptorMethods.Length);
+ HashSet? descriptorMethodSet = areAllTestMethodsSupported || descriptorMethods.Length == 0
+ ? null
+ : [.. descriptorMethods];
// Instead of asking reflect helper to query the type for every method we have, we ask once for the type.
bool classDisablesParallelization = _reflectHelper.IsAttributeDefined(_type);
@@ -86,15 +104,29 @@ internal List GetTests(List warnings)
// Test class is already valid. Verify methods.
// PERF: GetRuntimeMethods is used here to get all methods, including non-public, and static methods.
// if we rely on analyzers to identify all invalid methods on build, we can change this to fit the current settings.
- foreach (MethodInfo method in PlatformServiceProvider.Instance.ReflectionOperations.GetRuntimeMethods(_type))
+ foreach (MethodInfo method in descriptorMethods)
+ {
+ foundDuplicateTests = foundDuplicateTests || !foundTests.Add(method.ToString() ?? method.Name);
+ tests.Add(GetTestFromMethod(method, classDisablesParallelization, warnings, isFromGeneratedDescriptor: true));
+ }
+
+ if (!areAllTestMethodsSupported)
{
- if (_testMethodValidator.IsValidTestMethod(method, _type, warnings))
+ foreach (MethodInfo method in PlatformServiceProvider.Instance.ReflectionOperations.GetRuntimeMethods(_type))
{
- // ToString() outputs method name and its signature. This is necessary for overloaded methods to be recognized as distinct tests.
- foundDuplicateTests = foundDuplicateTests || !foundTests.Add(method.ToString() ?? method.Name);
- UnitTestElement testMethod = GetTestFromMethod(method, classDisablesParallelization, warnings);
+ if (descriptorMethodSet?.Contains(method) ?? false)
+ {
+ continue;
+ }
+
+ if (_testMethodValidator.IsValidTestMethod(method, _type, warnings))
+ {
+ // ToString() outputs method name and its signature. This is necessary for overloaded methods to be recognized as distinct tests.
+ foundDuplicateTests = foundDuplicateTests || !foundTests.Add(method.ToString() ?? method.Name);
+ UnitTestElement testMethod = GetTestFromMethod(method, classDisablesParallelization, warnings);
- tests.Add(testMethod);
+ tests.Add(testMethod);
+ }
}
}
@@ -136,6 +168,17 @@ internal List GetTests(List warnings)
/// Contains warnings if any, that need to be passed back to the caller.
/// Returns a UnitTestElement.
internal UnitTestElement GetTestFromMethod(MethodInfo method, bool classDisablesParallelization, ICollection warnings)
+ => GetTestFromMethod(method, classDisablesParallelization, warnings, isFromGeneratedDescriptor: false);
+
+ ///
+ /// Gets a UnitTestElement from a MethodInfo object filling it up with appropriate values.
+ ///
+ /// The reflected method.
+ /// Whether the test class disables parallelization.
+ /// Contains warnings if any, that need to be passed back to the caller.
+ /// Whether native MTP discovery selected this method from generated metadata.
+ /// Returns a UnitTestElement.
+ internal UnitTestElement GetTestFromMethod(MethodInfo method, bool classDisablesParallelization, ICollection warnings, bool isFromGeneratedDescriptor)
{
// null if the current instance represents a generic type parameter.
DebugEx.Assert(_type.AssemblyQualifiedName != null, "AssemblyQualifiedName for method is null.");
@@ -157,6 +200,7 @@ internal UnitTestElement GetTestFromMethod(MethodInfo method, bool classDisables
IReflectionOperations reflectionOperations = PlatformServiceProvider.Instance.ReflectionOperations;
var testElement = new UnitTestElement(testMethod)
{
+ IsFromGeneratedDescriptor = isFromGeneratedDescriptor,
TestCategory = reflectionOperations.GetTestCategories(method, _type),
DoNotParallelize = classDisablesParallelization || _reflectHelper.IsAttributeDefined(method),
ResourceLocks = MergeResourceLocks(GetClassResourceLocks(), ReadResourceLocks(method)),
diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Interfaces/IReflectionOperations.cs b/src/Adapter/MSTestAdapter.PlatformServices/Interfaces/IReflectionOperations.cs
index c909c79d63..cf3433587e 100644
--- a/src/Adapter/MSTestAdapter.PlatformServices/Interfaces/IReflectionOperations.cs
+++ b/src/Adapter/MSTestAdapter.PlatformServices/Interfaces/IReflectionOperations.cs
@@ -66,6 +66,15 @@ internal interface IReflectionOperations
/// A constructor invoker delegate, or in reflection mode.
Func? GetConstructorInvoker(Type type);
+ ///
+ /// Gets source-generated test descriptors for native discovery.
+ ///
+ /// The test class to inspect.
+ /// Methods with complete generated discovery metadata.
+ /// Whether the generated set is authoritative for the class.
+ /// when generated descriptor metadata exists for the class.
+ bool TryGetTestMethodDescriptors(Type type, [NotNullWhen(true)] out MethodInfo[]? methods, out bool areAllTestMethodsSupported);
+
///
/// Gets a delegate that assigns directly (without
/// ), or when no
diff --git a/src/Adapter/MSTestAdapter.PlatformServices/InternalAPI/InternalAPI.Unshipped.txt b/src/Adapter/MSTestAdapter.PlatformServices/InternalAPI/InternalAPI.Unshipped.txt
index 38060ad0dd..b31dea32e1 100644
--- a/src/Adapter/MSTestAdapter.PlatformServices/InternalAPI/InternalAPI.Unshipped.txt
+++ b/src/Adapter/MSTestAdapter.PlatformServices/InternalAPI/InternalAPI.Unshipped.txt
@@ -103,6 +103,17 @@ Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.TestOutputCaptureMode.Non
Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.TestOutputCaptureMode.Result = 1 -> Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.TestOutputCaptureMode
Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.TestRunCancellationToken.CancellationToken.get -> System.Threading.CancellationToken
Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Helpers.AttributeQueryHelper
+Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Discovery.AssemblyEnumerator.EnumerateAssembly(string! assemblyFileName, bool mustSerialize, bool useGeneratedDescriptors) -> Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Discovery.AssemblyEnumerationResult!
+Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Discovery.TypeEnumerator.GetTestFromMethod(System.Reflection.MethodInfo! method, bool classDisablesParallelization, System.Collections.Generic.ICollection! warnings, bool isFromGeneratedDescriptor) -> Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel.UnitTestElement!
+Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel.UnitTestElement.IsFromGeneratedDescriptor.get -> bool
+Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel.UnitTestElement.IsFromGeneratedDescriptor.set -> void
+Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface.IReflectionOperations.TryGetTestMethodDescriptors(System.Type! type, out System.Reflection.MethodInfo![]? methods, out bool areAllTestMethodsSupported) -> bool
+Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.ReflectionOperations.TryGetTestMethodDescriptors(System.Type! type, out System.Reflection.MethodInfo![]? methods, out bool areAllTestMethodsSupported) -> bool
+Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.SourceGeneration.SourceGeneratedReflectionDataProvider.DescriptorCompleteTypes.get -> System.Collections.Generic.IReadOnlyDictionary!
+Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.SourceGeneration.SourceGeneratedReflectionDataProvider.DescriptorCompleteTypes.init -> void
+Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.SourceGeneration.SourceGeneratedReflectionDataProvider.DescriptorTestMethods.get -> System.Collections.Generic.IReadOnlyDictionary!
+Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.SourceGeneration.SourceGeneratedReflectionDataProvider.DescriptorTestMethods.init -> void
+Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.SourceGeneration.SourceGeneratedReflectionOperations.TryGetTestMethodDescriptors(System.Type! type, out System.Reflection.MethodInfo![]? methods, out bool areAllTestMethodsSupported) -> bool
Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.SourceGeneration.SourceGeneratedReflectionDataProvider.Assembly.get -> System.Reflection.Assembly?
Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.TestContextImplementation.StandardErrorBuilder.get -> Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.TestContextImplementation.SynchronizedStringBuilder!
Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.TestContextImplementation.StandardOutputBuilder.get -> Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.TestContextImplementation.SynchronizedStringBuilder!
@@ -136,3 +147,4 @@ static Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Helper
static Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.TestContextImplementation.ConfigureLiveOutputWriter(System.IO.TextWriter! liveOutputWriter) -> void
static Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.TestContextImplementation.SetLiveOutputWriterForTesting(System.IO.TextWriter! liveOutputWriter) -> System.IDisposable!
static Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Execution.TypeCache.IsTestFilterProviderMarkerType(System.Type? attributeType) -> bool
+virtual Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Discovery.TypeEnumerator.Enumerate(System.Collections.Generic.List! warnings, bool useGeneratedDescriptors) -> System.Collections.Generic.List?
diff --git a/src/Adapter/MSTestAdapter.PlatformServices/ObjectModel/UnitTestElement.cs b/src/Adapter/MSTestAdapter.PlatformServices/ObjectModel/UnitTestElement.cs
index 6e799c5d9c..8f09b5ab50 100644
--- a/src/Adapter/MSTestAdapter.PlatformServices/ObjectModel/UnitTestElement.cs
+++ b/src/Adapter/MSTestAdapter.PlatformServices/ObjectModel/UnitTestElement.cs
@@ -131,6 +131,15 @@ public UnitTestElement(TestMethod testMethod)
#endif
internal Guid? CachedTestNodeUid { get; set; }
+ ///
+ /// Gets or sets a value indicating whether native MTP discovery materialized this element from
+ /// a complete source-generated descriptor instead of the legacy runtime-method scan.
+ ///
+#if NETFRAMEWORK
+ [field: NonSerialized]
+#endif
+ internal bool IsFromGeneratedDescriptor { get; set; }
+
internal UnitTestElement Clone()
{
var clone = (UnitTestElement)MemberwiseClone();
diff --git a/src/Adapter/MSTestAdapter.PlatformServices/PublicAPI/PublicAPI.Unshipped.txt b/src/Adapter/MSTestAdapter.PlatformServices/PublicAPI/PublicAPI.Unshipped.txt
index 68d2b9bff8..01e173f723 100644
--- a/src/Adapter/MSTestAdapter.PlatformServices/PublicAPI/PublicAPI.Unshipped.txt
+++ b/src/Adapter/MSTestAdapter.PlatformServices/PublicAPI/PublicAPI.Unshipped.txt
@@ -1,2 +1,3 @@
#nullable enable
static Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.SourceGeneration.ReflectionMetadataHook.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) -> void
+static Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.SourceGeneration.ReflectionMetadataHook.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) -> void
diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Services/ReflectionOperations.cs b/src/Adapter/MSTestAdapter.PlatformServices/Services/ReflectionOperations.cs
index c849612c30..da64ade684 100644
--- a/src/Adapter/MSTestAdapter.PlatformServices/Services/ReflectionOperations.cs
+++ b/src/Adapter/MSTestAdapter.PlatformServices/Services/ReflectionOperations.cs
@@ -98,6 +98,13 @@ public MethodInfo[] GetRuntimeMethods(Type type)
public Func? GetConstructorInvoker(Type type) => null;
+ public bool TryGetTestMethodDescriptors(Type type, [NotNullWhen(true)] out MethodInfo[]? methods, out bool areAllTestMethodsSupported)
+ {
+ methods = null;
+ areAllTestMethodsSupported = false;
+ return false;
+ }
+
public Action? GetPropertySetter(PropertyInfo property) => null;
///
diff --git a/src/Adapter/MSTestAdapter.PlatformServices/SourceGeneration/CompositeSourceGeneratedReflectionDataProvider.cs b/src/Adapter/MSTestAdapter.PlatformServices/SourceGeneration/CompositeSourceGeneratedReflectionDataProvider.cs
index 7dd61b80cd..591a2ee1bc 100644
--- a/src/Adapter/MSTestAdapter.PlatformServices/SourceGeneration/CompositeSourceGeneratedReflectionDataProvider.cs
+++ b/src/Adapter/MSTestAdapter.PlatformServices/SourceGeneration/CompositeSourceGeneratedReflectionDataProvider.cs
@@ -188,6 +188,8 @@ private static SourceGeneratedReflectionDataProvider BuildMergedSnapshot(IReadOn
var typeConstructorsInvoker = new Dictionary();
var typeMethodInvokers = new Dictionary>();
var typePropertySetters = new Dictionary>();
+ var descriptorTestMethods = new Dictionary();
+ var descriptorCompleteTypes = new Dictionary();
foreach (SourceGeneratedReflectionDataProvider provider in providers)
{
@@ -203,6 +205,8 @@ private static SourceGeneratedReflectionDataProvider BuildMergedSnapshot(IReadOn
MergeInto(typeConstructorsInvoker, provider.TypeConstructorsInvoker);
MergeInto(typeMethodInvokers, provider.TypeMethodInvokers);
MergeInto(typePropertySetters, provider.TypePropertySetters);
+ MergeDescriptorMethods(descriptorTestMethods, provider.DescriptorTestMethods);
+ MergeDescriptorCompleteness(descriptorCompleteTypes, provider.DescriptorTestMethods.Keys, provider.DescriptorCompleteTypes);
}
return new SourceGeneratedReflectionDataProvider
@@ -219,6 +223,8 @@ private static SourceGeneratedReflectionDataProvider BuildMergedSnapshot(IReadOn
TypeConstructorsInvoker = typeConstructorsInvoker,
TypeMethodInvokers = typeMethodInvokers,
TypePropertySetters = typePropertySetters,
+ DescriptorTestMethods = descriptorTestMethods,
+ DescriptorCompleteTypes = descriptorCompleteTypes,
};
}
@@ -230,5 +236,45 @@ private static void MergeInto(Dictionary target, IRe
target[kvp.Key] = kvp.Value;
}
}
+
+ private static void MergeDescriptorMethods(
+ Dictionary target,
+ IReadOnlyDictionary source)
+ {
+ foreach (KeyValuePair kvp in source)
+ {
+ if (!target.TryGetValue(kvp.Key, out MethodInfo[]? existing))
+ {
+ target[kvp.Key] = kvp.Value;
+ continue;
+ }
+
+ var seen = new HashSet(existing);
+ var merged = new List(existing);
+ foreach (MethodInfo method in kvp.Value)
+ {
+ if (seen.Add(method))
+ {
+ merged.Add(method);
+ }
+ }
+
+ target[kvp.Key] = [.. merged];
+ }
+ }
+
+ private static void MergeDescriptorCompleteness(
+ Dictionary target,
+ IEnumerable descriptorTypes,
+ IReadOnlyDictionary source)
+ {
+ foreach (Type type in descriptorTypes)
+ {
+ bool isComplete = source.TryGetValue(type, out bool value) && value;
+ target[type] = !target.TryGetValue(type, out bool existing)
+ ? isComplete
+ : existing && isComplete;
+ }
+ }
}
}
diff --git a/src/Adapter/MSTestAdapter.PlatformServices/SourceGeneration/ReflectionMetadataHook.cs b/src/Adapter/MSTestAdapter.PlatformServices/SourceGeneration/ReflectionMetadataHook.cs
index 5d94eaeffb..a528f0b6b0 100644
--- a/src/Adapter/MSTestAdapter.PlatformServices/SourceGeneration/ReflectionMetadataHook.cs
+++ b/src/Adapter/MSTestAdapter.PlatformServices/SourceGeneration/ReflectionMetadataHook.cs
@@ -190,6 +190,42 @@ public static void Register(
IReadOnlyDictionary> methodInvokers,
IReadOnlyDictionary constructorInvokers,
IReadOnlyDictionary> propertySetters)
+ => Register(
+ assembly,
+ types,
+ testMethods,
+ typeAttributes,
+ assemblyAttributes,
+ methodAttributes,
+ methodInvokers,
+ constructorInvokers,
+ propertySetters,
+ EmptyDescriptorTestMethods,
+ []);
+
+ ///
+ /// Infrastructure. Publishes reflection metadata and the deliberately constrained test
+ /// descriptors that native Microsoft.Testing.Platform discovery may consume directly.
+ ///
+ ///
+ /// Descriptor methods must have complete, compatibility-safe metadata. A type listed in
+ /// declares that every test method on that type is
+ /// represented in ; omitted types retain per-method
+ /// fallback to legacy discovery. This API is exclusively for generated code.
+ ///
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public static void Register(
+ Assembly assembly,
+ Type[] types,
+ IReadOnlyDictionary testMethods,
+ IReadOnlyDictionary typeAttributes,
+ object[] assemblyAttributes,
+ IReadOnlyDictionary methodAttributes,
+ IReadOnlyDictionary> methodInvokers,
+ IReadOnlyDictionary constructorInvokers,
+ IReadOnlyDictionary> propertySetters,
+ IReadOnlyDictionary descriptorTestMethods,
+ Type[] descriptorCompleteTypes)
{
if (assembly is null)
{
@@ -236,6 +272,16 @@ public static void Register(
throw new ArgumentNullException(nameof(propertySetters));
}
+ if (descriptorTestMethods is null)
+ {
+ throw new ArgumentNullException(nameof(descriptorTestMethods));
+ }
+
+ if (descriptorCompleteTypes is null)
+ {
+ throw new ArgumentNullException(nameof(descriptorCompleteTypes));
+ }
+
// Ownership transfer (see the remarks on this method): the source generator hands over
// freshly-built, throwaway collections and never mutates them after the call, so we store
// the passed arrays and read-only dictionaries directly instead of copying them.
@@ -273,6 +319,13 @@ public static void Register(
}
}
+ var descriptorCompleteTypeSet = new HashSet(descriptorCompleteTypes);
+ var descriptorCompleteness = new Dictionary(descriptorTestMethods.Count);
+ foreach (Type type in descriptorTestMethods.Keys)
+ {
+ descriptorCompleteness[type] = descriptorCompleteTypeSet.Contains(type);
+ }
+
var provider = new SourceGeneratedReflectionDataProvider
{
Assembly = assembly,
@@ -286,6 +339,8 @@ public static void Register(
TypeMethodInvokers = methodInvokers,
TypeConstructorsInvoker = constructorInvokersMap,
TypePropertySetters = propertySetters,
+ DescriptorTestMethods = descriptorTestMethods,
+ DescriptorCompleteTypes = descriptorCompleteness,
};
lock (Lock)
@@ -311,4 +366,6 @@ public static void Register(
private static readonly Dictionary EmptyConstructorInvokers = [];
private static readonly Dictionary> EmptyPropertySetters = [];
+
+ private static readonly Dictionary EmptyDescriptorTestMethods = [];
}
diff --git a/src/Adapter/MSTestAdapter.PlatformServices/SourceGeneration/SourceGeneratedReflectionDataProvider.cs b/src/Adapter/MSTestAdapter.PlatformServices/SourceGeneration/SourceGeneratedReflectionDataProvider.cs
index 95e942f8e1..99a8ba8e96 100644
--- a/src/Adapter/MSTestAdapter.PlatformServices/SourceGeneration/SourceGeneratedReflectionDataProvider.cs
+++ b/src/Adapter/MSTestAdapter.PlatformServices/SourceGeneration/SourceGeneratedReflectionDataProvider.cs
@@ -119,6 +119,18 @@ internal class SourceGeneratedReflectionDataProvider
///
public IReadOnlyDictionary> TypeMethodInvokers { get; init; } = new Dictionary>();
+ ///
+ /// Gets test methods whose generated metadata is complete enough for descriptor-native discovery.
+ /// The runtime still uses the existing -based lifecycle after discovery.
+ ///
+ public IReadOnlyDictionary DescriptorTestMethods { get; init; } = new Dictionary();
+
+ ///
+ /// Gets whether represents every test method for each
+ /// registered type. A value requires a legacy scan for unsupported methods.
+ ///
+ public IReadOnlyDictionary DescriptorCompleteTypes { get; init; } = new Dictionary();
+
///
/// Gets the delegate-based property setters, keyed by the the
/// adapter holds (today: the TestContext property). Each delegate assigns the value
diff --git a/src/Adapter/MSTestAdapter.PlatformServices/SourceGeneration/SourceGeneratedReflectionOperations.cs b/src/Adapter/MSTestAdapter.PlatformServices/SourceGeneration/SourceGeneratedReflectionOperations.cs
index 829a0e1400..be84a05e5c 100644
--- a/src/Adapter/MSTestAdapter.PlatformServices/SourceGeneration/SourceGeneratedReflectionOperations.cs
+++ b/src/Adapter/MSTestAdapter.PlatformServices/SourceGeneration/SourceGeneratedReflectionOperations.cs
@@ -235,6 +235,13 @@ public Type[] GetDefinedTypes(Assembly assembly)
: null;
}
+ public bool TryGetTestMethodDescriptors(Type type, [NotNullWhen(true)] out MethodInfo[]? methods, out bool areAllTestMethodsSupported)
+ {
+ SourceGeneratedReflectionDataProvider data = DataProvider.GetSnapshot();
+ areAllTestMethodsSupported = data.DescriptorCompleteTypes.TryGetValue(type, out bool isComplete) && isComplete;
+ return data.DescriptorTestMethods.TryGetValue(type, out methods);
+ }
+
public Func? GetTestMethodInvoker(MethodInfo method)
{
SourceGeneratedReflectionDataProvider data = DataProvider.GetSnapshot();
diff --git a/src/Analyzers/MSTest.SourceGeneration/Generators/MetadataRegistryEmitter.cs b/src/Analyzers/MSTest.SourceGeneration/Generators/MetadataRegistryEmitter.cs
index 883877570e..ae4cf782b3 100644
--- a/src/Analyzers/MSTest.SourceGeneration/Generators/MetadataRegistryEmitter.cs
+++ b/src/Analyzers/MSTest.SourceGeneration/Generators/MetadataRegistryEmitter.cs
@@ -48,6 +48,8 @@ public static string EmitSupportTypes()
sb.AppendLine("public Type Type { get; set; } = null!;");
sb.AppendLine("public Attribute[] Attributes { get; set; } = Array.Empty();");
sb.AppendLine("public bool AreAttributesComplete { get; set; }");
+ sb.AppendLine("public bool SupportsGeneratedDescriptors { get; set; }");
+ sb.AppendLine("public bool AreGeneratedDescriptorsComplete { get; set; }");
sb.AppendLine("public IReadOnlyList Methods { get; set; } = Array.Empty();");
sb.AppendLine("public IReadOnlyList Properties { get; set; } = Array.Empty();");
sb.AppendLine("public IReadOnlyList Constructors { get; set; } = Array.Empty();");
@@ -64,6 +66,7 @@ public static string EmitSupportTypes()
sb.AppendLine("public bool ReturnsTask { get; set; }");
sb.AppendLine("public bool ReturnsValueTask { get; set; }");
sb.AppendLine("public bool ReturnsVoid { get; set; }");
+ sb.AppendLine("public bool IsDescriptorSupported { get; set; }");
sb.AppendLine("public Type[] ParameterTypes { get; set; } = Array.Empty();");
sb.AppendLine("public Attribute[] Attributes { get; set; } = Array.Empty();");
sb.AppendLine("public bool AreAttributesComplete { get; set; }");
@@ -192,6 +195,8 @@ private static void EmitTestClass(IndentedStringBuilder sb, TestClassModel model
EmitAttributesProperty(sb, "Attributes", model.Attributes);
sb.AppendLine(",");
sb.AppendLine($"AreAttributesComplete = {Bool(model.AreAttributesComplete)},");
+ sb.AppendLine($"SupportsGeneratedDescriptors = {Bool(model.SupportsGeneratedDescriptors)},");
+ sb.AppendLine($"AreGeneratedDescriptorsComplete = {Bool(model.AreGeneratedDescriptorsComplete)},");
EmitConstructors(sb, fqn, model);
sb.AppendLine(",");
@@ -250,6 +255,7 @@ private static void EmitMethods(IndentedStringBuilder sb, string fqn, TestClassM
sb.AppendLine($"ReturnsTask = {Bool(method.ReturnsTask)},");
sb.AppendLine($"ReturnsValueTask = {Bool(method.ReturnsValueTask)},");
sb.AppendLine($"ReturnsVoid = {Bool(method.ReturnsVoid)},");
+ sb.AppendLine($"IsDescriptorSupported = {Bool(method.IsDescriptorSupported)},");
EmitParameterTypes(sb, method.Parameters);
sb.AppendLine(",");
EmitAttributesProperty(sb, "Attributes", method.Attributes);
diff --git a/src/Analyzers/MSTest.SourceGeneration/Generators/RuntimeRegistrationEmitter.cs b/src/Analyzers/MSTest.SourceGeneration/Generators/RuntimeRegistrationEmitter.cs
index 86a4b1e5de..83dc4307c4 100644
--- a/src/Analyzers/MSTest.SourceGeneration/Generators/RuntimeRegistrationEmitter.cs
+++ b/src/Analyzers/MSTest.SourceGeneration/Generators/RuntimeRegistrationEmitter.cs
@@ -123,6 +123,8 @@ private static void EmitInitializeBody(IndentedStringBuilder sb, IReadOnlyList>({methodCount});");
sb.AppendLine($"var constructorInvokers = new Dictionary(testClasses.Count);");
sb.AppendLine($"var propertySetters = new Dictionary>({propertySetterCount});");
+ sb.AppendLine("var descriptorTestMethods = new Dictionary(testClasses.Count);");
+ sb.AppendLine("var descriptorCompleteTypes = new List(testClasses.Count);");
sb.AppendLine();
using (sb.Block("for (int classIndex = 0; classIndex < testClasses.Count; classIndex++)"))
@@ -131,6 +133,7 @@ private static void EmitInitializeBody(IndentedStringBuilder sb, IReadOnlyList(testClass.Methods.Count);");
+ sb.AppendLine("var descriptorMethodRoots = new List(testClass.Methods.Count);");
using (sb.Block("for (int methodIndex = 0; methodIndex < testClass.Methods.Count; methodIndex++)"))
{
sb.AppendLine($"{RegistryNamespace}.TestMethodReflectionInfo method = testClass.Methods[methodIndex];");
@@ -184,9 +188,18 @@ private static void EmitInitializeBody(IndentedStringBuilder sb, IReadOnlyList diagnostics)
{
@@ -46,6 +50,8 @@ public static TestClassModel Build(INamedTypeSymbol typeSymbol, List.Builder properties = ImmutableArray.CreateBuilder();
ImmutableArray.Builder ctors = ImmutableArray.CreateBuilder();
ImmutableArray.Builder baseTypes = ImmutableArray.CreateBuilder();
+ bool hasUnsupportedTestMethod = false;
+ bool hasPartialTypeInHierarchy = false;
string leafFqn = typeSymbol.ToDisplayString(SymbolDisplayFormats.FullyQualified);
@@ -59,6 +65,7 @@ public static TestClassModel Build(INamedTypeSymbol typeSymbol, List inheritedAttributes = AttributeMaterializationHelper.CollectInheritedAttributes(method);
+ bool isTestMethod = TestMemberValidationHelper.IsTestMethodAttributePresent(inheritedAttributes);
+ if (!TestMemberValidationHelper.IsAccessibleFromConsumer(method))
+ {
+ hasUnsupportedTestMethod |= isTestMethod;
+ break;
+ }
+
if (TestMemberValidationHelper.TryReportUnsupportedMethod(method, leafFqn, diagnostics))
{
+ hasUnsupportedTestMethod |= isTestMethod;
+
// Skip generic / by-ref methods entirely so the emitter does not produce
// code that references unbound type parameters or ref/in/out arguments.
break;
@@ -85,15 +101,18 @@ when TestMemberValidationHelper.IsAccessibleFromConsumer(method):
string key = TestMemberValidationHelper.BuildMethodSignatureKey(method);
if (!methodsByKey.ContainsKey(key))
{
- TestMethodModel model = BuildMethod(method, consumingAssembly);
+ TestMethodModel model = BuildMethod(method, consumingAssembly, inheritedAttributes, isTestMethod);
methodsByKey[key] = model;
methods.Add(model);
}
break;
- case IPropertySymbol property
- when !property.IsIndexer && TestMemberValidationHelper.IsAccessibleFromConsumer(property):
- if (!propertiesByName.ContainsKey(property.Name))
+ case IPropertySymbol property:
+ hasUnsupportedTestMethod |= HasTestMethodAttribute(property.GetMethod)
+ || HasTestMethodAttribute(property.SetMethod);
+ if (!property.IsIndexer
+ && TestMemberValidationHelper.IsAccessibleFromConsumer(property)
+ && !propertiesByName.ContainsKey(property.Name))
{
TestPropertyModel model = BuildProperty(property, consumingAssembly);
propertiesByName[property.Name] = model;
@@ -121,6 +140,14 @@ when TestMemberValidationHelper.IsAccessibleFromConsumer(method):
ctors.Add(new TestConstructorModel(BuildParameters(ctor)));
break;
+ case IEventSymbol eventSymbol:
+ hasUnsupportedTestMethod |= HasTestMethodAttribute(eventSymbol.AddMethod)
+ || HasTestMethodAttribute(eventSymbol.RemoveMethod)
+ || HasTestMethodAttribute(eventSymbol.RaiseMethod);
+ break;
+ case IMethodSymbol method:
+ hasUnsupportedTestMethod |= HasTestMethodAttribute(method);
+ break;
}
}
}
@@ -129,6 +156,27 @@ when TestMemberValidationHelper.IsAccessibleFromConsumer(method):
AttributeMaterializationHelper.BuildAttributesWithCompleteness(
AttributeMaterializationHelper.CollectInheritedAttributes(typeSymbol),
consumingAssembly);
+ bool supportsGeneratedDescriptors = classAttributes.IsComplete
+ && classAttributes.Attributes.Length == 1
+ && classAttributes.Attributes[0].FullyQualifiedAttributeType == TestClassAttributeName;
+
+ var duplicateTestMethodNames = new HashSet(
+ methods
+ .Where(static method => method.IsTestMethod)
+ .GroupBy(static method => method.Name, StringComparer.Ordinal)
+ .Where(static group => group.Count() > 1)
+ .Select(static group => group.Key),
+ StringComparer.Ordinal);
+
+ var finalizedMethods = methods
+ .Select(method => duplicateTestMethodNames.Contains(method.Name)
+ ? method with { IsDescriptorSupported = false }
+ : method)
+ .ToImmutableArray();
+ bool areGeneratedDescriptorsComplete = supportsGeneratedDescriptors
+ && !hasUnsupportedTestMethod
+ && !hasPartialTypeInHierarchy
+ && finalizedMethods.Where(static method => method.IsTestMethod).All(static method => method.IsDescriptorSupported);
return new TestClassModel(
FullyQualifiedTypeName: leafFqn,
@@ -139,14 +187,28 @@ when TestMemberValidationHelper.IsAccessibleFromConsumer(method):
IsAbstract: typeSymbol.IsAbstract,
IsStatic: typeSymbol.IsStatic,
Constructors: new EquatableArray(ctors.ToImmutable()),
- Methods: new EquatableArray(methods.ToImmutable()),
+ Methods: new EquatableArray(finalizedMethods),
Properties: new EquatableArray(properties.ToImmutable()),
Attributes: classAttributes.Attributes,
AreAttributesComplete: classAttributes.IsComplete,
+ SupportsGeneratedDescriptors: supportsGeneratedDescriptors,
+ AreGeneratedDescriptorsComplete: areGeneratedDescriptorsComplete,
BaseTypeFullyQualifiedNames: new EquatableArray(baseTypes.ToImmutable()));
}
- private static TestMethodModel BuildMethod(IMethodSymbol method, IAssemblySymbol consumingAssembly)
+ private static bool IsPartial(INamedTypeSymbol type)
+ => type.DeclaringSyntaxReferences.Any(static syntaxReference =>
+ syntaxReference.GetSyntax().ChildTokens().Any(static token => token.IsKind(SyntaxKind.PartialKeyword)));
+
+ private static bool HasTestMethodAttribute(IMethodSymbol? method)
+ => method is not null
+ && TestMemberValidationHelper.IsTestMethodAttributePresent(AttributeMaterializationHelper.CollectInheritedAttributes(method));
+
+ private static TestMethodModel BuildMethod(
+ IMethodSymbol method,
+ IAssemblySymbol consumingAssembly,
+ ImmutableArray inheritedAttributes,
+ bool isTestMethod)
{
ITypeSymbol returnType = method.ReturnType;
string returnTypeFqn = returnType.ToDisplayString(SymbolDisplayFormats.FullyQualified);
@@ -159,12 +221,19 @@ private static TestMethodModel BuildMethod(IMethodSymbol method, IAssemblySymbol
|| returnTypeFqn.StartsWith("global::System.Threading.Tasks.ValueTask<", System.StringComparison.Ordinal);
bool returnsVoid = returnType.SpecialType == SpecialType.System_Void;
- ImmutableArray inheritedAttributes = AttributeMaterializationHelper.CollectInheritedAttributes(method);
ImmutableArray attributesToMaterialize = method.IsAsync
? inheritedAttributes.Where(static attribute => !IsCompilerSpecialAsyncAttribute(attribute)).ToImmutableArray()
: inheritedAttributes;
AttributeMaterializationHelper.AttributeMaterializationResult methodAttributes =
AttributeMaterializationHelper.BuildAttributesWithCompleteness(attributesToMaterialize, consumingAssembly);
+ bool isDescriptorSupported = isTestMethod
+ && methodAttributes.IsComplete
+ && method.DeclaredAccessibility == Accessibility.Public
+ && !method.IsStatic
+ && !method.IsAbstract
+ && !method.IsAsync
+ && returnsVoid
+ && HasOnlyDescriptorSupportedAttributes(methodAttributes.Attributes);
return new TestMethodModel(
Name: method.Name,
@@ -173,13 +242,36 @@ private static TestMethodModel BuildMethod(IMethodSymbol method, IAssemblySymbol
ReturnsTask: returnsTask,
ReturnsValueTask: returnsValueTask,
ReturnsVoid: returnsVoid,
- IsTestMethod: TestMemberValidationHelper.IsTestMethodAttributePresent(method),
+ IsTestMethod: isTestMethod,
+ IsDescriptorSupported: isDescriptorSupported,
Parameters: BuildParameters(method),
Attributes: methodAttributes.Attributes,
AreAttributesComplete: methodAttributes.IsComplete,
DynamicDataSources: DynamicDataSourceBuilder.BuildDynamicDataSources(inheritedAttributes, method, consumingAssembly));
}
+ private static bool HasOnlyDescriptorSupportedAttributes(EquatableArray attributes)
+ {
+ int testMethodAttributeCount = 0;
+ foreach (AttributeApplicationModel attribute in attributes)
+ {
+ switch (attribute.FullyQualifiedAttributeType)
+ {
+ case TestMethodAttributeName:
+ testMethodAttributeCount++;
+ break;
+
+ case DataRowAttributeName:
+ break;
+
+ default:
+ return false;
+ }
+ }
+
+ return testMethodAttributeCount == 1;
+ }
+
private static bool IsCompilerSpecialAsyncAttribute(AttributeData attribute)
=> attribute.AttributeClass?.ToDisplayString(SymbolDisplayFormats.FullyQualified) is
AsyncStateMachineAttributeName or DebuggerStepThroughAttributeName;
diff --git a/src/Analyzers/MSTest.SourceGeneration/Generators/TestMemberValidationHelper.cs b/src/Analyzers/MSTest.SourceGeneration/Generators/TestMemberValidationHelper.cs
index 077ac71ec2..4328995ba6 100644
--- a/src/Analyzers/MSTest.SourceGeneration/Generators/TestMemberValidationHelper.cs
+++ b/src/Analyzers/MSTest.SourceGeneration/Generators/TestMemberValidationHelper.cs
@@ -27,9 +27,9 @@ internal static bool IsAccessibleFromConsumer(ISymbol symbol)
or Accessibility.Internal
or Accessibility.ProtectedOrInternal;
- internal static bool IsTestMethodAttributePresent(IMethodSymbol method)
+ internal static bool IsTestMethodAttributePresent(ImmutableArray attributes)
{
- foreach (AttributeData attribute in method.GetAttributes())
+ foreach (AttributeData attribute in attributes)
{
for (INamedTypeSymbol? attributeClass = attribute.AttributeClass;
attributeClass is not null;
diff --git a/src/Analyzers/MSTest.SourceGeneration/Models/TestClassModel.cs b/src/Analyzers/MSTest.SourceGeneration/Models/TestClassModel.cs
index eca91515a3..fe64b6dd22 100644
--- a/src/Analyzers/MSTest.SourceGeneration/Models/TestClassModel.cs
+++ b/src/Analyzers/MSTest.SourceGeneration/Models/TestClassModel.cs
@@ -71,6 +71,7 @@ internal sealed record TestMethodModel(
bool ReturnsValueTask,
bool ReturnsVoid,
bool IsTestMethod,
+ bool IsDescriptorSupported,
EquatableArray Parameters,
EquatableArray Attributes,
bool AreAttributesComplete,
@@ -106,4 +107,6 @@ internal sealed record TestClassModel(
EquatableArray Properties,
EquatableArray Attributes,
bool AreAttributesComplete,
+ bool SupportsGeneratedDescriptors,
+ bool AreGeneratedDescriptorsComplete,
EquatableArray BaseTypeFullyQualifiedNames);
diff --git a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/NativeAotTests.cs b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/NativeAotTests.cs
index 0b63218110..cf25c31a38 100644
--- a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/NativeAotTests.cs
+++ b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/NativeAotTests.cs
@@ -157,6 +157,14 @@ public async Task NativeAotTests_WillRunWithExitCodeZero(string tfm)
"MSTestReflectionMetadata.Registry.g.cs",
SearchOption.AllDirectories).Single();
string registry = File.ReadAllText(registryPath);
+ int synchronousMethodIndex = registry.IndexOf("Name = \"TestMethod1\"", StringComparison.Ordinal);
+ int synchronousNextMethodIndex = registry.IndexOf("Name = \"TestMethod2\"", synchronousMethodIndex, StringComparison.Ordinal);
+ Assert.IsGreaterThan(-1, synchronousMethodIndex);
+ Assert.IsGreaterThan(synchronousMethodIndex, synchronousNextMethodIndex);
+ StringAssert.Contains(
+ registry.Substring(synchronousMethodIndex, synchronousNextMethodIndex - synchronousMethodIndex),
+ "IsDescriptorSupported = true");
+
int asyncMethodIndex = registry.IndexOf("Name = \"TestMethod3\"", StringComparison.Ordinal);
int nextMethodIndex = registry.IndexOf("Name = \"TestMethod4\"", asyncMethodIndex, StringComparison.Ordinal);
Assert.IsGreaterThan(-1, asyncMethodIndex);
@@ -164,6 +172,9 @@ public async Task NativeAotTests_WillRunWithExitCodeZero(string tfm)
StringAssert.Contains(
registry.Substring(asyncMethodIndex, nextMethodIndex - asyncMethodIndex),
"AreAttributesComplete = true");
+ StringAssert.Contains(
+ registry.Substring(asyncMethodIndex, nextMethodIndex - asyncMethodIndex),
+ "IsDescriptorSupported = false");
var testHost = TestHost.LocateFrom(generator.TargetAssetPath, "MSTestNativeAotTests", tfm, RID, Verb.publish);
diff --git a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/SourceGenerationNonAotTests.cs b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/SourceGenerationNonAotTests.cs
index 83fd058200..a1acee3eee 100644
--- a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/SourceGenerationNonAotTests.cs
+++ b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/SourceGenerationNonAotTests.cs
@@ -184,12 +184,17 @@ public async Task SourceGenerationNonAot_BuildsAndRunsTests_WithExitCodeZero(str
string registry = File.ReadAllText(generatedFiles.Single(path => path.EndsWith("MSTestReflectionMetadata.Registry.g.cs", StringComparison.Ordinal)));
Assert.DoesNotContain("DataRows", registry, "DataRowAttribute instances are authoritative; a second argument-array descriptor is redundant.");
Assert.DoesNotContain("ParameterNames", registry, "runtime registration only resolves overloads by parameter type.");
+ StringAssert.Contains(registry, "SupportsGeneratedDescriptors = true");
+ StringAssert.Contains(registry, "IsDescriptorSupported = true");
+ StringAssert.Contains(registry, "AreGeneratedDescriptorsComplete = false");
string registration = File.ReadAllText(generatedFiles.Single(path => path.EndsWith("MSTestReflectionMetadata.Registration.g.cs", StringComparison.Ordinal)));
StringAssert.Contains(registration, "availableMethods ??= type.GetMethods(memberFlags)");
StringAssert.Contains(registration, "ResolveMethod(availableMethods, method.Name, method.ParameterTypes)");
StringAssert.Contains(registration, "methodInfo.GetCustomAttributes(typeof(AsyncStateMachineAttribute), inherit: false)");
StringAssert.Contains(registration, "methodInfo.GetCustomAttributes(typeof(DebuggerStepThroughAttribute), inherit: false)");
+ StringAssert.Contains(registration, "descriptorTestMethods[type] = descriptorMethodRoots.ToArray()");
+ StringAssert.Contains(registration, "descriptorTestMethods, descriptorCompleteTypes.ToArray()");
// Behavioral evidence: tests still discover and run when the source-generated
// ReflectionMetadataHook is the only metadata provider wired in at module init.
diff --git a/test/UnitTests/MSTest.SourceGeneration.UnitTests/MSTestReflectionMetadataGeneratorTests.cs b/test/UnitTests/MSTest.SourceGeneration.UnitTests/MSTestReflectionMetadataGeneratorTests.cs
index 046129fdb6..88a090f4b7 100644
--- a/test/UnitTests/MSTest.SourceGeneration.UnitTests/MSTestReflectionMetadataGeneratorTests.cs
+++ b/test/UnitTests/MSTest.SourceGeneration.UnitTests/MSTestReflectionMetadataGeneratorTests.cs
@@ -111,6 +111,7 @@ public static void Register(System.Reflection.Assembly assembly, System.Type[] t
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) { }
}
}
""";
@@ -151,6 +152,7 @@ public class MyTests
[TestMethod]
public void Test1() { }
}
+
}
""";
@@ -164,6 +166,264 @@ public void Test1() { }
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]
@@ -2420,7 +2680,7 @@ public void NotATest() { }
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);");
+ 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.
@@ -3131,7 +3391,7 @@ public void Test1() { }
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);");
+ 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)
diff --git a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Discovery/AssemblyEnumeratorWrapperTests.cs b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Discovery/AssemblyEnumeratorWrapperTests.cs
index 2da82a5af1..c2ab6fa1d6 100644
--- a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Discovery/AssemblyEnumeratorWrapperTests.cs
+++ b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Discovery/AssemblyEnumeratorWrapperTests.cs
@@ -5,8 +5,10 @@
using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter;
using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Discovery;
+using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel;
using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface;
using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Resources;
+using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.SourceGeneration;
using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.UnitTests.TestableImplementations;
using Moq;
@@ -83,6 +85,50 @@ public void GetTestsShouldReturnTestElements()
tests.Any(t => t.TestMethod.Name == "ValidTestMethod").Should().BeTrue();
}
+#if NETCOREAPP
+ [DoNotParallelize]
+ public void GetTestsShouldSelectGeneratedDescriptorsOnlyForMtp()
+ {
+ Type testType = typeof(GeneratedDescriptorTestClass);
+ MethodInfo method = testType.GetMethod(nameof(GeneratedDescriptorTestClass.GeneratedDescriptorTestMethod))!;
+ string assemblyPath = Assembly.GetExecutingAssembly().Location;
+ _mockTestSourceHandler
+ .Setup(handler => handler.IsAssemblyReferenced(It.IsAny(), assemblyPath))
+ .Returns(true);
+
+ PlatformServiceProvider.Instance = null;
+ try
+ {
+ ReflectionMetadataHook.Register(
+ Assembly.GetExecutingAssembly(),
+ [testType],
+ new Dictionary { [testType] = [method] },
+ new Dictionary { [testType] = [new TestClassAttribute()] },
+ [],
+ new Dictionary { [method] = [new TestMethodAttribute()] },
+ new Dictionary>(),
+ new Dictionary(),
+ new Dictionary>(),
+ new Dictionary { [testType] = [method] },
+ [testType]);
+
+ UnitTestElement mtpTest = AssemblyEnumeratorWrapper
+ .GetTests(assemblyPath, null, _mockTestSourceHandler.Object, isMTP: true, out _)!
+ .Single(test => test.TestMethod.MethodInfo == method);
+ UnitTestElement vstestTest = AssemblyEnumeratorWrapper
+ .GetTests(assemblyPath, null, _mockTestSourceHandler.Object, isMTP: false, out _)!
+ .Single(test => test.TestMethod.MethodInfo == method);
+
+ mtpTest.IsFromGeneratedDescriptor.Should().BeTrue();
+ vstestTest.IsFromGeneratedDescriptor.Should().BeFalse();
+ }
+ finally
+ {
+ PlatformServiceProvider.Instance = _testablePlatformServiceProvider;
+ }
+ }
+#endif
+
public void GetTestsShouldCreateAnIsolatedInstanceOfAssemblyEnumerator()
{
string assemblyName = Assembly.GetExecutingAssembly().FullName!;
@@ -147,5 +193,14 @@ public void ValidTestMethod()
}
}
+ [TestClass]
+ public class GeneratedDescriptorTestClass
+ {
+ [TestMethod]
+ public void GeneratedDescriptorTestMethod()
+ {
+ }
+ }
+
#endregion
}
diff --git a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Discovery/TypeEnumeratorTests.cs b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Discovery/TypeEnumeratorTests.cs
index 9730abf1ce..6ca91c45cf 100644
--- a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Discovery/TypeEnumeratorTests.cs
+++ b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Discovery/TypeEnumeratorTests.cs
@@ -9,6 +9,7 @@
using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Extensions;
using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Helpers;
using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices;
+using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.SourceGeneration;
using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.UnitTests.TestableImplementations;
using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter;
using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging;
@@ -74,6 +75,96 @@ public void EnumerateShouldReturnEmptyCollectionWhenNoValidTestMethodsExist()
tests.Should().HaveCount(0);
}
+ public void EnumerateShouldUseCompleteGeneratedDescriptorsWithoutLegacyMethodValidation()
+ {
+ SetupTestClassAndTestMethods(isValidTestClass: true, isValidTestMethod: false);
+ MethodInfo method = typeof(DescriptorTestClass).GetMethod(nameof(DescriptorTestClass.PlainTest))!;
+ SetGeneratedDescriptorOperations(
+ typeof(DescriptorTestClass),
+ [method],
+ areAllTestMethodsSupported: true);
+ TypeEnumerator typeEnumerator = GetTypeEnumeratorInstance(typeof(DescriptorTestClass), Assembly.GetExecutingAssembly().Location);
+
+ List? tests = typeEnumerator.Enumerate(_warnings, useGeneratedDescriptors: true);
+
+ tests.Should().ContainSingle();
+ tests[0].TestMethod.MethodInfo.Should().BeSameAs(method);
+ tests[0].IsFromGeneratedDescriptor.Should().BeTrue();
+ _mockTestMethodValidator.Verify(
+ validator => validator.IsValidTestMethod(It.IsAny(), It.IsAny(), It.IsAny>()),
+ Times.Never);
+ }
+
+ public void EnumerateShouldFallBackPerMethodWhenGeneratedDescriptorsAreIncomplete()
+ {
+ _mockTypeValidator.Setup(validator => validator.IsValidTestClass(It.IsAny(), It.IsAny>()))
+ .Returns(true);
+ _mockTestMethodValidator.Setup(
+ validator => validator.IsValidTestMethod(It.IsAny(), It.IsAny(), It.IsAny>()))
+ .Returns((MethodInfo method, Type _, ICollection _) => method.Name == nameof(DescriptorTestClass.FallbackTest));
+ MethodInfo descriptorMethod = typeof(DescriptorTestClass).GetMethod(nameof(DescriptorTestClass.PlainTest))!;
+ SetGeneratedDescriptorOperations(
+ typeof(DescriptorTestClass),
+ [descriptorMethod],
+ areAllTestMethodsSupported: false);
+ TypeEnumerator typeEnumerator = GetTypeEnumeratorInstance(typeof(DescriptorTestClass), Assembly.GetExecutingAssembly().Location);
+
+ List? tests = typeEnumerator.Enumerate(_warnings, useGeneratedDescriptors: true);
+
+ tests.Should().HaveCount(2);
+ tests.Single(test => test.TestMethod.Name == nameof(DescriptorTestClass.PlainTest))
+ .IsFromGeneratedDescriptor.Should().BeTrue();
+ tests.Single(test => test.TestMethod.Name == nameof(DescriptorTestClass.FallbackTest))
+ .IsFromGeneratedDescriptor.Should().BeFalse();
+ _mockTestMethodValidator.Verify(
+ validator => validator.IsValidTestMethod(descriptorMethod, It.IsAny(), It.IsAny>()),
+ Times.Never);
+ }
+
+ public void EnumerateShouldIgnoreGeneratedDescriptorsOutsideNativeMtp()
+ {
+ SetupTestClassAndTestMethods(isValidTestClass: true, isValidTestMethod: true);
+ MethodInfo descriptorMethod = typeof(DescriptorTestClass).GetMethod(nameof(DescriptorTestClass.PlainTest))!;
+ SetGeneratedDescriptorOperations(
+ typeof(DescriptorTestClass),
+ [descriptorMethod],
+ areAllTestMethodsSupported: true);
+ TypeEnumerator typeEnumerator = GetTypeEnumeratorInstance(typeof(DescriptorTestClass), Assembly.GetExecutingAssembly().Location);
+
+ List? tests = typeEnumerator.Enumerate(_warnings);
+
+ tests.Should().NotBeEmpty();
+ tests.Should().OnlyContain(test => !test.IsFromGeneratedDescriptor);
+ _mockTestMethodValidator.Verify(
+ validator => validator.IsValidTestMethod(descriptorMethod, It.IsAny(), It.IsAny>()),
+ Times.Once);
+ }
+
+ public void EnumerateShouldSelectPlainAndDataRowDescriptorsWhenComplete()
+ {
+ Type type = typeof(DescriptorCompleteTestClass);
+ MethodInfo[] methods =
+ [
+ type.GetMethod(nameof(DescriptorCompleteTestClass.PlainTest))!,
+ type.GetMethod(nameof(DescriptorCompleteTestClass.DataRowTest))!,
+ ];
+ SetGeneratedDescriptorOperations(type, methods, areAllTestMethodsSupported: true);
+ SetupTestClassAndTestMethods(isValidTestClass: true, isValidTestMethod: false);
+ TypeEnumerator typeEnumerator = GetTypeEnumeratorInstance(type, Assembly.GetExecutingAssembly().Location);
+
+ List? tests = typeEnumerator.Enumerate(_warnings, useGeneratedDescriptors: true);
+
+ tests.Should().HaveCount(2);
+ tests.Should().OnlyContain(test => test.IsFromGeneratedDescriptor);
+ tests.Select(test => test.TestMethod.Name)
+ .Should().BeEquivalentTo(nameof(DescriptorCompleteTestClass.PlainTest), nameof(DescriptorCompleteTestClass.DataRowTest));
+ tests.Select(test => test.TestMethod.Name)
+ .Should().NotContain(nameof(DescriptorCompleteTestClass.FallbackOnlyTest));
+ _mockTestMethodValidator.Verify(
+ validator => validator.IsValidTestMethod(It.IsAny(), It.IsAny(), It.IsAny>()),
+ Times.Never);
+ }
+
#endregion
#region GetTests tests
@@ -592,6 +683,27 @@ private TypeEnumerator GetTypeEnumeratorInstance(Type type, string assemblyName)
_mockTypeValidator.Object,
_mockTestMethodValidator.Object);
+ private void SetGeneratedDescriptorOperations(Type type, MethodInfo[] descriptorMethods, bool areAllTestMethodsSupported)
+ {
+ Attribute[] typeAttributes = type.GetCustomAttributes(inherit: true).OfType().ToArray();
+ var methodAttributes = new Dictionary();
+ foreach (MethodInfo method in type.GetMethods())
+ {
+ methodAttributes[method] = method.GetCustomAttributes(inherit: true).OfType().ToArray();
+ }
+
+ var provider = new SourceGeneratedReflectionDataProvider
+ {
+ TypeAttributes = new Dictionary { [type] = typeAttributes },
+ TypeMethodAttributes = methodAttributes,
+ DescriptorTestMethods = new Dictionary { [type] = descriptorMethods },
+ DescriptorCompleteTypes = areAllTestMethodsSupported
+ ? new Dictionary { [type] = true }
+ : [],
+ };
+ _testablePlatformServiceProvider.SetReflectionOperations(new SourceGeneratedReflectionOperations(provider));
+ }
+
#endregion
}
@@ -640,6 +752,41 @@ public class DummySecondHidingTestClass : DummyOverridingTestClass
}
}
+[TestClass]
+public class DescriptorTestClass
+{
+ [TestMethod]
+ public void PlainTest()
+ {
+ }
+
+ [TestMethod]
+ [TestCategory("fallback")]
+ public void FallbackTest()
+ {
+ }
+}
+
+[TestClass]
+public class DescriptorCompleteTestClass
+{
+ [TestMethod]
+ public void PlainTest()
+ {
+ }
+
+ [TestMethod]
+ [DataRow(1)]
+ public void DataRowTest(int value)
+ {
+ }
+
+ [TestMethod]
+ public void FallbackOnlyTest()
+ {
+ }
+}
+
[TestClass]
internal class DummyGenericTestClass
{
diff --git a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/SourceGeneration/SourceGeneratedReflectionOperationsTests.cs b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/SourceGeneration/SourceGeneratedReflectionOperationsTests.cs
index c296e2a208..4848bec0c8 100644
--- a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/SourceGeneration/SourceGeneratedReflectionOperationsTests.cs
+++ b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/SourceGeneration/SourceGeneratedReflectionOperationsTests.cs
@@ -59,6 +59,47 @@ public void GetTestMethodInvoker_ReturnsNull_WhenMethodNotRegistered()
operations.GetTestMethodInvoker(unregistered).Should().BeNull();
}
+ public void TryGetTestMethodDescriptors_ReturnsRegisteredMethodsAndCompleteness()
+ {
+ MethodInfo method = typeof(Sample).GetMethod(nameof(Sample.Add))!;
+ var provider = new SourceGeneratedReflectionDataProvider
+ {
+ DescriptorTestMethods = new Dictionary { [typeof(Sample)] = [method] },
+ DescriptorCompleteTypes = new Dictionary { [typeof(Sample)] = true },
+ };
+ var operations = new SourceGeneratedReflectionOperations(provider);
+
+ operations.TryGetTestMethodDescriptors(typeof(Sample), out MethodInfo[]? methods, out bool isComplete)
+ .Should().BeTrue();
+ methods.Should().ContainSingle().Which.Should().BeSameAs(method);
+ isComplete.Should().BeTrue();
+ }
+
+ public void TryGetTestMethodDescriptors_RetainsPerMethodFallbackWhenRegistrationIsIncomplete()
+ {
+ MethodInfo add = typeof(Sample).GetMethod(nameof(Sample.Add))!;
+ MethodInfo subtract = typeof(Sample).GetMethod(nameof(Sample.Subtract))!;
+ var composite = new CompositeSourceGeneratedReflectionDataProvider();
+ composite.Add(new SourceGeneratedReflectionDataProvider
+ {
+ DescriptorTestMethods = new Dictionary { [typeof(Sample)] = [add] },
+ DescriptorCompleteTypes = new Dictionary { [typeof(Sample)] = true },
+ });
+ composite.Add(new SourceGeneratedReflectionDataProvider
+ {
+ DescriptorTestMethods = new Dictionary { [typeof(Sample)] = [subtract] },
+ DescriptorCompleteTypes = new Dictionary { [typeof(Sample)] = false },
+ });
+ var operations = new SourceGeneratedReflectionOperations(composite);
+
+ operations.TryGetTestMethodDescriptors(typeof(Sample), out MethodInfo[]? methods, out bool isComplete)
+ .Should().BeTrue();
+ methods.Should().HaveCount(2);
+ methods.Should().Contain(add);
+ methods.Should().Contain(subtract);
+ isComplete.Should().BeFalse();
+ }
+
public void GetConstructorInvoker_CreatesInstance_WithoutActivator()
{
SourceGeneratedReflectionDataProvider.ConstructorInvoker[] invokers =
@@ -338,6 +379,12 @@ public int Add(int first, int second)
LastSum = first + second;
return LastSum;
}
+
+ public int Subtract(int first, int second)
+ {
+ LastSum = first - second;
+ return LastSum;
+ }
}
[Marker("reflection")]
diff --git a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/TestableImplementations/MockableReflectionOperations.cs b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/TestableImplementations/MockableReflectionOperations.cs
index c63d5d2e73..375237953b 100644
--- a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/TestableImplementations/MockableReflectionOperations.cs
+++ b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/TestableImplementations/MockableReflectionOperations.cs
@@ -58,6 +58,9 @@ public static MockableReflectionOperations Create(Mock mo
public Func? GetConstructorInvoker(Type type)
=> mock.Object.GetConstructorInvoker(type);
+ public bool TryGetTestMethodDescriptors(Type type, [NotNullWhen(true)] out MethodInfo[]? methods, out bool areAllTestMethodsSupported)
+ => mock.Object.TryGetTestMethodDescriptors(type, out methods, out areAllTestMethodsSupported);
+
public Action? GetPropertySetter(PropertyInfo property)
=> mock.Object.GetPropertySetter(property);
diff --git a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/TestableImplementations/TestablePlatformServiceProvider.cs b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/TestableImplementations/TestablePlatformServiceProvider.cs
index baf709a8e7..abdce19990 100644
--- a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/TestableImplementations/TestablePlatformServiceProvider.cs
+++ b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/TestableImplementations/TestablePlatformServiceProvider.cs
@@ -94,4 +94,11 @@ public void SetupMockReflectionOperations(Mock mock)
MockReflectionOperations = mock;
_reflectionOperationsWrapper = MockableReflectionOperations.Create(mock);
}
+
+ public void SetReflectionOperations(IReflectionOperations reflectionOperations)
+ {
+ MockReflectionOperations = null!;
+ _reflectionOperationsWrapper = null;
+ ReflectionOperations = reflectionOperations;
+ }
}