Skip to content

Commit

Permalink
Split method filters just-in-time so as not to change the method filt…
Browse files Browse the repository at this point in the history
…er contract.
  • Loading branch information
bradwilson committed Nov 6, 2016
1 parent 196a51f commit 47b648c
Show file tree
Hide file tree
Showing 3 changed files with 99 additions and 63 deletions.
2 changes: 1 addition & 1 deletion src/xunit.console/CommandLine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,7 @@ protected XunitProject Parse(Predicate<string> fileExists)
if (option.Value == null)
throw new ArgumentException("missing argument for -method");

project.Filters.AddIncludedMethod(option.Value);
project.Filters.IncludedMethods.Add(option.Value);
}
else if (optionName == "namespace")
{
Expand Down
152 changes: 94 additions & 58 deletions src/xunit.runner.utility/Project/XunitFilters.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
Expand All @@ -11,7 +12,10 @@ namespace Xunit
/// </summary>
public class XunitFilters
{
List<Regex> includedMethodRegexes;
DateTimeOffset cacheDataDate;
ChangeTrackingHashSet<string> includedMethods;
List<Regex> methodRegexFilters;
HashSet<string> methodStandardFilters;

/// <summary>
/// Initializes a new instance of the <see cref="XunitFilters"/> class.
Expand All @@ -21,8 +25,7 @@ public XunitFilters()
ExcludedTraits = new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase);
IncludedTraits = new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase);
IncludedClasses = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
IncludedMethods = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
includedMethodRegexes = new List<Regex>();
includedMethods = new ChangeTrackingHashSet<string>(StringComparer.OrdinalIgnoreCase);
IncludedNameSpaces = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
}

Expand All @@ -44,45 +47,7 @@ public XunitFilters()
/// <summary>
/// Gets the set of method filters for tests to include.
/// </summary>
public HashSet<string> IncludedMethods { get; }

/// <summary>
/// Add an include method filter.
/// It can either be a fully-qualified method name, or a wildcard pattern.
/// </summary>
public void AddIncludedMethod(string methodNameOrWildcard)
{
if (methodNameOrWildcard.Contains("*"))
{
var regexPattern = WildcardToRegex(methodNameOrWildcard);
var regex = new Regex(regexPattern);
includedMethodRegexes.Add(regex);
}
else
{
IncludedMethods.Add(methodNameOrWildcard);
}
}

/// <summary>
/// Checks whether there are any included method filters.
/// </summary>
public bool HasIncludedMethods()
{
return IncludedMethods.Count != 0 || includedMethodRegexes.Count != 0;
}

/// <summary>
/// Converts a wildcard to a regex.
/// </summary>
/// <param name="pattern">The wildcard pattern to convert.</param>
/// <returns>A regex equivalent of the given wildcard.</returns>
public string WildcardToRegex(string pattern)
{
return "^" + Regex.Escape(pattern).
Replace("\\*", ".*").
Replace("\\?", ".") + "$";
}
public ICollection<string> IncludedMethods => includedMethods;

/// <summary>
/// Gets the set of assembly filters for tests to include.
Expand All @@ -96,6 +61,8 @@ public string WildcardToRegex(string pattern)
/// <returns>Returns <c>true</c> if the test case passed the filter; returns <c>false</c> otherwise.</returns>
public bool Filter(ITestCase testCase)
{
SplitMethodFilters();

if (!FilterIncludedMethodsAndClasses(testCase))
return false;
if (!FilterIncludedTraits(testCase))
Expand Down Expand Up @@ -123,31 +90,22 @@ bool FilterIncludedNameSpaces(ITestCase testCase)
bool FilterIncludedMethodsAndClasses(ITestCase testCase)
{
// No methods or classes in the filter == everything is okay
if (!HasIncludedMethods() && IncludedClasses.Count == 0)
if (methodStandardFilters.Count == 0 && methodRegexFilters.Count == 0 && IncludedClasses.Count == 0)
return true;

if (IncludedClasses.Count != 0 && IncludedClasses.Contains(testCase.TestMethod.TestClass.Class.Name))
return true;

var testCaseMethod = $"{testCase.TestMethod.TestClass.Class.Name}.{testCase.TestMethod.Method.Name}";
if (IncludedMethods.Count != 0 && IncludedMethods.Contains(testCaseMethod))
return true;
var methodName = $"{testCase.TestMethod.TestClass.Class.Name}.{testCase.TestMethod.Method.Name}";

if (includedMethodRegexes != null && FilterIncludedMethodWildcards(testCaseMethod))
if (methodStandardFilters.Count != 0 && methodStandardFilters.Contains(methodName))
return true;

return false;
}
if (methodRegexFilters.Count != 0)
foreach (var regex in methodRegexFilters)
if (regex.IsMatch(methodName))
return true;

bool FilterIncludedMethodWildcards(string testCaseMethod)
{
foreach(var regex in includedMethodRegexes)
{
if (regex.IsMatch(testCaseMethod))
{
return true;
}
}
return false;
}

Expand Down Expand Up @@ -186,5 +144,83 @@ bool FilterIncludedTraits(ITestCase testCase)

return false;
}

void SplitMethodFilters()
{
if (cacheDataDate >= includedMethods.LastMutation)
return;

lock (includedMethods)
{
if (cacheDataDate >= includedMethods.LastMutation)
return;

var standardFilters = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var regexFilters = new List<Regex>();

foreach (var filter in IncludedMethods)
if (filter.Contains("*") || filter.Contains("?"))
regexFilters.Add(new Regex(WildcardToRegex(filter)));
else
standardFilters.Add(filter);

methodStandardFilters = standardFilters;
methodRegexFilters = regexFilters;
cacheDataDate = includedMethods.LastMutation;
}
}

string WildcardToRegex(string pattern)
=> $"^{Regex.Escape(pattern).Replace("\\*", ".*").Replace("\\?", ".")}$";

// This class wraps HashSet<T>, tracking the last mutation date, and using itself
// as a lock for mutation (so that we can guarantee a stable data set when transferring
// the data into caches).
class ChangeTrackingHashSet<T> : ICollection<T>
{
HashSet<T> innerCollection;

public ChangeTrackingHashSet(IEqualityComparer<T> comparer)
{
innerCollection = new HashSet<T>(comparer);
}

public int Count => innerCollection.Count;
public bool IsReadOnly => false;

public DateTimeOffset LastMutation { get; private set; } = DateTimeOffset.UtcNow;

public void Add(T item)
{
lock (this)
{
LastMutation = DateTimeOffset.UtcNow;
innerCollection.Add(item);
}
}

public void Clear()
{
lock (this)
{
LastMutation = DateTimeOffset.UtcNow;
innerCollection.Clear();
}
}

public bool Contains(T item) => innerCollection.Contains(item);
public void CopyTo(T[] array, int arrayIndex) => innerCollection.CopyTo(array, arrayIndex);
public IEnumerator<T> GetEnumerator() => innerCollection.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => innerCollection.GetEnumerator();

public bool Remove(T item)
{
lock (this)
{
LastMutation = DateTimeOffset.UtcNow;
return innerCollection.Remove(item);
}
}
}
}
}
8 changes: 4 additions & 4 deletions test/test.xunit.runner.utility/Project/XunitFiltersTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ public static void CanFilterFactsByFullName()
var method2 = Mocks.TestCase<Namespace1.ClassInNamespace1.InnerClass1>("Name2");
var method3 = Mocks.TestCase<Namespace1.ClassInNamespace1.InnerClass1>("Name3");
filters.IncludedMethods.Add("Namespace1.ClassInNamespace1+InnerClass1.Name1");
filters.AddIncludedMethod("Namespace1.ClassInNamespace1+InnerClass1.Name2");
filters.IncludedMethods.Add("Namespace1.ClassInNamespace1+InnerClass1.Name2");

Assert.True(filters.Filter(method1));
Assert.True(filters.Filter(method2));
Expand All @@ -195,9 +195,9 @@ public static void CanFilterFactsByWildcardName()
var method2 = Mocks.TestCase<Namespace1.ClassInNamespace1.InnerClass1>("Name2");
var method3 = Mocks.TestCase<Namespace1.ClassInNamespace1.InnerClass1>("Name3");
var method4 = Mocks.TestCase<Namespace1.ClassInNamespace1.InnerClass2>("Name3");
filters.AddIncludedMethod("*.Name1");
filters.AddIncludedMethod("Namespace1.*.Name2");
filters.AddIncludedMethod("*+InnerClass2.*");
filters.IncludedMethods.Add("Namespace1.ClassInNamespace1+InnerClass1.N?me1");
filters.IncludedMethods.Add("Namespace1.*.Name2");
filters.IncludedMethods.Add("*+InnerClass2.*");

Assert.True(filters.Filter(method1));
Assert.True(filters.Filter(method2));
Expand Down

0 comments on commit 47b648c

Please sign in to comment.