Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 90a1f99

Browse files
committed
Added a new filter, PartitionFilter, that allows you to filter to a subset of arbitrary tests based on a partition number and count
This is helpful when you may want to run a subset of tests (eg, across 3 machines - or partitions), each with a separately assigned partition number and fixed partition count Fixes #4391
1 parent 4f009ef commit 90a1f99

File tree

3 files changed

+205
-0
lines changed

3 files changed

+205
-0
lines changed
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
// Copyright (c) Charlie Poole, Rob Prouse and Contributors. MIT License - see LICENSE.txt
2+
3+
using System;
4+
using System.Security.Cryptography;
5+
using System.Text;
6+
using NUnit.Framework.Interfaces;
7+
8+
namespace NUnit.Framework.Internal.Filters
9+
{
10+
/// <summary>
11+
/// PartitionFilter filter matches a subset of tests based upon a chosen partition number and partition count
12+
///
13+
/// This is helpful when you may want to run a subset of tests (eg, across 3 machines - or partitions), each with a separately assigned partition number and fixed partition count
14+
/// </summary>
15+
internal sealed class PartitionFilter : TestFilter
16+
{
17+
/// <summary>
18+
/// The matching partition number (between 1 and Partition Count, inclusive) this filter should match on
19+
/// </summary>
20+
public uint PartitionNumber { get; private set; }
21+
22+
/// <summary>
23+
/// The number of partitions available to use when assigning a matching partition number for each test this filter should match on
24+
/// </summary>
25+
public uint PartitionCount { get; private set; }
26+
27+
/// <summary>
28+
/// Construct a PartitionFilter that matches tests that have the assigned partition number from the total partition count
29+
/// </summary>
30+
/// <param name="partitionNumber">The partition number this filter will recognize and match on.</param>
31+
/// <param name="partitionCount">The total number of partitions that should be configured when assigning each test to a partition number.</param>
32+
public PartitionFilter(uint partitionNumber, uint partitionCount)
33+
{
34+
PartitionNumber = partitionNumber;
35+
PartitionCount = partitionCount;
36+
}
37+
38+
/// <summary>
39+
/// Create a new PartitionFilter from the provided string value, or return false if the value could not be parsed
40+
/// </summary>
41+
/// <param name="value">The partition value (eg, 1/10 to indicate partition 1 of 10)</param>
42+
/// <param name="partitionFilter">The created PartitionFilter if the parsing succeeded</param>
43+
/// <returns>True on successful parsing, or False if there is an error</returns>
44+
public static bool TryCreate(string value, out PartitionFilter partitionFilter)
45+
{
46+
// Split our numberWithCount into two parts, such that "1/10" becomes PartitionNumber 1, PartitionCount 10
47+
string[] parts = value.Split('/');
48+
49+
// Parts must be exactly 2, and be in the format of "number/count"
50+
if (parts.Length == 2 && uint.TryParse(parts[0], out uint number) && uint.TryParse(parts[1], out uint count)) {
51+
// Number must be between 1 and Count, inclusive
52+
// Return a new PartitionFilter with the parsed values
53+
if (number >= 1 && number <= count)
54+
{
55+
partitionFilter = new PartitionFilter(number, count);
56+
return true;
57+
}
58+
}
59+
60+
// Could not parse partition information
61+
partitionFilter = null;
62+
return false;
63+
}
64+
65+
/// <summary>
66+
/// Match a test against a single value.
67+
/// </summary>
68+
public override bool Match(ITest test)
69+
{
70+
// Do not match a test Suite, only match individual tests
71+
if (test.IsSuite)
72+
return false;
73+
74+
// Calculate the partition number for the provided Test
75+
var partitionForTest = ComputePartitionNumber(test);
76+
77+
// Return a match if the calculated partition number matches our configured Partition Number
78+
return partitionForTest == PartitionNumber;
79+
}
80+
81+
/// <summary>
82+
/// Adds a PartitionFilter XML node to the provided parentNode
83+
/// </summary>
84+
/// <param name="parentNode">Parent node</param>
85+
/// <param name="recursive">True if recursive</param>
86+
/// <returns>The added XML node</returns>
87+
public override TNode AddToXml(TNode parentNode, bool recursive)
88+
{
89+
return parentNode.AddElement("partition", $"{PartitionNumber}/{PartitionCount}");
90+
}
91+
92+
/// <summary>
93+
/// Computes the Partition Number that has been assigned to the provided ITest value (based upon the configured Partition Count)
94+
/// </summary>
95+
/// <param name="value">A partition value between 1 and PartitionCount, inclusive</param>
96+
/// <returns>A partition value between 1 and PartitionCount, inclusive</returns>
97+
public uint ComputePartitionNumber(ITest value)
98+
{
99+
return ComputeHashValue(value.FullName) % PartitionCount + 1;
100+
}
101+
102+
/// <summary>
103+
/// Computes an unsigned integer hash value based upon the provided string
104+
/// </summary>
105+
private static uint ComputeHashValue(string name)
106+
{
107+
using var hashAlgorithm = SHA256.Create();
108+
109+
// SHA256 ComputeHash will return 32 bytes, we will use the first 4 bytes of that to convert to an unsigned integer
110+
var hashValue = hashAlgorithm.ComputeHash(Encoding.UTF8.GetBytes(name));
111+
112+
return BitConverter.ToUInt32(hashValue, 0);
113+
}
114+
}
115+
}

src/NUnitFramework/framework/Internal/TestFilter.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,11 @@ public static TestFilter FromXml(TNode node)
228228
if (name != null)
229229
return new PropertyFilter(name, node.Value) { IsRegex = isRegex };
230230
break;
231+
232+
case "partition":
233+
if (PartitionFilter.TryCreate(node.Value, out var partitionFilter))
234+
return partitionFilter;
235+
break;
231236
}
232237

233238
throw new ArgumentException("Invalid filter element: " + node.Name, "xmlNode");
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
// Copyright (c) Charlie Poole, Rob Prouse and Contributors. MIT License - see LICENSE.txt
2+
3+
using NUnit.Framework.Interfaces;
4+
5+
namespace NUnit.Framework.Internal.Filters
6+
{
7+
public class PartitionFilterTests : TestFilterTests
8+
{
9+
private PartitionFilter _filter;
10+
private ITest _testMatchingPartition;
11+
private ITest _testNotMatchingPartition;
12+
13+
[SetUp]
14+
public void CreateFilter()
15+
{
16+
// Configure a new PartitionFilter with the provided partition count and number
17+
_filter = new PartitionFilter(7, 10);
18+
19+
_testMatchingPartition = _fixtureWithMultipleTests.Tests[1];
20+
_testNotMatchingPartition = _fixtureWithMultipleTests.Tests[0];
21+
}
22+
23+
[Test]
24+
public void IsNotEmpty()
25+
{
26+
Assert.That(_filter.IsEmpty, Is.False);
27+
}
28+
29+
[Test]
30+
public void MatchTest()
31+
{
32+
// Validate
33+
Assert.That(_filter.ComputePartitionNumber(_testMatchingPartition), Is.EqualTo(7));
34+
Assert.That(_filter.ComputePartitionNumber(_testNotMatchingPartition), Is.EqualTo(8));
35+
36+
// Assert
37+
Assert.That(_filter.Match(_testMatchingPartition), Is.True);
38+
Assert.That(_filter.Match(_testNotMatchingPartition), Is.False);
39+
}
40+
41+
[Test]
42+
public void PassTest()
43+
{
44+
// This test fixture contains both one matching and one non-matching test
45+
// The fixture should therefore pass as True because one of the child tests are a match
46+
Assert.That(_filter.Pass(_fixtureWithMultipleTests), Is.True);
47+
48+
// Validate that our matching and non-matching tests return the correct Pass result
49+
Assert.That(_filter.Pass(_testMatchingPartition), Is.True);
50+
Assert.That(_filter.Pass(_testNotMatchingPartition), Is.False);
51+
52+
// This other test fixture has no matching tests for this partition number
53+
Assert.That(_filter.Pass(_specialFixture), Is.False);
54+
}
55+
56+
[Test]
57+
public void ExplicitMatchTest()
58+
{
59+
// Top level TestFixture should always Pass
60+
Assert.That(_filter.IsExplicitMatch(_fixtureWithMultipleTests));
61+
62+
// Assert
63+
Assert.That(_filter.IsExplicitMatch(_testMatchingPartition), Is.True);
64+
Assert.That(_filter.IsExplicitMatch(_testNotMatchingPartition), Is.False);
65+
}
66+
67+
[Test]
68+
public void FromXml()
69+
{
70+
TestFilter filter = TestFilter.FromXml(@"<filter><partition>7/10</partition></filter>");
71+
72+
Assert.That(filter, Is.TypeOf<PartitionFilter>());
73+
74+
var partitionFilter = (PartitionFilter)filter;
75+
Assert.That(partitionFilter.PartitionNumber, Is.EqualTo(7));
76+
Assert.That(partitionFilter.PartitionCount, Is.EqualTo(10));
77+
}
78+
79+
[Test]
80+
public void ToXml()
81+
{
82+
Assert.That(_filter.ToXml(false).OuterXml, Is.EqualTo(@"<partition>7/10</partition>"));
83+
}
84+
}
85+
}

0 commit comments

Comments
 (0)