Skip to content

Commit a466400

Browse files
CopilotEvangelinkAmaury LevéCopilot
authored
Refactor TestMethod type metadata to a single semantic class identity (#8213)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Evangelink <11340282+Evangelink@users.noreply.github.com> Co-authored-by: Amaury Levé <amaury@nerdyduck.dev> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent e2209d7 commit a466400

11 files changed

Lines changed: 124 additions & 17 deletions

File tree

src/Adapter/MSTestAdapter.PlatformServices/Discovery/TypeEnumerator.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,9 +125,13 @@ internal UnitTestElement GetTestFromMethod(MethodInfo method, bool classDisables
125125
// null if the current instance represents a generic type parameter.
126126
DebugEx.Assert(_type.AssemblyQualifiedName != null, "AssemblyQualifiedName for method is null.");
127127

128-
ManagedNameHelper.GetManagedNameAndHierarchy(method, out string managedType, out string managedMethod, out string?[] hierarchyValues);
128+
// Note: We pass _type.FullName (the closed-generic CLR name) as FullClassName because TypeCache.LoadType
129+
// calls assembly.GetType(FullClassName) which requires the closed-generic form to instantiate the type.
130+
// The managed type name (open-generic form, used for VSTest ManagedType property) is derived from
131+
// FullClassName by TestMethod.ManagedTypeName via stripping the generic argument list.
132+
ManagedNameHelper.GetManagedNameAndHierarchy(method, out _, out string managedMethod, out string?[] hierarchyValues);
129133
ParameterInfo[] parameters = method.GetParameters();
130-
var testMethod = new TestMethod(managedType, managedMethod, hierarchyValues, method.Name, _type.FullName!, _assemblyFilePath, null,
134+
var testMethod = new TestMethod(managedMethod, hierarchyValues, method.Name, _type.FullName!, _assemblyFilePath, null,
131135
parameters.Length == 0 ? string.Empty : string.Join(",", Array.ConvertAll(parameters, static p => p.ParameterType.ToString())))
132136
{
133137
MethodInfo = method,

src/Adapter/MSTestAdapter.PlatformServices/Extensions/TestCaseExtensions.cs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,10 +88,15 @@ internal static UnitTestElement ToUnitTestElementWithUpdatedSource(this TestCase
8888
: unitTestElement.CloneWithUpdatedSource(source);
8989
}
9090

91-
string? testClassName = testCase.GetPropertyValue(EngineConstants.TestClassNameProperty) as string;
91+
string? managedTypeName = testCase.GetManagedType();
92+
string? legacyTestClassName = testCase.GetPropertyValue(EngineConstants.TestClassNameProperty) as string;
93+
string? testClassName = testCase.GetClassNameWhenFullyQualifiedNameStartsWith(managedTypeName)
94+
?? testCase.GetClassNameWhenFullyQualifiedNameStartsWith(legacyTestClassName)
95+
?? managedTypeName
96+
?? legacyTestClassName;
9297
string name = testCase.GetTestName(testClassName);
9398

94-
var testMethod = new TestMethod(testCase.GetManagedType(), testCase.GetManagedMethod(), testCase.GetHierarchy(), name, testClassName!, source, testCase.DisplayName, testCase.GetPropertyValue<string>(EngineConstants.ParameterTypesProperty, null));
99+
var testMethod = new TestMethod(testCase.GetManagedMethod(), testCase.GetHierarchy(), name, testClassName!, source, testCase.DisplayName, testCase.GetPropertyValue<string>(EngineConstants.ParameterTypesProperty, null));
95100

96101
var dataType = (DynamicDataType)testCase.GetPropertyValue(EngineConstants.TestDynamicDataTypeProperty, (int)DynamicDataType.None);
97102
if (dataType != DynamicDataType.None)
@@ -139,6 +144,11 @@ internal static UnitTestElement ToUnitTestElementWithUpdatedSource(this TestCase
139144

140145
internal static string? GetManagedMethod(this TestCase testCase) => testCase.GetPropertyValue<string>(ManagedMethodProperty, null);
141146

147+
private static string? GetClassNameWhenFullyQualifiedNameStartsWith(this TestCase testCase, string? testClassName)
148+
=> testClassName is not null && testCase.FullyQualifiedName.StartsWith($"{testClassName}.", StringComparison.Ordinal)
149+
? testClassName
150+
: null;
151+
142152
internal static string[]? GetHierarchy(this TestCase testCase) => testCase.GetPropertyValue<string[]>(HierarchyProperty, null);
143153

144154
internal static void SetHierarchy(this TestCase testCase, string?[] value) => testCase.SetPropertyValue(HierarchyProperty, value);

src/Adapter/MSTestAdapter.PlatformServices/Interfaces/ObjectModel/ITestMethod.cs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ internal interface ITestMethod
1616
string Name { get; }
1717

1818
/// <summary>
19-
/// Gets the full class name of the test method.
19+
/// Gets the semantic full class name of the test method.
2020
/// </summary>
2121
string FullClassName { get; }
2222

@@ -31,6 +31,10 @@ internal interface ITestMethod
3131
/// <example>
3232
/// <c>NamespaceA.NamespaceB.ClassName`1+InnerClass`2</c>.
3333
/// </example>
34+
/// <remarks>
35+
/// This value is derived from <see cref="FullClassName"/>. Closed generic type arguments are omitted because
36+
/// managed type metadata uses the open generic type definition.
37+
/// </remarks>
3438
string? ManagedTypeName { get; }
3539

3640
/// <summary>
@@ -42,7 +46,8 @@ internal interface ITestMethod
4246
string? ManagedMethodName { get; }
4347

4448
/// <summary>
45-
/// Gets a value indicating whether both <see cref="ManagedTypeName"/> and <see cref="ManagedMethodName"/> are not null or whitespace.
49+
/// Gets a value indicating whether the managed method metadata is available.
50+
/// <see cref="ManagedTypeName"/> is derived from <see cref="FullClassName"/>.
4651
/// </summary>
4752
bool HasManagedMethodAndTypeProperties { get; }
4853

src/Adapter/MSTestAdapter.PlatformServices/ObjectModel/TestMethod.cs

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,17 +25,16 @@ internal sealed class TestMethod : ITestMethod
2525
/// Initializes a new instance of the <see cref="TestMethod"/> class.
2626
/// </summary>
2727
/// <param name="name">The name of the method.</param>
28-
/// <param name="fullClassName">The full name of the class declaring the method.</param>
28+
/// <param name="fullClassName">The full name of the class containing the method in execution context.</param>
2929
/// <param name="assemblyName">The full assembly name.</param>
3030
/// <param name="displayName">The display name of the test method.</param>
3131
// This constructor is used in testing only, and we should remove it in the future and update the tests.
3232
internal TestMethod(string name, string fullClassName, string assemblyName, string? displayName)
33-
: this(fullClassName, null, null, name, fullClassName, assemblyName, displayName, null)
33+
: this(null, null, name, fullClassName, assemblyName, displayName, null)
3434
{
3535
}
3636

3737
internal TestMethod(
38-
string? managedTypeName,
3938
string? managedMethodName,
4039
string?[]? hierarchyValues,
4140
string name,
@@ -58,7 +57,6 @@ internal TestMethod(
5857
Hierarchy = new ReadOnlyCollection<string?>(hierarchyValues);
5958
}
6059

61-
ManagedTypeName = managedTypeName;
6260
ManagedMethodName = managedMethodName;
6361
}
6462

@@ -74,14 +72,15 @@ internal TestMethod(
7472
public string AssemblyName { get; private set; }
7573

7674
/// <inheritdoc />
77-
public string? ManagedTypeName { get; }
75+
public string? ManagedTypeName => GetManagedTypeName(FullClassName);
7876

7977
/// <inheritdoc />
8078
public string? ManagedMethodName { get; }
8179

8280
/// <inheritdoc />
83-
[MemberNotNullWhen(true, nameof(ManagedTypeName), nameof(ManagedMethodName))]
84-
public bool HasManagedMethodAndTypeProperties => !StringEx.IsNullOrWhiteSpace(ManagedTypeName) && !StringEx.IsNullOrWhiteSpace(ManagedMethodName);
81+
[MemberNotNullWhen(true, nameof(ManagedMethodName))]
82+
// ManagedTypeName is derived from FullClassName, so this only gates the managed method metadata.
83+
public bool HasManagedMethodAndTypeProperties => !StringEx.IsNullOrWhiteSpace(ManagedMethodName);
8584

8685
/// <inheritdoc />
8786
public IReadOnlyCollection<string?>? Hierarchy { get; }
@@ -107,6 +106,14 @@ internal TestMethod(
107106
[field: NonSerialized]
108107
internal MethodInfo? MethodInfo { get; set; }
109108

109+
private static string GetManagedTypeName(string fullClassName)
110+
{
111+
int genericArgumentsStart = fullClassName.IndexOf('[');
112+
return genericArgumentsStart >= 0
113+
? fullClassName[..genericArgumentsStart]
114+
: fullClassName;
115+
}
116+
110117
/// <summary>
111118
/// Gets or sets the test data source ignore message.
112119
/// </summary>

test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Discovery/TypeEnumeratorTests.cs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,13 @@
55

66
using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter;
77
using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Discovery;
8+
using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Execution;
9+
using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Extensions;
810
using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Helpers;
911
using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.UnitTests.TestableImplementations;
1012
using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter;
1113
using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging;
14+
using Microsoft.VisualStudio.TestTools.UnitTesting;
1215

1316
using Moq;
1417

@@ -225,6 +228,30 @@ public void GetTestFromMethodShouldInitiateTestMethodWithCorrectParameters()
225228
testElement.TestMethod.AssemblyName.Should().Be("DummyAssemblyName");
226229
}
227230

231+
public void GetTestFromMethodShouldUseClosedFullClassNameAndOpenManagedTypeNameForGenericTypes()
232+
{
233+
Type closedType = typeof(DummyGenericTestClass<int>);
234+
string assemblyName = Assembly.GetExecutingAssembly().FullName!;
235+
TypeEnumerator typeEnumerator = GetTypeEnumeratorInstance(closedType, assemblyName);
236+
237+
MSTest.TestAdapter.ObjectModel.UnitTestElement testElement = typeEnumerator.GetTestFromMethod(closedType.GetMethod(nameof(DummyGenericTestClass<>.GenericTestMethod))!, classDisablesParallelization: false, _warnings);
238+
239+
testElement.TestMethod.FullClassName.Should().Be(closedType.FullName);
240+
testElement.TestMethod.ManagedTypeName.Should().Be(typeof(DummyGenericTestClass<>).FullName);
241+
242+
var testCase = testElement.ToTestCase();
243+
(testCase.GetPropertyValue(TestCaseExtensions.ManagedTypeProperty) as string).Should().Be(typeof(DummyGenericTestClass<>).FullName);
244+
245+
testCase.LocalExtensionData = null;
246+
MSTest.TestAdapter.ObjectModel.UnitTestElement roundTrippedTestElement = testCase.ToUnitTestElementWithUpdatedSource(testCase.Source);
247+
// Simulate execution after serialization, where MethodInfo is not available and managed metadata must resolve the method.
248+
roundTrippedTestElement.TestMethod.MethodInfo = null;
249+
250+
_testablePlatformServiceProvider.MockFileOperations.Setup(fo => fo.LoadAssembly(assemblyName))
251+
.Returns(Assembly.GetExecutingAssembly());
252+
new TypeCache().GetTestMethodInfo(roundTrippedTestElement.TestMethod).Should().NotBeNull();
253+
}
254+
228255
public void GetTestFromMethodShouldInitializeAsyncTypeNameCorrectly()
229256
{
230257
SetupTestClassAndTestMethods(isValidTestClass: true, isValidTestMethod: true);
@@ -517,4 +544,13 @@ public class DummySecondHidingTestClass : DummyOverridingTestClass
517544
}
518545
}
519546

547+
[TestClass]
548+
internal class DummyGenericTestClass<T>
549+
{
550+
[TestMethod]
551+
public void GenericTestMethod()
552+
{
553+
}
554+
}
555+
520556
#endregion

test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Discovery/UnitTestDiscovererTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ public UnitTestDiscovererTests()
5454
}
5555

5656
private TestMethod CreateTestMethod(string methodName, string typeFullName, string assemblyName, string? displayName)
57-
=> new(typeFullName, methodName, null, methodName, typeFullName, assemblyName, displayName, null);
57+
=> new(methodName, null, methodName, typeFullName, assemblyName, displayName, null);
5858

5959
protected override void Dispose(bool disposing)
6060
{

test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestExecutionManagerTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -810,7 +810,7 @@ private async Task RunTestsForTestShouldRunTestsInTheParentDomainsApartmentState
810810
private static TestCase GetTestCase(Type typeOfClass, string testName)
811811
{
812812
MethodInfo methodInfo = typeOfClass.GetMethod(testName)!;
813-
var testMethod = new TestMethod(typeOfClass.FullName!, methodInfo.Name, hierarchyValues: null, methodInfo.Name, typeOfClass.FullName!, Assembly.GetExecutingAssembly().Location, displayName: null, null);
813+
var testMethod = new TestMethod(methodInfo.Name, hierarchyValues: null, methodInfo.Name, typeOfClass.FullName!, Assembly.GetExecutingAssembly().Location, displayName: null, null);
814814
UnitTestElement element = new(testMethod);
815815
return element.ToTestCase();
816816
}

test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TypeCacheTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ protected override void Dispose(bool disposing)
5454
#region GetTestMethodInfo tests
5555

5656
private static TestMethod CreateTestMethod(string methodName, string className, string assemblyName, string? displayName)
57-
=> new(className, methodName, null, methodName, className, assemblyName, displayName, null);
57+
=> new(methodName, null, methodName, className, assemblyName, displayName, null);
5858

5959
public void GetTestMethodInfoShouldReturnNullIfClassInfoForTheMethodIsNull()
6060
{

test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/UnitTestRunnerTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ public UnitTestRunnerTests()
3131
}
3232

3333
private TestMethod CreateTestMethod(string methodName, string typeFullName, string assemblyName, string? displayName)
34-
=> new(typeFullName, methodName, null, methodName, typeFullName, assemblyName, displayName, null);
34+
=> new(methodName, null, methodName, typeFullName, assemblyName, displayName, null);
3535

3636
private UnitTestRunner CreateUnitTestRunner(UnitTestElement[] testsToRun)
3737
=> new(GetSettingsWithDebugTrace(false), testsToRun);

test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Extensions/TestCaseExtensionsTests.cs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,4 +55,40 @@ public void ToUnitTestElementShouldAddDeclaringClassNameToTestElementWhenAvailab
5555

5656
resultUnitTestElement.TestMethod.FullClassName.Should().Be("DummyClassName");
5757
}
58+
59+
public void ToUnitTestElementShouldPreferManagedTypeOverTestClassNameWhenAvailable()
60+
{
61+
TestCase testCase = new("SemanticClassName.DummyMethod", new("DummyUri", UriKind.Relative), Assembly.GetCallingAssembly().FullName!);
62+
testCase.SetPropertyValue(EngineConstants.TestClassNameProperty, "SyntacticClassName");
63+
testCase.SetPropertyValue(TestCaseExtensions.ManagedTypeProperty, "SemanticClassName");
64+
testCase.SetPropertyValue(TestCaseExtensions.ManagedMethodProperty, "DummyMethod");
65+
66+
UnitTestElement resultUnitTestElement = testCase.ToUnitTestElementWithUpdatedSource(testCase.Source);
67+
68+
resultUnitTestElement.TestMethod.FullClassName.Should().Be("SemanticClassName");
69+
resultUnitTestElement.TestMethod.ManagedTypeName.Should().Be("SemanticClassName");
70+
}
71+
72+
public void ToUnitTestElementShouldParseLegacyClosedGenericFullyQualifiedNameWhenManagedTypeIsOpenGeneric()
73+
{
74+
Type closedType = typeof(DummyGenericTestClass<int>);
75+
string methodName = nameof(DummyGenericTestClass<>.GenericTestMethod);
76+
TestCase testCase = new($"{closedType.FullName}.{methodName}", new("DummyUri", UriKind.Relative), Assembly.GetCallingAssembly().FullName!);
77+
testCase.SetPropertyValue(EngineConstants.TestClassNameProperty, closedType.FullName);
78+
testCase.SetPropertyValue(TestCaseExtensions.ManagedTypeProperty, typeof(DummyGenericTestClass<>).FullName);
79+
testCase.SetPropertyValue(TestCaseExtensions.ManagedMethodProperty, methodName);
80+
81+
UnitTestElement resultUnitTestElement = testCase.ToUnitTestElementWithUpdatedSource(testCase.Source);
82+
83+
resultUnitTestElement.TestMethod.Name.Should().Be(methodName);
84+
resultUnitTestElement.TestMethod.FullClassName.Should().Be(closedType.FullName);
85+
resultUnitTestElement.TestMethod.ManagedTypeName.Should().Be(typeof(DummyGenericTestClass<>).FullName);
86+
}
87+
88+
private class DummyGenericTestClass<T>
89+
{
90+
public void GenericTestMethod()
91+
{
92+
}
93+
}
5894
}

0 commit comments

Comments
 (0)