Skip to content

Commit 1290183

Browse files
authored
Merge pull request #26 from AlexRadch/master
Add CombinatorialRandomAttribute to get random values for arguments
2 parents d12b4ed + fee7384 commit 1290183

10 files changed

+339
-20
lines changed

src/Directory.Build.props

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
44
<BaseIntermediateOutputPath>$(MSBuildThisFileDirectory)..\obj\$(MSBuildProjectName)\</BaseIntermediateOutputPath>
55
<OutputPath>$(MSBuildThisFileDirectory)..\bin\$(MSBuildProjectName)\$(Configuration)\</OutputPath>
6+
<LangVersion>8</LangVersion>
67
<DebugType Condition=" '$(Configuration)' == 'Debug' ">full</DebugType>
78
<DebugType Condition=" '$(Configuration)' == 'Release' ">pdbonly</DebugType>
89
</PropertyGroup>
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
namespace Xunit.Combinatorial.Tests
2+
{
3+
using System;
4+
using System.Collections.Generic;
5+
using System.Linq;
6+
7+
public class CombinatorialRandomAttributeTests
8+
{
9+
[Fact]
10+
public void ConstructorNoParams()
11+
=> Check(new CombinatorialRandomAttribute());
12+
13+
[Theory]
14+
[InlineData(1)]
15+
[InlineData(3)]
16+
[InlineData(25)]
17+
public void ConstructorCount(int count)
18+
=> Check(new CombinatorialRandomAttribute { Count = count });
19+
20+
[Theory]
21+
[InlineData(1, 25)]
22+
[InlineData(3, 100)]
23+
[InlineData(24, 1000)]
24+
public void ConstructorCountMaxValue(int count, int maxValue)
25+
=> Check(new CombinatorialRandomAttribute { Count = count, Maximum = maxValue });
26+
27+
[Theory]
28+
[InlineData(1, 25, 35)]
29+
[InlineData(3, 100, 200)]
30+
public void ConstructorCountMinMaxValues(int count, int minValue, int maxValue)
31+
=> Check(new CombinatorialRandomAttribute { Count = count, Minimum = minValue, Maximum = maxValue });
32+
33+
[Theory]
34+
[InlineData(1, 25, 35, 234)]
35+
[InlineData(3, 100, 200, 1343)]
36+
[InlineData(24, 1000, 3000, 56732)]
37+
[InlineData(12, 576, 765, 0)]
38+
public void ConstructorCountMinMaxValuesSeed(int count, int minValue, int maxValue, int seed)
39+
=> Check(new CombinatorialRandomAttribute { Count = count, Minimum = minValue, Maximum = maxValue, Seed = seed });
40+
41+
[Theory]
42+
[InlineData(0)]
43+
[InlineData(-1)]
44+
[InlineData(int.MinValue)]
45+
public void ConstructorOutOfRangeCount(int count)
46+
{
47+
Assert.Throws<InvalidOperationException>(() => new CombinatorialRandomAttribute { Count = count }.Values);
48+
}
49+
50+
[Fact]
51+
public void CountHigherThanRangeSize()
52+
{
53+
Assert.Throws<InvalidOperationException>(() => new CombinatorialRandomAttribute { Count = 6, Minimum = 1, Maximum = 5 }.Values);
54+
Assert.Throws<InvalidOperationException>(() => new CombinatorialRandomAttribute { Count = 4, Minimum = -3, Maximum = -1 }.Values);
55+
_ = new CombinatorialRandomAttribute { Count = 3, Minimum = 1, Maximum = 3 }.Values;
56+
_ = new CombinatorialRandomAttribute { Count = 3, Minimum = -3, Maximum = -1 }.Values;
57+
}
58+
59+
internal void Check(CombinatorialRandomAttribute attribute)
60+
{
61+
Assert.NotNull(attribute.Values);
62+
Assert.Equal(attribute.Count, attribute.Values.Length);
63+
64+
Assert.All(attribute.Values, value =>
65+
{
66+
Assert.IsType<int>(value);
67+
var intValue = (int)value;
68+
Assert.InRange(intValue, attribute.Minimum, attribute.Maximum);
69+
});
70+
71+
if (attribute.Seed != CombinatorialRandomAttribute.NoSeed)
72+
{
73+
Assert.Equal(RandomIterator(new Random(attribute.Seed), attribute.Minimum, attribute.Maximum).Distinct().Take(attribute.Count).ToArray(), attribute.Values.Cast<int>());
74+
}
75+
}
76+
77+
internal static IEnumerable<int> RandomIterator(Random random, int minValue, int maxValue)
78+
{
79+
while (true)
80+
{
81+
int value = random.Next(minValue, maxValue + 1);
82+
yield return value;
83+
}
84+
}
85+
}
86+
}

src/Xunit.Combinatorial.Tests/CombinatorialRangeAttributeTests.cs

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ public class CombinatorialRangeAttributeTests
1212
[InlineData(0, 5)]
1313
public void CountOfIntegers_HappyPath_SetsAttributeWithRange(int from, int count)
1414
{
15-
object[] values = { 0, 1, 2, 3, 4 };
15+
object[] values = Enumerable.Range(from, count).Cast<object>().ToArray();
1616
var attribute = new CombinatorialRangeAttribute(from, count);
1717
Assert.Equal(values, attribute.Values);
1818
}
@@ -25,11 +25,13 @@ public void CountOfIntegers_NegativeCount_ArgOutOfRange(int from, int count)
2525
}
2626

2727
[Theory]
28-
[InlineData(0, 7, 2, true)]
29-
[InlineData(0, 8, 2, false)]
30-
public void IntegerStep_HappyPath_SetsAttributeWithRange(int from, int to, int step, bool unevenInterval)
28+
[InlineData(0, 7, 2)]
29+
[InlineData(0, 8, 2)]
30+
[InlineData(7, 0, -2)]
31+
[InlineData(0, -8, -2)]
32+
public void IntegerStep_HappyPath_SetsAttributeWithRange(int from, int to, int step)
3133
{
32-
object[] expectedValues = unevenInterval ? new object[]{ 0, 2, 4, 6 } : new object[]{ 0, 2, 4, 6, 8 };
34+
object[] expectedValues = Sequence(from, to, step).Cast<object>().ToArray();
3335

3436
var attribute = new CombinatorialRangeAttribute(from, to, step);
3537
Assert.Equal(expectedValues, attribute.Values);
@@ -42,5 +44,34 @@ public void IntegerStep_InvalidIntervalAndStep_ArgOutOfRange(int from, int to, i
4244
{
4345
Assert.Throws<ArgumentOutOfRangeException>(() => new CombinatorialRangeAttribute(from, to, step));
4446
}
47+
48+
internal static IEnumerable<int> Sequence(int from, int to, int step)
49+
=> step >= 0 ? SequenceIterator(from, to, step) : SequenceReverseIterator(from, to, step);
50+
51+
private static IEnumerable<int> SequenceIterator(int from, int to, int step)
52+
{
53+
var value = from;
54+
while (value <= to)
55+
{
56+
yield return value;
57+
unchecked
58+
{
59+
value += step;
60+
}
61+
}
62+
}
63+
64+
private static IEnumerable<int> SequenceReverseIterator(int from, int to, int step)
65+
{
66+
var value = from;
67+
while (value >= to)
68+
{
69+
yield return value;
70+
unchecked
71+
{
72+
value += step;
73+
}
74+
}
75+
}
4576
}
4677
}

src/Xunit.Combinatorial.Tests/SampleUses.cs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,36 @@ public void CombinatorialCustomRange([CombinatorialRange(0, 5)] int p1, [Combina
6868
Assert.True(p2 == 0 || p2 == 2);
6969
}
7070

71+
[Theory, CombinatorialData]
72+
public void CombinatorialRandomValuesDefault([CombinatorialRandom()] int p1)
73+
{
74+
Assert.InRange(p1, 0, int.MaxValue);
75+
}
76+
77+
[Theory, CombinatorialData]
78+
public void CombinatorialRandomValuesCount([CombinatorialRandom(Count = 10)] int p1)
79+
{
80+
Assert.InRange(p1, 0, int.MaxValue);
81+
}
82+
83+
[Theory, CombinatorialData]
84+
public void CombinatorialRandomValuesCountMaxValue([CombinatorialRandom(Count = 10, Maximum = 35)] int p1)
85+
{
86+
Assert.InRange(p1, 0, 35);
87+
}
88+
89+
[Theory, CombinatorialData]
90+
public void CombinatorialRandomValuesCountMinMaxValues([CombinatorialRandom(Count = 10, Minimum = -20, Maximum = -5)] int p1)
91+
{
92+
Assert.InRange(p1, -20, -5);
93+
}
94+
95+
[Theory, CombinatorialData]
96+
public void CombinatorialRandomValuesCountMinMaxValuesSeed([CombinatorialRandom(Count = 10, Minimum = -5, Maximum = 6, Seed = 567)] int p1)
97+
{
98+
Assert.InRange(p1, -5, 6);
99+
}
100+
71101
[AttributeUsage(AttributeTargets.Parameter)]
72102
private class CustomValuesAttribute : CombinatorialValuesAttribute
73103
{

src/Xunit.Combinatorial/CombinatorialMemberDataAttribute.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
namespace Xunit
1+
// Copyright (c) Andrew Arnott. All rights reserved. Licensed under the Ms-PL.
2+
3+
namespace Xunit
24
{
35
using System;
46
using System.Collections;
@@ -35,7 +37,7 @@ public CombinatorialMemberDataAttribute(string memberName, params object[] param
3537
public Type MemberType { get; set; }
3638

3739
/// <summary>
38-
/// Gets or sets the parameters passed to the member. Only supported for static methods.
40+
/// Gets the parameters passed to the member. Only supported for static methods.
3941
/// </summary>
4042
public object[] Parameters { get; }
4143

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
// Copyright (c) Andrew Arnott. All rights reserved. Licensed under the Ms-PL.
2+
3+
namespace Xunit
4+
{
5+
using System;
6+
using System.Collections.Generic;
7+
using System.Globalization;
8+
9+
/// <summary>
10+
/// Specifies which range of values for this parameter should be used for running the test method.
11+
/// </summary>
12+
[AttributeUsage(AttributeTargets.Parameter)]
13+
public class CombinatorialRandomAttribute : Attribute
14+
{
15+
/// <summary>
16+
/// Special seed value to create System.Random class without seed.
17+
/// </summary>
18+
public const int NoSeed = 0;
19+
20+
private object[] values;
21+
22+
/// <summary>
23+
/// Gets or sets the number of values to generate. Must be positive.
24+
/// </summary>
25+
/// <value>The default value is 5.</value>
26+
public int Count { get; set; } = 5;
27+
28+
/// <summary>
29+
/// Gets or sets the minimum value (inclusive) that may be randomly generated.
30+
/// </summary>
31+
/// <value>The default value is 0.</value>
32+
public int Minimum { get; set; }
33+
34+
/// <summary>
35+
/// Gets or sets the maximum value (inclusive) that may be randomly generated.
36+
/// </summary>
37+
/// <value>The default value is <c><see cref="int.MaxValue"/> - 1</c>, which is the maximum allowable value.</value>
38+
public int Maximum { get; set; } = int.MaxValue - 1;
39+
40+
/// <summary>
41+
/// Gets or sets the seed to use for random number generation.
42+
/// </summary>
43+
/// <value>The default value of <see cref="NoSeed"/> allows for a new seed to be used each time.</value>
44+
public int Seed { get; set; } = NoSeed;
45+
46+
/// <summary>
47+
/// Gets the values that should be passed to this parameter on the test method.
48+
/// </summary>
49+
/// <value>An array of values.</value>
50+
public object[] Values => this.values ??= this.GenerateValues();
51+
52+
private object[] GenerateValues()
53+
{
54+
if (this.Count < 1)
55+
{
56+
throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, Strings.ValueMustBePositive, nameof(this.Count)));
57+
}
58+
59+
if (this.Minimum > this.Maximum)
60+
{
61+
throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, Strings.XMustNotBeGreaterThanY, nameof(this.Minimum), nameof(this.Maximum)));
62+
}
63+
64+
int maxPossibleValues = this.Maximum - this.Minimum + 1;
65+
if (this.Count > maxPossibleValues)
66+
{
67+
throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, Strings.MoreRandomValuesRequestedThanPossibleOnes, nameof(this.Count), nameof(this.Minimum), nameof(this.Maximum)));
68+
}
69+
70+
Random random = this.Seed != NoSeed ? new Random(this.Seed) : new Random();
71+
72+
HashSet<int> collisionChecker = new HashSet<int>();
73+
object[] values = new object[this.Count];
74+
int collisionCount = 0;
75+
int i = 0;
76+
while (collisionChecker.Count < this.Count)
77+
{
78+
int value = random.Next(this.Minimum, this.Maximum + 1);
79+
if (collisionChecker.Add(value))
80+
{
81+
values[i++] = value;
82+
}
83+
else
84+
{
85+
collisionCount++;
86+
}
87+
88+
if (collisionCount > collisionChecker.Count * 5)
89+
{
90+
// We have collided in random values far more than we have successfully generated values.
91+
// Rather than spin in this loop, throw.
92+
throw new InvalidOperationException(Strings.TooManyRandomCollisions);
93+
}
94+
}
95+
96+
return values;
97+
}
98+
}
99+
}

src/Xunit.Combinatorial/CombinatorialRangeAttribute.cs

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,12 +50,21 @@ public CombinatorialRangeAttribute(int from, int count)
5050
/// </param>
5151
public CombinatorialRangeAttribute(int from, int to, int step)
5252
{
53-
if (to <= from)
53+
if (step > 0)
5454
{
55-
throw new ArgumentOutOfRangeException(nameof(to));
55+
if (to < from)
56+
{
57+
throw new ArgumentOutOfRangeException(nameof(to));
58+
}
5659
}
57-
58-
if (step < 1)
60+
else if (step < 0)
61+
{
62+
if (to > from)
63+
{
64+
throw new ArgumentOutOfRangeException(nameof(to));
65+
}
66+
}
67+
else
5968
{
6069
throw new ArgumentOutOfRangeException(nameof(step));
6170
}

src/Xunit.Combinatorial/Strings.Designer.cs

Lines changed: 36 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)