The OP example was about an NUnit feature that easily allows to have a cartesian product of provided values. As far as I was able to tell, no answer here covered that part. I saw this as a little challenge and ended up with the following implementation.
[Edit: Array based refactoring + Zip values]
I did some refactoring to the original Enumerator based version (see post history) to now use only Arrays and loop throuch indices instead. I also took the opporunity to add a new Zip type of values that will match a different value to every set created by the cartesian produce. This may be useful to add an ExpectedResult for instance.
It is still not really optimized so feel free to suggest improvements.
#nullable enable
public enum ValuesType
{
Undefined = 0,
Cartesian = 1,
/// <summary>
/// Values will be <see cref="Enumerable.Zip{TFirst, TSecond, TResult}(IEnumerable{TFirst}, IEnumerable{TSecond}, Func{TFirst, TSecond, TResult})">Zipped</see> with the cartesian produce of the other parameters.
/// </summary>
Zip = 2
}
[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false)]
public class ValuesAttribute : Attribute
{
public ValuesType ValuesType { get; }
public object[] Values { get; }
public ValuesAttribute(params object[] values)
: this(ValuesType.Cartesian, values)
{ }
public ValuesAttribute(ValuesType valuesType, params object[] values)
{
ValuesType = valuesType;
Values = values;
}
}
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public class ValuesDataSourceAttribute : Attribute, ITestDataSource
{
public IEnumerable<object[]> GetData(MethodInfo methodInfo)
{
var parameters = methodInfo.GetParameters();
var values = new (ValuesType Type, object[] Values, int Index)[parameters.Length];
for(var i=0; i < parameters.Length; i++)
{
var parameter = parameters[i];
var attribute = parameter.GetCustomAttribute<ValuesAttribute>();
if (attribute != null)
{
if (attribute.Values.Any(v => !parameter.ParameterType.IsAssignableFrom(v.GetType())))
throw new InvalidOperationException($"All values of {nameof(ValuesAttribute)} must be of type {parameter.ParameterType.Name}. ParameterName: {parameter.Name}.");
switch (attribute.ValuesType)
{
case ValuesType.Cartesian:
values[i] = (ValuesType.Cartesian, attribute.Values, 0);
break;
case ValuesType.Zip:
values[i] = (ValuesType.Zip, attribute.Values, 0);
break;
}
}
else if (parameter.ParameterType == typeof(bool))
values[i] = (ValuesType.Cartesian, new object[] { false, true }, 0);
else if (parameter.ParameterType.IsEnum)
values[i] = (ValuesType.Cartesian, Enum.GetValues(parameter.ParameterType).Cast<Object>().ToArray(), 0);
else
throw new InvalidOperationException($"All parameters must have either {nameof(ValuesAttribute)} attached or be a bool or an Enum . ParameterName: {parameter.Name}.");
}
//Since we are using ValueTuples, it is essential that once we created our collection, we stick to it. If we were to create a new one, we would end up with a copy of the ValueTuples that won't be synced anymore.
var cartesianTotalCount = values.Where(v => v.Type == ValuesType.Cartesian).Aggregate(1, (actualTotal, currentValues) => actualTotal * currentValues.Values.Length);
if (values.Any(v => v.Type == ValuesType.Zip && v.Values.Length != cartesianTotalCount))
throw new InvalidOperationException($"{nameof(ValuesType.Zip)} typed attributes must have as many values as the produce of all the others ({cartesianTotalCount}).");
bool doIncrement;
for(var globalPosition = 0; globalPosition < cartesianTotalCount; globalPosition++)
{
yield return values.Select(v => v.Values[v.Index]).ToArray();
doIncrement = true;
for (var i = values.Length - 1; i >= 0 && doIncrement; i--)
{
switch (values[i].Type)
{
case ValuesType.Zip:
values[i].Index++;
break;
case ValuesType.Cartesian:
if (doIncrement && ++values[i].Index >= values[i].Values.Length)
values[i].Index = 0;
else
doIncrement = false;
break;
default:
throw new InvalidOperationException($"{values[i].Type} is not supported.");
}
}
}
}
public string GetDisplayName(MethodInfo methodInfo, object[] data)
{
return data.JoinStrings(" / ");
}
}
Usage:
[TestMethod]
[ValuesDataSource]
public void Test([Values("a1", "a2")] string a, [Values(1, 2)] int b, bool c, System.ConsoleModifiers d, [Values(ValuesType.Zip, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24)] int asserts)
{
//Arrange / Act / Assert
//Cases would be
// a1, 1, false, System.ConsoleModifiers.Alt, 1
// a1, 1, false, System.ConsoleModifiers.Shift, 2
// a1, 1, false, System.ConsoleModifiers.Control, 3
// a1, 1, true, System.ConsoleModifiers.Alt, 4
// a1, 1, true, System.ConsoleModifiers.Shift, 5
// a1, 1, true, System.ConsoleModifiers.Control, 6
// a1, 2, false, System.ConsoleModifiers.Alt, 7
// a1, 2, false, System.ConsoleModifiers.Shift, 8
// a1, 2, false, System.ConsoleModifiers.Control, 9
// a1, 2, true, System.ConsoleModifiers.Alt, 10
// a1, 2, true, System.ConsoleModifiers.Shift, 11
// a1, 2, true, System.ConsoleModifiers.Control, 12
// a2, 1, false, System.ConsoleModifiers.Alt, 13
// a2, 1, false, System.ConsoleModifiers.Shift, 14
// a2, 1, false, System.ConsoleModifiers.Control, 15
// a2, 1, true, System.ConsoleModifiers.Alt, 16
// a2, 1, true, System.ConsoleModifiers.Shift, 17
// a2, 1, true, System.ConsoleModifiers.Control, 18
// a2, 2, false, System.ConsoleModifiers.Alt, 19
// a2, 2, false, System.ConsoleModifiers.Shift, 20
// a2, 2, false, System.ConsoleModifiers.Control, 21
// a2, 2, true, System.ConsoleModifiers.Alt, 22
// a2, 2, true, System.ConsoleModifiers.Shift, 23
// a2, 2, true, System.ConsoleModifiers.Control, 24
}