Skip to content

Commit

Permalink
Allow any IEnumerable as return type to DynamicData source member (#4389
Browse files Browse the repository at this point in the history
)
  • Loading branch information
Evangelink authored Dec 19, 2024
1 parent 3fc020b commit 5bdcacd
Show file tree
Hide file tree
Showing 23 changed files with 229 additions and 293 deletions.
62 changes: 37 additions & 25 deletions src/Adapter/MSTest.TestAdapter/DynamicDataOperations.cs
Original file line number Diff line number Diff line change
Expand Up @@ -160,50 +160,62 @@ private static bool TryGetData(object dataSource, [NotNullWhen(true)] out IEnume
return true;
}

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

object[] array = new object[tuple.Length];
for (int i = 0; i < tuple.Length; i++)
if (!TryHandleTupleDataSource(entry, objects))
{
array[i] = tuple[i]!;
objects.Add(new[] { entry });
}
}

objects.Add(array);
#else
Type type = entry.GetType();
if (!IsTupleOrValueTuple(entry.GetType(), out int tupleSize)
|| (objects.Count > 0 && objects[objects.Count - 1].Length != tupleSize))
{
data = null;
return false;
}
data = objects;
return true;
}

object[] array = new object[tupleSize];
for (int i = 0; i < tupleSize; i++)
{
array[i] = type.GetField($"Item{i + 1}")?.GetValue(entry)!;
}
data = null;
return false;
}

objects.Add(array);
#endif
private static bool TryHandleTupleDataSource(object data, List<object[]> objects)
{
#if NET471_OR_GREATER || NETCOREAPP
if (data is ITuple tuple
&& (objects.Count == 0 || objects[^1].Length == tuple.Length))
{
object[] array = new object[tuple.Length];
for (int i = 0; i < tuple.Length; i++)
{
array[i] = tuple[i]!;
}

data = objects;
objects.Add(array);
return true;
}
#else
Type type = data.GetType();
if (IsTupleOrValueTuple(data.GetType(), out int tupleSize)
&& (objects.Count == 0 || objects[objects.Count - 1].Length == tupleSize))
{
object[] array = new object[tupleSize];
for (int i = 0; i < tupleSize; i++)
{
array[i] = type.GetField($"Item{i + 1}")?.GetValue(data)!;
}

objects.Add(array);
return true;
}
#endif

data = null;
return false;
}

Expand Down
42 changes: 11 additions & 31 deletions src/Analyzers/MSTest.Analyzers/DynamicDataShouldBeValidAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -85,20 +85,19 @@ public override void Initialize(AnalysisContext context)
&& context.Compilation.TryGetOrCreateTypeByMetadataName(WellKnownTypeNames.MicrosoftVisualStudioTestToolsUnitTestingDynamicDataAttribute, out INamedTypeSymbol? dynamicDataAttributeSymbol)
&& context.Compilation.TryGetOrCreateTypeByMetadataName(WellKnownTypeNames.MicrosoftVisualStudioTestToolsUnitTestingDynamicDataSourceType, out INamedTypeSymbol? dynamicDataSourceTypeSymbol)
&& context.Compilation.TryGetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemCollectionsGenericIEnumerable1, out INamedTypeSymbol? ienumerableTypeSymbol)
&& context.Compilation.TryGetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemRuntimeCompilerServicesITuple, out INamedTypeSymbol? itupleTypeSymbol)
&& context.Compilation.TryGetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemReflectionMethodInfo, out INamedTypeSymbol? methodInfoTypeSymbol))
{
context.RegisterSymbolAction(
context => AnalyzeSymbol(context, testMethodAttributeSymbol, dynamicDataAttributeSymbol, dynamicDataSourceTypeSymbol,
ienumerableTypeSymbol, itupleTypeSymbol, methodInfoTypeSymbol),
ienumerableTypeSymbol, methodInfoTypeSymbol),
SymbolKind.Method);
}
});
}

private static void AnalyzeSymbol(SymbolAnalysisContext context, INamedTypeSymbol testMethodAttributeSymbol,
INamedTypeSymbol dynamicDataAttributeSymbol, INamedTypeSymbol dynamicDataSourceTypeSymbol, INamedTypeSymbol ienumerableTypeSymbol,
INamedTypeSymbol itupleTypeSymbol, INamedTypeSymbol methodInfoTypeSymbol)
INamedTypeSymbol methodInfoTypeSymbol)
{
var methodSymbol = (IMethodSymbol)context.Symbol;

Expand All @@ -116,7 +115,7 @@ private static void AnalyzeSymbol(SymbolAnalysisContext context, INamedTypeSymbo
if (SymbolEqualityComparer.Default.Equals(methodAttribute.AttributeClass, dynamicDataAttributeSymbol))
{
hasDynamicDataAttribute = true;
AnalyzeAttribute(context, methodAttribute, methodSymbol, dynamicDataSourceTypeSymbol, ienumerableTypeSymbol, itupleTypeSymbol, methodInfoTypeSymbol);
AnalyzeAttribute(context, methodAttribute, methodSymbol, dynamicDataSourceTypeSymbol, ienumerableTypeSymbol, methodInfoTypeSymbol);
}
}

Expand All @@ -128,22 +127,19 @@ private static void AnalyzeSymbol(SymbolAnalysisContext context, INamedTypeSymbo
}

private static void AnalyzeAttribute(SymbolAnalysisContext context, AttributeData attributeData, IMethodSymbol methodSymbol,
INamedTypeSymbol dynamicDataSourceTypeSymbol, INamedTypeSymbol ienumerableTypeSymbol, INamedTypeSymbol itupleTypeSymbol,
INamedTypeSymbol methodInfoTypeSymbol)
INamedTypeSymbol dynamicDataSourceTypeSymbol, INamedTypeSymbol ienumerableTypeSymbol, INamedTypeSymbol methodInfoTypeSymbol)
{
if (attributeData.ApplicationSyntaxReference?.GetSyntax() is not { } attributeSyntax)
{
return;
}

AnalyzeDataSource(context, attributeData, attributeSyntax, methodSymbol, dynamicDataSourceTypeSymbol, ienumerableTypeSymbol,
itupleTypeSymbol);
AnalyzeDataSource(context, attributeData, attributeSyntax, methodSymbol, dynamicDataSourceTypeSymbol, ienumerableTypeSymbol);
AnalyzeDisplayNameSource(context, attributeData, attributeSyntax, methodSymbol, methodInfoTypeSymbol);
}

private static void AnalyzeDataSource(SymbolAnalysisContext context, AttributeData attributeData, SyntaxNode attributeSyntax,
IMethodSymbol methodSymbol, INamedTypeSymbol dynamicDataSourceTypeSymbol, INamedTypeSymbol ienumerableTypeSymbol,
INamedTypeSymbol itupleTypeSymbol)
IMethodSymbol methodSymbol, INamedTypeSymbol dynamicDataSourceTypeSymbol, INamedTypeSymbol ienumerableTypeSymbol)
{
string? memberName = null;
int dataSourceType = DynamicDataSourceTypeAutoDetect;
Expand Down Expand Up @@ -234,28 +230,12 @@ private static void AnalyzeDataSource(SymbolAnalysisContext context, AttributeDa

// Validate member return type.
ITypeSymbol? memberTypeSymbol = member.GetMemberType();
if (memberTypeSymbol is INamedTypeSymbol memberNamedType)
if (memberTypeSymbol is INamedTypeSymbol memberNamedType
&& (!SymbolEqualityComparer.Default.Equals(memberNamedType.ConstructedFrom, ienumerableTypeSymbol)
|| memberNamedType.TypeArguments.Length != 1))
{
if (!SymbolEqualityComparer.Default.Equals(memberNamedType.ConstructedFrom, ienumerableTypeSymbol)
|| memberNamedType.TypeArguments.Length != 1)
{
context.ReportDiagnostic(attributeSyntax.CreateDiagnostic(MemberTypeRule, declaringType.Name, memberName));
return;
}

ITypeSymbol collectionBoundType = memberNamedType.TypeArguments[0];
if (!collectionBoundType.Inherits(itupleTypeSymbol)
&& collectionBoundType is not IArrayTypeSymbol)
{
context.ReportDiagnostic(attributeSyntax.CreateDiagnostic(MemberTypeRule, declaringType.Name, memberName));
}
}
else if (memberTypeSymbol is IArrayTypeSymbol arrayType)
{
if (arrayType.ElementType is not IArrayTypeSymbol)
{
context.ReportDiagnostic(attributeSyntax.CreateDiagnostic(MemberTypeRule, declaringType.Name, memberName));
}
context.ReportDiagnostic(attributeSyntax.CreateDiagnostic(MemberTypeRule, declaringType.Name, memberName));
return;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ internal static class WellKnownTypeNames
public const string SystemIAsyncDisposable = "System.IAsyncDisposable";
public const string SystemIDisposable = "System.IDisposable";
public const string SystemReflectionMethodInfo = "System.Reflection.MethodInfo";
public const string SystemRuntimeCompilerServicesITuple = "System.Runtime.CompilerServices.ITuple";
public const string SystemThreadingTasksTask = "System.Threading.Tasks.Task";
public const string SystemThreadingTasksTask1 = "System.Threading.Tasks.Task`1";
public const string SystemThreadingTasksValueTask = "System.Threading.Tasks.ValueTask";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

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

Expand Down Expand Up @@ -122,75 +117,41 @@ public DynamicDataAttribute(string dynamicDataSourceName, Type dynamicDataDeclar
public TestDataSourceUnfoldingStrategy UnfoldingStrategy { get; set; } = TestDataSourceUnfoldingStrategy.Auto;

/// <inheritdoc />
public IEnumerable<object[]> GetData(MethodInfo methodInfo) => DynamicDataProvider.Instance.GetData(_dynamicDataDeclaringType, _dynamicDataSourceType, _dynamicDataSourceName, methodInfo);
public IEnumerable<object[]> GetData(MethodInfo methodInfo)
=> DynamicDataProvider.Instance.GetData(_dynamicDataDeclaringType, _dynamicDataSourceType, _dynamicDataSourceName, methodInfo);

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

MethodInfo method = dynamicDisplayNameDeclaringType.GetTypeInfo().GetDeclaredMethod(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;
return TestDataSourceUtilities.ComputeDefaultDisplayName(methodInfo, data, TestIdGenerationStrategy);
}

return TestDataSourceUtilities.ComputeDefaultDisplayName(methodInfo, data, TestIdGenerationStrategy);
}

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)
Type? dynamicDisplayNameDeclaringType = DynamicDataDisplayNameDeclaringType ?? methodInfo.DeclaringType;
DebugEx.Assert(dynamicDisplayNameDeclaringType is not null, "Declaring type of test data cannot be null.");

MethodInfo method = dynamicDisplayNameDeclaringType.GetTypeInfo().GetDeclaredMethod(DynamicDataDisplayName)
?? throw new ArgumentNullException($"{DynamicDataSourceType.Method} {DynamicDataDisplayName}");
ParameterInfo[] parameters = method.GetParameters();
if (parameters.Length != 2
|| parameters[0].ParameterType != typeof(MethodInfo)
|| parameters[1].ParameterType != typeof(object[])
|| method.ReturnType != typeof(string)
|| !method.IsStatic
|| !method.IsPublic)
{
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;
throw new ArgumentNullException(
string.Format(
CultureInfo.InvariantCulture,
FrameworkMessages.DynamicDataDisplayName,
DynamicDataDisplayName,
nameof(String),
string.Join(", ", nameof(MethodInfo), typeof(object[]).Name)));
}
#endif

data = null;
return false;
// Try to get the display name from the method.
return method.Invoke(null, [methodInfo, data]) as string;
}

string? ITestDataSourceEmptyDataSourceExceptionInfo.GetPropertyOrMethodNameForEmptyDataSourceException()
Expand Down

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

Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,7 @@ Actual: {2}</value>
<value>UITestMethodAttribute.DispatcherQueue should not be null. To use UITestMethodAttribute within a WinUI Desktop App, remember to set the static UITestMethodAttribute.DispatcherQueue during the test initialization.</value>
</data>
<data name="DynamicDataIEnumerableNull" xml:space="preserve">
<value>Property or method {0} on {1} return type is not assignable to 'IEnumerable&lt;object[]&gt;' (nor 'IEnumerable&lt;ITuple&gt;' for .NET Core).</value>
<value>Property or method {0} on {1} return type is not assignable to 'IEnumerable'.</value>
</data>
<data name="DynamicDataValueNull" xml:space="preserve">
<value>Value returned by property or method {0} shouldn't be null.</value>
Expand All @@ -287,4 +287,4 @@ Actual: {2}</value>
<data name="DynamicDataInvalidPropertyLayout" xml:space="preserve">
<value>Dynamic data property '{0}' should be static and have a getter.</value>
</data>
</root>
</root>
Original file line number Diff line number Diff line change
Expand Up @@ -253,8 +253,8 @@ Skutečnost: {2}</target>
<note></note>
</trans-unit>
<trans-unit id="DynamicDataIEnumerableNull">
<source>Property or method {0} on {1} return type is not assignable to 'IEnumerable&lt;object[]&gt;' (nor 'IEnumerable&lt;ITuple&gt;' for .NET Core).</source>
<target state="translated">Vlastnost nebo metoda {0} na návratovém typu {1} se nedá přiřadit k „IEnumerable&lt;object[]&gt;“ (ani „IEnumerable&lt;ITuple&gt;“ pro .NET Core).</target>
<source>Property or method {0} on {1} return type is not assignable to 'IEnumerable'.</source>
<target state="needs-review-translation">Vlastnost nebo metoda {0} na návratovém typu {1} se nedá přiřadit k „IEnumerable&lt;object[]&gt;“ (ani „IEnumerable&lt;ITuple&gt;“ pro .NET Core).</target>
<note />
</trans-unit>
<trans-unit id="DynamicDataValueNull">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -253,8 +253,8 @@ Tatsächlich: {2}</target>
<note></note>
</trans-unit>
<trans-unit id="DynamicDataIEnumerableNull">
<source>Property or method {0} on {1} return type is not assignable to 'IEnumerable&lt;object[]&gt;' (nor 'IEnumerable&lt;ITuple&gt;' for .NET Core).</source>
<target state="translated">Die Eigenschaft oder Methode "{0}" für Rückgabetyp "{1}" kann "IEnumerable&lt;object[]&gt;" nicht zugewiesen werden (auch nicht "IEnumerable&lt;ITuple&gt;" für .NET Core).</target>
<source>Property or method {0} on {1} return type is not assignable to 'IEnumerable'.</source>
<target state="needs-review-translation">Die Eigenschaft oder Methode "{0}" für Rückgabetyp "{1}" kann "IEnumerable&lt;object[]&gt;" nicht zugewiesen werden (auch nicht "IEnumerable&lt;ITuple&gt;" für .NET Core).</target>
<note />
</trans-unit>
<trans-unit id="DynamicDataValueNull">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -253,8 +253,8 @@ Real: {2}</target>
<note></note>
</trans-unit>
<trans-unit id="DynamicDataIEnumerableNull">
<source>Property or method {0} on {1} return type is not assignable to 'IEnumerable&lt;object[]&gt;' (nor 'IEnumerable&lt;ITuple&gt;' for .NET Core).</source>
<target state="translated">La propiedad o el método {0} en {1} tipo de valor devuelto no se puede asignar a "IEnumerable&lt;objecto[]&gt;" (ni a "IEnumerable&lt;ITuple&gt;" para .NET Core).</target>
<source>Property or method {0} on {1} return type is not assignable to 'IEnumerable'.</source>
<target state="needs-review-translation">La propiedad o el método {0} en {1} tipo de valor devuelto no se puede asignar a "IEnumerable&lt;objecto[]&gt;" (ni a "IEnumerable&lt;ITuple&gt;" para .NET Core).</target>
<note />
</trans-unit>
<trans-unit id="DynamicDataValueNull">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -253,8 +253,8 @@ Réel : {2}</target>
<note></note>
</trans-unit>
<trans-unit id="DynamicDataIEnumerableNull">
<source>Property or method {0} on {1} return type is not assignable to 'IEnumerable&lt;object[]&gt;' (nor 'IEnumerable&lt;ITuple&gt;' for .NET Core).</source>
<target state="translated">La propriété ou la méthode {0} sur le type de retour {1} ne peut pas être attribuée à « IEnumerable&lt;object[]&gt; » (ni à « IEnumerable&lt;ITuple&gt; » pour .NET Core).</target>
<source>Property or method {0} on {1} return type is not assignable to 'IEnumerable'.</source>
<target state="needs-review-translation">La propriété ou la méthode {0} sur le type de retour {1} ne peut pas être attribuée à « IEnumerable&lt;object[]&gt; » (ni à « IEnumerable&lt;ITuple&gt; » pour .NET Core).</target>
<note />
</trans-unit>
<trans-unit id="DynamicDataValueNull">
Expand Down
Loading

0 comments on commit 5bdcacd

Please sign in to comment.