Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: extract reflection behind a provider #3801

Merged
merged 32 commits into from
Sep 21, 2024
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
6fc0aa1
Abstrace some parts of reflection
nohwnd Jun 13, 2024
99cb77e
some metadata through provider, but construction is still done via ct…
nohwnd Jun 13, 2024
e7ad398
Allow running in native with normal adapter
nohwnd Jun 14, 2024
d581268
allow-nativeaot-in-engine
nohwnd Jun 17, 2024
b983662
move so the hook works
nohwnd Jun 19, 2024
28d61c7
Merge branch 'main' into extract-reflection
nohwnd Jul 4, 2024
3a2e7f5
merges
nohwnd Jul 4, 2024
cff718e
Merge branch 'main' into extract-reflection
nohwnd Aug 12, 2024
e740ac4
Merge branch 'main' into extract-reflection
nohwnd Aug 13, 2024
62cd127
Merge branch 'main' into extract-reflection
nohwnd Aug 28, 2024
15d568a
main
nohwnd Sep 6, 2024
4ef31e0
Merge branch 'main' into extract-reflection
nohwnd Sep 6, 2024
eb091a6
Fixes
nohwnd Sep 9, 2024
1816090
fix
nohwnd Sep 10, 2024
dc2c58c
Fix various warnings
nohwnd Sep 10, 2024
28c32cf
Merge branch 'fix-warnings' into extract-reflection
nohwnd Sep 10, 2024
a7382b4
Merge branch 'main' into extract-reflection
nohwnd Sep 10, 2024
4bc35f9
Replace env variable with a static class
nohwnd Sep 10, 2024
d8c5ded
one test fixed....
nohwnd Sep 10, 2024
8c1c18f
Fix tests
nohwnd Sep 11, 2024
f6749ed
Merge branch 'main' into extract-reflection
nohwnd Sep 11, 2024
afd042a
fix whitespace
nohwnd Sep 11, 2024
3e731bb
Fix getting memebers for data driven tests
nohwnd Sep 11, 2024
a13acf1
Apply suggestions from code review
nohwnd Sep 11, 2024
ee723d9
Apply suggestions from code review
nohwnd Sep 11, 2024
6ddaf12
Apply suggestions from code review
nohwnd Sep 11, 2024
3bbaa27
seal and unpublish
nohwnd Sep 11, 2024
e59bb66
yeeey more whitespace
nohwnd Sep 11, 2024
38de9e4
class
nohwnd Sep 11, 2024
4188e16
uf
nohwnd Sep 13, 2024
fb1fa30
Update eng/Versions.props
nohwnd Sep 18, 2024
f2de560
Apply suggestions from code review
nohwnd Sep 18, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions eng/Versions.props
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
<Project>
<PropertyGroup Label="Version settings">
<!-- MSTest version -->
<VersionPrefix>3.7.0</VersionPrefix>
<VersionPrefix>3.8.0</VersionPrefix>
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this will go away before merge, but I need it for local development, and I expect some changes will be needed.

nohwnd marked this conversation as resolved.
Show resolved Hide resolved
<!-- Testing Platform version -->
<TestingPlatformVersionPrefix>1.5.0</TestingPlatformVersionPrefix>
<TestingPlatformVersionPrefix>1.6.0</TestingPlatformVersionPrefix>
nohwnd marked this conversation as resolved.
Show resolved Hide resolved
<PreReleaseVersionLabel>preview</PreReleaseVersionLabel>
</PropertyGroup>
<PropertyGroup Label="MSTest prod dependencies - darc updated">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,16 +83,15 @@ internal ICollection<UnitTestElement> EnumerateAssembly(string assemblyFileName,
Assembly assembly = PlatformServiceProvider.Instance.FileOperations.LoadAssembly(assemblyFileName, isReflectionOnly: false);

IReadOnlyList<Type> types = GetTypes(assembly, assemblyFileName, warningMessages);
bool discoverInternals = assembly.GetCustomAttribute<DiscoverInternalsAttribute>() != null;
TestIdGenerationStrategy testIdGenerationStrategy = assembly.GetCustomAttribute<TestIdGenerationStrategyAttribute>()?.Strategy
?? TestIdGenerationStrategy.FullyQualified;
bool discoverInternals = ReflectHelper.GetDiscoverInternalsAttribute(assembly) != null;
TestIdGenerationStrategy testIdGenerationStrategy = ReflectHelper.GetTestIdGenerationStrategy(assembly);

// Set the test ID generation strategy for DataRowAttribute and DynamicDataAttribute so we can improve display name without
// causing a breaking change.
DataRowAttribute.TestIdGenerationStrategy = testIdGenerationStrategy;
DynamicDataAttribute.TestIdGenerationStrategy = testIdGenerationStrategy;

TestDataSourceDiscoveryOption testDataSourceDiscovery = assembly.GetCustomAttribute<TestDataSourceDiscoveryAttribute>()?.DiscoveryOption
TestDataSourceDiscoveryOption testDataSourceDiscovery = ReflectHelper.GetTestDataSourceDiscoveryOption(assembly)
#pragma warning disable CS0618 // Type or member is obsolete

// When using legacy strategy, there is no point in trying to "read" data during discovery
Expand Down Expand Up @@ -128,7 +127,7 @@ internal static Type[] GetTypes(Assembly assembly, string assemblyFileName, ICol
{
try
{
return assembly.GetTypes();
return PlatformServiceProvider.Instance.ReflectionOperations.GetDefinedTypes(assembly);
}
catch (ReflectionTypeLoadException ex)
{
Expand Down
2 changes: 1 addition & 1 deletion src/Adapter/MSTest.TestAdapter/Discovery/TypeEnumerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ internal Collection<UnitTestElement> GetTests(ICollection<string> warnings)
// Test class is already valid. Verify methods.
// PERF: GetRuntimeMethods is used here to get all methods, including non-public, and static methods.
// if we rely on analyzers to identify all invalid methods on build, we can change this to fit the current settings.
foreach (MethodInfo method in _type.GetRuntimeMethods())
foreach (MethodInfo method in PlatformServiceProvider.Instance.ReflectionOperations.GetRuntimeMethods(_type))
{
bool isMethodDeclaredInTestTypeAssembly = _reflectHelper.IsMethodDeclaredInSameAssemblyAsType(method, _type);
bool enableMethodsFromOtherAssemblies = MSTestSettings.CurrentSettings.EnableBaseClassTestMethodsFromOtherAssemblies;
Expand Down
2 changes: 1 addition & 1 deletion src/Adapter/MSTest.TestAdapter/Discovery/TypeValidator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ internal static bool HasCorrectTestContextSignature(Type type)
{
DebugEx.Assert(type != null, "HasCorrectTestContextSignature type is null");

IEnumerable<PropertyInfo> propertyInfoEnumerable = type.GetTypeInfo().DeclaredProperties;
IEnumerable<PropertyInfo> propertyInfoEnumerable = PlatformServiceProvider.Instance.ReflectionOperations.GetDeclaredProperties(type);
var propertyInfo = new List<PropertyInfo>();

foreach (PropertyInfo pinfo in propertyInfoEnumerable)
Expand Down
172 changes: 172 additions & 0 deletions src/Adapter/MSTest.TestAdapter/DynamicDataOperations.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
// 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.TestTools.UnitTesting;

#if NET471_OR_GREATER || NETCOREAPP
using System.Collections;
using System.Runtime.CompilerServices;
#endif
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Reflection;

namespace Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter;

internal class DynamicDataOperations : IDynamicDataOperations
nohwnd marked this conversation as resolved.
Show resolved Hide resolved
{
public IEnumerable<object[]> GetData(Type? _dynamicDataDeclaringType, DynamicDataSourceType _dynamicDataSourceType, string _dynamicDataSourceName, MethodInfo methodInfo)
{
// Check if the declaring type of test data is passed in. If not, default to test method's class type.
_dynamicDataDeclaringType ??= methodInfo.DeclaringType;
DebugEx.Assert(_dynamicDataDeclaringType is not null, "Declaring type of test data cannot be null.");

object? obj = null;

switch (_dynamicDataSourceType)
{
case DynamicDataSourceType.Property:
PropertyInfo property = PlatformServiceProvider.Instance.ReflectionOperations.GetDeclaredProperty(_dynamicDataDeclaringType, _dynamicDataSourceName)
?? throw new ArgumentNullException($"{DynamicDataSourceType.Property} {_dynamicDataSourceName}");
if (property.GetGetMethod(true) is not { IsStatic: true })
{
throw new NotSupportedException(
string.Format(
CultureInfo.InvariantCulture,
FrameworkMessages.DynamicDataInvalidPropertyLayout,
property.DeclaringType?.FullName is { } typeFullName ? $"{typeFullName}.{property.Name}" : property.Name));
}

obj = property.GetValue(null, null);
break;

case DynamicDataSourceType.Method:
MethodInfo method = PlatformServiceProvider.Instance.ReflectionOperations.GetDeclaredMethod(_dynamicDataDeclaringType, _dynamicDataSourceName)
?? throw new ArgumentNullException($"{DynamicDataSourceType.Method} {_dynamicDataSourceName}");
if (!method.IsStatic
|| method.ContainsGenericParameters
|| method.GetParameters().Length > 0)
{
throw new NotSupportedException(
string.Format(
CultureInfo.InvariantCulture,
FrameworkMessages.DynamicDataInvalidPropertyLayout,
method.DeclaringType?.FullName is { } typeFullName ? $"{typeFullName}.{method.Name}" : method.Name));
}

obj = method.Invoke(null, null);
break;
}

if (obj == null)
{
throw new ArgumentNullException(
string.Format(
CultureInfo.InvariantCulture,
FrameworkMessages.DynamicDataValueNull,
_dynamicDataSourceName,
_dynamicDataDeclaringType.FullName));
}

if (!TryGetData(obj, out IEnumerable<object[]>? data))
{
throw new ArgumentNullException(
string.Format(
CultureInfo.InvariantCulture,
FrameworkMessages.DynamicDataIEnumerableNull,
_dynamicDataSourceName,
_dynamicDataDeclaringType.FullName));
}

if (!data.Any())
{
throw new ArgumentException(
string.Format(
CultureInfo.InvariantCulture,
FrameworkMessages.DynamicDataIEnumerableEmpty,
_dynamicDataSourceName,
_dynamicDataDeclaringType.FullName));
}

// Data is valid, return it.
return data;
}

/// <inheritdoc />
public string? GetDisplayName(string? DynamicDataDisplayName, Type? DynamicDataDisplayNameDeclaringType, MethodInfo methodInfo, object?[]? data)
{
if (DynamicDataDisplayName != null)
{
Type? dynamicDisplayNameDeclaringType = DynamicDataDisplayNameDeclaringType ?? methodInfo.DeclaringType;
DebugEx.Assert(dynamicDisplayNameDeclaringType is not null, "Declaring type of test data cannot be null.");

MethodInfo method = PlatformServiceProvider.Instance.ReflectionOperations.GetDeclaredMethod(dynamicDisplayNameDeclaringType, DynamicDataDisplayName)
?? throw new ArgumentNullException($"{DynamicDataSourceType.Method} {DynamicDataDisplayName}");
ParameterInfo[] parameters = method.GetParameters();
return parameters.Length != 2 ||
parameters[0].ParameterType != typeof(MethodInfo) ||
parameters[1].ParameterType != typeof(object[]) ||
method.ReturnType != typeof(string) ||
!method.IsStatic ||
!method.IsPublic
? throw new ArgumentNullException(
string.Format(
CultureInfo.InvariantCulture,
FrameworkMessages.DynamicDataDisplayName,
DynamicDataDisplayName,
nameof(String),
string.Join(", ", nameof(MethodInfo), typeof(object[]).Name)))
: method.Invoke(null, [methodInfo, data]) as string;
}

if (data != null)
{
// We want to force call to `data.AsEnumerable()` to ensure that objects are casted to strings (using ToString())
// so that null do appear as "null". If you remove the call, and do string.Join(",", new object[] { null, "a" }),
// you will get empty string while with the call you will get "null,a".
return string.Format(CultureInfo.CurrentCulture, FrameworkMessages.DataDrivenResultDisplayName, methodInfo.Name,
string.Join(",", data.AsEnumerable()));
}

return null;
}

private static bool TryGetData(object dataSource, [NotNullWhen(true)] out IEnumerable<object[]>? data)
{
if (dataSource is IEnumerable<object[]> enumerableObjectArray)
{
data = enumerableObjectArray;
return true;
}

#if NETCOREAPP || NET471_OR_GREATER
if (dataSource is IEnumerable enumerable)
{
List<object[]> objects = new();
foreach (object? entry in enumerable)
{
if (entry is not ITuple tuple
|| (objects.Count > 0 && objects[^1].Length != tuple.Length))
{
data = null;
return false;
}

object[] array = new object[tuple.Length];
for (int i = 0; i < tuple.Length; i++)
{
array[i] = tuple[i]!;
}

objects.Add(array);
}

data = objects;
return true;
}
#endif

data = null;
return false;
}
}
4 changes: 2 additions & 2 deletions src/Adapter/MSTest.TestAdapter/Execution/TestMethodInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ public virtual TestResult Invoke(object?[]? arguments)
// If this is the params parameter, instantiate a new object of that type
if (argumentIndex == parametersInfo.Length - 1)
{
paramsValues = Activator.CreateInstance(parametersInfo[argumentIndex].ParameterType, [arguments.Length - argumentIndex]);
paramsValues = PlatformServiceProvider.Instance.ReflectionOperations.CreateInstance(parametersInfo[argumentIndex].ParameterType, [arguments.Length - argumentIndex]);
newParameters[argumentIndex] = paramsValues;
}

Expand All @@ -203,7 +203,7 @@ public virtual TestResult Invoke(object?[]? arguments)
// If this is the params parameters, set it to an empty
// array of that type as DefaultValue is DBNull
newParameters[parameterNotProvidedIndex] = hasParamsValue && parameterNotProvidedIndex == parametersInfo.Length - 1
? Activator.CreateInstance(parametersInfo[parameterNotProvidedIndex].ParameterType, 0)
? PlatformServiceProvider.Instance.ReflectionOperations.CreateInstance(parametersInfo[parameterNotProvidedIndex].ParameterType, [0])
: parametersInfo[parameterNotProvidedIndex].DefaultValue;
}

Expand Down
21 changes: 10 additions & 11 deletions src/Adapter/MSTest.TestAdapter/Execution/TypeCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -225,15 +225,15 @@ private static bool TryGetUnescapedManagedTypeName(TestMethod testMethod, [NotNu
// found in (i.e. GAC, current directory, path)
// If this fails, we will try to load the type from the assembly
// location in the Out directory.
var t = Type.GetType(typeName);
Type? t = PlatformServiceProvider.Instance.ReflectionOperations.GetType(typeName);

if (t == null)
{
Assembly assembly = PlatformServiceProvider.Instance.FileOperations.LoadAssembly(assemblyName, isReflectionOnly: false);

// Attempt to load the type from the test assembly.
// Allow this call to throw if the type can't be loaded.
t = assembly.GetType(typeName);
t = PlatformServiceProvider.Instance.ReflectionOperations.GetType(assembly, typeName);
}

return t;
Expand All @@ -259,8 +259,9 @@ private static bool TryGetUnescapedManagedTypeName(TestMethod testMethod, [NotNu
/// <returns> The <see cref="TestClassInfo"/>. </returns>
private TestClassInfo CreateClassInfo(Type classType, TestMethod testMethod)
{
IEnumerable<ConstructorInfo> constructors = PlatformServiceProvider.Instance.ReflectionOperations.GetDeclaredConstructors(classType);
ConstructorInfo? constructor = constructors.FirstOrDefault(ctor => ctor.GetParameters().Length == 0 && ctor.IsPublic);
bool isParameterLessConstructor;
ConstructorInfo constructor;

if (classType.GetConstructor([typeof(TestContext)]) is { } testContextCtor)
{
Expand Down Expand Up @@ -295,7 +296,7 @@ private TestClassInfo CreateClassInfo(Type classType, TestMethod testMethod)
// the method is overridden in the derived type.
var instanceMethods = new Dictionary<string, string?>();

foreach (MethodInfo methodInfo in classType.GetTypeInfo().DeclaredMethods)
foreach (MethodInfo methodInfo in PlatformServiceProvider.Instance.ReflectionOperations.GetDeclaredMethods(classType))
{
UpdateInfoIfTestInitializeOrCleanupMethod(classInfo, methodInfo, false, instanceMethods);

Expand All @@ -306,7 +307,7 @@ private TestClassInfo CreateClassInfo(Type classType, TestMethod testMethod)
// PERF: Don't inspect object, no test methods or setups can be defined on it.
while (baseType != null && baseType != typeof(object))
{
foreach (MethodInfo methodInfo in baseType.GetTypeInfo().DeclaredMethods)
foreach (MethodInfo methodInfo in PlatformServiceProvider.Instance.ReflectionOperations.GetDeclaredMethods(baseType))
{
if (methodInfo is { IsPublic: true, IsStatic: false })
{
Expand Down Expand Up @@ -336,7 +337,7 @@ private TestClassInfo CreateClassInfo(Type classType, TestMethod testMethod)
{
try
{
PropertyInfo? testContextProperty = classType.GetRuntimeProperty(TestContextPropertyName);
PropertyInfo? testContextProperty = PlatformServiceProvider.Instance.ReflectionOperations.GetRuntimeProperty(classType, TestContextPropertyName);
if (testContextProperty == null)
{
// that's okay may be the property was not defined
Expand Down Expand Up @@ -408,7 +409,7 @@ private TestAssemblyInfo GetAssemblyInfo(Type type)
}

// Enumerate through all methods and identify the Assembly Init and cleanup methods.
foreach (MethodInfo methodInfo in t.GetTypeInfo().DeclaredMethods)
foreach (MethodInfo methodInfo in PlatformServiceProvider.Instance.ReflectionOperations.GetDeclaredMethods(t))
{
if (IsAssemblyOrClassInitializeMethod<AssemblyInitializeAttribute>(methodInfo))
{
Expand Down Expand Up @@ -831,7 +832,7 @@ private MethodInfo GetMethodInfoForTestMethod(TestMethod testMethod, TestClassIn
else if (methodBase != null)
{
Type[] parameters = methodBase.GetParameters().Select(i => i.ParameterType).ToArray();
testMethodInfo = methodBase.DeclaringType!.GetRuntimeMethod(methodBase.Name, parameters);
testMethodInfo = PlatformServiceProvider.Instance.ReflectionOperations.GetRuntimeMethod(methodBase.DeclaringType!, methodBase.Name, parameters);
}

return testMethodInfo is null
Expand All @@ -842,9 +843,7 @@ private MethodInfo GetMethodInfoForTestMethod(TestMethod testMethod, TestClassIn

private static MethodInfo? GetMethodInfoUsingRuntimeMethods(TestMethod testMethod, TestClassInfo testClassInfo, bool discoverInternals)
{
IEnumerable<MethodInfo> methods = testClassInfo
.ClassType
.GetRuntimeMethods()
IEnumerable<MethodInfo> methods = PlatformServiceProvider.Instance.ReflectionOperations.GetRuntimeMethods(testClassInfo.ClassType)
.Where(method => method.Name == testMethod.Name &&
method.HasCorrectTestMethodSignature(true, discoverInternals));

Expand Down
Loading
Loading