diff --git a/src/Microsoft.TestPlatform.Filter.Source/Condition.cs b/src/Microsoft.TestPlatform.Filter.Source/Condition.cs
index 322b6a016a..12b0556273 100644
--- a/src/Microsoft.TestPlatform.Filter.Source/Condition.cs
+++ b/src/Microsoft.TestPlatform.Filter.Source/Condition.cs
@@ -71,6 +71,11 @@ internal sealed class Condition
///
public const Operation DefaultOperation = Operation.Contains;
+ ///
+ /// Reserved filter value that matches tests with no value for a given property (uncategorized).
+ ///
+ internal const string NoneFilterValue = "None";
+
#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.";
@@ -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)
{
diff --git a/src/Microsoft.TestPlatform.Filter.Source/FastFilter.cs b/src/Microsoft.TestPlatform.Filter.Source/FastFilter.cs
index 379f3addf4..7c90a0b67a 100644
--- a/src/Microsoft.TestPlatform.Filter.Source/FastFilter.cs
+++ b/src/Microsoft.TestPlatform.Filter.Source/FastFilter.cs
@@ -82,9 +82,18 @@ internal bool Evaluate(Func 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;
}
@@ -93,11 +102,16 @@ internal bool Evaluate(Func 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)
{
diff --git a/test/Microsoft.TestPlatform.Acceptance.IntegrationTests/TestCaseFilterTests.cs b/test/Microsoft.TestPlatform.Acceptance.IntegrationTests/TestCaseFilterTests.cs
index 72084bc4ea..0afd627f46 100644
--- a/test/Microsoft.TestPlatform.Acceptance.IntegrationTests/TestCaseFilterTests.cs
+++ b/test/Microsoft.TestPlatform.Acceptance.IntegrationTests/TestCaseFilterTests.cs
@@ -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);
+ }
+
}
diff --git a/test/Microsoft.TestPlatform.Common.UnitTests/Filtering/ConditionTests.cs b/test/Microsoft.TestPlatform.Common.UnitTests/Filtering/ConditionTests.cs
index 02c65418b3..af2d454e25 100644
--- a/test/Microsoft.TestPlatform.Common.UnitTests/Filtering/ConditionTests.cs
+++ b/test/Microsoft.TestPlatform.Common.UnitTests/Filtering/ConditionTests.cs
@@ -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());
+
+ 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));
+ }
+
+ [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());
+
+ 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
}
diff --git a/test/Microsoft.TestPlatform.Common.UnitTests/Filtering/FastFilterTests.cs b/test/Microsoft.TestPlatform.Common.UnitTests/Filtering/FastFilterTests.cs
index 7c1bad5344..243a7e072c 100644
--- a/test/Microsoft.TestPlatform.Common.UnitTests/Filtering/FastFilterTests.cs
+++ b/test/Microsoft.TestPlatform.Common.UnitTests/Filtering/FastFilterTests.cs
@@ -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()));
+ // 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" }));
+ }
+
+ [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
}