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

Add generic attributes and add class data attribute #99

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -355,3 +355,6 @@ MigrationBackup/

# Analysis results
*.sarif

# JetBrains Rider
.idea
63 changes: 63 additions & 0 deletions src/Xunit.Combinatorial/CombinatorialClassDataAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// Copyright (c) Andrew Arnott. All rights reserved.
// Licensed under the Ms-PL license. See LICENSE file in the project root for full license information.

using System.Globalization;
using System.Reflection;

namespace Xunit;

/// <summary>
/// Specifies a class that provides the values for a combinatorial test.
/// </summary>
public class CombinatorialClassDataAttribute : Attribute, ICombinatorialValuesProvider
{
private readonly object?[] values;

/// <summary>
/// Initializes a new instance of the <see cref="CombinatorialClassDataAttribute" /> class.
/// </summary>
/// <param name="valuesSourceType">The type of the class that provides the values for a combinatorial test.</param>
/// <param name="arguments">The arguments to pass to the constructor of <paramref name="valuesSourceType" />.</param>
public CombinatorialClassDataAttribute(Type valuesSourceType, params object[]? arguments)
{
this.values = GetValues(valuesSourceType, arguments);
}

/// <inheritdoc />
public object?[] GetValues(ParameterInfo parameter)
{
return this.values;
}

private static object?[] GetValues(Type valuesSourceType, object[]? args)
{
Requires.NotNull(valuesSourceType, nameof(valuesSourceType));

if (!typeof(IEnumerable<object[]>).IsAssignableFrom(valuesSourceType))
{
throw new InvalidOperationException(
$"The values source must be assignable to {typeof(IEnumerable<object?[]>)}).");
}

IEnumerable<object[]>? values;

try
{
values = (IEnumerable<object[]>)Activator.CreateInstance(
valuesSourceType,
BindingFlags.CreateInstance | BindingFlags.OptionalParamBinding,
null,
args,
CultureInfo.InvariantCulture);
}
catch (Exception ex)
{
throw new InvalidOperationException(
$"Failed to create an instance of {valuesSourceType}. " +
$"Please make sure the type has a public constructor and the arguments match.",
ex);
}

return values.SelectMany(rows => rows).ToArray();
}
}
24 changes: 24 additions & 0 deletions src/Xunit.Combinatorial/CombinatorialClassDataAttribute`1.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Copyright (c) Andrew Arnott. All rights reserved.
// Licensed under the Ms-PL license. See LICENSE file in the project root for full license information.

using System.Collections;

namespace Xunit;

#if NETSTANDARD2_0_OR_GREATER
/// <inheritdoc />
/// <typeparam name="TValueSource">The type of the class that provides the values for a combinatorial test.</typeparam>
/// <remarks><typeparamref name="TValueSource" /> must implement <see cref="IEnumerable" />.</remarks>
public class CombinatorialClassDataAttribute<TValueSource> : CombinatorialClassDataAttribute
where TValueSource : IEnumerable<object?[]>
{
/// <summary>
/// Initializes a new instance of the <see cref="CombinatorialClassDataAttribute{TValueSource}" /> class.
/// </summary>
/// <param name="arguments">The arguments to pass to the constructor of <typeparamref name="TValueSource" />.</param>
public CombinatorialClassDataAttribute(params object[]? arguments)
: base(typeof(TValueSource), arguments)
{
}
}
#endif
39 changes: 24 additions & 15 deletions src/Xunit.Combinatorial/CombinatorialMemberDataAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,14 @@ namespace Xunit;
/// Specifies which member should provide data for this parameter used for running the test method.
/// </summary>
[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = true)]
public class CombinatorialMemberDataAttribute : Attribute
public class CombinatorialMemberDataAttribute : Attribute, ICombinatorialValuesProvider
{
/// <summary>
/// Initializes a new instance of the <see cref="CombinatorialMemberDataAttribute"/> class.
/// </summary>
/// <param name="memberName">The name of the public static member on the test class that will provide the test data.</param>
/// <param name="arguments">The arguments for the member (only supported for methods; ignored for everything else).</param>
/// <remarks>Optional parameters on methods are not supported.</remarks>
public CombinatorialMemberDataAttribute(string memberName, params object?[]? arguments)
{
this.MemberName = memberName ?? throw new ArgumentNullException(nameof(memberName));
Expand Down Expand Up @@ -64,8 +65,14 @@ public CombinatorialMemberDataAttribute(string memberName, params object?[]? arg
throw new ArgumentException($"Could not find public static member (property, field, or method) named '{this.MemberName}' on {type.FullName}{parameterText}.");
}

var obj = (IEnumerable)accessor();
return obj.Cast<object>().ToArray();
var values = (IEnumerable)accessor();

if (values is IEnumerable<object[]> theoryData)
{
return theoryData.SelectMany(rows => rows).ToArray();
}

return values.Cast<object>().ToArray();
}

/// <summary>
Expand All @@ -75,19 +82,10 @@ public CombinatorialMemberDataAttribute(string memberName, params object?[]? arg
/// <returns>The generic type argument for (one of) the <see cref="IEnumerable{T}"/> interface)s) implemented by the <paramref name="enumerableType"/>.</returns>
private static TypeInfo? GetEnumeratedType(Type enumerableType)
{
if (enumerableType.IsGenericType)
if (enumerableType.IsGenericType && enumerableType.GetGenericTypeDefinition() == typeof(IEnumerable<>))
{
if (enumerableType.GetGenericTypeDefinition() == typeof(IEnumerable<>))
{
Type[] enumerableGenericTypeArgs = enumerableType.GetTypeInfo().GetGenericArguments();
return enumerableGenericTypeArgs[0].GetTypeInfo();
}

if (enumerableType.GetGenericTypeDefinition() == typeof(TheoryData<>))
{
Type[] enumerableGenericTypeArgs = enumerableType.GetTypeInfo().GetGenericArguments();
return enumerableGenericTypeArgs[0].GetTypeInfo();
}
Type[] enumerableGenericTypeArgs = enumerableType.GetTypeInfo().GetGenericArguments();
return enumerableGenericTypeArgs[0].GetTypeInfo();
}

foreach (Type implementedInterface in enumerableType.GetTypeInfo().ImplementedInterfaces)
Expand Down Expand Up @@ -157,6 +155,11 @@ private bool ParameterTypesCompatible(ParameterInfo[] parameters, object?[]? arg
return false;
}

if (parameters.Length != arguments.Length)
{
return false;
}

for (int i = 0; i < parameters.Length; i++)
{
if (arguments[i] is object arg)
Expand Down Expand Up @@ -211,7 +214,13 @@ private bool ParameterTypesCompatible(ParameterInfo[] parameters, object?[]? arg
/// <exception cref="ArgumentException">Throw when <paramref name="enumerableType"/> does not conform to requirements or does not produce values assignable to <paramref name="parameterInfo"/>.</exception>
private void EnsureValidMemberDataType(Type enumerableType, Type declaringType, ParameterInfo parameterInfo)
{
if (typeof(IEnumerable<object[]>).IsAssignableFrom(enumerableType))
{
return;
}

TypeInfo? enumeratedType = GetEnumeratedType(enumerableType);

if (enumeratedType is null)
{
throw new ArgumentException($"Member {this.MemberName} on {declaringType.FullName} must return a type that implements IEnumerable<T>.");
Expand Down
28 changes: 28 additions & 0 deletions src/Xunit.Combinatorial/CombinatorialMemberDataAttribute`1.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Copyright (c) Andrew Arnott. All rights reserved.
// Licensed under the Ms-PL license. See LICENSE file in the project root for full license information.

namespace Xunit;

#if NETSTANDARD2_0_OR_GREATER
/// <inheritdoc />
/// <typeparam name="T">The type of the class that provides the values for a combinatorial test.</typeparam>
public class CombinatorialMemberDataAttribute<T> : CombinatorialMemberDataAttribute
{
/// <summary>
/// Initializes a new instance of the <see cref="CombinatorialMemberDataAttribute{T}" /> class.
/// </summary>
/// <param name="memberName">The name of the public static member on the test class that will provide the test data.</param>
/// <param name="arguments">The arguments for the member (only supported for methods; ignored for everything else).</param>
/// <remarks>Optional parameters on methods are not supported.</remarks>
public CombinatorialMemberDataAttribute(string memberName, params object?[]? arguments)
: base(memberName, arguments)
{
base.MemberType = typeof(T);
}

/// <summary>
/// Gets type to retrieve the member from.
/// </summary>
public new Type? MemberType => base.MemberType;
}
#endif
15 changes: 8 additions & 7 deletions src/Xunit.Combinatorial/CombinatorialRandomDataAttribute.cs
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
// Copyright (c) Andrew Arnott. All rights reserved.
// Copyright (c) Andrew Arnott. All rights reserved.
// Licensed under the Ms-PL license. See LICENSE file in the project root for full license information.

using System.Globalization;
using System.Reflection;

namespace Xunit;

/// <summary>
/// Specifies which range of values for this parameter should be used for running the test method.
/// </summary>
[AttributeUsage(AttributeTargets.Parameter)]
public class CombinatorialRandomDataAttribute : Attribute
public class CombinatorialRandomDataAttribute : Attribute, ICombinatorialValuesProvider
{
/// <summary>
/// Special seed value to create System.Random class without seed.
Expand Down Expand Up @@ -42,11 +43,11 @@ public class CombinatorialRandomDataAttribute : Attribute
/// <value>The default value of <see cref="NoSeed"/> allows for a new seed to be used each time.</value>
public int Seed { get; set; } = NoSeed;

/// <summary>
/// Gets the values that should be passed to this parameter on the test method.
/// </summary>
/// <value>An array of values.</value>
public object[] Values => this.values ??= this.GenerateValues();
/// <inheritdoc />
public object[] GetValues(ParameterInfo parameter)
{
return this.values ??= this.GenerateValues();
}

private object[] GenerateValues()
{
Expand Down
26 changes: 15 additions & 11 deletions src/Xunit.Combinatorial/CombinatorialRangeAttribute.cs
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
// Copyright (c) Andrew Arnott. All rights reserved.
// Copyright (c) Andrew Arnott. All rights reserved.
// Licensed under the Ms-PL license. See LICENSE file in the project root for full license information.

using System.Reflection;

namespace Xunit;

/// <summary>
/// Specifies which range of values for this parameter should be used for running the test method.
/// </summary>
[AttributeUsage(AttributeTargets.Parameter)]
public class CombinatorialRangeAttribute : Attribute
public class CombinatorialRangeAttribute : Attribute, ICombinatorialValuesProvider
{
private readonly object[] values;

/// <summary>
/// Initializes a new instance of the <see cref="CombinatorialRangeAttribute"/> class.
/// </summary>
Expand All @@ -30,7 +34,7 @@ public CombinatorialRangeAttribute(int from, int count)
values[i] = from + i;
}

this.Values = values;
this.values = values;
}

/// <summary>
Expand Down Expand Up @@ -75,7 +79,7 @@ public CombinatorialRangeAttribute(int from, int to, int step)
values[i] = from + (i * step);
}

this.Values = values;
this.values = values;
}

/// <summary>
Expand All @@ -99,7 +103,7 @@ public CombinatorialRangeAttribute(uint from, uint count)
values[i] = from + i;
}

this.Values = values;
this.values = values;
}

/// <summary>
Expand Down Expand Up @@ -140,12 +144,12 @@ public CombinatorialRangeAttribute(uint from, uint to, uint step)
}
}

this.Values = values.Cast<object>().ToArray();
this.values = values.Cast<object>().ToArray();
}

/// <summary>
/// Gets the values that should be passed to this parameter on the test method.
/// </summary>
/// <value>An array of values.</value>
public object[] Values { get; }
/// <inheritdoc />
public object[] GetValues(ParameterInfo parameter)
{
return this.values;
}
}
20 changes: 12 additions & 8 deletions src/Xunit.Combinatorial/CombinatorialValuesAttribute.cs
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
// Copyright (c) Andrew Arnott. All rights reserved.
// Copyright (c) Andrew Arnott. All rights reserved.
// Licensed under the Ms-PL license. See LICENSE file in the project root for full license information.

using System.Reflection;

namespace Xunit;

/// <summary>
/// Specifies which values for this parameter should be used for running the test method.
/// </summary>
[AttributeUsage(AttributeTargets.Parameter)]
public class CombinatorialValuesAttribute : Attribute
public class CombinatorialValuesAttribute : Attribute, ICombinatorialValuesProvider
{
private readonly object?[] values;

/// <summary>
/// Initializes a new instance of the <see cref="CombinatorialValuesAttribute"/> class.
/// </summary>
Expand All @@ -17,12 +21,12 @@ public CombinatorialValuesAttribute(params object?[]? values)
{
// When values is `null`, it's because the user passed in `null` as the only value and C# interpreted it as a null array.
// Re-interpret that.
this.Values = values ?? new object?[] { null };
this.values = values ?? new object?[] { null };
}

/// <summary>
/// Gets the values that should be passed to this parameter on the test method.
/// </summary>
/// <value>An array of values.</value>
public object?[] Values { get; }
/// <inheritdoc />
public object?[] GetValues(ParameterInfo parameter)
{
return this.values;
}
}
19 changes: 19 additions & 0 deletions src/Xunit.Combinatorial/ICombinatorialValuesProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Copyright (c) Andrew Arnott. All rights reserved.
// Licensed under the Ms-PL license. See LICENSE file in the project root for full license information.

using System.Reflection;

namespace Xunit;

/// <summary>
/// Defines a class that provides values for a parameter on a test method.
/// </summary>
public interface ICombinatorialValuesProvider
{
/// <summary>
/// Gets the values that should be passed to this parameter on the test method.
/// </summary>
/// <param name="parameter">The parameter to get values for.</param>
/// <returns>An array of values.</returns>
object?[] GetValues(ParameterInfo parameter);
}
Loading