diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Discovery/AssemblyEnumerator.cs b/src/Adapter/MSTestAdapter.PlatformServices/Discovery/AssemblyEnumerator.cs index 22da848f4a..5c33cc15a9 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Discovery/AssemblyEnumerator.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Discovery/AssemblyEnumerator.cs @@ -222,7 +222,7 @@ private static bool TryUnfoldITestDataSources(UnitTestElement test, DiscoveryTes // PERF: Access the cached attribute array directly to avoid allocating two iterator state machines // for GetAttributes().OfType() on every data-driven test during discovery. - Attribute[] allAttributes = ReflectHelper.Instance.GetCustomAttributesCached(testMethodInfo.MethodInfo); + Attribute[] allAttributes = ReflectHelper.GetCustomAttributesCached(testMethodInfo.MethodInfo); // We need to use a temporary list to avoid adding tests to the main list if we fail to expand any data source. List tempListOfTests = []; diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Discovery/TypeEnumerator.cs b/src/Adapter/MSTestAdapter.PlatformServices/Discovery/TypeEnumerator.cs index 238640f70a..f7a63e0e05 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Discovery/TypeEnumerator.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Discovery/TypeEnumerator.cs @@ -4,6 +4,7 @@ using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Helpers; using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Helpers; +using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Discovery; @@ -134,17 +135,18 @@ internal UnitTestElement GetTestFromMethod(MethodInfo method, bool classDisables // TODO: For every test method in a class, we are asking reflect helper multiple times for the same // information (like test categories, traits, deployment items) which is not optimal. + IReflectionOperations reflectionOperations = PlatformServiceProvider.Instance.ReflectionOperations; var testElement = new UnitTestElement(testMethod) { - TestCategory = _reflectHelper.GetTestCategories(method, _type), + TestCategory = reflectionOperations.GetTestCategories(method, _type), DoNotParallelize = classDisablesParallelization || _reflectHelper.IsAttributeDefined(method), #if !WINDOWS_UWP && !WIN_UI DeploymentItems = PlatformServiceProvider.Instance.TestDeployment.GetDeploymentItems(method, _type, warnings), #endif - Traits = [.. _reflectHelper.GetTestPropertiesAsTraits(method)], + Traits = [.. reflectionOperations.GetTestPropertiesAsTraits(method)], }; - Attribute[] attributes = _reflectHelper.GetCustomAttributesCached(method); + Attribute[] attributes = reflectionOperations.GetCustomAttributesCached(method); TestMethodAttribute? testMethodAttribute = null; List? workItemIds = null; diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestAssemblySettingsProvider.cs b/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestAssemblySettingsProvider.cs index 9c396ef9a5..af9aa3154d 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestAssemblySettingsProvider.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestAssemblySettingsProvider.cs @@ -3,8 +3,8 @@ using System.Security; -using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Helpers; using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; +using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Execution; @@ -34,9 +34,10 @@ internal TestAssemblySettings GetSettings(string source) // Load the source. Assembly testAssembly = PlatformServiceProvider.Instance.FileOperations.LoadAssembly(source); - ParallelizeAttribute? parallelizeAttribute = ReflectHelper.GetParallelizeAttribute(testAssembly); + IReflectionOperations reflectionOperations = PlatformServiceProvider.Instance.ReflectionOperations; + ParallelizeAttribute? parallelizeAttribute = reflectionOperations.GetSingleAttributeOrDefault(testAssembly); - if (parallelizeAttribute != null) + if (parallelizeAttribute is not null) { testAssemblySettings.Workers = parallelizeAttribute.Workers; testAssemblySettings.Scope = parallelizeAttribute.Scope; @@ -47,7 +48,7 @@ internal TestAssemblySettings GetSettings(string source) } } - testAssemblySettings.CanParallelizeAssembly = !ReflectHelper.IsDoNotParallelizeSet(testAssembly); + testAssemblySettings.CanParallelizeAssembly = !reflectionOperations.IsAttributeDefined(testAssembly); return testAssemblySettings; } diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestMethodInfo.cs b/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestMethodInfo.cs index 9a71fce426..f9c86c74b6 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestMethodInfo.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestMethodInfo.cs @@ -2,7 +2,6 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. 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.Interface; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -99,7 +98,7 @@ internal ITestContext TestContext /// /// An array of the attributes. public Attribute[]? GetAllAttributes() - => [.. ReflectHelper.Instance.GetAttributes(MethodInfo)]; + => [.. PlatformServiceProvider.Instance.ReflectionOperations.GetAttributes(MethodInfo)]; /// /// Gets all attributes of the test method. @@ -108,7 +107,7 @@ internal ITestContext TestContext /// An array of the attributes. public TAttributeType[] GetAttributes() where TAttributeType : Attribute - => [.. ReflectHelper.Instance.GetAttributes(MethodInfo)]; + => [.. PlatformServiceProvider.Instance.ReflectionOperations.GetAttributes(MethodInfo)]; /// /// Execute test method. Capture failures, handle async and return result. @@ -167,7 +166,7 @@ private TestMethodAttribute GetTestMethodAttribute() { // Get the derived TestMethod attribute from reflection. // It should be non-null as it was already validated by IsValidTestMethod. - TestMethodAttribute testMethodAttribute = ReflectHelper.Instance.GetSingleAttributeOrDefault(MethodInfo)!; + TestMethodAttribute testMethodAttribute = PlatformServiceProvider.Instance.ReflectionOperations.GetSingleAttributeOrDefault(MethodInfo)!; // Get the derived TestMethod attribute from Extended TestClass Attribute // If the extended TestClass Attribute doesn't have extended TestMethod attribute then base class returns back the original testMethod Attribute @@ -183,22 +182,21 @@ private TestMethodAttribute GetTestMethodAttribute() /// private RetryBaseAttribute? GetRetryAttribute() { - // Inline the GetSingleAttributeOrDefault pattern to avoid allocating the - // yield-return state machine that GetAttributes() would create. - RetryBaseAttribute? foundAttribute = null; - foreach (Attribute attribute in ReflectHelper.Instance.GetCustomAttributesCached(MethodInfo)) + Attribute[] attributes = PlatformServiceProvider.Instance.ReflectionOperations.GetCustomAttributesCached(MethodInfo); + RetryBaseAttribute? result = null; + foreach (Attribute attribute in attributes) { if (attribute is RetryBaseAttribute retryAttribute) { - if (foundAttribute is not null) + if (result is not null) { ThrowMultipleAttributesException(nameof(RetryBaseAttribute)); } - foundAttribute = retryAttribute; + result = retryAttribute; } } - return foundAttribute; + return result; } } diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestMethodRunner.cs b/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestMethodRunner.cs index 79bda21dcf..f0bc06bbe6 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestMethodRunner.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestMethodRunner.cs @@ -2,7 +2,6 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Extensions; - using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Helpers; using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; @@ -181,7 +180,7 @@ private async Task TryExecuteDataSourceBasedTestsAsync(List re // Iterate cached attributes directly to preserve the previous semantics: // execute only when there is exactly one DataSourceAttribute. bool hasSingleDataSource = false; - foreach (Attribute attribute in ReflectHelper.Instance.GetCustomAttributesCached(_testMethodInfo.MethodInfo)) + foreach (Attribute attribute in PlatformServiceProvider.Instance.ReflectionOperations.GetCustomAttributesCached(_testMethodInfo.MethodInfo)) { if (attribute is not DataSourceAttribute) { @@ -208,7 +207,7 @@ private async Task TryExecuteDataSourceBasedTestsAsync(List re private async Task TryExecuteFoldedDataDrivenTestsAsync(List results) { bool hasTestDataSource = false; - foreach (Attribute attribute in ReflectHelper.Instance.GetCustomAttributesCached(_testMethodInfo.MethodInfo)) + foreach (Attribute attribute in PlatformServiceProvider.Instance.ReflectionOperations.GetCustomAttributesCached(_testMethodInfo.MethodInfo)) { if (attribute is not UTF.ITestDataSource testDataSource) { diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Helpers/AttributeHelpers.cs b/src/Adapter/MSTestAdapter.PlatformServices/Helpers/AttributeHelpers.cs index a9fc301f7c..f1a740feaa 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Helpers/AttributeHelpers.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Helpers/AttributeHelpers.cs @@ -9,7 +9,7 @@ internal static class AttributeExtensions { public static bool IsIgnored(this ICustomAttributeProvider type, out string? ignoreMessage) { - Attribute[] allAttributes = ReflectHelper.Instance.GetCustomAttributesCached(type); + Attribute[] allAttributes = PlatformServiceProvider.Instance.ReflectionOperations.GetCustomAttributesCached(type); // Fast path: no ConditionBaseAttribute present (common case) → zero allocations bool hasConditionAttribute = false; diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Helpers/ReflectHelper.cs b/src/Adapter/MSTestAdapter.PlatformServices/Helpers/ReflectHelper.cs index fb4a09794e..586ca03fd2 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Helpers/ReflectHelper.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Helpers/ReflectHelper.cs @@ -3,6 +3,7 @@ using System.Security; +using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; using Microsoft.VisualStudio.TestPlatform.ObjectModel; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -15,29 +16,27 @@ internal class ReflectHelper : MarshalByRefObject private static readonly Lazy InstanceValue = new(() => new()); #pragma warning restore RS0030 // Do not use banned APIs - // PERF: This was moved from Dictionary> to Concurrent - // storing an array allows us to store multiple attributes of the same type if we find them. It also has lower memory footprint, and is faster - // when we are going through the whole collection. Giving us overall better perf. - private readonly ConcurrentDictionary _attributeCache = []; - + // PERF: Attribute caching is now centralized in ReflectionOperations._attributeCache. + // ReflectHelper delegates to PlatformServiceProvider.Instance.ReflectionOperations.GetCustomAttributesCached + // so that discovery and execution paths share a single cache, avoiding double memory usage. public static ReflectHelper Instance => InstanceValue.Value; /// /// Checks to see if a member or type is decorated with the given attribute, or an attribute that derives from it. e.g. [MyTestClass] from [TestClass] will match if you look for [TestClass]. The inherit parameter does not impact this checking. /// /// Attribute to search for. - /// Member to inspect for attributes. - /// True if the attribute of the specified type is defined on this member or a class. - public virtual /* for testing */ bool IsAttributeDefined(MemberInfo memberInfo) + /// The type, assembly or method to inspect for attributes. + /// True if the attribute of the specified type is defined. + public virtual /* for testing */ bool IsAttributeDefined(ICustomAttributeProvider attributeProvider) where TAttribute : Attribute { - if (memberInfo is null) + if (attributeProvider is null) { - throw new ArgumentNullException(nameof(memberInfo)); + throw new ArgumentNullException(nameof(attributeProvider)); } // Get all attributes on the member. - Attribute[] attributes = GetCustomAttributesCached(memberInfo); + Attribute[] attributes = GetCustomAttributesCached(attributeProvider); // Try to find the attribute that is derived from baseAttrType. foreach (Attribute attribute in attributes) @@ -141,7 +140,7 @@ internal static bool MatchReturnType(MethodInfo method, Type returnType) /// The member to inspect. /// The reflected type that owns . /// Categories defined. - internal string[] GetTestCategories(MemberInfo categoryAttributeProvider, Type owningType) + internal static string[] GetTestCategories(MemberInfo categoryAttributeProvider, Type owningType) { Attribute[] methodAttributes = GetCustomAttributesCached(categoryAttributeProvider); Attribute[] typeAttributes = GetCustomAttributesCached(owningType); @@ -228,7 +227,7 @@ internal static bool IsDoNotParallelizeSet(Assembly assembly) /// /// The member to inspect. /// List of traits. - internal Trait[] GetTestPropertiesAsTraits(MethodInfo testPropertyProvider) + internal static Trait[] GetTestPropertiesAsTraits(MethodInfo testPropertyProvider) { Attribute[] attributesFromMethod = GetCustomAttributesCached(testPropertyProvider); Attribute[] attributesFromClass = testPropertyProvider.ReflectedType is { } testClass ? GetCustomAttributesCached(testClass) : []; @@ -307,7 +306,7 @@ internal Trait[] GetTestPropertiesAsTraits(MethodInfo testPropertyProvider) /// The member to inspect. /// The action to perform. /// The state to pass to action. - internal void PerformActionOnAttribute(ICustomAttributeProvider attributeProvider, Action action, TState? state) + internal static void PerformActionOnAttribute(ICustomAttributeProvider attributeProvider, Action action, TState? state) where TAttributeType : Attribute { Attribute[] attributes = GetCustomAttributesCached(attributeProvider); @@ -324,69 +323,20 @@ internal void PerformActionOnAttribute(ICustomAttributeP /// /// Gets and caches the attributes for the given type, or method. + /// Delegates to so that + /// discovery and execution share a single attribute cache. /// /// The member to inspect. /// attributes defined. - internal Attribute[] GetCustomAttributesCached(ICustomAttributeProvider attributeProvider) - { - // If the information is cached, then use it otherwise populate the cache using - // the reflection APIs. - return _attributeCache.GetOrAdd(attributeProvider, GetAttributes); - - // We are avoiding func allocation here. - static Attribute[] GetAttributes(ICustomAttributeProvider attributeProvider) - { - // Populate the cache - try - { - object[]? attributes = NotCachedReflectionAccessor.GetCustomAttributesNotCached(attributeProvider); - return attributes is null ? [] : attributes as Attribute[] ?? [.. attributes.Cast()]; - } - catch (Exception ex) - { - // Get the exception description - string description; - try - { - // Can throw if the Message or StackTrace properties throw exceptions - description = ex.ToString(); - } - catch (Exception ex2) - { - description = string.Format(CultureInfo.CurrentCulture, Resource.ExceptionOccuredWhileGettingTheExceptionDescription, ex.GetType().FullName, ex2.GetType().FullName); // ex.GetType().FullName + - } + internal static Attribute[] GetCustomAttributesCached(ICustomAttributeProvider attributeProvider) + => PlatformServiceProvider.Instance.ReflectionOperations.GetCustomAttributesCached(attributeProvider); - if (PlatformServiceProvider.Instance.AdapterTraceLogger.IsWarningEnabled) - { - PlatformServiceProvider.Instance.AdapterTraceLogger.Warning(Resource.FailedToGetCustomAttribute, attributeProvider.GetType().FullName!, description); - } - - return []; - } - } - } - - /// - /// Reflection helper that is accessing Reflection directly, and won't cache the results. - /// - internal static class NotCachedReflectionAccessor + internal static /* for tests */ void ClearCache() { - /// - /// Get custom attributes on a member without cache. Be CAREFUL where you use this, repeatedly accessing reflection without caching the results degrades the performance. - /// - /// Member for which attributes needs to be retrieved. - /// All attributes of give type on member. - public static object[]? GetCustomAttributesNotCached(ICustomAttributeProvider attributeProvider) + // Delegate to the shared cache in ReflectionOperations. + if (PlatformServiceProvider.Instance?.ReflectionOperations is ReflectionOperations reflectionOperations) { - object[] attributesArray = attributeProvider is MemberInfo memberInfo - ? PlatformServiceProvider.Instance.ReflectionOperations.GetCustomAttributes(memberInfo) - : PlatformServiceProvider.Instance.ReflectionOperations.GetCustomAttributes((Assembly)attributeProvider, typeof(Attribute)); - - return attributesArray; // TODO: Investigate if we rely on NRE + reflectionOperations.ClearCache(); } } - - internal /* for tests */ void ClearCache() - // Tests manipulate the platform reflection provider, and we end up caching different attributes than the class / method actually has. - => _attributeCache.Clear(); } diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Helpers/ReflectionHelper.cs b/src/Adapter/MSTestAdapter.PlatformServices/Helpers/ReflectionHelper.cs new file mode 100644 index 0000000000..721f5afd7a --- /dev/null +++ b/src/Adapter/MSTestAdapter.PlatformServices/Helpers/ReflectionHelper.cs @@ -0,0 +1,112 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; +using Microsoft.VisualStudio.TestPlatform.ObjectModel; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Helpers; + +/// +/// Helper methods for extracting test metadata from reflection operations. +/// +internal static class ReflectionHelper +{ + /// + /// Get categories applied to the test method. + /// + /// The reflection operations to use. + /// The member to inspect. + /// The reflected type that owns . + /// Categories defined. + public static string[] GetTestCategories(this IReflectionOperations reflectionOperations, MemberInfo categoryAttributeProvider, Type owningType) + { + // Iterate the cached attribute arrays directly to avoid LINQ iterator/state-machine + // allocations. This follows the same allocation-free pattern used by GetTestPropertiesAsTraits. + Attribute[] methodAttributes = reflectionOperations.GetCustomAttributesCached(categoryAttributeProvider); + Attribute[] typeAttributes = reflectionOperations.GetCustomAttributesCached(owningType); + Attribute[] assemblyAttributes = reflectionOperations.GetCustomAttributesCached(owningType.Assembly); + + List? categories = null; + + foreach (Attribute attribute in methodAttributes) + { + if (attribute is TestCategoryBaseAttribute categoryAttr) + { + (categories ??= []).AddRange(categoryAttr.TestCategories); + } + } + + foreach (Attribute attribute in typeAttributes) + { + if (attribute is TestCategoryBaseAttribute categoryAttr) + { + (categories ??= []).AddRange(categoryAttr.TestCategories); + } + } + + foreach (Attribute attribute in assemblyAttributes) + { + if (attribute is TestCategoryBaseAttribute categoryAttr) + { + (categories ??= []).AddRange(categoryAttr.TestCategories); + } + } + + return categories is null ? [] : [.. categories]; + } + + /// + /// KeyValue pairs that are provided by TestPropertyAttributes of the given test method. + /// + /// The reflection operations to use. + /// The member to inspect. + /// List of traits. + public static Trait[] GetTestPropertiesAsTraits(this IReflectionOperations reflectionOperations, MethodInfo testPropertyProvider) + { + Attribute[] attributesFromMethod = reflectionOperations.GetCustomAttributesCached(testPropertyProvider); + Attribute[] attributesFromClass = testPropertyProvider.ReflectedType is { } testClass ? reflectionOperations.GetCustomAttributesCached(testClass) : []; + int countTestPropertyAttribute = 0; + foreach (Attribute attribute in attributesFromMethod) + { + if (attribute is TestPropertyAttribute) + { + countTestPropertyAttribute++; + } + } + + foreach (Attribute attribute in attributesFromClass) + { + if (attribute is TestPropertyAttribute) + { + countTestPropertyAttribute++; + } + } + + if (countTestPropertyAttribute == 0) + { + // This is the common case that we optimize for. This method used to be an iterator (uses yield return) which is allocating unnecessarily in common cases. + return []; + } + + var traits = new Trait[countTestPropertyAttribute]; + int index = 0; + foreach (Attribute attribute in attributesFromMethod) + { + if (attribute is TestPropertyAttribute testProperty) + { + traits[index++] = new Trait(testProperty.Name, testProperty.Value); + } + } + + foreach (Attribute attribute in attributesFromClass) + { + if (attribute is TestPropertyAttribute testProperty) + { + traits[index++] = new Trait(testProperty.Name, testProperty.Value); + } + } + + return traits; + } +} diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Interfaces/IReflectionOperations.cs b/src/Adapter/MSTestAdapter.PlatformServices/Interfaces/IReflectionOperations.cs index b154b531cc..912c2f4f97 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Interfaces/IReflectionOperations.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Interfaces/IReflectionOperations.cs @@ -43,4 +43,60 @@ internal interface IReflectionOperations Type? GetType(Assembly assembly, string typeName); object? CreateInstance(Type type, object?[] parameters); + + /// + /// Checks to see if a member or type is decorated with the given attribute, or an attribute that derives from it. + /// + /// Attribute to search for. + /// The type, assembly or method to inspect for attributes. + /// True if the attribute of the specified type is defined. + bool IsAttributeDefined(ICustomAttributeProvider attributeProvider) + where TAttribute : Attribute; + + /// + /// Gets first attribute that matches the type. + /// Use this together with attribute that does not allow multiple and is sealed. In such case there cannot be more attributes, and this will avoid the cost of + /// checking for more than one attribute. + /// + /// Type of the attribute to find. + /// The type, assembly or method. + /// The attribute that is found or null. + TAttribute? GetFirstAttributeOrDefault(ICustomAttributeProvider attributeProvider) + where TAttribute : Attribute; + + /// + /// Gets first attribute that matches the type or is derived from it. + /// Use this together with attribute that does not allow multiple. In such case there cannot be more attributes, and this will avoid the cost of + /// checking for more than one attribute. + /// + /// Type of the attribute to find. + /// The type, assembly or method. + /// The attribute that is found or null. + /// Throws when multiple attributes are found (the attribute must allow multiple). + TAttribute? GetSingleAttributeOrDefault(ICustomAttributeProvider attributeProvider) + where TAttribute : Attribute; + + /// + /// Get attribute defined on a member which is of given type of subtype of given type. + /// + /// The attribute type. + /// The member to inspect. + /// An instance of the attribute. + IEnumerable GetAttributes(ICustomAttributeProvider attributeProvider) + where TAttributeType : Attribute; + + /// + /// Gets and caches the attributes for the given type, or method. + /// + /// The member to inspect. + /// Attributes defined. + Attribute[] GetCustomAttributesCached(ICustomAttributeProvider attributeProvider); + + /// + /// Checks whether the declaring type of the method is declared in the same assembly as the given type. + /// + /// The method to check. + /// The type whose assembly to compare against. + /// True if the method's declaring type is in the same assembly as . + bool IsMethodDeclaredInSameAssemblyAsType(MethodInfo method, Type type); } diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Services/ReflectionOperations.cs b/src/Adapter/MSTestAdapter.PlatformServices/Services/ReflectionOperations.cs index df7e49c9b2..4cb8ce6729 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Services/ReflectionOperations.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Services/ReflectionOperations.cs @@ -1,21 +1,27 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +using System.Security; + +using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; -#if NETFRAMEWORK -using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Utilities; -#endif +using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; /// /// This service is responsible for platform specific reflection operations. /// -internal sealed class ReflectionOperations : IReflectionOperations +internal sealed class ReflectionOperations : MarshalByRefObject, IReflectionOperations { private const BindingFlags DeclaredOnlyLookup = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly; private const BindingFlags Everything = BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static | BindingFlags.Instance; + // PERF: This was moved from Dictionary> to Concurrent + // storing an array allows us to store multiple attributes of the same type if we find them. It also has lower memory footprint, and is faster + // when we are going through the whole collection. Giving us overall better perf. + private readonly ConcurrentDictionary _attributeCache = []; + /// /// Gets all the custom attributes adorned on a member. /// @@ -23,20 +29,15 @@ internal sealed class ReflectionOperations : IReflectionOperations /// The list of attributes on the member. Empty list if none found. [return: NotNullIfNotNull(nameof(memberInfo))] public object[]? GetCustomAttributes(MemberInfo memberInfo) -#if NETFRAMEWORK - => [.. ReflectionOperationsNetFrameworkAttributeHelpers.GetCustomAttributes(memberInfo)]; -#else { object[] attributes = memberInfo.GetCustomAttributes(typeof(Attribute), inherit: true); - // Ensures that when the return of this method is used here: - // https://github.com/microsoft/testfx/blob/e101a9d48773cc935c7b536d25d378d9a3211fee/src/Adapter/MSTest.TestAdapter/Helpers/ReflectHelper.cs#L461 + // Ensures that when the return of this method is used with GetCustomAttributesCached // then we are already Attribute[] to avoid LINQ Cast and extra array allocation. // This assert is solely for performance. Nothing "functional" will go wrong if the assert failed. Debug.Assert(attributes is Attribute[], $"Expected Attribute[], found '{attributes.GetType()}'."); return attributes; } -#endif /// /// Gets all the custom attributes of a given type on an assembly. @@ -44,12 +45,8 @@ internal sealed class ReflectionOperations : IReflectionOperations /// The assembly. /// The attribute type. /// The list of attributes of the given type on the member. Empty list if none found. - public object[] GetCustomAttributes(Assembly assembly, Type type) => -#if NETFRAMEWORK - ReflectionOperationsNetFrameworkAttributeHelpers.GetCustomAttributes(assembly, type).ToArray(); -#else - assembly.GetCustomAttributes(type, inherit: true); -#endif + public object[] GetCustomAttributes(Assembly assembly, Type type) + => assembly.GetCustomAttributes(type, inherit: true); #pragma warning disable IL2070 // this' argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to 'target method'. #pragma warning disable IL2026 // Members attributed with RequiresUnreferencedCode may break when trimming @@ -92,4 +89,222 @@ public MethodInfo[] GetRuntimeMethods(Type type) #pragma warning restore IL2026 // Members attributed with RequiresUnreferencedCode may break when trimming #pragma warning restore IL2067 // 'target parameter' argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to 'target method'. #pragma warning restore IL2057 // Unrecognized value passed to the typeName parameter of 'System.Type.GetType(String)' + + /// + /// Checks to see if a member or type is decorated with the given attribute, or an attribute that derives from it. e.g. [MyTestClass] from [TestClass] will match if you look for [TestClass]. The inherit parameter does not impact this checking. + /// + /// Attribute to search for. + /// The type, assembly or method to inspect for attributes. + /// True if the attribute of the specified type is defined. + public bool IsAttributeDefined(ICustomAttributeProvider attributeProvider) + where TAttribute : Attribute + { + if (attributeProvider is null) + { + throw new ArgumentNullException(nameof(attributeProvider)); + } + + // Get all attributes on the member. + Attribute[] attributes = GetCustomAttributesCached(attributeProvider); + + // Try to find the attribute that is derived from baseAttrType. + foreach (Attribute attribute in attributes) + { + DebugEx.Assert(attribute != null, $"{nameof(ReflectionOperations)}.{nameof(GetCustomAttributesCached)}: internal error: wrong value in the attributes dictionary."); + + if (attribute is TAttribute) + { + return true; + } + } + + return false; + } + + /// + /// Returns object to be used for controlling lifetime, null means infinite lifetime. + /// + /// + /// The . + /// + [SecurityCritical] +#if NET5_0_OR_GREATER + [Obsolete("MarshalByRefObject.InitializeLifetimeService is obsolete in .NET 5+. This override is required to maintain infinite lifetime service.")] +#endif + public override object InitializeLifetimeService() => null!; + + /// + /// Gets first attribute that matches the type. + /// Use this together with attribute that does not allow multiple and is sealed. In such case there cannot be more attributes, and this will avoid the cost of + /// checking for more than one attribute. + /// + /// Type of the attribute to find. + /// The type, assembly or method. + /// The attribute that is found or null. + public TAttribute? GetFirstAttributeOrDefault(ICustomAttributeProvider attributeProvider) + where TAttribute : Attribute + { + // If the attribute is not sealed, then it can allow multiple, even if AllowMultiple is false. + // This happens when a derived type is also applied along with the base type. + // Or, if the derived type modifies the attribute usage to allow multiple. + // So we want to ensure this is only called for sealed attributes. + DebugEx.Assert(typeof(TAttribute).IsSealed, $"Expected '{typeof(TAttribute)}' to be sealed, but was not."); + + Attribute[] cachedAttributes = GetCustomAttributesCached(attributeProvider); + + foreach (Attribute cachedAttribute in cachedAttributes) + { + if (cachedAttribute is TAttribute cachedAttributeAsTAttribute) + { + return cachedAttributeAsTAttribute; + } + } + + return null; + } + + /// + /// Gets first attribute that matches the type or is derived from it. + /// Use this together with attribute that does not allow multiple. In such case there cannot be more attributes, and this will avoid the cost of + /// checking for more than one attribute. + /// + /// Type of the attribute to find. + /// The type, assembly or method. + /// The attribute that is found or null. + /// Throws when multiple attributes are found (the attribute must allow multiple). + public TAttribute? GetSingleAttributeOrDefault(ICustomAttributeProvider attributeProvider) + where TAttribute : Attribute + { + Attribute[] cachedAttributes = GetCustomAttributesCached(attributeProvider); + + TAttribute? foundAttribute = null; + foreach (Attribute cachedAttribute in cachedAttributes) + { + if (cachedAttribute is TAttribute cachedAttributeAsTAttribute) + { + if (foundAttribute is not null) + { + throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, Resource.DuplicateAttributeError, typeof(TAttribute))); + } + + foundAttribute = cachedAttributeAsTAttribute; + } + } + + return foundAttribute; + } + + /// + /// Get attribute defined on a method which is of given type of subtype of given type. + /// + /// The attribute type. + /// The member to inspect. + /// An instance of the attribute. + public IEnumerable GetAttributes(ICustomAttributeProvider attributeProvider) + where TAttributeType : Attribute + { + Attribute[] attributes = GetCustomAttributesCached(attributeProvider); + + // Try to find the attribute that is derived from baseAttrType. + foreach (Attribute attribute in attributes) + { + DebugEx.Assert(attribute != null, "ReflectionOperations.DefinesAttributeDerivedFrom: internal error: wrong value in the attributes dictionary."); + + if (attribute is TAttributeType attributeAsAttributeType) + { + yield return attributeAsAttributeType; + } + } + } + + /// + /// Gets and caches the attributes for the given type, or method. + /// + /// The member to inspect. + /// attributes defined. + public Attribute[] GetCustomAttributesCached(ICustomAttributeProvider attributeProvider) + { + if (attributeProvider is null) + { + throw new ArgumentNullException(nameof(attributeProvider)); + } + + if (attributeProvider is not (MemberInfo or Assembly)) + { + throw new ArgumentException( + $"Unsupported attribute provider type: {attributeProvider.GetType()}. Only MemberInfo and Assembly are supported.", + nameof(attributeProvider)); + } + + // If the information is cached, then use it otherwise populate the cache using + // the reflection APIs. + return _attributeCache.GetOrAdd(attributeProvider, GetAttributes); + + // We are avoiding func allocation here. + static Attribute[] GetAttributes(ICustomAttributeProvider attributeProvider) + { + // Populate the cache + try + { + object[]? attributes = NotCachedReflectionAccessor.GetCustomAttributesNotCached(attributeProvider); + return attributes is null ? [] : attributes as Attribute[] ?? [.. attributes.Cast()]; + } + catch (Exception ex) + { + // Get the exception description + string description; + try + { + // Can throw if the Message or StackTrace properties throw exceptions + description = ex.ToString(); + } + catch (Exception ex2) + { + description = string.Format(CultureInfo.CurrentCulture, Resource.ExceptionOccuredWhileGettingTheExceptionDescription, ex.GetType().FullName, ex2.GetType().FullName); // ex.GetType().FullName + + } + + if (PlatformServiceProvider.Instance.AdapterTraceLogger.IsWarningEnabled) + { + PlatformServiceProvider.Instance.AdapterTraceLogger.Warning(Resource.FailedToGetCustomAttribute, attributeProvider.GetType().FullName!, description); + } + + return []; + } + } + } + + /// + /// Reflection helper that is accessing Reflection directly, and won't cache the results. + /// + internal static class NotCachedReflectionAccessor + { + /// + /// Get custom attributes on a member without cache. Be CAREFUL where you use this, repeatedly accessing reflection without caching the results degrades the performance. + /// + /// Member for which attributes needs to be retrieved. + /// All attributes of give type on member. + public static object[]? GetCustomAttributesNotCached(ICustomAttributeProvider attributeProvider) + { + IReflectionOperations reflectionOperations = PlatformServiceProvider.Instance.ReflectionOperations; + object[] attributesArray = attributeProvider switch + { + null => throw new ArgumentNullException(nameof(attributeProvider)), + MemberInfo memberInfo => reflectionOperations.GetCustomAttributes(memberInfo), + Assembly assembly => reflectionOperations.GetCustomAttributes(assembly, typeof(Attribute)), + _ => throw new ArgumentException( + $"Unsupported attribute provider type: {attributeProvider.GetType()}. Only MemberInfo and Assembly are supported.", + nameof(attributeProvider)), + }; + + return attributesArray; + } + } + + /// + public bool IsMethodDeclaredInSameAssemblyAsType(MethodInfo method, Type type) + => method.DeclaringType?.Assembly.Equals(type.Assembly) ?? false; + + internal /* for tests */ void ClearCache() + // Tests manipulate the platform reflection provider, and we end up caching different attributes than the class / method actually has. + => _attributeCache.Clear(); } diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Services/TestDeployment.cs b/src/Adapter/MSTestAdapter.PlatformServices/Services/TestDeployment.cs index 87858290dc..11a4cd49cb 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Services/TestDeployment.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Services/TestDeployment.cs @@ -4,7 +4,6 @@ #if !WINDOWS_UWP && !WIN_UI using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter; -using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Helpers; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Deployment; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Utilities; @@ -35,7 +34,7 @@ internal sealed class TestDeployment : ITestDeployment /// Initializes a new instance of the class. /// public TestDeployment() - : this(new DeploymentItemUtility(ReflectHelper.Instance), new DeploymentUtility(), new FileUtility()) + : this(new DeploymentItemUtility(PlatformServiceProvider.Instance.ReflectionOperations), new DeploymentUtility(), new FileUtility()) { } diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Utilities/DeploymentItemUtility.cs b/src/Adapter/MSTestAdapter.PlatformServices/Utilities/DeploymentItemUtility.cs index 294c2366cf..50546a66e2 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Utilities/DeploymentItemUtility.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Utilities/DeploymentItemUtility.cs @@ -3,8 +3,8 @@ #if !WINDOWS_UWP && !WIN_UI -using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Helpers; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Deployment; +using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; using Microsoft.VisualStudio.TestPlatform.ObjectModel; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -15,7 +15,7 @@ namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Uti /// internal sealed class DeploymentItemUtility { - private readonly ReflectHelper _reflectHelper; + private readonly IReflectionOperations _reflectionOperations; /// /// A cache for class level deployment items. @@ -25,10 +25,10 @@ internal sealed class DeploymentItemUtility /// /// Initializes a new instance of the class. /// - /// The reflect helper. - internal DeploymentItemUtility(ReflectHelper reflectHelper) + /// The reflection operations. + internal DeploymentItemUtility(IReflectionOperations reflectionOperations) { - _reflectHelper = reflectHelper; + _reflectionOperations = reflectionOperations; _classLevelDeploymentItems = []; } @@ -42,7 +42,7 @@ internal IList GetClassLevelDeploymentItems(Type type, ICollecti { if (!_classLevelDeploymentItems.TryGetValue(type, out IList? value)) { - IEnumerable deploymentItemAttributes = _reflectHelper.GetAttributes(type); + IEnumerable deploymentItemAttributes = _reflectionOperations.GetAttributes(type); value = GetDeploymentItems(deploymentItemAttributes, warnings); _classLevelDeploymentItems[type] = value; } @@ -59,7 +59,7 @@ internal IList GetClassLevelDeploymentItems(Type type, ICollecti internal KeyValuePair[]? GetDeploymentItems(MethodInfo method, IEnumerable classLevelDeploymentItems, ICollection warnings) { - List testLevelDeploymentItems = GetDeploymentItems(_reflectHelper.GetAttributes(method), warnings); + List testLevelDeploymentItems = GetDeploymentItems(_reflectionOperations.GetAttributes(method), warnings); return ToKeyValuePairs(Concat(testLevelDeploymentItems, classLevelDeploymentItems)); } diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Utilities/DeploymentUtilityBase.cs b/src/Adapter/MSTestAdapter.PlatformServices/Utilities/DeploymentUtilityBase.cs index c9c93d3fd6..f86755558b 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Utilities/DeploymentUtilityBase.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Utilities/DeploymentUtilityBase.cs @@ -4,7 +4,6 @@ #if !WINDOWS_UWP && !WIN_UI using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter; -using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Helpers; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Deployment; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Extensions; @@ -26,7 +25,7 @@ internal abstract class DeploymentUtilityBase protected const string DeploymentFolderPrefix = "Deploy"; public DeploymentUtilityBase() - : this(new DeploymentItemUtility(ReflectHelper.Instance), new AssemblyUtility(), new FileUtility()) + : this(new DeploymentItemUtility(PlatformServiceProvider.Instance.ReflectionOperations), new AssemblyUtility(), new FileUtility()) { } diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Utilities/ReflectionOperationsNetFrameworkAttributeHelpers.cs b/src/Adapter/MSTestAdapter.PlatformServices/Utilities/ReflectionOperationsNetFrameworkAttributeHelpers.cs deleted file mode 100644 index 7dfd9fbf17..0000000000 --- a/src/Adapter/MSTestAdapter.PlatformServices/Utilities/ReflectionOperationsNetFrameworkAttributeHelpers.cs +++ /dev/null @@ -1,272 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -#if NETFRAMEWORK - -namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Utilities; - -/// -/// This class is only intended to be used by ReflectionOperations on .NET Framework. -/// TODO: Investigate why we need complicated logic under .NET Framework, and whether it -/// can be simplified to have unified simple logic for .NET Core and .NET Framework. -/// -internal static class ReflectionOperationsNetFrameworkAttributeHelpers -{ - /// - /// Gets all the custom attributes adorned on a member. - /// - /// The member. - /// The list of attributes on the member. Empty list if none found. - internal static IReadOnlyList GetCustomAttributes(MemberInfo memberInfo) - => GetCustomAttributesCore(memberInfo, type: null); - - /// - /// Get custom attributes on a member for both normal and reflection only load. - /// - /// Member for which attributes needs to be retrieved. - /// Type of attribute to retrieve. - /// All attributes of give type on member. -#pragma warning disable CA1859 // Use concrete types when possible for improved performance - private static IReadOnlyList GetCustomAttributesCore(MemberInfo memberInfo, Type? type) -#pragma warning restore CA1859 - { - bool shouldGetAllAttributes = type == null; - - if (!IsReflectionOnlyLoad(memberInfo)) - { - return shouldGetAllAttributes ? memberInfo.GetCustomAttributes(inherit: true) : memberInfo.GetCustomAttributes(type, inherit: true); - } - else - { - List nonUniqueAttributes = []; - Dictionary uniqueAttributes = []; - - int inheritanceThreshold = 10; - int inheritanceLevel = 0; - - if (memberInfo.MemberType == MemberTypes.TypeInfo) - { - // This code is based on the code for fetching CustomAttributes in System.Reflection.CustomAttribute(RuntimeType type, RuntimeType caType, bool inherit) - var tempTypeInfo = memberInfo as TypeInfo; - - do - { - IList attributes = CustomAttributeData.GetCustomAttributes(tempTypeInfo); - AddNewAttributes( - attributes, - shouldGetAllAttributes, - type!, - uniqueAttributes, - nonUniqueAttributes); - tempTypeInfo = tempTypeInfo!.BaseType?.GetTypeInfo(); - inheritanceLevel++; - } - while (tempTypeInfo != null && tempTypeInfo != typeof(object).GetTypeInfo() - && inheritanceLevel < inheritanceThreshold); - } - else if (memberInfo.MemberType == MemberTypes.Method) - { - // This code is based on the code for fetching CustomAttributes in System.Reflection.CustomAttribute(RuntimeMethodInfo method, RuntimeType caType, bool inherit). - var tempMethodInfo = memberInfo as MethodInfo; - - do - { - IList attributes = CustomAttributeData.GetCustomAttributes(tempMethodInfo); - AddNewAttributes( - attributes, - shouldGetAllAttributes, - type!, - uniqueAttributes, - nonUniqueAttributes); - MethodInfo? baseDefinition = tempMethodInfo!.GetBaseDefinition(); - - if (baseDefinition != null - && string.Equals( - string.Concat(tempMethodInfo.DeclaringType.FullName, tempMethodInfo.Name), - string.Concat(baseDefinition.DeclaringType.FullName, baseDefinition.Name), StringComparison.Ordinal)) - { - break; - } - - tempMethodInfo = baseDefinition; - inheritanceLevel++; - } - while (tempMethodInfo != null && inheritanceLevel < inheritanceThreshold); - } - else - { - // Ideally we should not be reaching here. We only query for attributes on types/methods currently. - // Return the attributes that CustomAttributeData returns in this cases not considering inheritance. - IList firstLevelAttributes = - CustomAttributeData.GetCustomAttributes(memberInfo); - AddNewAttributes(firstLevelAttributes, shouldGetAllAttributes, type!, uniqueAttributes, nonUniqueAttributes); - } - - nonUniqueAttributes.AddRange(uniqueAttributes.Values); - return nonUniqueAttributes; - } - } - - internal static List GetCustomAttributes(Assembly assembly, Type type) - { - if (!assembly.ReflectionOnly) - { - return [.. assembly.GetCustomAttributes(type)]; - } - - List customAttributes = [.. CustomAttributeData.GetCustomAttributes(assembly)]; - - List attributesArray = []; - - foreach (CustomAttributeData attribute in customAttributes) - { - if (!IsTypeInheriting(attribute.Constructor.DeclaringType, type) - && !attribute.Constructor.DeclaringType.AssemblyQualifiedName.Equals( - type.AssemblyQualifiedName, StringComparison.Ordinal)) - { - continue; - } - - Attribute? attributeInstance = CreateAttributeInstance(attribute); - if (attributeInstance != null) - { - attributesArray.Add(attributeInstance); - } - } - - return attributesArray; - } - - /// - /// Create instance of the attribute for reflection only load. - /// - /// The attribute data. - /// An attribute. - private static Attribute? CreateAttributeInstance(CustomAttributeData attributeData) - { - object? attribute = null; - try - { - // Create instance of attribute. For some case, constructor param is returned as ReadOnlyCollection - // instead of array. So convert it to array else constructor invoke will fail. - var attributeType = Type.GetType(attributeData.Constructor.DeclaringType.AssemblyQualifiedName); - - List constructorParameters = []; - List constructorArguments = []; - foreach (CustomAttributeTypedArgument parameter in attributeData.ConstructorArguments) - { - var parameterType = Type.GetType(parameter.ArgumentType.AssemblyQualifiedName); - constructorParameters.Add(parameterType); - if (!parameterType.IsArray - || parameter.Value is not IEnumerable enumerable) - { - constructorArguments.Add(parameter.Value); - continue; - } - - ArrayList list = []; - foreach (object? item in enumerable) - { - if (item is CustomAttributeTypedArgument argument) - { - list.Add(argument.Value); - } - else - { - list.Add(item); - } - } - - constructorArguments.Add(list.ToArray(parameterType.GetElementType())); - } - - ConstructorInfo constructor = attributeType.GetConstructor([.. constructorParameters]); - attribute = constructor.Invoke([.. constructorArguments]); - - foreach (CustomAttributeNamedArgument namedArgument in attributeData.NamedArguments) - { - attributeType.GetProperty(namedArgument.MemberInfo.Name).SetValue(attribute, namedArgument.TypedValue.Value, null); - } - } - - // If not able to create instance of attribute ignore attribute. (May happen for custom user defined attributes). - catch (BadImageFormatException) - { - } - catch (FileLoadException) - { - } - catch (TypeLoadException) - { - } - - return attribute as Attribute; - } - - private static void AddNewAttributes( - IList customAttributes, - bool shouldGetAllAttributes, - Type type, - Dictionary uniqueAttributes, - List nonUniqueAttributes) - { - foreach (CustomAttributeData attribute in customAttributes) - { - if (!shouldGetAllAttributes - && !IsTypeInheriting(attribute.Constructor.DeclaringType, type) - && !attribute.Constructor.DeclaringType.AssemblyQualifiedName.Equals( - type.AssemblyQualifiedName, StringComparison.Ordinal)) - { - continue; - } - - Attribute? attributeInstance = CreateAttributeInstance(attribute); - if (attributeInstance == null) - { - continue; - } - - Type attributeType = attributeInstance.GetType(); - IReadOnlyList attributeUsageAttributes = GetCustomAttributesCore( - attributeType, - typeof(AttributeUsageAttribute)); - if (attributeUsageAttributes.Count > 0 - && attributeUsageAttributes[0] is AttributeUsageAttribute { AllowMultiple: false }) - { - if (!uniqueAttributes.ContainsKey(attributeType.FullName)) - { - uniqueAttributes.Add(attributeType.FullName, attributeInstance); - } - } - else - { - nonUniqueAttributes.Add(attributeInstance); - } - } - } - - /// - /// Check whether the member is loaded in a reflection only context. - /// - /// The member Info. - /// True if the member is loaded in a reflection only context. - private static bool IsReflectionOnlyLoad(MemberInfo? memberInfo) - => memberInfo != null && memberInfo.Module.Assembly.ReflectionOnly; - - private static bool IsTypeInheriting(Type? type1, Type type2) - { - while (type1 != null) - { - if (type1.AssemblyQualifiedName.Equals(type2.AssemblyQualifiedName, StringComparison.Ordinal)) - { - return true; - } - - type1 = type1.BaseType; - } - - return false; - } -} - -#endif diff --git a/test/IntegrationTests/PlatformServices.Desktop.IntegrationTests/ReflectionUtilityTests.cs b/test/IntegrationTests/PlatformServices.Desktop.IntegrationTests/ReflectionUtilityTests.cs index 1fc4f89cd2..93160611fa 100644 --- a/test/IntegrationTests/PlatformServices.Desktop.IntegrationTests/ReflectionUtilityTests.cs +++ b/test/IntegrationTests/PlatformServices.Desktop.IntegrationTests/ReflectionUtilityTests.cs @@ -4,7 +4,6 @@ using AwesomeAssertions; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; -using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Utilities; using SampleFrameworkExtensions; @@ -12,9 +11,13 @@ namespace PlatformServices.Desktop.ComponentTests; +/// +/// Integration tests for ReflectionOperations which provides platform-specific reflection operations. +/// public class ReflectionUtilityTests : TestContainer { private readonly Assembly _testAsset; + private readonly ReflectionOperations _reflectionOperations = new(); public ReflectionUtilityTests() { @@ -30,62 +33,88 @@ public ReflectionUtilityTests() #endif currentAssemblyDirectory.Name /* TFM (e.g. net462) */, "TestProjectForDiscovery.dll"); - _testAsset = Assembly.ReflectionOnlyLoadFrom(testAssetPath); - - // This is needed for System assemblies. - AppDomain.CurrentDomain.ReflectionOnlyAssemblyResolve += ReflectionOnlyOnResolve; + _testAsset = Assembly.LoadFrom(testAssetPath); } public void GetCustomAttributesShouldReturnAllAttributes() { MethodInfo methodInfo = _testAsset.GetType("TestProjectForDiscovery.AttributeTestBaseClass").GetMethod("DummyVTestMethod1"); - IReadOnlyList attributes = new ReflectionOperations().GetCustomAttributes(methodInfo); + object[]? attributes = _reflectionOperations.GetCustomAttributes(methodInfo); attributes.Should().NotBeNull(); - attributes.Should().HaveCount(2); - string[] expectedAttributes = ["TestCategory : base", "Owner : base"]; - GetAttributeValuePairs(attributes).Should().Equal(expectedAttributes); + // Filter to known test attributes to avoid fragility if the test asset ever + // enables nullable annotations (which would add compiler-generated attributes). + string[] expectedAttributes = ["Owner : base", "TestCategory : base"]; + GetAttributeValuePairs(attributes!).Should().Equal(expectedAttributes); } public void GetCustomAttributesShouldReturnAllAttributesWithBaseInheritance() { MethodInfo methodInfo = _testAsset.GetType("TestProjectForDiscovery.AttributeTestClass").GetMethod("DummyVTestMethod1"); - IReadOnlyList attributes = new ReflectionOperations().GetCustomAttributes(methodInfo); + object[]? attributes = _reflectionOperations.GetCustomAttributes(methodInfo); attributes.Should().NotBeNull(); - attributes.Should().HaveCount(3); // Notice that the Owner on the base method does not show up since it can only be defined once. - string[] expectedAttributes = ["TestCategory : derived", "TestCategory : base", "Owner : derived"]; - GetAttributeValuePairs(attributes).Should().Equal(expectedAttributes); + // Filter to known test attributes to avoid fragility if the test asset ever + // enables nullable annotations (which would add compiler-generated attributes). + string[] expectedAttributes = ["Owner : derived", "TestCategory : derived", "TestCategory : base"]; + GetAttributeValuePairs(attributes!).Should().Equal(expectedAttributes); } public void GetCustomAttributesOnTypeShouldReturnAllAttributes() { Type type = _testAsset.GetType("TestProjectForDiscovery.AttributeTestBaseClass"); - IReadOnlyList attributes = new ReflectionOperations().GetCustomAttributes(type); + object[]? attributes = _reflectionOperations.GetCustomAttributes(type); attributes.Should().NotBeNull(); - attributes.Should().HaveCount(1); + // Filter to known test attributes to avoid fragility with compiler-generated attributes. string[] expectedAttributes = ["TestCategory : ba"]; - GetAttributeValuePairs(attributes).Should().Equal(expectedAttributes); + GetAttributeValuePairs(attributes!).Should().Equal(expectedAttributes); } public void GetCustomAttributesOnTypeShouldReturnAllAttributesWithBaseInheritance() { Type type = _testAsset.GetType("TestProjectForDiscovery.AttributeTestClass"); - IReadOnlyList attributes = new ReflectionOperations().GetCustomAttributes(type); + object[]? attributes = _reflectionOperations.GetCustomAttributes(type); attributes.Should().NotBeNull(); - attributes.Should().HaveCount(2); + // Filter to known test attributes to avoid fragility with compiler-generated attributes. string[] expectedAttributes = ["TestCategory : a", "TestCategory : ba"]; + GetAttributeValuePairs(attributes!).Should().Equal(expectedAttributes); + } + + public void GetSpecificCustomAttributesShouldReturnAllAttributes() + { + MethodInfo methodInfo = _testAsset.GetType("TestProjectForDiscovery.AttributeTestBaseClass").GetMethod("DummyVTestMethod1"); + + TestCategoryAttribute[] attributes = _reflectionOperations.GetAttributes(methodInfo).ToArray(); + + attributes.Should().NotBeNull(); + attributes.Should().HaveCount(1); + + string[] expectedAttributes = ["TestCategory : base"]; + GetAttributeValuePairs(attributes).Should().Equal(expectedAttributes); + } + + public void GetSpecificCustomAttributesShouldReturnAllAttributesWithBaseInheritance() + { + MethodInfo methodInfo = + _testAsset.GetType("TestProjectForDiscovery.AttributeTestClass").GetMethod("DummyVTestMethod1"); + + TestCategoryAttribute[] attributes = _reflectionOperations.GetAttributes(methodInfo).ToArray(); + + attributes.Should().NotBeNull(); + attributes.Should().HaveCount(2); + + string[] expectedAttributes = ["TestCategory : derived", "TestCategory : base"]; GetAttributeValuePairs(attributes).Should().Equal(expectedAttributes); } @@ -93,33 +122,78 @@ public void GetCustomAttributesShouldReturnAllAttributesIncludingUserDefinedAttr { MethodInfo methodInfo = _testAsset.GetType("TestProjectForDiscovery.AttributeTestClassWithCustomAttributes").GetMethod("DummyVTestMethod1"); - IReadOnlyList attributes = new ReflectionOperations().GetCustomAttributes(methodInfo); + object[]? attributes = _reflectionOperations.GetCustomAttributes(methodInfo); attributes.Should().NotBeNull(); - attributes.Should().HaveCount(3); - string[] expectedAttributes = ["Duration : superfast", "TestCategory : base", "Owner : base"]; + // Filter to known test attributes to avoid fragility with compiler-generated attributes. + string[] expectedAttributes = ["Duration : superfast", "Owner : base", "TestCategory : base"]; + GetAttributeValuePairs(attributes!).Should().Equal(expectedAttributes); + } + + public void GetSpecificCustomAttributesShouldReturnAllAttributesIncludingUserDefinedAttributes() + { + MethodInfo methodInfo = _testAsset.GetType("TestProjectForDiscovery.AttributeTestClassWithCustomAttributes").GetMethod("DummyVTestMethod1"); + + TestPropertyAttribute[] attributes = _reflectionOperations.GetAttributes(methodInfo).ToArray(); + + attributes.Should().NotBeNull(); + attributes.Should().HaveCount(2); + + string[] expectedAttributes = ["Duration : superfast", "Owner : base"]; GetAttributeValuePairs(attributes).Should().Equal(expectedAttributes); } - public void GetSpecificCustomAttributesOnAssemblyShouldReturnAllAttributes() + public void GetSpecificCustomAttributesShouldReturnArrayAttributesAsWell() { - Assembly asm = _testAsset.GetType("TestProjectForDiscovery.AttributeTestClass").Assembly; + MethodInfo methodInfo = _testAsset.GetType("TestProjectForDiscovery.AttributeTestClassWithCustomAttributes").GetMethod("DummyTestMethod2"); - object[] attributes = new ReflectionOperations().GetCustomAttributes(asm, typeof(TestCategoryAttribute)); + CategoryArrayAttribute[] attributes = _reflectionOperations.GetAttributes(methodInfo).ToArray(); + + attributes.Should().NotBeNull(); + attributes.Should().HaveCount(1); + + string[] expectedAttributes = ["CategoryAttribute : foo,foo2"]; + GetAttributeValuePairs(attributes).Should().Equal(expectedAttributes); + } + + public void GetSpecificCustomAttributesOnTypeShouldReturnAllAttributes() + { + Type type = _testAsset.GetType("TestProjectForDiscovery.AttributeTestBaseClass"); + + TestCategoryAttribute[] attributes = _reflectionOperations.GetAttributes(type).ToArray(); + + attributes.Should().NotBeNull(); + attributes.Should().HaveCount(1); + + string[] expectedAttributes = ["TestCategory : ba"]; + GetAttributeValuePairs(attributes).Should().Equal(expectedAttributes); + } + + public void GetSpecificCustomAttributesOnTypeShouldReturnAllAttributesWithBaseInheritance() + { + Type type = _testAsset.GetType("TestProjectForDiscovery.AttributeTestClass"); + + TestCategoryAttribute[] attributes = _reflectionOperations.GetAttributes(type).ToArray(); attributes.Should().NotBeNull(); attributes.Should().HaveCount(2); - string[] expectedAttributes = ["TestCategory : a1", "TestCategory : a2"]; + string[] expectedAttributes = ["TestCategory : a", "TestCategory : ba"]; GetAttributeValuePairs(attributes).Should().Equal(expectedAttributes); } - private static Assembly ReflectionOnlyOnResolve(object sender, ResolveEventArgs args) + public void GetSpecificCustomAttributesOnAssemblyShouldReturnAllAttributes() { - string assemblyNameToLoad = AppDomain.CurrentDomain.ApplyPolicy(args.Name); + Assembly asm = _testAsset.GetType("TestProjectForDiscovery.AttributeTestClass").Assembly; + + object[] attributes = _reflectionOperations.GetCustomAttributes(asm, typeof(TestCategoryAttribute)); + + attributes.Should().NotBeNull(); + attributes.Should().HaveCount(2); - return Assembly.ReflectionOnlyLoad(assemblyNameToLoad); + string[] expectedAttributes = ["TestCategory : a1", "TestCategory : a2"]; + GetAttributeValuePairs(attributes).Should().Equal(expectedAttributes); } private static string[] GetAttributeValuePairs(IEnumerable attributes) diff --git a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/ClassCleanupManagerTests.cs b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/ClassCleanupManagerTests.cs index 3d2211b3fe..4ecae20ebc 100644 --- a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/ClassCleanupManagerTests.cs +++ b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/ClassCleanupManagerTests.cs @@ -4,7 +4,6 @@ using AwesomeAssertions; using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Execution; -using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Helpers; using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; using Moq; @@ -17,7 +16,6 @@ public class ClassCleanupManagerTests : TestContainer { public void AssemblyCleanupRunsAfterAllTestsFinishEvenIfWeScheduleTheSameTestMultipleTime() { - ReflectHelper reflectHelper = Mock.Of(); MethodInfo classCleanupMethodInfo = typeof(FakeTestClass).GetMethod(nameof(FakeTestClass.FakeClassCleanupMethod), BindingFlags.Instance | BindingFlags.NonPublic)!; // Full class name must agree between unitTestElement.TestMethod.FullClassName and testMethod.FullClassName; string fullClassName = typeof(FakeTestClass).FullName!; diff --git a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestMethodRunnerTests.cs b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestMethodRunnerTests.cs index 39c74ce194..45db64b582 100644 --- a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestMethodRunnerTests.cs +++ b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestMethodRunnerTests.cs @@ -5,7 +5,6 @@ using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter; using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Execution; -using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Helpers; using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; @@ -56,8 +55,6 @@ public TestMethodRunnerTests() _testablePlatformServiceProvider = new TestablePlatformServiceProvider(); _testablePlatformServiceProvider.SetupMockReflectionOperations(); PlatformServiceProvider.Instance = _testablePlatformServiceProvider; - - ReflectHelper.Instance.ClearCache(); } private static TestClassInfo GetTestClassInfo() diff --git a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestPropertyAttributeTests.cs b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestPropertyAttributeTests.cs index b3579dd42c..e5cb2d9162 100644 --- a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestPropertyAttributeTests.cs +++ b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestPropertyAttributeTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using AwesomeAssertions; @@ -22,7 +22,7 @@ public TestPropertyAttributeTests() testablePlatformServiceProvider.MockFileOperations.Setup(x => x.LoadAssembly(It.IsAny())).Returns(GetType().Assembly); PlatformServiceProvider.Instance = testablePlatformServiceProvider; - ReflectHelper.Instance.ClearCache(); + ReflectHelper.ClearCache(); } protected override void Dispose(bool disposing) @@ -39,7 +39,7 @@ protected override void Dispose(bool disposing) public void GetTestMethodInfoShouldAddPropertiesFromContainingClassCorrectly() { - TestPlatform.ObjectModel.Trait[] traits = [.. ReflectHelper.Instance.GetTestPropertiesAsTraits(typeof(DummyTestClassBase).GetMethod(nameof(DummyTestClassBase.VirtualTestMethodInBaseAndDerived))!)]; + TestPlatform.ObjectModel.Trait[] traits = [.. ReflectHelper.GetTestPropertiesAsTraits(typeof(DummyTestClassBase).GetMethod(nameof(DummyTestClassBase.VirtualTestMethodInBaseAndDerived))!)]; traits.Length.Should().Be(3); traits[0].Name.Should().Be("TestMethodKeyFromBase"); traits[0].Value.Should().Be("TestMethodValueFromBase"); @@ -51,7 +51,7 @@ public void GetTestMethodInfoShouldAddPropertiesFromContainingClassCorrectly() public void GetTestMethodInfoShouldAddPropertiesFromContainingClassAndBaseClassesAndOverriddenMethodsCorrectly_OverriddenIsTestMethod() { - TestPlatform.ObjectModel.Trait[] traits = [.. ReflectHelper.Instance.GetTestPropertiesAsTraits(typeof(DummyTestClassDerived).GetMethod(nameof(DummyTestClassDerived.VirtualTestMethodInBaseAndDerived))!)]; + TestPlatform.ObjectModel.Trait[] traits = [.. ReflectHelper.GetTestPropertiesAsTraits(typeof(DummyTestClassDerived).GetMethod(nameof(DummyTestClassDerived.VirtualTestMethodInBaseAndDerived))!)]; traits.Length.Should().Be(6); traits[0].Name.Should().Be("DerivedMethod1Key"); traits[0].Value.Should().Be("DerivedMethod1Value"); @@ -69,7 +69,7 @@ public void GetTestMethodInfoShouldAddPropertiesFromContainingClassAndBaseClasse public void GetTestMethodInfoShouldAddPropertiesFromContainingClassAndBaseClassesAndOverriddenMethodsCorrectly_OverriddenIsNotTestMethod() { - TestPlatform.ObjectModel.Trait[] traits = [.. ReflectHelper.Instance.GetTestPropertiesAsTraits(typeof(DummyTestClassDerived).GetMethod(nameof(DummyTestClassDerived.VirtualTestMethodInDerivedButNotTestMethodInBase))!)]; + TestPlatform.ObjectModel.Trait[] traits = [.. ReflectHelper.GetTestPropertiesAsTraits(typeof(DummyTestClassDerived).GetMethod(nameof(DummyTestClassDerived.VirtualTestMethodInDerivedButNotTestMethodInBase))!)]; traits.Length.Should().Be(6); traits[0].Name.Should().Be("DerivedMethod2Key"); traits[0].Value.Should().Be("DerivedMethod2Value"); diff --git a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TypeCacheTests.cs b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TypeCacheTests.cs index 3f05b89a42..e962e79e01 100644 --- a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TypeCacheTests.cs +++ b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TypeCacheTests.cs @@ -36,7 +36,7 @@ public TypeCacheTests() _testablePlatformServiceProvider = new TestablePlatformServiceProvider(); PlatformServiceProvider.Instance = _testablePlatformServiceProvider; - ReflectHelper.Instance.ClearCache(); + ReflectHelper.ClearCache(); SetupMocks(); } diff --git a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Helpers/ReflectHelperTests.cs b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Helpers/ReflectHelperTests.cs deleted file mode 100644 index 89979ac6df..0000000000 --- a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Helpers/ReflectHelperTests.cs +++ /dev/null @@ -1,305 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using AwesomeAssertions; - -using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter; -using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Helpers; -using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; -using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.UnitTests.TestableImplementations; - -using Moq; - -using TestFramework.ForTestingMSTest; - -namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.UnitTests; - -public class ReflectHelperTests : TestContainer -{ - private readonly ReflectHelper _reflectHelper; - private readonly AttributeMockingHelper _attributeMockingHelper; - private readonly Mock _method; - private readonly TestablePlatformServiceProvider _testablePlatformServiceProvider; - - public ReflectHelperTests() - { - _reflectHelper = new(); - _method = new Mock(); - _method.Setup(x => x.MemberType).Returns(MemberTypes.Method); - - _testablePlatformServiceProvider = new TestablePlatformServiceProvider(); - _testablePlatformServiceProvider.SetupMockReflectionOperations(); - _attributeMockingHelper = new(_testablePlatformServiceProvider.MockReflectionOperations); - - PlatformServiceProvider.Instance = _testablePlatformServiceProvider; - } - - protected override void Dispose(bool disposing) - { - if (!IsDisposed) - { - base.Dispose(disposing); - PlatformServiceProvider.Instance = null; - } - } - - /// - /// Testing test category attribute adorned at class level. - /// - public void GetTestCategoryAttributeShouldIncludeTestCategoriesAtClassLevel() - { - _attributeMockingHelper.SetCustomAttribute(typeof(TestCategoryBaseAttribute), [new TestCategoryAttribute("ClassLevel")], MemberTypes.TypeInfo); - - string[] expected = ["ClassLevel"]; - string[] actual = [.. _reflectHelper.GetTestCategories(_method.Object, typeof(ReflectHelperTests))]; - - expected.SequenceEqual(actual).Should().BeTrue(); - } - - /// - /// Testing test category attributes adorned at class, assembly and method level are getting collected. - /// - public void GetTestCategoryAttributeShouldIncludeTestCategoriesAtAllLevels() - { - _attributeMockingHelper.SetCustomAttribute(typeof(TestCategoryBaseAttribute), [new TestCategoryAttribute("AsmLevel1"), new TestCategoryAttribute("AsmLevel2")], MemberTypes.All); - _attributeMockingHelper.SetCustomAttribute(typeof(TestCategoryBaseAttribute), [new TestCategoryAttribute("AsmLevel3")], MemberTypes.All); - _attributeMockingHelper.SetCustomAttribute(typeof(TestCategoryBaseAttribute), [new TestCategoryAttribute("ClassLevel")], MemberTypes.TypeInfo); - _attributeMockingHelper.SetCustomAttribute(typeof(TestCategoryBaseAttribute), [new TestCategoryAttribute("MethodLevel")], MemberTypes.Method); - - string[] actual = [.. _reflectHelper.GetTestCategories(_method.Object, typeof(ReflectHelperTests))]; - string[] expected = ["MethodLevel", "ClassLevel", "AsmLevel1", "AsmLevel2", "AsmLevel3"]; - - expected.SequenceEqual(actual).Should().BeTrue(); - } - - /// - /// Testing test category attributes adorned at class, assembly and method level are getting collected. - /// - public void GetTestCategoryAttributeShouldConcatCustomAttributeOfSameType() - { - _attributeMockingHelper.SetCustomAttribute(typeof(TestCategoryBaseAttribute), [new TestCategoryAttribute("AsmLevel1")], MemberTypes.All); - _attributeMockingHelper.SetCustomAttribute(typeof(TestCategoryBaseAttribute), [new TestCategoryAttribute("AsmLevel2")], MemberTypes.All); - _attributeMockingHelper.SetCustomAttribute(typeof(TestCategoryBaseAttribute), [new TestCategoryAttribute("ClassLevel1")], MemberTypes.TypeInfo); - _attributeMockingHelper.SetCustomAttribute(typeof(TestCategoryBaseAttribute), [new TestCategoryAttribute("ClassLevel2")], MemberTypes.TypeInfo); - _attributeMockingHelper.SetCustomAttribute(typeof(TestCategoryBaseAttribute), [new TestCategoryAttribute("MethodLevel1")], MemberTypes.Method); - _attributeMockingHelper.SetCustomAttribute(typeof(TestCategoryBaseAttribute), [new TestCategoryAttribute("MethodLevel2")], MemberTypes.Method); - - string[] actual = [.. _reflectHelper.GetTestCategories(_method.Object, typeof(ReflectHelperTests))]; - string[] expected = ["MethodLevel1", "MethodLevel2", "ClassLevel1", "ClassLevel2", "AsmLevel1", "AsmLevel2"]; - - expected.SequenceEqual(actual).Should().BeTrue(); - } - - /// - /// Testing test category attributes adorned at assembly level. - /// - public void GetTestCategoryAttributeShouldIncludeTestCategoriesAtAssemblyLevel() - { - _attributeMockingHelper.SetCustomAttribute(typeof(TestCategoryBaseAttribute), [new TestCategoryAttribute("AsmLevel")], MemberTypes.All); - - string[] expected = ["AsmLevel"]; - - string[] actual = [.. _reflectHelper.GetTestCategories(_method.Object, typeof(ReflectHelperTests))]; - - expected.SequenceEqual(actual).Should().BeTrue(); - } - - /// - /// Testing multiple test category attribute adorned at class level. - /// - public void GetTestCategoryAttributeShouldIncludeMultipleTestCategoriesAtClassLevel() - { - _attributeMockingHelper.SetCustomAttribute(typeof(TestCategoryBaseAttribute), [new TestCategoryAttribute("ClassLevel"), new TestCategoryAttribute("ClassLevel1")], MemberTypes.TypeInfo); - - string[] expected = ["ClassLevel", "ClassLevel1"]; - string[] actual = [.. _reflectHelper.GetTestCategories(_method.Object, typeof(ReflectHelperTests))]; - - expected.SequenceEqual(actual).Should().BeTrue(); - } - - /// - /// Testing multiple test category attributes adorned at assembly level. - /// - public void GetTestCategoryAttributeShouldIncludeMultipleTestCategoriesAtAssemblyLevel() - { - _attributeMockingHelper.SetCustomAttribute(typeof(TestCategoryBaseAttribute), [new TestCategoryAttribute("AsmLevel"), new TestCategoryAttribute("AsmLevel1")], MemberTypes.All); - - string[] expected = ["AsmLevel", "AsmLevel1"]; - string[] actual = [.. _reflectHelper.GetTestCategories(_method.Object, typeof(ReflectHelperTests))]; - expected.SequenceEqual(actual).Should().BeTrue(); - } - - /// - /// Testing test category attributes adorned at method level - regression. - /// - public void GetTestCategoryAttributeShouldIncludeTestCategoriesAtMethodLevel() - { - _attributeMockingHelper.SetCustomAttribute(typeof(TestCategoryBaseAttribute), [new TestCategoryAttribute("MethodLevel")], MemberTypes.Method); - - string[] expected = ["MethodLevel"]; - string[] actual = [.. _reflectHelper.GetTestCategories(_method.Object, typeof(ReflectHelperTests))]; - - expected.SequenceEqual(actual).Should().BeTrue(); - } - - public void IsAttributeDefinedShouldReturnTrueIfSpecifiedAttributeIsDefinedOnAMember() - { - var rh = new ReflectHelper(); - var mockMemberInfo = new Mock(); - var attributes = new Attribute[] { new TestMethodAttribute() }; - - _testablePlatformServiceProvider.MockReflectionOperations. - Setup(ro => ro.GetCustomAttributes(mockMemberInfo.Object)). - Returns(attributes); - - rh.IsAttributeDefined(mockMemberInfo.Object).Should().BeTrue(); - } - - public void IsAttributeDefinedShouldReturnFalseIfSpecifiedAttributeIsNotDefinedOnAMember() - { - var rh = new ReflectHelper(); - var mockMemberInfo = new Mock(); - var attributes = new Attribute[] { new TestClassAttribute() }; - - _testablePlatformServiceProvider.MockReflectionOperations. - Setup(ro => ro.GetCustomAttributes(mockMemberInfo.Object)). - Returns(attributes); - - rh.IsAttributeDefined(mockMemberInfo.Object).Should().BeFalse(); - } - - public void IsAttributeDefinedShouldReturnFromCache() - { - var rh = new ReflectHelper(); - - // Not using mocks here because for some reason a dictionary match of the mock is not returning true in the product code. - MethodInfo memberInfo = typeof(ReflectHelperTests).GetMethod("IsAttributeDefinedShouldReturnFromCache")!; - - // new Mock(); - var attributes = new Attribute[] { new TestMethodAttribute() }; - - _testablePlatformServiceProvider.MockReflectionOperations. - Setup(ro => ro.GetCustomAttributes(memberInfo)). - Returns(attributes); - - rh.IsAttributeDefined(memberInfo).Should().BeTrue(); - - // Validate that reflection APIs are not called again. - rh.IsAttributeDefined(memberInfo).Should().BeTrue(); - _testablePlatformServiceProvider.MockReflectionOperations.Verify(ro => ro.GetCustomAttributes(memberInfo), Times.Once); - } - - public void HasAttributeDerivedFromShouldReturnTrueIfSpecifiedAttributeIsDefinedOnAMember() - { - var rh = new ReflectHelper(); - var mockMemberInfo = new Mock(); - var attributes = new Attribute[] { new TestableExtendedTestMethod() }; - - _testablePlatformServiceProvider.MockReflectionOperations. - Setup(ro => ro.GetCustomAttributes(mockMemberInfo.Object)). - Returns(attributes); - - rh.IsAttributeDefined(mockMemberInfo.Object).Should().BeTrue(); - } - - public void HasAttributeDerivedFromShouldReturnFalseIfSpecifiedAttributeIsNotDefinedOnAMember() - { - var rh = new ReflectHelper(); - var mockMemberInfo = new Mock(); - var attributes = new Attribute[] { new TestableExtendedTestMethod() }; - - _testablePlatformServiceProvider.MockReflectionOperations. - Setup(ro => ro.GetCustomAttributes(mockMemberInfo.Object)). - Returns(attributes); - - rh.IsAttributeDefined(mockMemberInfo.Object).Should().BeFalse(); - } - - public void HasAttributeDerivedFromShouldReturnFromCache() - { - var rh = new ReflectHelper(); - - // Not using mocks here because for some reason a dictionary match of the mock is not returning true in the product code. - MethodInfo memberInfo = typeof(ReflectHelperTests).GetMethod("HasAttributeDerivedFromShouldReturnFromCache")!; - - // new Mock(); - var attributes = new Attribute[] { new TestableExtendedTestMethod() }; - - _testablePlatformServiceProvider.MockReflectionOperations. - Setup(ro => ro.GetCustomAttributes(memberInfo)). - Returns(attributes); - - rh.IsAttributeDefined(memberInfo).Should().BeTrue(); - - // Validate that reflection APIs are not called again. - rh.IsAttributeDefined(memberInfo).Should().BeTrue(); - _testablePlatformServiceProvider.MockReflectionOperations.Verify(ro => ro.GetCustomAttributes(memberInfo), Times.Once); - } - - public void HasAttributeDerivedFromShouldReturnFalseQueryingProvidedAttributesExistenceIfGettingAllAttributesFail() - { - var rh = new ReflectHelper(); - var mockMemberInfo = new Mock(); - - _testablePlatformServiceProvider.MockReflectionOperations. - Setup(ro => ro.GetCustomAttributes(mockMemberInfo.Object)). - Returns((object[])null!); - - rh.IsAttributeDefined(mockMemberInfo.Object).Should().BeFalse(); - } - - internal class AttributeMockingHelper - { - public AttributeMockingHelper(Mock mockReflectionOperations) => _mockReflectionOperations = mockReflectionOperations; - - /// - /// A collection to hold mock custom attributes. - /// MemberTypes.All for assembly level - /// MemberTypes.TypeInfo for class level - /// MemberTypes.Method for method level. - /// - private readonly List<(Type Type, Attribute Attribute, MemberTypes MemberType)> _data = []; - private readonly Mock _mockReflectionOperations; - - public void SetCustomAttribute(Type type, Attribute[] values, MemberTypes memberTypes) - { - foreach (Attribute attribute in values) - { - _data.Add((type, attribute, memberTypes)); - } - - _mockReflectionOperations.Setup(r => r.GetCustomAttributes(It.IsAny())) - .Returns(GetCustomAttributesNotCached); - _mockReflectionOperations.Setup(r => r.GetCustomAttributes(It.IsAny(), It.IsAny())) - .Returns((assembly, _) => GetCustomAttributesNotCached(assembly)); - } - - public object[] GetCustomAttributesNotCached(ICustomAttributeProvider attributeProvider) - { - var foundAttributes = new List(); - foreach ((Type Type, Attribute Attribute, MemberTypes MemberType) attributeData in _data) - { - if (attributeProvider is MethodInfo && (attributeData.MemberType == MemberTypes.Method)) - { - foundAttributes.Add(attributeData.Attribute); - } - else if (attributeProvider is TypeInfo && (attributeData.MemberType == MemberTypes.TypeInfo)) - { - foundAttributes.Add(attributeData.Attribute); - } - else if (attributeProvider is Assembly && attributeData.MemberType == MemberTypes.All) - { - foundAttributes.Add(attributeData.Attribute); - } - } - - return foundAttributes.ToArray(); - } - } -} - -#region Dummy Implementations - -public class TestableExtendedTestMethod : TestMethodAttribute; - -#endregion diff --git a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Services/DesktopReflectionOperationsTests.cs b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Services/DesktopReflectionOperationsTests.cs index b92337a744..c06086479a 100644 --- a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Services/DesktopReflectionOperationsTests.cs +++ b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Services/DesktopReflectionOperationsTests.cs @@ -38,10 +38,12 @@ public void GetCustomAttributesOnTypeShouldReturnAllAttributes() object[] attributes = _reflectionOperations.GetCustomAttributes(type); attributes.Should().NotBeNull(); - attributes.Length.Should().Be(1); + // Filter to only our test attributes (excludes compiler-generated attributes like NullableContextAttribute) + List testAttributes = ReflectionUtilityTests.GetAttributeValuePairs(attributes); + testAttributes.Count.Should().Be(1); string[] expectedAttributes = ["DummyA : ba"]; - expectedAttributes.SequenceEqual(ReflectionUtilityTests.GetAttributeValuePairs(attributes)).Should().BeTrue(); + expectedAttributes.SequenceEqual(testAttributes).Should().BeTrue(); } public void GetSpecificCustomAttributesOnAssemblyShouldReturnAllAttributes() diff --git a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Services/DesktopTestDeploymentTests.cs b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Services/DesktopTestDeploymentTests.cs index ddd508f5c6..ad2b3917b3 100644 --- a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Services/DesktopTestDeploymentTests.cs +++ b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Services/DesktopTestDeploymentTests.cs @@ -4,9 +4,9 @@ #if NETFRAMEWORK using AwesomeAssertions; -using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Helpers; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Deployment; +using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Utilities; using Microsoft.VisualStudio.TestPlatform.ObjectModel; using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; @@ -24,6 +24,7 @@ public class DesktopTestDeploymentTests : TestContainer private const string DefaultDeploymentItemPath = @"c:\temp"; private const string DefaultDeploymentItemOutputDirectory = "out"; + private readonly Mock _mockReflectionOperations; private readonly Mock _mockFileUtility; #pragma warning disable IDE0052 // Remove unread private members @@ -32,6 +33,7 @@ public class DesktopTestDeploymentTests : TestContainer public DesktopTestDeploymentTests() { + _mockReflectionOperations = new Mock(); _mockFileUtility = new Mock(); _warnings = []; @@ -148,7 +150,7 @@ private TestDeployment CreateAndSetupDeploymentRelatedUtilities(out TestRunDirec _mockFileUtility.Setup(fu => fu.GetNextIterationDirectoryName(It.IsAny(), It.IsAny())) .Returns(testRunDirectories.RootDeploymentDirectory); - var deploymentItemUtility = new DeploymentItemUtility(new ReflectHelper()); + var deploymentItemUtility = new DeploymentItemUtility(_mockReflectionOperations.Object); return new TestDeployment( deploymentItemUtility, diff --git a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Services/ReflectionOperationsTests.cs b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Services/ReflectionOperationsTests.cs index be9a7893b4..24e8c7b697 100644 --- a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Services/ReflectionOperationsTests.cs +++ b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Services/ReflectionOperationsTests.cs @@ -3,17 +3,56 @@ using AwesomeAssertions; +using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; +using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Helpers; +using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; +using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.UnitTests.TestableImplementations; +using Microsoft.VisualStudio.TestPlatform.ObjectModel; + +using Moq; using TestFramework.ForTestingMSTest; namespace MSTestAdapter.PlatformServices.UnitTests.Services; +[DoNotParallelize] public class ReflectionOperationsTests : TestContainer { private readonly ReflectionOperations _reflectionOperations; + private readonly AttributeMockingHelper _attributeMockingHelper; + private readonly Mock _method; + private readonly TestablePlatformServiceProvider _testablePlatformServiceProvider; + + // NOTE: This class mutates the shared static PlatformServiceProvider.Instance (set in + // the constructor, restored in Dispose). Tests that call GetCustomAttributesCached or + // methods that go through NotCachedReflectionAccessor depend on this static being set + // to _testablePlatformServiceProvider. [DoNotParallelize] is applied to prevent + // cross-class race conditions with other test classes that follow the same pattern. + public ReflectionOperationsTests() + { + _reflectionOperations = new ReflectionOperations(); + _method = new Mock(); + _method.Setup(x => x.MemberType).Returns(MemberTypes.Method); + _method.Setup(x => x.ReflectedType).Returns(typeof(ReflectionOperationsTests)); - public ReflectionOperationsTests() => _reflectionOperations = new ReflectionOperations(); + _testablePlatformServiceProvider = new TestablePlatformServiceProvider(); + _testablePlatformServiceProvider.SetupMockReflectionOperations(); + _attributeMockingHelper = new(_testablePlatformServiceProvider.MockReflectionOperations); + + PlatformServiceProvider.Instance = _testablePlatformServiceProvider; + } + + protected override void Dispose(bool disposing) + { + if (!IsDisposed) + { + base.Dispose(disposing); + PlatformServiceProvider.Instance = null; + } + } + + #region GetCustomAttributes Tests public void GetCustomAttributesShouldReturnAllAttributes() { @@ -25,7 +64,7 @@ public void GetCustomAttributesShouldReturnAllAttributes() attributes.Length.Should().Be(2); string[] expectedAttributes = ["DummyA : base", "DummySingleA : base"]; - GetAttributeValuePairs(attributes).SequenceEqual(expectedAttributes).Should().BeTrue(); + GetAttributeValuePairs(attributes).Should().Equal(expectedAttributes); } public void GetCustomAttributesShouldReturnAllAttributesWithBaseInheritance() @@ -39,7 +78,7 @@ public void GetCustomAttributesShouldReturnAllAttributesWithBaseInheritance() // Notice that the DummySingleA on the base method does not show up since it can only be defined once. string[] expectedAttributes = ["DummyA : derived", "DummySingleA : derived", "DummyA : base"]; - GetAttributeValuePairs(attributes).SequenceEqual(expectedAttributes).Should().BeTrue(); + GetAttributeValuePairs(attributes).Should().Equal(expectedAttributes); } public void GetCustomAttributesOnTypeShouldReturnAllAttributes() @@ -52,7 +91,7 @@ public void GetCustomAttributesOnTypeShouldReturnAllAttributes() attributes.Length.Should().Be(1); string[] expectedAttributes = ["DummyA : ba"]; - GetAttributeValuePairs(attributes).SequenceEqual(expectedAttributes).Should().BeTrue(); + GetAttributeValuePairs(attributes).Should().Equal(expectedAttributes); } private object[] GetMemberAttributes(Type type) @@ -68,7 +107,7 @@ public void GetCustomAttributesOnTypeShouldReturnAllAttributesWithBaseInheritanc attributes.Should().HaveCount(2); string[] expectedAttributes = ["DummyA : a", "DummyA : ba"]; - GetAttributeValuePairs(attributes).SequenceEqual(expectedAttributes).Should().BeTrue(); + GetAttributeValuePairs(attributes).Should().Equal(expectedAttributes); } public void GetSpecificCustomAttributesOnAssemblyShouldReturnAllAttributes() @@ -81,9 +120,495 @@ public void GetSpecificCustomAttributesOnAssemblyShouldReturnAllAttributes() attributes.Length.Should().Be(2); string[] expectedAttributes = ["DummyA : a1", "DummyA : a2"]; - GetAttributeValuePairs(attributes).SequenceEqual(expectedAttributes).Should().BeTrue(); + GetAttributeValuePairs(attributes).Should().Equal(expectedAttributes); } + #endregion + + #region GetTestCategories Tests + + /// + /// Testing test category attribute adorned at class level. + /// + public void GetTestCategoryAttributeShouldIncludeTestCategoriesAtClassLevel() + { + _attributeMockingHelper.AddCustomAttribute(typeof(TestCategoryBaseAttribute), [new TestCategoryAttribute("ClassLevel")], MemberTypes.TypeInfo); + + string[] expected = ["ClassLevel"]; + string[] actual = [.. _reflectionOperations.GetTestCategories(_method.Object, typeof(ReflectionOperationsTests))]; + + actual.Should().Equal(expected); + } + + /// + /// Testing test category attributes adorned at class, assembly and method level are getting collected. + /// + public void GetTestCategoryAttributeShouldIncludeTestCategoriesAtAllLevels() + { + _attributeMockingHelper.AddCustomAttribute(typeof(TestCategoryBaseAttribute), [new TestCategoryAttribute("AsmLevel1"), new TestCategoryAttribute("AsmLevel2")], MemberTypes.All); + _attributeMockingHelper.AddCustomAttribute(typeof(TestCategoryBaseAttribute), [new TestCategoryAttribute("AsmLevel3")], MemberTypes.All); + _attributeMockingHelper.AddCustomAttribute(typeof(TestCategoryBaseAttribute), [new TestCategoryAttribute("ClassLevel")], MemberTypes.TypeInfo); + _attributeMockingHelper.AddCustomAttribute(typeof(TestCategoryBaseAttribute), [new TestCategoryAttribute("MethodLevel")], MemberTypes.Method); + + string[] actual = [.. _reflectionOperations.GetTestCategories(_method.Object, typeof(ReflectionOperationsTests))]; + string[] expected = ["MethodLevel", "ClassLevel", "AsmLevel1", "AsmLevel2", "AsmLevel3"]; + + actual.Should().Equal(expected); + } + + /// + /// Testing test category attributes adorned at class, assembly and method level are getting collected. + /// + public void GetTestCategoryAttributeShouldConcatCustomAttributeOfSameType() + { + _attributeMockingHelper.AddCustomAttribute(typeof(TestCategoryBaseAttribute), [new TestCategoryAttribute("AsmLevel1")], MemberTypes.All); + _attributeMockingHelper.AddCustomAttribute(typeof(TestCategoryBaseAttribute), [new TestCategoryAttribute("AsmLevel2")], MemberTypes.All); + _attributeMockingHelper.AddCustomAttribute(typeof(TestCategoryBaseAttribute), [new TestCategoryAttribute("ClassLevel1")], MemberTypes.TypeInfo); + _attributeMockingHelper.AddCustomAttribute(typeof(TestCategoryBaseAttribute), [new TestCategoryAttribute("ClassLevel2")], MemberTypes.TypeInfo); + _attributeMockingHelper.AddCustomAttribute(typeof(TestCategoryBaseAttribute), [new TestCategoryAttribute("MethodLevel1")], MemberTypes.Method); + _attributeMockingHelper.AddCustomAttribute(typeof(TestCategoryBaseAttribute), [new TestCategoryAttribute("MethodLevel2")], MemberTypes.Method); + + string[] actual = [.. _reflectionOperations.GetTestCategories(_method.Object, typeof(ReflectionOperationsTests))]; + string[] expected = ["MethodLevel1", "MethodLevel2", "ClassLevel1", "ClassLevel2", "AsmLevel1", "AsmLevel2"]; + + actual.Should().Equal(expected); + } + + /// + /// Testing test category attributes adorned at assembly level. + /// + public void GetTestCategoryAttributeShouldIncludeTestCategoriesAtAssemblyLevel() + { + _attributeMockingHelper.AddCustomAttribute(typeof(TestCategoryBaseAttribute), [new TestCategoryAttribute("AsmLevel")], MemberTypes.All); + + string[] expected = ["AsmLevel"]; + + string[] actual = [.. _reflectionOperations.GetTestCategories(_method.Object, typeof(ReflectionOperationsTests))]; + + actual.Should().Equal(expected); + } + + /// + /// Testing multiple test category attribute adorned at class level. + /// + public void GetTestCategoryAttributeShouldIncludeMultipleTestCategoriesAtClassLevel() + { + _attributeMockingHelper.AddCustomAttribute(typeof(TestCategoryBaseAttribute), [new TestCategoryAttribute("ClassLevel"), new TestCategoryAttribute("ClassLevel1")], MemberTypes.TypeInfo); + + string[] expected = ["ClassLevel", "ClassLevel1"]; + string[] actual = [.. _reflectionOperations.GetTestCategories(_method.Object, typeof(ReflectionOperationsTests))]; + + actual.Should().Equal(expected); + } + + /// + /// Testing multiple test category attributes adorned at assembly level. + /// + public void GetTestCategoryAttributeShouldIncludeMultipleTestCategoriesAtAssemblyLevel() + { + _attributeMockingHelper.AddCustomAttribute(typeof(TestCategoryBaseAttribute), [new TestCategoryAttribute("AsmLevel"), new TestCategoryAttribute("AsmLevel1")], MemberTypes.All); + + string[] expected = ["AsmLevel", "AsmLevel1"]; + string[] actual = [.. _reflectionOperations.GetTestCategories(_method.Object, typeof(ReflectionOperationsTests))]; + actual.Should().Equal(expected); + } + + /// + /// Testing test category attributes adorned at method level - regression. + /// + public void GetTestCategoryAttributeShouldIncludeTestCategoriesAtMethodLevel() + { + _attributeMockingHelper.AddCustomAttribute(typeof(TestCategoryBaseAttribute), [new TestCategoryAttribute("MethodLevel")], MemberTypes.Method); + + string[] expected = ["MethodLevel"]; + string[] actual = [.. _reflectionOperations.GetTestCategories(_method.Object, typeof(ReflectionOperationsTests))]; + + actual.Should().Equal(expected); + } + + #endregion + + #region GetTestPropertiesAsTraits Tests + + /// + /// Testing test property attributes adorned at method level. + /// + public void GetTestPropertiesAsTraitsShouldIncludeTestPropertiesAtMethodLevel() + { + _attributeMockingHelper.AddCustomAttribute(typeof(TestPropertyAttribute), [new TestPropertyAttribute("MethodKey", "MethodValue")], MemberTypes.Method); + + Trait[] actual = _reflectionOperations.GetTestPropertiesAsTraits(_method.Object); + + actual.Should().Equal([new Trait("MethodKey", "MethodValue")]); + } + + /// + /// Testing test property attributes adorned at class level. + /// + public void GetTestPropertiesAsTraitsShouldIncludeTestPropertiesAtClassLevel() + { + _attributeMockingHelper.AddCustomAttribute(typeof(TestPropertyAttribute), [new TestPropertyAttribute("ClassKey", "ClassValue")], MemberTypes.TypeInfo); + + Trait[] actual = _reflectionOperations.GetTestPropertiesAsTraits(_method.Object); + + actual.Should().Equal([new Trait("ClassKey", "ClassValue")]); + } + + /// + /// Testing test property attributes adorned at both method and class level. + /// + public void GetTestPropertiesAsTraitsShouldIncludeTestPropertiesAtAllLevels() + { + _attributeMockingHelper.AddCustomAttribute(typeof(TestPropertyAttribute), [new TestPropertyAttribute("MethodKey", "MethodValue")], MemberTypes.Method); + _attributeMockingHelper.AddCustomAttribute(typeof(TestPropertyAttribute), [new TestPropertyAttribute("ClassKey", "ClassValue")], MemberTypes.TypeInfo); + + Trait[] actual = _reflectionOperations.GetTestPropertiesAsTraits(_method.Object); + + actual.Should().Equal([new Trait("MethodKey", "MethodValue"), new Trait("ClassKey", "ClassValue")]); + } + + /// + /// Testing multiple test property attributes adorned at method level. + /// + public void GetTestPropertiesAsTraitsShouldIncludeMultipleTestPropertiesAtMethodLevel() + { + _attributeMockingHelper.AddCustomAttribute(typeof(TestPropertyAttribute), [new TestPropertyAttribute("Key1", "Value1"), new TestPropertyAttribute("Key2", "Value2")], MemberTypes.Method); + + Trait[] actual = _reflectionOperations.GetTestPropertiesAsTraits(_method.Object); + + actual.Should().Equal([new Trait("Key1", "Value1"), new Trait("Key2", "Value2")]); + } + + /// + /// Testing that empty array is returned when no test properties are defined. + /// + public void GetTestPropertiesAsTraitsShouldReturnEmptyWhenNoTestProperties() + { + _attributeMockingHelper.AddCustomAttribute(typeof(TestCategoryBaseAttribute), [new TestCategoryAttribute("SomeCategory")], MemberTypes.Method); + + Trait[] actual = _reflectionOperations.GetTestPropertiesAsTraits(_method.Object); + + actual.Should().BeEmpty(); + } + + #endregion + + #region IsAttributeDefined Tests + + public void IsAttributeDefinedShouldReturnTrueIfSpecifiedAttributeIsDefinedOnAMember() + { + var rh = new ReflectionOperations(); + var mockMemberInfo = new Mock(); + var attributes = new Attribute[] { new TestMethodAttribute() }; + + _testablePlatformServiceProvider.MockReflectionOperations. + Setup(ro => ro.GetCustomAttributes(mockMemberInfo.Object)). + Returns(attributes); + + rh.IsAttributeDefined(mockMemberInfo.Object).Should().BeTrue(); + } + + public void IsAttributeDefinedShouldReturnFalseIfSpecifiedAttributeIsNotDefinedOnAMember() + { + var rh = new ReflectionOperations(); + var mockMemberInfo = new Mock(); + var attributes = new Attribute[] { new TestClassAttribute() }; + + _testablePlatformServiceProvider.MockReflectionOperations. + Setup(ro => ro.GetCustomAttributes(mockMemberInfo.Object)). + Returns(attributes); + + rh.IsAttributeDefined(mockMemberInfo.Object).Should().BeFalse(); + } + + public void IsAttributeDefinedShouldReturnFromCache() + { + var rh = new ReflectionOperations(); + + // Not using mocks here because for some reason a dictionary match of the mock is not returning true in the product code. + var memberInfo = (MethodInfo)MethodBase.GetCurrentMethod()!; + + // new Mock(); + var attributes = new Attribute[] { new TestMethodAttribute() }; + + _testablePlatformServiceProvider.MockReflectionOperations. + Setup(ro => ro.GetCustomAttributes(memberInfo)). + Returns(attributes); + + rh.IsAttributeDefined(memberInfo).Should().BeTrue(); + + // Validate that reflection APIs are not called again. + rh.IsAttributeDefined(memberInfo).Should().BeTrue(); + _testablePlatformServiceProvider.MockReflectionOperations.Verify(ro => ro.GetCustomAttributes(memberInfo), Times.Once); + } + + public void HasAttributeDerivedFromShouldReturnTrueIfSpecifiedAttributeIsDefinedOnAMember() + { + var rh = new ReflectionOperations(); + var mockMemberInfo = new Mock(); + var attributes = new Attribute[] { new TestableExtendedTestMethod() }; + + _testablePlatformServiceProvider.MockReflectionOperations. + Setup(ro => ro.GetCustomAttributes(mockMemberInfo.Object)). + Returns(attributes); + + rh.IsAttributeDefined(mockMemberInfo.Object).Should().BeTrue(); + } + + public void HasAttributeDerivedFromShouldReturnFalseIfSpecifiedAttributeIsNotDefinedOnAMember() + { + var rh = new ReflectionOperations(); + var mockMemberInfo = new Mock(); + var attributes = new Attribute[] { new TestableExtendedTestMethod() }; + + _testablePlatformServiceProvider.MockReflectionOperations. + Setup(ro => ro.GetCustomAttributes(mockMemberInfo.Object)). + Returns(attributes); + + rh.IsAttributeDefined(mockMemberInfo.Object).Should().BeFalse(); + } + + public void HasAttributeDerivedFromShouldReturnFromCache() + { + var rh = new ReflectionOperations(); + + // Not using mocks here because for some reason a dictionary match of the mock is not returning true in the product code. + var memberInfo = (MethodInfo)MethodBase.GetCurrentMethod()!; + + // new Mock(); + var attributes = new Attribute[] { new TestableExtendedTestMethod() }; + + _testablePlatformServiceProvider.MockReflectionOperations. + Setup(ro => ro.GetCustomAttributes(memberInfo)). + Returns(attributes); + + rh.IsAttributeDefined(memberInfo).Should().BeTrue(); + + // Validate that reflection APIs are not called again. + rh.IsAttributeDefined(memberInfo).Should().BeTrue(); + _testablePlatformServiceProvider.MockReflectionOperations.Verify(ro => ro.GetCustomAttributes(memberInfo), Times.Once); + } + + public void HasAttributeDerivedFromShouldReturnFalseQueryingProvidedAttributesExistenceIfGettingAllAttributesFail() + { + var rh = new ReflectionOperations(); + var mockMemberInfo = new Mock(); + + _testablePlatformServiceProvider.MockReflectionOperations. + Setup(ro => ro.GetCustomAttributes(mockMemberInfo.Object)). + Returns((object[])null!); + + rh.IsAttributeDefined(mockMemberInfo.Object).Should().BeFalse(); + } + + #endregion + + #region GetFirstAttributeOrDefault Tests + + public void GetFirstAttributeOrDefaultShouldReturnAttributeWhenPresent() + { + var rh = new ReflectionOperations(); + var mockMemberInfo = new Mock(); + var expectedAttribute = new DummySealedAttribute("value"); + var attributes = new Attribute[] { expectedAttribute }; + + _testablePlatformServiceProvider.MockReflectionOperations + .Setup(ro => ro.GetCustomAttributes(mockMemberInfo.Object)) + .Returns(attributes); + + rh.GetFirstAttributeOrDefault(mockMemberInfo.Object).Should().BeSameAs(expectedAttribute); + } + + public void GetFirstAttributeOrDefaultShouldReturnNullWhenNotPresent() + { + var rh = new ReflectionOperations(); + var mockMemberInfo = new Mock(); + var attributes = new Attribute[] { new DummyAAttribute("foo") }; + + _testablePlatformServiceProvider.MockReflectionOperations + .Setup(ro => ro.GetCustomAttributes(mockMemberInfo.Object)) + .Returns(attributes); + + rh.GetFirstAttributeOrDefault(mockMemberInfo.Object).Should().BeNull(); + } + + #endregion + + #region GetSingleAttributeOrDefault Tests + + public void GetSingleAttributeOrDefaultShouldReturnAttributeWhenPresent() + { + var rh = new ReflectionOperations(); + var mockMemberInfo = new Mock(); + var expectedAttribute = new DummyAAttribute("value"); + var attributes = new Attribute[] { expectedAttribute }; + + _testablePlatformServiceProvider.MockReflectionOperations + .Setup(ro => ro.GetCustomAttributes(mockMemberInfo.Object)) + .Returns(attributes); + + rh.GetSingleAttributeOrDefault(mockMemberInfo.Object).Should().BeSameAs(expectedAttribute); + } + + public void GetSingleAttributeOrDefaultShouldReturnNullWhenNotPresent() + { + var rh = new ReflectionOperations(); + var mockMemberInfo = new Mock(); + var attributes = new Attribute[] { new DummySingleAAttribute("foo") }; + + _testablePlatformServiceProvider.MockReflectionOperations + .Setup(ro => ro.GetCustomAttributes(mockMemberInfo.Object)) + .Returns(attributes); + + rh.GetSingleAttributeOrDefault(mockMemberInfo.Object).Should().BeNull(); + } + + public void GetSingleAttributeOrDefaultShouldThrowWhenMultipleAttributesPresent() + { + var rh = new ReflectionOperations(); + var mockMemberInfo = new Mock(); + var attributes = new Attribute[] { new DummyAAttribute("first"), new DummyAAttribute("second") }; + + _testablePlatformServiceProvider.MockReflectionOperations + .Setup(ro => ro.GetCustomAttributes(mockMemberInfo.Object)) + .Returns(attributes); + + Action action = () => rh.GetSingleAttributeOrDefault(mockMemberInfo.Object); + action.Should().Throw() + .WithMessage($"*{nameof(DummyAAttribute)}*"); + } + + #endregion + + #region Null Guard Tests + + public void IsAttributeDefinedShouldThrowWhenProviderIsNull() + { + var rh = new ReflectionOperations(); + Action action = () => rh.IsAttributeDefined(null!); + action.Should().Throw().WithParameterName("attributeProvider"); + } + + public void GetFirstAttributeOrDefaultShouldThrowWhenProviderIsNull() + { + var rh = new ReflectionOperations(); + Action action = () => rh.GetFirstAttributeOrDefault(null!); + action.Should().Throw().WithParameterName("attributeProvider"); + } + + public void GetSingleAttributeOrDefaultShouldThrowWhenProviderIsNull() + { + var rh = new ReflectionOperations(); + Action action = () => rh.GetSingleAttributeOrDefault(null!); + action.Should().Throw().WithParameterName("attributeProvider"); + } + + public void GetAttributesShouldThrowWhenProviderIsNull() + { + var rh = new ReflectionOperations(); + Action action = () => _ = rh.GetAttributes(null!).ToArray(); + action.Should().Throw().WithParameterName("attributeProvider"); + } + + public void GetCustomAttributesCachedShouldThrowWhenProviderIsNull() + { + var rh = new ReflectionOperations(); + Action action = () => rh.GetCustomAttributesCached(null!); + action.Should().Throw().WithParameterName("attributeProvider"); + } + + #endregion + + #region GetAttributes Tests + + public void GetAttributesShouldReturnMatchingAttributes() + { + var rh = new ReflectionOperations(); + var mockMemberInfo = new Mock(); + var expected = new DummyAAttribute("x"); + + _testablePlatformServiceProvider.MockReflectionOperations + .Setup(ro => ro.GetCustomAttributes(mockMemberInfo.Object)) + .Returns(new Attribute[] { expected, new DummySingleAAttribute("y") }); + + rh.GetAttributes(mockMemberInfo.Object).Should().ContainSingle().Which.Should().BeSameAs(expected); + } + + public void GetAttributesShouldReturnEmptyWhenNoMatchingAttribute() + { + var rh = new ReflectionOperations(); + var mockMemberInfo = new Mock(); + + _testablePlatformServiceProvider.MockReflectionOperations + .Setup(ro => ro.GetCustomAttributes(mockMemberInfo.Object)) + .Returns(new Attribute[] { new DummySingleAAttribute("y") }); + + rh.GetAttributes(mockMemberInfo.Object).Should().BeEmpty(); + } + + public void GetAttributesShouldReturnDerivedAttributes() + { + var rh = new ReflectionOperations(); + var mockMemberInfo = new Mock(); + var derivedAttr = new DummySingleAAttribute("derived"); + + _testablePlatformServiceProvider.MockReflectionOperations + .Setup(ro => ro.GetCustomAttributes(mockMemberInfo.Object)) + .Returns(new Attribute[] { derivedAttr }); + + // DummySingleAAttribute does NOT derive from DummyAAttribute, so this should be empty. + rh.GetAttributes(mockMemberInfo.Object).Should().BeEmpty(); + + // But should match its own type. + rh.GetAttributes(mockMemberInfo.Object).Should().ContainSingle().Which.Should().BeSameAs(derivedAttr); + } + + #endregion + + #region GetTestPropertiesAsTraits Additional Tests + + public void GetTestPropertiesAsTraitsShouldNotIncludeClassLevelAttributesWhenReflectedTypeIsNull() + { + var methodWithNullReflectedType = new Mock(); + methodWithNullReflectedType.Setup(x => x.MemberType).Returns(MemberTypes.Method); + methodWithNullReflectedType.Setup(x => x.ReflectedType).Returns((Type)null!); + _attributeMockingHelper.AddCustomAttribute( + typeof(TestPropertyAttribute), + [new TestPropertyAttribute("ClassKey", "ClassValue")], + MemberTypes.TypeInfo); + + Trait[] actual = _reflectionOperations.GetTestPropertiesAsTraits(methodWithNullReflectedType.Object); + + actual.Should().BeEmpty(); + } + + #endregion + + #region MockableReflectionOperations Tests + + public void MockableReflectionOperationsGetCustomAttributesCachedShouldThrowForUnsupportedProvider() + { + var mockReflectionOps = new Mock(); + var mockable = MockableReflectionOperations.Create(mockReflectionOps); + var mockProvider = new Mock(); + + Action action = () => mockable.GetCustomAttributesCached(mockProvider.Object); + action.Should().Throw().WithParameterName("attributeProvider"); + } + + public void MockableReflectionOperationsGetCustomAttributesCachedShouldThrowForNullProvider() + { + var mockReflectionOps = new Mock(); + var mockable = MockableReflectionOperations.Create(mockReflectionOps); + + Action action = () => mockable.GetCustomAttributesCached(null!); + action.Should().Throw().WithParameterName("attributeProvider"); + } + + #endregion + + #region Helpers + private static string[] GetAttributeValuePairs(object[] attributes) { var attribValuePairs = new List(); @@ -102,6 +627,73 @@ private static string[] GetAttributeValuePairs(object[] attributes) return [.. attribValuePairs]; } + internal class AttributeMockingHelper + { + public AttributeMockingHelper(Mock mockReflectionOperations) => _mockReflectionOperations = mockReflectionOperations; + + /// + /// A collection to hold mock custom attributes. + /// MemberTypes.All for assembly level + /// MemberTypes.TypeInfo for class level + /// MemberTypes.Method for method level. + /// + private readonly List<(Type Type, Attribute Attribute, MemberTypes MemberType)> _data = []; + private readonly Mock _mockReflectionOperations; + + /// + /// Adds (not replaces) attribute entries of + /// to the internal list used by the mock handler. Successive calls for the + /// same are cumulative — previous entries are + /// kept alongside new ones. + /// + public void AddCustomAttribute(Type type, Attribute[] values, MemberTypes memberTypes) + { + foreach (Attribute attribute in values) + { + _data.Add((type, attribute, memberTypes)); + } + + _mockReflectionOperations.Setup(r => r.GetCustomAttributes(It.IsAny())) + .Returns(GetCustomAttributesNotCached); + _mockReflectionOperations.Setup(r => r.GetCustomAttributes(It.IsAny(), It.IsAny())) + .Returns((assembly, _) => GetCustomAttributesNotCached(assembly)); + } + + public object[] GetCustomAttributesNotCached(ICustomAttributeProvider attributeProvider) + { + var foundAttributes = new List(); + foreach ((Type Type, Attribute Attribute, MemberTypes MemberType) attributeData in _data) + { + if (attributeProvider is MethodInfo && (attributeData.MemberType == MemberTypes.Method)) + { + foundAttributes.Add(attributeData.Attribute); + } + else if (attributeProvider is TypeInfo && (attributeData.MemberType == MemberTypes.TypeInfo)) + { + foundAttributes.Add(attributeData.Attribute); + } + else if (attributeProvider is Assembly && attributeData.MemberType == MemberTypes.All) + { + foundAttributes.Add(attributeData.Attribute); + } + } + + return foundAttributes.ToArray(); + } + } + + #endregion + + #region Dummy Test Classes + + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class | AttributeTargets.Assembly)] + public sealed class DummySealedAttribute : Attribute + { + public DummySealedAttribute(string foo) => Value = foo; + + public string Value { get; set; } + } + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class | AttributeTargets.Assembly, AllowMultiple = true)] public class DummyAAttribute : Attribute { @@ -146,7 +738,13 @@ public void DummyTestMethod2() { } } + + #endregion } -#pragma warning restore SA1649 // SA1649FileNameMustMatchTypeName +#region Dummy Implementations + +public class TestableExtendedTestMethod : TestMethodAttribute; + +#endregion diff --git a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Services/TestDeploymentTests.cs b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Services/TestDeploymentTests.cs index 42e2e9d5d6..c88f094732 100644 --- a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Services/TestDeploymentTests.cs +++ b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Services/TestDeploymentTests.cs @@ -4,9 +4,9 @@ #if !WINDOWS_UWP && !WIN_UI using AwesomeAssertions; -using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Helpers; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Deployment; +using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Utilities; using Microsoft.VisualStudio.TestPlatform.ObjectModel; using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; @@ -25,7 +25,7 @@ public class TestDeploymentTests : TestContainer private const string DefaultDeploymentItemPath = @"c:\temp"; private const string DefaultDeploymentItemOutputDirectory = "out"; - private readonly Mock _mockReflectHelper; + private readonly Mock _mockReflectionOperations; private readonly Mock _mockFileUtility; #pragma warning disable IDE0044 // Add readonly modifier @@ -34,7 +34,7 @@ public class TestDeploymentTests : TestContainer public TestDeploymentTests() { - _mockReflectHelper = new Mock(); + _mockReflectionOperations = new Mock(); _mockFileUtility = new Mock(); _warnings = []; @@ -53,7 +53,7 @@ public void GetDeploymentItemsReturnsNullWhenNoDeploymentItems() public void GetDeploymentItemsReturnsDeploymentItems() { // Arrange. - var testDeployment = new TestDeployment(new DeploymentItemUtility(_mockReflectHelper.Object), null!, null!); + var testDeployment = new TestDeployment(new DeploymentItemUtility(_mockReflectionOperations.Object), null!, null!); // setup mocks KeyValuePair[] methodLevelDeploymentItems = @@ -182,7 +182,7 @@ public void DeployShouldReturnFalseWhenDeploymentEnabledSetToFalseButHasDeployme testCase.SetPropertyValue(DeploymentItemUtilityTests.DeploymentItemsProperty, kvpArray); var testDeployment = new TestDeployment( - new DeploymentItemUtility(_mockReflectHelper.Object), + new DeploymentItemUtility(_mockReflectionOperations.Object), new DeploymentUtility(), _mockFileUtility.Object); @@ -205,7 +205,7 @@ public void DeployShouldReturnFalseWhenDeploymentEnabledSetToFalseAndHasNoDeploy var testCase = new TestCase("A.C.M", new Uri("executor://testExecutor"), "path/to/asm.dll"); testCase.SetPropertyValue(DeploymentItemUtilityTests.DeploymentItemsProperty, null); var testDeployment = new TestDeployment( - new DeploymentItemUtility(_mockReflectHelper.Object), + new DeploymentItemUtility(_mockReflectionOperations.Object), new DeploymentUtility(), _mockFileUtility.Object); @@ -232,7 +232,7 @@ public void DeployShouldReturnFalseWhenDeploymentEnabledSetToTrueButHasNoDeploym var testCase = new TestCase("A.C.M", new Uri("executor://testExecutor"), "path/to/asm.dll"); testCase.SetPropertyValue(DeploymentItemUtilityTests.DeploymentItemsProperty, null); var testDeployment = new TestDeployment( - new DeploymentItemUtility(_mockReflectHelper.Object), + new DeploymentItemUtility(_mockReflectionOperations.Object), new DeploymentUtility(), _mockFileUtility.Object); @@ -266,7 +266,7 @@ internal void DeployShouldReturnTrueWhenDeploymentEnabledSetToTrueAndHasDeployme ]; testCase.SetPropertyValue(DeploymentItemUtilityTests.DeploymentItemsProperty, kvpArray); var testDeployment = new TestDeployment( - new DeploymentItemUtility(_mockReflectHelper.Object), + new DeploymentItemUtility(_mockReflectionOperations.Object), new DeploymentUtility(), _mockFileUtility.Object); @@ -370,7 +370,7 @@ public void GetDeploymentInformationShouldReturnRunDirectoryInformationIfSourceI #region private methods - private void SetupDeploymentItems(MemberInfo memberInfo, KeyValuePair[] deploymentItems) + private void SetupDeploymentItems(ICustomAttributeProvider attributeProvider, KeyValuePair[] deploymentItems) { var deploymentItemAttributes = new List(); @@ -379,9 +379,8 @@ private void SetupDeploymentItems(MemberInfo memberInfo, KeyValuePair ru.GetAttributes(memberInfo)) - .Returns(deploymentItemAttributes.ToArray()); + _mockReflectionOperations.Setup( + ru => ru.GetAttributes(attributeProvider)).Returns(deploymentItemAttributes); } private static TestCase GetTestCase(string source) @@ -425,7 +424,7 @@ private TestDeployment CreateAndSetupDeploymentRelatedUtilities(out TestRunDirec _mockFileUtility.Setup(fu => fu.GetNextIterationDirectoryName(It.IsAny(), It.IsAny())) .Returns(testRunDirectories.RootDeploymentDirectory); - var deploymentItemUtility = new DeploymentItemUtility(_mockReflectHelper.Object); + var deploymentItemUtility = new DeploymentItemUtility(_mockReflectionOperations.Object); return new TestDeployment( deploymentItemUtility, diff --git a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/TestableImplementations/MockableReflectionOperations.cs b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/TestableImplementations/MockableReflectionOperations.cs new file mode 100644 index 0000000000..3d37619069 --- /dev/null +++ b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/TestableImplementations/MockableReflectionOperations.cs @@ -0,0 +1,109 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; +using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Resources; + +using Moq; + +namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.UnitTests.TestableImplementations; + +/// +/// An implementation that delegates non-generic interface methods to a Moq +/// mock, and implements the higher-level generic methods by filtering the mock's +/// results. +/// This bridges the gap where Moq cannot set up generic methods with type constraints. +/// Tests should set up GetCustomAttributes(MemberInfo) or +/// GetCustomAttributes(Assembly, Type) on the mock, and the generic methods will filter those results. +/// +internal sealed class MockableReflectionOperations(Mock mock) : IReflectionOperations +{ + /// + /// Creates a new from a mock. + /// + public static MockableReflectionOperations Create(Mock mock) + => new(mock); + + // Pre-existing interface methods → delegate to mock + [return: NotNullIfNotNull(nameof(memberInfo))] + public object[]? GetCustomAttributes(MemberInfo memberInfo) => mock.Object.GetCustomAttributes(memberInfo); + + public object[] GetCustomAttributes(Assembly assembly, Type type) => mock.Object.GetCustomAttributes(assembly, type); + + public ConstructorInfo[] GetDeclaredConstructors(Type classType) => mock.Object.GetDeclaredConstructors(classType); + + public MethodInfo[] GetDeclaredMethods(Type classType) => mock.Object.GetDeclaredMethods(classType); + + public PropertyInfo[] GetDeclaredProperties(Type type) => mock.Object.GetDeclaredProperties(type); + + public Type[] GetDefinedTypes(Assembly assembly) => mock.Object.GetDefinedTypes(assembly); + + public MethodInfo[] GetRuntimeMethods(Type type) => mock.Object.GetRuntimeMethods(type); + + public MethodInfo? GetRuntimeMethod(Type declaringType, string methodName, Type[] parameters, bool includeNonPublic) + => mock.Object.GetRuntimeMethod(declaringType, methodName, parameters, includeNonPublic); + + public PropertyInfo? GetRuntimeProperty(Type classType, string propertyName, bool includeNonPublic) + => mock.Object.GetRuntimeProperty(classType, propertyName, includeNonPublic); + + public Type? GetType(string typeName) => mock.Object.GetType(typeName); + + public Type? GetType(Assembly assembly, string typeName) => mock.Object.GetType(assembly, typeName); + + public object? CreateInstance(Type type, object?[] parameters) => mock.Object.CreateInstance(type, parameters); + + // Higher-level generic methods → filter results from mock's GetCustomAttributes + public bool IsAttributeDefined(ICustomAttributeProvider attributeProvider) + where TAttribute : Attribute + => GetCustomAttributesCached(attributeProvider).OfType().Any(); + + public TAttribute? GetFirstAttributeOrDefault(ICustomAttributeProvider attributeProvider) + where TAttribute : Attribute + => GetCustomAttributesCached(attributeProvider).OfType().FirstOrDefault(); + + public TAttribute? GetSingleAttributeOrDefault(ICustomAttributeProvider attributeProvider) + where TAttribute : Attribute + { + // Mirror the production ReflectionOperations implementation so that the error + // message on duplicate attributes matches what callers would see at runtime. + TAttribute? found = null; + foreach (Attribute attr in GetCustomAttributesCached(attributeProvider)) + { + if (attr is TAttribute match) + { + if (found is not null) + { + throw new InvalidOperationException( + string.Format(CultureInfo.InvariantCulture, Resource.DuplicateAttributeError, typeof(TAttribute))); + } + + found = match; + } + } + + return found; + } + + public IEnumerable GetAttributes(ICustomAttributeProvider attributeProvider) + where TAttributeType : Attribute + => GetCustomAttributesCached(attributeProvider).OfType(); + + // Note: no caching by design — each call re-queries the mock so that test setups + // can change the returned attributes between calls without needing to clear a cache. + public Attribute[] GetCustomAttributesCached(ICustomAttributeProvider attributeProvider) + { + _ = attributeProvider ?? throw new ArgumentNullException(nameof(attributeProvider)); + + return attributeProvider switch + { + MemberInfo memberInfo => mock.Object.GetCustomAttributes(memberInfo)?.OfType().ToArray() ?? [], + Assembly assembly => mock.Object.GetCustomAttributes(assembly, typeof(Attribute))?.OfType().ToArray() ?? [], + _ => throw new ArgumentException( + $"Unsupported attribute provider type: {attributeProvider.GetType()}. Only MemberInfo and Assembly are supported.", + nameof(attributeProvider)), + }; + } + + public bool IsMethodDeclaredInSameAssemblyAsType(MethodInfo method, Type type) + => mock.Object.IsMethodDeclaredInSameAssemblyAsType(method, type); +} diff --git a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/TestableImplementations/TestablePlatformServiceProvider.cs b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/TestableImplementations/TestablePlatformServiceProvider.cs index fa61772872..925f215bd2 100644 --- a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/TestableImplementations/TestablePlatformServiceProvider.cs +++ b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/TestableImplementations/TestablePlatformServiceProvider.cs @@ -35,6 +35,8 @@ internal class TestablePlatformServiceProvider : IPlatformServiceProvider public Mock MockReflectionOperations { get; set; } = null!; + private MockableReflectionOperations? _reflectionOperationsWrapper; + #endregion public IFileOperations FileOperations => MockFileOperations.Object; @@ -54,7 +56,7 @@ internal class TestablePlatformServiceProvider : IPlatformServiceProvider public IReflectionOperations ReflectionOperations { get => MockReflectionOperations != null - ? MockReflectionOperations.Object + ? (_reflectionOperationsWrapper ?? MockReflectionOperations.Object) : field ??= new ReflectionOperations(); private set; } @@ -74,5 +76,15 @@ public ITestContext GetTestContext(ITestMethod? testMethod, string? testClassFul public ITestSourceHost CreateTestSourceHost(string source, TestPlatform.ObjectModel.Adapter.IRunSettings? runSettings) => MockTestSourceHost.Object; - public void SetupMockReflectionOperations() => MockReflectionOperations = new Mock(); + public void SetupMockReflectionOperations() + { + MockReflectionOperations = new Mock(); + _reflectionOperationsWrapper = MockableReflectionOperations.Create(MockReflectionOperations); + } + + public void SetupMockReflectionOperations(Mock mock) + { + MockReflectionOperations = mock; + _reflectionOperationsWrapper = MockableReflectionOperations.Create(mock); + } } diff --git a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Utilities/DeploymentItemUtilityTests.cs b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Utilities/DeploymentItemUtilityTests.cs index bedfa7f4d6..c1a11d0c2b 100644 --- a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Utilities/DeploymentItemUtilityTests.cs +++ b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Utilities/DeploymentItemUtilityTests.cs @@ -4,8 +4,9 @@ #if !WINDOWS_UWP && !WIN_UI using AwesomeAssertions; -using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Helpers; +using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Deployment; +using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Resources; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Utilities; using Microsoft.VisualStudio.TestPlatform.ObjectModel; @@ -25,7 +26,7 @@ public class DeploymentItemUtilityTests : TestContainer TestPropertyAttributes.Hidden, typeof(TestCase)); - private readonly Mock _mockReflectHelper; + private readonly Mock _mockReflectionOperations; private readonly DeploymentItemUtility _deploymentItemUtility; private readonly ICollection _warnings; @@ -34,8 +35,8 @@ public class DeploymentItemUtilityTests : TestContainer public DeploymentItemUtilityTests() { - _mockReflectHelper = new Mock(); - _deploymentItemUtility = new DeploymentItemUtility(_mockReflectHelper.Object); + _mockReflectionOperations = new Mock(); + _deploymentItemUtility = new DeploymentItemUtility(_mockReflectionOperations.Object); _warnings = []; } @@ -43,7 +44,7 @@ public DeploymentItemUtilityTests() public void GetClassLevelDeploymentItemsShouldReturnEmptyListWhenNoDeploymentItems() { - _mockReflectHelper.Setup(x => x.GetAttributes(typeof(DeploymentItemUtilityTests))) + _mockReflectionOperations.Setup(x => x.GetAttributes(typeof(DeploymentItemUtilityTests))) .Returns([]); IList deploymentItems = _deploymentItemUtility.GetClassLevelDeploymentItems(typeof(DeploymentItemUtilityTests), _warnings); @@ -164,7 +165,7 @@ public void GetClassLevelDeploymentItemsShouldReportWarningsForInvalidDeployment public void GetDeploymentItemsShouldReturnNullOnNoDeploymentItems() { MethodInfo method = typeof(DeploymentItemUtilityTests).GetMethod("GetDeploymentItemsShouldReturnNullOnNoDeploymentItems")!; - _mockReflectHelper.Setup(x => x.GetAttributes(method)) + _mockReflectionOperations.Setup(x => x.GetAttributes(method)) .Returns([]); _deploymentItemUtility.GetDeploymentItems(method, null!, _warnings).Should().BeNull(); @@ -209,7 +210,7 @@ public void GetDeploymentItemsShouldReturnClassLevelDeploymentItemsOnly() }; MethodInfo method = typeof(DeploymentItemUtilityTests).GetMethod("GetDeploymentItemsShouldReturnNullOnNoDeploymentItems")!; - _mockReflectHelper.Setup(x => x.GetAttributes(method)) + _mockReflectionOperations.Setup(x => x.GetAttributes(method)) .Returns([]); // Act. @@ -414,7 +415,7 @@ public void HasDeployItemsShouldReturnTrueWhenDeploymentItemsArePresent() #region private methods - private void SetupDeploymentItems(MemberInfo memberInfo, KeyValuePair[] deploymentItems) + private void SetupDeploymentItems(ICustomAttributeProvider attributeProvider, KeyValuePair[] deploymentItems) { var deploymentItemAttributes = new List(); @@ -423,9 +424,8 @@ private void SetupDeploymentItems(MemberInfo memberInfo, KeyValuePair ru.GetAttributes(memberInfo)) - .Returns(deploymentItemAttributes.ToArray()); + _mockReflectionOperations.Setup( + ru => ru.GetAttributes(attributeProvider)).Returns(deploymentItemAttributes); } #endregion diff --git a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Utilities/DeploymentUtilityTests.cs b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Utilities/DeploymentUtilityTests.cs index 14543a254b..c66fcba062 100644 --- a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Utilities/DeploymentUtilityTests.cs +++ b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Utilities/DeploymentUtilityTests.cs @@ -4,8 +4,9 @@ #if !WINDOWS_UWP && !WIN_UI using AwesomeAssertions; -using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Helpers; +using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Deployment; +using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Resources; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Utilities; using Microsoft.VisualStudio.TestPlatform.ObjectModel; @@ -25,6 +26,7 @@ public class DeploymentUtilityTests : TestContainer private const string DefaultDeploymentItemPath = @"c:\temp"; private const string DefaultDeploymentItemOutputDirectory = "out"; + private readonly Mock _mockReflectionOperations; private readonly Mock _mockFileUtility; private readonly Mock _mockAssemblyUtility; private readonly Mock _mockRunContext; @@ -40,12 +42,13 @@ public class DeploymentUtilityTests : TestContainer public DeploymentUtilityTests() { + _mockReflectionOperations = new Mock(); _mockFileUtility = new Mock(); _mockAssemblyUtility = new Mock(); _warnings = []; _deploymentUtility = new DeploymentUtility( - new DeploymentItemUtility(new ReflectHelper()), + new DeploymentItemUtility(_mockReflectionOperations.Object), _mockAssemblyUtility.Object, _mockFileUtility.Object); diff --git a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Utilities/ReflectionUtilityTests.cs b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Utilities/ReflectionUtilityTests.cs index 5b331a5fc0..56510822c7 100644 --- a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Utilities/ReflectionUtilityTests.cs +++ b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Utilities/ReflectionUtilityTests.cs @@ -4,20 +4,24 @@ using AwesomeAssertions; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; -using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Utilities; using TestFramework.ForTestingMSTest; namespace MSTestAdapter.PlatformServices.UnitTests.Utilities; +/// +/// Tests for ReflectionOperations which provides platform-specific reflection operations. +/// public class ReflectionUtilityTests : TestContainer { + private readonly ReflectionOperations _reflectionOperations = new(); + #if NETFRAMEWORK public void GetSpecificCustomAttributesOnAssemblyShouldReturnAllAttributes() { Assembly asm = typeof(DummyTestClass).Assembly; - object[] attributes = new ReflectionOperations().GetCustomAttributes(asm, typeof(DummyAAttribute)); + object[] attributes = _reflectionOperations.GetCustomAttributes(asm, typeof(DummyAAttribute)); attributes.Should().NotBeNull(); attributes.Length.Should().Be(2); @@ -31,34 +35,90 @@ public void GetCustomAttributesShouldReturnAllAttributes() { MethodInfo methodInfo = typeof(DummyBaseTestClass).GetMethod("DummyVTestMethod1")!; - IReadOnlyList attributes = new ReflectionOperations().GetCustomAttributes(methodInfo); + object[]? attributes = _reflectionOperations.GetCustomAttributes(methodInfo); attributes.Should().NotBeNull(); attributes.Should().HaveCount(2); string[] expectedAttributes = ["DummyA : base", "DummySingleA : base"]; - GetAttributeValuePairs(attributes).Should().Equal(expectedAttributes); + GetAttributeValuePairs(attributes!).Should().Equal(expectedAttributes); } public void GetCustomAttributesShouldReturnAllAttributesWithBaseInheritance() { MethodInfo methodInfo = typeof(DummyTestClass).GetMethod("DummyVTestMethod1")!; - IReadOnlyList attributes = new ReflectionOperations().GetCustomAttributes(methodInfo); + object[]? attributes = _reflectionOperations.GetCustomAttributes(methodInfo); attributes.Should().NotBeNull(); attributes.Should().HaveCount(3); // Notice that the DummySingleA on the base method does not show up since it can only be defined once. string[] expectedAttributes = ["DummyA : derived", "DummySingleA : derived", "DummyA : base"]; - GetAttributeValuePairs(attributes).Should().Equal(expectedAttributes); + GetAttributeValuePairs(attributes!).Should().Equal(expectedAttributes); } public void GetCustomAttributesOnTypeShouldReturnAllAttributes() { Type type = typeof(DummyBaseTestClass); - IReadOnlyList attributes = new ReflectionOperations().GetCustomAttributes(type); + object[]? attributes = _reflectionOperations.GetCustomAttributes(type); + + attributes.Should().NotBeNull(); + // Filter to only our test attributes (excludes compiler-generated attributes like NullableContextAttribute) + List testAttributes = GetAttributeValuePairs(attributes!); + testAttributes.Should().HaveCount(1); + + string[] expectedAttributes = ["DummyA : ba"]; + testAttributes.Should().Equal(expectedAttributes); + } + + public void GetCustomAttributesOnTypeShouldReturnAllAttributesWithBaseInheritance() + { + Type type = typeof(DummyTestClass); + + object[]? attributes = _reflectionOperations.GetCustomAttributes(type); + + attributes.Should().NotBeNull(); + // Filter to only our test attributes (excludes compiler-generated attributes like NullableContextAttribute) + List testAttributes = GetAttributeValuePairs(attributes!); + testAttributes.Should().HaveCount(2); + + string[] expectedAttributes = ["DummyA : a", "DummyA : ba"]; + testAttributes.Should().Equal(expectedAttributes); + } + + public void GetSpecificCustomAttributesShouldReturnAllAttributes() + { + MethodInfo methodInfo = typeof(DummyBaseTestClass).GetMethod("DummyVTestMethod1")!; + + DummyAAttribute[] attributes = _reflectionOperations.GetAttributes(methodInfo).ToArray(); + + attributes.Should().NotBeNull(); + attributes.Should().HaveCount(1); + + string[] expectedAttributes = ["DummyA : base"]; + GetAttributeValuePairs(attributes).Should().Equal(expectedAttributes); + } + + public void GetSpecificCustomAttributesShouldReturnAllAttributesWithBaseInheritance() + { + MethodInfo methodInfo = typeof(DummyTestClass).GetMethod("DummyVTestMethod1")!; + + DummyAAttribute[] attributes = _reflectionOperations.GetAttributes(methodInfo).ToArray(); + + attributes.Should().NotBeNull(); + attributes.Should().HaveCount(2); + + string[] expectedAttributes = ["DummyA : derived", "DummyA : base"]; + GetAttributeValuePairs(attributes).Should().Equal(expectedAttributes); + } + + public void GetSpecificCustomAttributesOnTypeShouldReturnAllAttributes() + { + Type type = typeof(DummyBaseTestClass); + + DummyAAttribute[] attributes = _reflectionOperations.GetAttributes(type).ToArray(); attributes.Should().NotBeNull(); attributes.Should().HaveCount(1); @@ -67,11 +127,11 @@ public void GetCustomAttributesOnTypeShouldReturnAllAttributes() GetAttributeValuePairs(attributes).Should().Equal(expectedAttributes); } - public void GetCustomAttributesOnTypeShouldReturnAllAttributesWithBaseInheritance() + public void GetSpecificCustomAttributesOnTypeShouldReturnAllAttributesWithBaseInheritance() { Type type = typeof(DummyTestClass); - IReadOnlyList attributes = new ReflectionOperations().GetCustomAttributes(type); + DummyAAttribute[] attributes = _reflectionOperations.GetAttributes(type).ToArray(); attributes.Should().NotBeNull(); attributes.Should().HaveCount(2); diff --git a/test/UnitTests/MSTestAdapter.UnitTests/MSTestAdapter.UnitTests.csproj b/test/UnitTests/MSTestAdapter.UnitTests/MSTestAdapter.UnitTests.csproj index 4cd17ec7e7..b04c4a168e 100644 --- a/test/UnitTests/MSTestAdapter.UnitTests/MSTestAdapter.UnitTests.csproj +++ b/test/UnitTests/MSTestAdapter.UnitTests/MSTestAdapter.UnitTests.csproj @@ -17,6 +17,7 @@ +