// 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.MSTest.TestAdapter.Extensions; using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Helpers; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Discovery; /// /// Determines if a method is a valid test method. /// [SuppressMessage("Performance", "CA1852: Seal internal types", Justification = "Overrides required for testability")] internal class TestMethodValidator { private readonly ReflectHelper _reflectHelper; private readonly bool _discoverInternals; /// /// Initializes a new instance of the class. /// /// An instance to reflection helper for type information. /// True to discover methods which are declared internal in addition to methods /// which are declared public. internal TestMethodValidator(ReflectHelper reflectHelper, bool discoverInternals) { _reflectHelper = reflectHelper; _discoverInternals = discoverInternals; } /// /// Determines if a method is a valid test method. /// /// The reflected method. /// The reflected type. /// Contains warnings if any, that need to be passed back to the caller. /// Return true if a method is a valid test method. internal virtual bool IsValidTestMethod(MethodInfo testMethodInfo, Type type, ICollection warnings) { // PERF: We are doing caching reflection here, meaning we will cache every method info in the // assembly, this is because when we discover and run we will repeatedly inspect all the methods in the assembly, and this // gives us a better performance. // It would be possible to use non-caching reflection here if we knew that we are only doing discovery that won't be followed by run, // but the difference is quite small, and we don't expect a huge amount of non-test methods in the assembly. // // Also skip all methods coming from object, because they cannot be tests. if (testMethodInfo.DeclaringType == typeof(object) || !_reflectHelper.IsAttributeDefined(testMethodInfo)) { return false; } bool isAccessible = testMethodInfo.IsPublic || (_discoverInternals && testMethodInfo.IsAssembly); bool hasValidInstanceKind = testMethodInfo.IsStatic == type.IsFSharpModule(); // Todo: Decide whether parameter count matters. bool isValidTestMethod = isAccessible && !testMethodInfo.IsAbstract && hasValidInstanceKind && testMethodInfo.IsValidReturnType(_reflectHelper); if (!isValidTestMethod) { // Only emit the targeted ValueTask message when the return type is the actual reason the method // is invalid. If the method is also inaccessible/static/abstract, the generic signature message is // more accurate, otherwise the user could "fix" the return type and still have an invalid method. bool isInvalidOnlyBecauseOfReturnType = isAccessible && !testMethodInfo.IsAbstract && hasValidInstanceKind && IsGenericValueTaskReturnType(testMethodInfo); string signatureMessage = type.IsFSharpModule() ? Resource.UTA_ErrorIncorrectFSharpTestMethodSignature : Resource.UTA_ErrorIncorrectTestMethodSignature; string message = isInvalidOnlyBecauseOfReturnType ? string.Format(CultureInfo.CurrentCulture, Resource.UTA_ErrorGenericValueTaskReturnType, type.FullName, testMethodInfo.Name) : string.Format(CultureInfo.CurrentCulture, signatureMessage, type.FullName, testMethodInfo.Name); warnings.Add(message); return false; } // Check for out/ref parameters and add a warning (but still return true) ParameterInfo[] parameters = testMethodInfo.GetParameters(); if (parameters.Length > 0 && parameters.Any(p => p.IsOut || p.ParameterType.IsByRef)) { string warningMessage = string.Format(CultureInfo.CurrentCulture, Resource.UTA_OutRefParametersNotSupported, type.FullName, testMethodInfo.Name); warnings.Add(warningMessage); } return true; } private static bool IsGenericValueTaskReturnType(MethodInfo testMethodInfo) { Type returnType = testMethodInfo.ReturnType; // Compare by name to avoid forcing a load of System.Threading.Tasks.Extensions on platforms where // ValueTask lives in a separate assembly (netstandard2.0 / .NET Framework). return returnType.IsGenericType && returnType.GetGenericTypeDefinition().FullName == "System.Threading.Tasks.ValueTask`1"; } }