Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
37 changes: 29 additions & 8 deletions src/Microsoft.TestPlatform.Filter.Source/Condition.cs
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,12 @@ internal Condition(string name, Operation operation, string value)

private bool EvaluateEqualOperation(string[]? multiValue)
{
// Special case: empty string filter value matches null/empty property (uncategorized tests).
if (Value.Length == 0)
{
return multiValue is null or { Length: 0 };
}

// if any value in multi-valued property matches 'this.Value', for Equal to evaluate true.
if (multiValue != null)
{
Expand All @@ -120,6 +126,12 @@ private bool EvaluateEqualOperation(string[]? multiValue)

private bool EvaluateContainsOperation(string[]? multiValue)
{
// Special case: empty string filter value matches null/empty property (uncategorized tests).
if (Value.Length == 0)
{
return multiValue is null or { Length: 0 };
}

if (multiValue != null)
{
foreach (string propertyValue in multiValue)
Expand Down Expand Up @@ -184,24 +196,33 @@ internal static Condition Parse(string? conditionString)
return new Condition(DefaultPropertyName, DefaultOperation, FilterHelper.Unescape(conditionString!.Trim()));
}

if (parts.Length == 2)
{
// Two parts means property name and operator with no value (e.g. "TestCategory=").
// Treat the value as empty string to support filtering for uncategorized tests.
parts = new[] { parts[0], parts[1], string.Empty };
}

if (parts.Length != 3)
{
ThrownFormatExceptionForInvalidCondition(conditionString);
}

for (int index = 0; index < 3; index++)
{
// Property name (parts[0]) and operator (parts[1]) must not be empty.
// parts[2] (value) can be empty to support filtering for uncategorized tests.
#if IS_VSTEST_REPO
if (parts[index].IsNullOrWhiteSpace())
if (parts[0].IsNullOrWhiteSpace() || parts[1].IsNullOrWhiteSpace())
#else
if (string.IsNullOrWhiteSpace(parts[index]))
if (string.IsNullOrWhiteSpace(parts[0]) || string.IsNullOrWhiteSpace(parts[1]))
#endif
{
ThrownFormatExceptionForInvalidCondition(conditionString);
}
parts[index] = parts[index].Trim();
{
ThrownFormatExceptionForInvalidCondition(conditionString);
}

parts[0] = parts[0].Trim();
parts[1] = parts[1].Trim();
parts[2] = parts[2].Trim();

Operation operation = GetOperator(parts[1]);
Condition condition = new(parts[0], operation, FilterHelper.Unescape(parts[2]));
return condition;
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.
// Special case: if filter contains empty string, check if property is null/empty (uncategorized).
bool hasEmptyStringFilter = FilterProperties[name].Contains(string.Empty);

// If there is no value corresponding to given name, treat it as unmatched unless filtering for empty string.
if (!TryGetPropertyValue(name, propertyValueProvider, out var singleValue, out var multiValues))
{
if (hasEmptyStringFilter)
{
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 (hasEmptyStringFilter)
{
// Empty array matches empty string filter.
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 RunSelectedTestsWithEmptyTestCategoryFilterMatchesUncategorizedTests(RunnerInfo runnerInfo)
{
SetTestEnvironment(_testEnvironment, runnerInfo);

var arguments = PrepareArguments(
GetSampleTestAssembly(),
GetTestAdapterPath(),
string.Empty, FrameworkArgValue,
runnerInfo.InIsolationValue, resultsDirectory: TempDirectory.Path);
// Empty TestCategory value should match 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=\"");
InvokeVsTest(arguments);
ValidateSummaryStatus(1, 0, 1);
}

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

var arguments = PrepareArguments(
GetSampleTestAssembly(),
GetTestAdapterPath(),
string.Empty, FrameworkArgValue,
runnerInfo.InIsolationValue, resultsDirectory: TempDirectory.Path);
// NotEqual to empty TestCategory should match tests WITH categories.
// In SimpleTestProject: only FailingTest has TestCategory("CategoryA").
arguments = string.Concat(arguments, " /TestCaseFilter:\"TestCategory!=\"");
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 @@ -28,10 +28,13 @@ public void ParseShouldThrownFormatExceptionOnEmptyConditionString()
}

[TestMethod]
public void ParseShouldThrownFormatExceptionOnIncompleteConditionString()
public void ParseShouldCreateConditionWithEmptyValueOnTrailingOperator()
{
var conditionString = "PropertyName=";
Assert.ThrowsExactly<FormatException>(() => Condition.Parse(conditionString));
Condition condition = Condition.Parse(conditionString);
Assert.AreEqual("PropertyName", condition.Name);
Assert.AreEqual(Operation.Equal, condition.Operation);
Assert.AreEqual(string.Empty, condition.Value);
}

[TestMethod]
Expand Down Expand Up @@ -235,4 +238,165 @@ public void TokenizeConditionShouldHandleSingleBangAtEnd()
Assert.AreEqual("FullyQualifiedName", tokens[0]);
Assert.AreEqual("!", tokens[1]);
}

#region Empty value filter tests (uncategorized tests support)

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

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

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

Assert.AreEqual("TestCategory", condition.Name);
Assert.AreEqual(Operation.Contains, condition.Operation);
Assert.AreEqual(string.Empty, condition.Value);
}

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

Assert.AreEqual("TestCategory", condition.Name);
Assert.AreEqual(Operation.NotContains, condition.Operation);
Assert.AreEqual(string.Empty, condition.Value);
}

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

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

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

Assert.IsTrue(result);
}

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

Assert.IsTrue(result);
}

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

Assert.IsFalse(result);
}

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

Assert.IsFalse(result);
}

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

Assert.IsFalse(result);
}

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

Assert.IsTrue(result);
}

[TestMethod]
public void EvaluateEmptyStringContainsWithNullPropertyShouldReturnTrue()
{
var condition = new Condition("TestCategory", Operation.Contains, string.Empty);
bool result = condition.Evaluate(propertyName => null);

Assert.IsTrue(result);
}

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

Assert.IsTrue(result);
}

[TestMethod]
public void EvaluateEmptyStringContainsWithNonEmptyArrayShouldReturnFalse()
{
var condition = new Condition("TestCategory", Operation.Contains, string.Empty);
bool result = condition.Evaluate(propertyName => new[] { "CategoryA" });

Assert.IsFalse(result);
}
Comment thread
Evangelink marked this conversation as resolved.

[TestMethod]
public void EvaluateEmptyStringNotContainsWithNullPropertyShouldReturnFalse()
{
var condition = new Condition("TestCategory", Operation.NotContains, string.Empty);
bool result = condition.Evaluate(propertyName => null);

Assert.IsFalse(result);
}

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

Assert.IsFalse(result);
}

[TestMethod]
public void EvaluateEmptyStringNotContainsWithNonEmptyArrayShouldReturnTrue()
{
var condition = new Condition("TestCategory", Operation.NotContains, string.Empty);
bool result = condition.Evaluate(propertyName => new[] { "CategoryA" });

Assert.IsTrue(result);
}

[TestMethod]
public void EvaluateNonEmptyStringEqualShouldStillWorkNormally()
{
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));
}

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

#region Empty value filter tests (uncategorized tests support)

[TestMethod]
public void FastFilterWithEmptyEqualShouldMatchNullProperty()
{
var filterExpressionWrapper = new FilterExpressionWrapper("TestCategory=");
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 FastFilterWithEmptyEqualOrSpecificCategoryShouldMatchBoth()
{
var filterExpressionWrapper = new FilterExpressionWrapper("TestCategory=|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 FastFilterWithEmptyNotEqualShouldMatchCategorizedTests()
{
var filterExpressionWrapper = new FilterExpressionWrapper("TestCategory!=");
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.

#endregion
}
Loading