// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
// Note: System.Runtime.Serialization is intentionally unconditional — SerializationException is caught
// outside the #if NETFRAMEWORK block (see catch in EnumerateAssembly).
using System.Runtime.Serialization;
#if NETFRAMEWORK
using System.Security;
#endif
using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Execution;
using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Helpers;
using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Microsoft.VisualStudio.TestTools.UnitTesting.Internal;
namespace Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Discovery;
///
/// Enumerates through all types in the assembly in search of valid test methods.
///
[SuppressMessage("Performance", "CA1852: Seal internal types", Justification = "Overrides required for testability")]
internal class AssemblyEnumerator
#if NETFRAMEWORK
: MarshalByRefObject
#endif
{
///
/// Helper for reflection API's.
///
private static readonly ReflectHelper ReflectHelper = ReflectHelper.Instance;
///
/// Type cache.
///
private readonly TypeCache _typeCache = new(ReflectHelper);
///
/// Initializes a new instance of the class.
///
public AssemblyEnumerator()
{
}
///
/// Initializes a new instance of the class.
///
/// The settings for the session.
/// Use this constructor when creating this object in a new app domain so the settings for this app domain are set.
public AssemblyEnumerator(MSTestSettings settings) =>
// Populate the settings into the domain(Desktop workflow) performing discovery.
// This would just be resetting the settings to itself in non desktop workflows.
MSTestSettings.PopulateSettings(settings);
#if NETFRAMEWORK
///
/// Returns object to be used for controlling lifetime, null means infinite lifetime.
///
///
/// The .
///
[SecurityCritical]
public override object? InitializeLifetimeService() => null;
#endif
///
/// Enumerates through all types in the assembly in search of valid test methods.
///
/// The assembly file name.
/// Flag set to true when parameterized test data must be serialized.
/// A collection of Test Elements.
internal AssemblyEnumerationResult EnumerateAssembly(string assemblyFileName, bool mustSerialize)
=> EnumerateAssembly(assemblyFileName, mustSerialize, useGeneratedDescriptors: false);
///
/// Enumerates through all types in the assembly in search of valid test methods.
///
/// The assembly file name.
/// Flag set to true when parameterized test data must be serialized.
/// Whether native MTP discovery may consume complete source-generated descriptors.
/// A collection of Test Elements.
internal AssemblyEnumerationResult EnumerateAssembly(string assemblyFileName, bool mustSerialize, bool useGeneratedDescriptors)
=> EnumerateAssembly(assemblyFileName, mustSerialize, useGeneratedDescriptors, null, null);
internal AssemblyEnumerationResult EnumerateAssembly(
string assemblyFileName,
bool mustSerialize,
bool useGeneratedDescriptors,
Type[]? types,
Action?>? typeObserver)
{
List warnings = [];
DebugEx.Assert(!StringEx.IsNullOrWhiteSpace(assemblyFileName), "Invalid assembly file name.");
var tests = new List();
Assembly assembly = PlatformServiceProvider.Instance.FileOperations.LoadAssembly(assemblyFileName);
types ??= GetTypes(assembly);
bool discoverInternals = ReflectHelper.HasDiscoverInternalsAttribute(assembly);
TestDataSourceUnfoldingStrategy dataSourcesUnfoldingStrategy = ReflectHelper.GetTestDataSourceOptions(assembly)?.UnfoldingStrategy switch
{
// When strategy is auto we want to unfold
TestDataSourceUnfoldingStrategy.Auto => TestDataSourceUnfoldingStrategy.Unfold,
// When strategy is set, let's use it
{ } value => value,
// When the attribute is not set, let's look at the legacy attribute
null => ReflectHelper.GetTestDataSourceDiscoveryOption(assembly) switch
{
TestDataSourceDiscoveryOption.DuringExecution => TestDataSourceUnfoldingStrategy.Fold,
_ => TestDataSourceUnfoldingStrategy.Unfold,
},
};
foreach (Type type in types)
{
typeObserver?.Invoke(type, null);
List testsInType = DiscoverTestsInType(assemblyFileName, type, warnings, discoverInternals,
dataSourcesUnfoldingStrategy, mustSerialize, useGeneratedDescriptors);
typeObserver?.Invoke(type, testsInType);
tests.AddRange(testsInType);
}
return new AssemblyEnumerationResult(tests, warnings);
}
///
/// Gets the types defined in an assembly.
///
/// The reflected assembly.
/// Gets the types defined in the provided assembly.
internal static Type[] GetTypes(Assembly assembly)
{
try
{
return PlatformServiceProvider.Instance.ReflectionOperations.GetDefinedTypes(assembly);
}
catch (ReflectionTypeLoadException ex)
{
if (ex.LoaderExceptions != null)
{
if (ex.LoaderExceptions.Length == 1 && ex.LoaderExceptions[0] is { } singleLoaderException)
{
// This exception might be more clear than the ReflectionTypeLoadException, so we throw it.
throw singleLoaderException;
}
// If we have multiple loader exceptions, we log them all as errors, and then throw the original exception.
foreach (Exception? loaderEx in ex.LoaderExceptions)
{
if (PlatformServiceProvider.Instance.AdapterTraceLogger.IsErrorEnabled)
{
PlatformServiceProvider.Instance.AdapterTraceLogger.Error("{0}", loaderEx);
}
}
}
throw;
}
}
///
/// Returns an instance of the class.
///
/// The type to enumerate.
/// The reflected assembly name.
/// True to discover test classes which are declared internal in
/// addition to test classes which are declared public.
/// a TypeEnumerator instance.
internal virtual TypeEnumerator GetTypeEnumerator(Type type, string assemblyFileName, bool discoverInternals)
{
var typeValidator = new TypeValidator(ReflectHelper, discoverInternals);
var testMethodValidator = new TestMethodValidator(ReflectHelper, discoverInternals);
return new TypeEnumerator(type, assemblyFileName, ReflectHelper, typeValidator, testMethodValidator);
}
private List DiscoverTestsInType(
string assemblyFileName,
Type type,
List warningMessages,
bool discoverInternals,
TestDataSourceUnfoldingStrategy dataSourcesUnfoldingStrategy,
bool mustSerialize,
bool useGeneratedDescriptors)
{
string? typeFullName = null;
var tests = new List();
try
{
typeFullName = type.FullName;
TypeEnumerator testTypeEnumerator = GetTypeEnumerator(type, assemblyFileName, discoverInternals);
List? unitTestCases = useGeneratedDescriptors
? testTypeEnumerator.Enumerate(warningMessages, useGeneratedDescriptors: true)
: testTypeEnumerator.Enumerate(warningMessages);
if (unitTestCases != null)
{
foreach (UnitTestElement test in unitTestCases)
{
if (_typeCache.GetTestMethodInfoForDiscovery(test.TestMethod) is { } testMethodInfo)
{
if (TryUnfoldITestDataSources(test, testMethodInfo, dataSourcesUnfoldingStrategy, tests, mustSerialize))
{
continue;
}
}
tests.Add(test);
}
}
}
catch (Exception exception)
{
// If we fail to discover type from a class, then don't abort the discovery
// Move to the next type.
string message = string.Format(CultureInfo.CurrentCulture, Resource.CouldNotInspectTypeDuringDiscovery, typeFullName, assemblyFileName, exception.Message);
if (PlatformServiceProvider.Instance.AdapterTraceLogger.IsInfoEnabled)
{
PlatformServiceProvider.Instance.AdapterTraceLogger.Info($"AssemblyEnumerator: {message}");
}
warningMessages.Add(message);
}
return tests;
}
private static bool TryUnfoldITestDataSources(UnitTestElement test, DiscoveryTestMethodInfo testMethodInfo, TestDataSourceUnfoldingStrategy dataSourcesUnfoldingStrategy, List tests, bool mustSerialize)
{
// It should always be `true`, but if any part of the chain is obsolete; it might not contain those.
// Since we depend on those properties, if they don't exist, we bail out early.
if (!test.TestMethod.HasManagedMethodAndTypeProperties)
{
return false;
}
// If the global strategy is to fold and local uses Auto then return false
if (dataSourcesUnfoldingStrategy == TestDataSourceUnfoldingStrategy.Fold
&& test.UnfoldingStrategy == TestDataSourceUnfoldingStrategy.Auto)
{
return false;
}
// If the data source specifies the unfolding strategy as fold then return false
if (test.UnfoldingStrategy == TestDataSourceUnfoldingStrategy.Fold)
{
return false;
}
// 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.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 = [];
try
{
bool isDataDriven = false;
int globalTestCaseIndex = 0;
foreach (Attribute attribute in allAttributes)
{
if (attribute is not ITestDataSource dataSource)
{
continue;
}
isDataDriven = true;
if (!TryUnfoldITestDataSource(dataSource, test, new(testMethodInfo.MethodInfo, test.TestMethod.DisplayName), tempListOfTests, ref globalTestCaseIndex, mustSerialize))
{
// The multi-source design could be improved.
// Ideally we would want to consider each data source separately but when one source cannot be expanded,
// we will run all sources from the given method so we need to bail-out "globally".
return false;
}
}
if (tempListOfTests.Count > 0)
{
tests.AddRange(tempListOfTests);
}
return isDataDriven;
}
catch (Exception ex)
{
string message = string.Format(CultureInfo.CurrentCulture, Resource.CannotEnumerateIDataSourceAttribute, test.TestMethod.ManagedTypeName, test.TestMethod.ManagedMethodName, ex);
if (PlatformServiceProvider.Instance.AdapterTraceLogger.IsInfoEnabled)
{
PlatformServiceProvider.Instance.AdapterTraceLogger.Info($"DynamicDataEnumerator: {message}");
}
if (tempListOfTests.Count > 0)
{
tests.AddRange(tempListOfTests);
}
return false;
}
}
private static bool TryUnfoldITestDataSource(ITestDataSource dataSource, UnitTestElement test, ReflectionTestMethodInfo methodInfo, List tests, ref int globalTestCaseIndex, bool mustSerialize)
{
// Otherwise, unfold the data source and verify it can be serialized.
// This code is to discover tests. To run the tests code is in TestMethodRunner.TryExecuteFoldedDataDrivenTestsAsync.
// Any change made here should be reflected in TestMethodRunner.TryExecuteFoldedDataDrivenTestsAsync as well.
IEnumerable