Skip to content
Merged
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
12 changes: 12 additions & 0 deletions src/Microsoft.TestPlatform.Filter.Source/Condition.cs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,11 @@ internal sealed class Condition
/// </summary>
public const Operation DefaultOperation = Operation.Contains;

/// <summary>
/// Reserved filter value that matches tests with no value for a given property (uncategorized).
/// </summary>
internal const string NoneFilterValue = "None";
Comment thread
Evangelink marked this conversation as resolved.

#if !IS_VSTEST_REPO
private const string TestCaseFilterFormatException = "Incorrect format for TestCaseFilter {0}. Specify the correct format and try again. Note that the incorrect format can lead to no test getting executed.";

Expand Down Expand Up @@ -103,6 +108,13 @@ internal Condition(string name, Operation operation, string value)

private bool EvaluateEqualOperation(string[]? multiValue)
{
// Reserved keyword: "None" matches tests with no value for this property (uncategorized).
if (multiValue is null or { Length: 0 }
&& string.Equals(Value, NoneFilterValue, StringComparison.OrdinalIgnoreCase))
{
return true;
}

// if any value in multi-valued property matches 'this.Value', for Equal to evaluate true.
if (multiValue != null)
{
Expand Down
18 changes: 16 additions & 2 deletions src/Microsoft.TestPlatform.Filter.Source/FastFilter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,18 @@ internal bool Evaluate(Func<string, object?> propertyValueProvider)
bool matched = false;
foreach (var name in FilterProperties.Keys)
{
// If there is no value corresponding to given name, treat it as unmatched.
// Reserved keyword: "None" matches tests with no value for this property (uncategorized).
bool hasNoneFilter = FilterProperties[name].Contains(Condition.NoneFilterValue);

// If there is no value corresponding to given name, treat it as unmatched unless filtering for "None".
if (!TryGetPropertyValue(name, propertyValueProvider, out var singleValue, out var multiValues))
{
if (hasNoneFilter)
{
matched = true;
break;
}

continue;
}

Expand All @@ -93,11 +102,16 @@ internal bool Evaluate(Func<string, object?> propertyValueProvider)
var value = PropertyValueRegex == null ? singleValue : ApplyRegex(singleValue);
matched = value != null && FilterProperties[name].Contains(value);
}
else
else if (multiValues is { Length: > 0 })
{
var values = PropertyValueRegex == null ? multiValues : multiValues?.Select(value => ApplyRegex(value));
matched = values?.Any(result => result != null && FilterProperties[name].Contains(result)) == true;
}
else if (hasNoneFilter)
{
// Empty array matches "None" filter (uncategorized).
matched = true;
}

if (matched)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -178,4 +178,43 @@ public void DiscoverMstestV1TestsWithAndOperatorTrait(RunnerInfo runnerInfo)
ValidateTestsNotDiscovered(listOfNotDiscoveredTests);
}

[TestMethod]
[NetFullTargetFrameworkDataSourceAttribute(inIsolation: true, inProcess: true)]
[NetCoreTargetFrameworkDataSource]
public void RunSelectedTestsWithNoneTestCategoryFilterMatchesUncategorizedTests(RunnerInfo runnerInfo)
{
SetTestEnvironment(_testEnvironment, runnerInfo);

var arguments = PrepareArguments(
GetSampleTestAssembly(),
GetTestAdapterPath(),
string.Empty, FrameworkArgValue,
runnerInfo.InIsolationValue, resultsDirectory: TempDirectory.Path);
// "None" is a reserved keyword that matches tests without any TestCategory attribute.
// In SimpleTestProject: PassingTest (no category) and SkippingTest (no category, ignored).
// FailingTest has TestCategory("CategoryA") and should NOT be matched.
arguments = string.Concat(arguments, " /TestCaseFilter:\"TestCategory=None\"");
InvokeVsTest(arguments);
ValidateSummaryStatus(1, 0, 1);
}

[TestMethod]
[NetFullTargetFrameworkDataSourceAttribute(inIsolation: true, inProcess: true)]
[NetCoreTargetFrameworkDataSource]
public void RunSelectedTestsWithNoneTestCategoryNotEqualFilterMatchesCategorizedTests(RunnerInfo runnerInfo)
{
SetTestEnvironment(_testEnvironment, runnerInfo);

var arguments = PrepareArguments(
GetSampleTestAssembly(),
GetTestAdapterPath(),
string.Empty, FrameworkArgValue,
runnerInfo.InIsolationValue, resultsDirectory: TempDirectory.Path);
// NotEqual to "None" should match tests WITH categories.
// In SimpleTestProject: only FailingTest has TestCategory("CategoryA").
arguments = string.Concat(arguments, " /TestCaseFilter:\"TestCategory!=None\"");
InvokeVsTest(arguments);
ValidateSummaryStatus(0, 1, 0);
Comment thread
Evangelink marked this conversation as resolved.
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -235,4 +235,134 @@ public void TokenizeConditionShouldHandleSingleBangAtEnd()
Assert.AreEqual("FullyQualifiedName", tokens[0]);
Assert.AreEqual("!", tokens[1]);
}

#region None filter value tests (uncategorized tests support)

[TestMethod]
public void ParseNoneValueShouldCreateCondition()
{
Condition condition = Condition.Parse("TestCategory=None");

Assert.AreEqual("TestCategory", condition.Name);
Assert.AreEqual(Operation.Equal, condition.Operation);
Assert.AreEqual("None", condition.Value);
}

[TestMethod]
public void ParseNoneValueWithNotEqualShouldCreateCondition()
{
Condition condition = Condition.Parse("TestCategory!=None");

Assert.AreEqual("TestCategory", condition.Name);
Assert.AreEqual(Operation.NotEqual, condition.Operation);
Assert.AreEqual("None", condition.Value);
}

[TestMethod]
public void EvaluateNoneEqualWithNullPropertyShouldReturnTrue()
{
var condition = new Condition("TestCategory", Operation.Equal, "None");
bool result = condition.Evaluate(propertyName => null);

Assert.IsTrue(result);
}

[TestMethod]
public void EvaluateNoneEqualWithEmptyArrayShouldReturnTrue()
{
var condition = new Condition("TestCategory", Operation.Equal, "None");
bool result = condition.Evaluate(propertyName => Array.Empty<string>());

Assert.IsTrue(result);
}

[TestMethod]
public void EvaluateNoneEqualWithNonEmptyArrayShouldReturnFalse()
{
var condition = new Condition("TestCategory", Operation.Equal, "None");
bool result = condition.Evaluate(propertyName => new[] { "CategoryA" });

Assert.IsFalse(result);
}

[TestMethod]
public void EvaluateNoneEqualIsCaseInsensitive()
{
var condition = new Condition("TestCategory", Operation.Equal, "none");
Assert.IsTrue(condition.Evaluate(propertyName => null));

var condition2 = new Condition("TestCategory", Operation.Equal, "NONE");
Assert.IsTrue(condition2.Evaluate(propertyName => null));
}
Comment thread
Evangelink marked this conversation as resolved.

[TestMethod]
public void EvaluateNoneNotEqualWithNullPropertyShouldReturnFalse()
{
var condition = new Condition("TestCategory", Operation.NotEqual, "None");
bool result = condition.Evaluate(propertyName => null);

Assert.IsFalse(result);
}

[TestMethod]
public void EvaluateNoneNotEqualWithEmptyArrayShouldReturnFalse()
{
var condition = new Condition("TestCategory", Operation.NotEqual, "None");
bool result = condition.Evaluate(propertyName => Array.Empty<string>());

Assert.IsFalse(result);
}

[TestMethod]
public void EvaluateNoneNotEqualWithNonEmptyArrayShouldReturnTrue()
{
var condition = new Condition("TestCategory", Operation.NotEqual, "None");
bool result = condition.Evaluate(propertyName => new[] { "CategoryA" });

Assert.IsTrue(result);
}

[TestMethod]
public void EvaluateNonNoneValueShouldStillWorkNormally()
{
var condition = new Condition("TestCategory", Operation.Equal, "CategoryA");
Assert.IsTrue(condition.Evaluate(propertyName => new[] { "CategoryA" }));
Assert.IsFalse(condition.Evaluate(propertyName => new[] { "CategoryB" }));
Assert.IsFalse(condition.Evaluate(propertyName => null));
}

[TestMethod]
public void EvaluateNoneEqualWithExplicitNoneCategoryShouldReturnTrue()
{
// A test with [TestCategory("None")] should also match TestCategory=None.
// "None" is reserved for uncategorized, but tests that literally use it
// are included as well (by design, to avoid silent mismatches).
var condition = new Condition("TestCategory", Operation.Equal, "None");
bool result = condition.Evaluate(propertyName => new[] { "None" });

Assert.IsTrue(result);
}

[TestMethod]
public void EvaluateNoneNotEqualWithExplicitNoneCategoryShouldReturnFalse()
{
// A test with [TestCategory("None")] should NOT match TestCategory!=None.
var condition = new Condition("TestCategory", Operation.NotEqual, "None");
bool result = condition.Evaluate(propertyName => new[] { "None" });

Assert.IsFalse(result);
}

[TestMethod]
public void EvaluateNoneEqualWithExplicitNoneCategoryAmongOthersShouldReturnTrue()
{
// A test with [TestCategory("None")] and [TestCategory("CategoryA")]
// should match TestCategory=None because one of the values equals "None".
var condition = new Condition("TestCategory", Operation.Equal, "None");
bool result = condition.Evaluate(propertyName => new[] { "None", "CategoryA" });

Assert.IsTrue(result);
}

#endregion
}
Original file line number Diff line number Diff line change
Expand Up @@ -442,4 +442,71 @@ public void MultiplePropertyNamesNotEqualAnd()
_ => null,
}));
}

#region None filter value tests (uncategorized tests support)

[TestMethod]
public void FastFilterWithNoneEqualShouldMatchNullProperty()
{
var filterExpressionWrapper = new FilterExpressionWrapper("TestCategory=None");
var fastFilter = filterExpressionWrapper.FastFilter;

Assert.IsNotNull(fastFilter);
Assert.IsFalse(fastFilter.IsFilteredOutWhenMatched);

// Null property value means uncategorized - should match.
Assert.IsTrue(fastFilter.Evaluate(s => null));
// Empty array means uncategorized - should match.
Assert.IsTrue(fastFilter.Evaluate(s => Array.Empty<string>()));
// Non-empty category - should not match.
Assert.IsFalse(fastFilter.Evaluate(s => new[] { "CategoryA" }));
}

[TestMethod]
public void FastFilterWithNoneEqualOrSpecificCategoryShouldMatchBoth()
{
var filterExpressionWrapper = new FilterExpressionWrapper("TestCategory=None|TestCategory=CategoryA");
var fastFilter = filterExpressionWrapper.FastFilter;

Assert.IsNotNull(fastFilter);
Assert.IsFalse(fastFilter.IsFilteredOutWhenMatched);

// Null property value means uncategorized - should match.
Assert.IsTrue(fastFilter.Evaluate(s => null));
// CategoryA - should match.
Assert.IsTrue(fastFilter.Evaluate(s => new[] { "CategoryA" }));
// CategoryB - should not match.
Assert.IsFalse(fastFilter.Evaluate(s => new[] { "CategoryB" }));
}

[TestMethod]
public void FastFilterWithNoneNotEqualShouldMatchCategorizedTests()
{
var filterExpressionWrapper = new FilterExpressionWrapper("TestCategory!=None");
var fastFilter = filterExpressionWrapper.FastFilter;

Assert.IsNotNull(fastFilter);
Assert.IsTrue(fastFilter.IsFilteredOutWhenMatched);

// Null property value means uncategorized - should NOT match (filtered out).
Assert.IsFalse(fastFilter.Evaluate(s => null));
// Non-empty category - should match (not filtered out).
Assert.IsTrue(fastFilter.Evaluate(s => new[] { "CategoryA" }));
}
Comment thread
Evangelink marked this conversation as resolved.

[TestMethod]
public void FastFilterWithNoneEqualShouldAlsoMatchExplicitNoneCategory()
{
var filterExpressionWrapper = new FilterExpressionWrapper("TestCategory=None");
var fastFilter = filterExpressionWrapper.FastFilter;

Assert.IsNotNull(fastFilter);

// A test with [TestCategory("None")] should also match, since "None" is in the filter set.
Assert.IsTrue(fastFilter.Evaluate(s => new[] { "None" }));
// Case-insensitive: "none" should also match.
Assert.IsTrue(fastFilter.Evaluate(s => new[] { "none" }));
}

#endregion
}
Loading