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; + } }