Skip to content

Commit

Permalink
Support generic test method (microsoft#4204)
Browse files Browse the repository at this point in the history
  • Loading branch information
Youssef1313 authored and Evangelink committed Dec 17, 2024
1 parent 215746e commit 60314a0
Show file tree
Hide file tree
Showing 40 changed files with 879 additions and 136 deletions.
12 changes: 1 addition & 11 deletions src/Adapter/MSTest.TestAdapter/Discovery/TestMethodValidator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,22 +62,12 @@ internal virtual bool IsValidTestMethod(MethodInfo testMethodInfo, Type type, IC
return false;
}

// Generic method Definitions are not valid.
if (testMethodInfo.IsGenericMethodDefinition)
{
string message = string.Format(CultureInfo.CurrentCulture, Resource.UTA_ErrorGenericTestMethod, testMethodInfo.DeclaringType!.FullName, testMethodInfo.Name);
warnings.Add(message);
return false;
}

bool isAccessible = testMethodInfo.IsPublic
|| (_discoverInternals && testMethodInfo.IsAssembly);

// Todo: Decide whether parameter count matters.
// The isGenericMethod check below id to verify that there are no closed generic methods slipping through.
// Closed generic methods being GenericMethod<int> and open being GenericMethod<TAttribute>.
bool isValidTestMethod = isAccessible &&
testMethodInfo is { IsAbstract: false, IsStatic: false, IsGenericMethod: false } &&
testMethodInfo is { IsAbstract: false, IsStatic: false } &&
testMethodInfo.IsValidReturnType();

if (!isValidTestMethod)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Helpers;
using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel;
using Microsoft.VisualStudio.TestPlatform.MSTestAdapter;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Extensions;
Expand Down Expand Up @@ -74,7 +75,7 @@ internal static bool HasCorrectTestMethodSignature(this MethodInfo method, bool
DebugEx.Assert(method != null, "method should not be null.");

return
method is { IsAbstract: false, IsStatic: false, IsGenericMethod: false } &&
method is { IsAbstract: false, IsStatic: false } &&
(method.IsPublic || (discoverInternals && method.IsAssembly)) &&
(method.GetParameters().Length == 0 || ignoreParameterLength) &&
method.IsValidReturnType(); // Match return type Task for async methods only. Else return type void.
Expand Down Expand Up @@ -160,6 +161,11 @@ internal static void InvokeAsSynchronousTask(this MethodInfo methodInfo, object?

try
{
if (methodInfo.IsGenericMethod)
{
methodInfo = ConstructGenericMethod(methodInfo, arguments);
}

invokeResult = methodInfo.Invoke(classInstance, arguments);
}
catch (Exception ex) when (ex is TargetParameterCountException or ArgumentException)
Expand Down Expand Up @@ -188,4 +194,93 @@ internal static void InvokeAsSynchronousTask(this MethodInfo methodInfo, object?
valueTask.GetAwaiter().GetResult();
}
}

// Scenarios to test:
//
// [DataRow(null, "Hello")]
// [DataRow("Hello", null)]
// public void TestMethod<T>(T t1, T t2) { }
//
// [DataRow(0, "Hello")]
// public void TestMethod<T1, T2>(T2 p0, T1, p1) { }
private static MethodInfo ConstructGenericMethod(MethodInfo methodInfo, object?[]? arguments)
{
DebugEx.Assert(methodInfo.IsGenericMethod, "ConstructGenericMethod should only be called for a generic method.");

if (arguments is null)
{
// An example where this could happen is:
// [TestMethod]
// public void MyTestMethod<T>() { }
throw new TestFailedException(ObjectModel.UnitTestOutcome.Error, string.Format(CultureInfo.InvariantCulture, Resource.GenericParameterCantBeInferredBecauseNoArguments, methodInfo.Name));
}

Type[] genericDefinitions = methodInfo.GetGenericArguments();
var map = new (Type GenericDefinition, Type? Substitution)[genericDefinitions.Length];
for (int i = 0; i < map.Length; i++)
{
map[i] = (genericDefinitions[i], null);
}

ParameterInfo[] parameters = methodInfo.GetParameters();
for (int i = 0; i < parameters.Length; i++)
{
Type parameterType = parameters[i].ParameterType;
if (!parameterType.IsGenericMethodParameter() || arguments[i] is null)
{
continue;
}

Type substitution = arguments[i]!/*Very strange nullability warning*/.GetType();
int mapIndexForParameter = GetMapIndexForParameterType(parameterType, map);
Type? existingSubstitution = map[mapIndexForParameter].Substitution;

if (existingSubstitution is null || substitution.IsAssignableFrom(existingSubstitution))
{
map[mapIndexForParameter] = (parameterType, substitution);
}
else if (existingSubstitution.IsAssignableFrom(substitution))
{
// Do nothing. We already have a good existing substitution.
}
else
{
throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, Resource.GenericParameterConflict, parameterType.Name, existingSubstitution, substitution));
}
}

for (int i = 0; i < map.Length; i++)
{
// TODO: Better to throw? or tolerate and transform to typeof(object)?
// This is reachable in the following case for example:
// [DataRow(null)]
// public void TestMethod<T>(T t) { }
Type substitution = map[i].Substitution ?? throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, Resource.GenericParameterCantBeInferred, map[i].GenericDefinition.Name));
genericDefinitions[i] = substitution;
}

try
{
return methodInfo.MakeGenericMethod(genericDefinitions);
}
catch (Exception e)
{
// The caller catches ArgumentExceptions and will lose the original exception details.
// We transform the exception to TestFailedException here to preserve its details.
throw new TestFailedException(ObjectModel.UnitTestOutcome.Error, e.TryGetMessage(), e.TryGetStackTraceInformation(), e);
}
}

private static int GetMapIndexForParameterType(Type parameterType, (Type GenericDefinition, Type? Substitution)[] map)
{
for (int i = 0; i < map.Length; i++)
{
if (parameterType == map[i].GenericDefinition)
{
return i;
}
}

throw ApplicationStateGuard.Unreachable();
}
}
36 changes: 27 additions & 9 deletions src/Adapter/MSTest.TestAdapter/Resources/Resource.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 9 additions & 3 deletions src/Adapter/MSTest.TestAdapter/Resources/Resource.resx
Original file line number Diff line number Diff line change
Expand Up @@ -175,9 +175,6 @@
<value>Unable to load types from the test source '{0}'. Some or all of the tests in this source may not be discovered.
Error: {1}</value>
</data>
<data name="UTA_ErrorGenericTestMethod" xml:space="preserve">
<value>UTA015: A generic method cannot be a test method. {0}.{1} has invalid signature</value>
</data>
<data name="TestAssembly_FileDoesNotExist" xml:space="preserve">
<value>File does not exist: {0}</value>
</data>
Expand Down Expand Up @@ -417,4 +414,13 @@ but received {4} argument(s), with types '{5}'.</value>
<data name="DuplicateConfigurationError" xml:space="preserve">
<value>Both '.runsettings' and '.testconfig.json' files have been detected. Please select only one of these test configuration files.</value>
</data>
<data name="GenericParameterCantBeInferred" xml:space="preserve">
<value>The type of the generic parameter '{0}' could not be inferred.</value>
</data>
<data name="GenericParameterCantBeInferredBecauseNoArguments" xml:space="preserve">
<value>The generic test method '{0}' doesn't have arguments, so the generic parameter cannot be inferred.</value>
</data>
<data name="GenericParameterConflict" xml:space="preserve">
<value>Found two conflicting types for generic parameter '{0}'. The conflicting types are '{1}' and '{2}'.</value>
</data>
</root>
20 changes: 15 additions & 5 deletions src/Adapter/MSTest.TestAdapter/Resources/xlf/Resource.cs.xlf
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,21 @@ byl však přijat tento počet argumentů: {4} s typy {5}.</target>
<target state="translated">Test {0} překročil časový limit spuštění.</target>
<note></note>
</trans-unit>
<trans-unit id="GenericParameterCantBeInferred">
<source>The type of the generic parameter '{0}' could not be inferred.</source>
<target state="new">The type of the generic parameter '{0}' could not be inferred.</target>
<note />
</trans-unit>
<trans-unit id="GenericParameterCantBeInferredBecauseNoArguments">
<source>The generic test method '{0}' doesn't have arguments, so the generic parameter cannot be inferred.</source>
<target state="new">The generic test method '{0}' doesn't have arguments, so the generic parameter cannot be inferred.</target>
<note />
</trans-unit>
<trans-unit id="GenericParameterConflict">
<source>Found two conflicting types for generic parameter '{0}'. The conflicting types are '{1}' and '{2}'.</source>
<target state="new">Found two conflicting types for generic parameter '{0}'. The conflicting types are '{1}' and '{2}'.</target>
<note />
</trans-unit>
<trans-unit id="InvalidValue">
<source>Invalid value '{0}' for runsettings entry '{1}', setting will be ignored.</source>
<target state="translated">Neplatná hodnota {0} pro položku runsettings {1}. Nastavení bude ignorováno.</target>
Expand Down Expand Up @@ -203,11 +218,6 @@ Error: {1}</source>
Chyba: {1}</target>
<note></note>
</trans-unit>
<trans-unit id="UTA_ErrorGenericTestMethod">
<source>UTA015: A generic method cannot be a test method. {0}.{1} has invalid signature</source>
<target state="translated">UTA015: Obecná metoda nemůže být testovací metodou. {0}.{1} má neplatný podpis.</target>
<note></note>
</trans-unit>
<trans-unit id="TestAssembly_FileDoesNotExist">
<source>File does not exist: {0}</source>
<target state="translated">Neexistující soubor: {0}</target>
Expand Down
20 changes: 15 additions & 5 deletions src/Adapter/MSTest.TestAdapter/Resources/xlf/Resource.de.xlf
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,21 @@ aber empfing {4} Argument(e) mit den Typen „{5}“.</target>
<target state="translated">Der Test "{0}" hat das Ausführungstimeout überschritten.</target>
<note></note>
</trans-unit>
<trans-unit id="GenericParameterCantBeInferred">
<source>The type of the generic parameter '{0}' could not be inferred.</source>
<target state="new">The type of the generic parameter '{0}' could not be inferred.</target>
<note />
</trans-unit>
<trans-unit id="GenericParameterCantBeInferredBecauseNoArguments">
<source>The generic test method '{0}' doesn't have arguments, so the generic parameter cannot be inferred.</source>
<target state="new">The generic test method '{0}' doesn't have arguments, so the generic parameter cannot be inferred.</target>
<note />
</trans-unit>
<trans-unit id="GenericParameterConflict">
<source>Found two conflicting types for generic parameter '{0}'. The conflicting types are '{1}' and '{2}'.</source>
<target state="new">Found two conflicting types for generic parameter '{0}'. The conflicting types are '{1}' and '{2}'.</target>
<note />
</trans-unit>
<trans-unit id="InvalidValue">
<source>Invalid value '{0}' for runsettings entry '{1}', setting will be ignored.</source>
<target state="translated">Ungültiger Wert "{0}" für runsettings-Eintrag "{1}". Die Einstellung wird ignoriert.</target>
Expand Down Expand Up @@ -203,11 +218,6 @@ Error: {1}</source>
Fehler: {1}</target>
<note></note>
</trans-unit>
<trans-unit id="UTA_ErrorGenericTestMethod">
<source>UTA015: A generic method cannot be a test method. {0}.{1} has invalid signature</source>
<target state="translated">UTA015: Eine generische Methode kann keine Testmethode sein. '{0}.{1}' weist eine ungültige Signatur auf.</target>
<note></note>
</trans-unit>
<trans-unit id="TestAssembly_FileDoesNotExist">
<source>File does not exist: {0}</source>
<target state="translated">Die Datei ist nicht vorhanden: {0}</target>
Expand Down
20 changes: 15 additions & 5 deletions src/Adapter/MSTest.TestAdapter/Resources/xlf/Resource.es.xlf
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,21 @@ pero recibió {4} argumento(s), con los tipos "{5}".</target>
<target state="translated">La prueba '{0}' superó el tiempo de espera de ejecución.</target>
<note></note>
</trans-unit>
<trans-unit id="GenericParameterCantBeInferred">
<source>The type of the generic parameter '{0}' could not be inferred.</source>
<target state="new">The type of the generic parameter '{0}' could not be inferred.</target>
<note />
</trans-unit>
<trans-unit id="GenericParameterCantBeInferredBecauseNoArguments">
<source>The generic test method '{0}' doesn't have arguments, so the generic parameter cannot be inferred.</source>
<target state="new">The generic test method '{0}' doesn't have arguments, so the generic parameter cannot be inferred.</target>
<note />
</trans-unit>
<trans-unit id="GenericParameterConflict">
<source>Found two conflicting types for generic parameter '{0}'. The conflicting types are '{1}' and '{2}'.</source>
<target state="new">Found two conflicting types for generic parameter '{0}'. The conflicting types are '{1}' and '{2}'.</target>
<note />
</trans-unit>
<trans-unit id="InvalidValue">
<source>Invalid value '{0}' for runsettings entry '{1}', setting will be ignored.</source>
<target state="translated">Valor ''{0}'' no válido para la entrada runsettings ''{1}'', se omitirá la configuración.</target>
Expand Down Expand Up @@ -203,11 +218,6 @@ Error: {1}</source>
Error: {1}</target>
<note></note>
</trans-unit>
<trans-unit id="UTA_ErrorGenericTestMethod">
<source>UTA015: A generic method cannot be a test method. {0}.{1} has invalid signature</source>
<target state="translated">UTA015: Un método genérico no puede ser un método de prueba. {0}.{1} tiene una firma no válida</target>
<note></note>
</trans-unit>
<trans-unit id="TestAssembly_FileDoesNotExist">
<source>File does not exist: {0}</source>
<target state="translated">El archivo no existe: {0}</target>
Expand Down
20 changes: 15 additions & 5 deletions src/Adapter/MSTest.TestAdapter/Resources/xlf/Resource.fr.xlf
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,21 @@ mais a reçu {4} argument(s), avec les types « {5} ».</target>
<target state="translated">Le test '{0}' a dépassé le délai d'attente de l'exécution.</target>
<note></note>
</trans-unit>
<trans-unit id="GenericParameterCantBeInferred">
<source>The type of the generic parameter '{0}' could not be inferred.</source>
<target state="new">The type of the generic parameter '{0}' could not be inferred.</target>
<note />
</trans-unit>
<trans-unit id="GenericParameterCantBeInferredBecauseNoArguments">
<source>The generic test method '{0}' doesn't have arguments, so the generic parameter cannot be inferred.</source>
<target state="new">The generic test method '{0}' doesn't have arguments, so the generic parameter cannot be inferred.</target>
<note />
</trans-unit>
<trans-unit id="GenericParameterConflict">
<source>Found two conflicting types for generic parameter '{0}'. The conflicting types are '{1}' and '{2}'.</source>
<target state="new">Found two conflicting types for generic parameter '{0}'. The conflicting types are '{1}' and '{2}'.</target>
<note />
</trans-unit>
<trans-unit id="InvalidValue">
<source>Invalid value '{0}' for runsettings entry '{1}', setting will be ignored.</source>
<target state="translated">Valeur non valide '{0}' pour l’entrée runsettings '{1}', le paramètre sera ignoré.</target>
Expand Down Expand Up @@ -203,11 +218,6 @@ Error: {1}</source>
Erreur : {1}</target>
<note></note>
</trans-unit>
<trans-unit id="UTA_ErrorGenericTestMethod">
<source>UTA015: A generic method cannot be a test method. {0}.{1} has invalid signature</source>
<target state="translated">UTA015 : une méthode générique ne peut pas être une méthode de test. {0}.{1} a une signature non valide</target>
<note></note>
</trans-unit>
<trans-unit id="TestAssembly_FileDoesNotExist">
<source>File does not exist: {0}</source>
<target state="translated">Fichier inexistant : {0}</target>
Expand Down
Loading

0 comments on commit 60314a0

Please sign in to comment.