diff --git a/src/Platform/Microsoft.Testing.Platform/Requests/TreeNodeFilter/FilterOperator.cs b/src/Platform/Microsoft.Testing.Platform/Requests/TreeNodeFilter/FilterOperator.cs
index 9743f49f87..d518d7548f 100644
--- a/src/Platform/Microsoft.Testing.Platform/Requests/TreeNodeFilter/FilterOperator.cs
+++ b/src/Platform/Microsoft.Testing.Platform/Requests/TreeNodeFilter/FilterOperator.cs
@@ -19,9 +19,4 @@ internal enum FilterOperator
/// Combine the following expressions with a logical OR.
///
Or,
-
- ///
- /// Filter the following expression by the given property.
- ///
- Equals,
}
diff --git a/src/Platform/Microsoft.Testing.Platform/Requests/TreeNodeFilter/OperatorKind.cs b/src/Platform/Microsoft.Testing.Platform/Requests/TreeNodeFilter/OperatorKind.cs
index 051583489c..b9c7da2819 100644
--- a/src/Platform/Microsoft.Testing.Platform/Requests/TreeNodeFilter/OperatorKind.cs
+++ b/src/Platform/Microsoft.Testing.Platform/Requests/TreeNodeFilter/OperatorKind.cs
@@ -41,6 +41,11 @@ internal enum OperatorKind
///
FilterEquals,
+ ///
+ /// Filter not equals operator.
+ ///
+ FilterNotEquals,
+
///
/// Operator used for combining multiple filters with a logical OR.
///
diff --git a/src/Platform/Microsoft.Testing.Platform/Requests/TreeNodeFilter/TreeNodeFilter.cs b/src/Platform/Microsoft.Testing.Platform/Requests/TreeNodeFilter/TreeNodeFilter.cs
index 9bec02318e..9a81e683df 100644
--- a/src/Platform/Microsoft.Testing.Platform/Requests/TreeNodeFilter/TreeNodeFilter.cs
+++ b/src/Platform/Microsoft.Testing.Platform/Requests/TreeNodeFilter/TreeNodeFilter.cs
@@ -38,6 +38,7 @@ internal TreeNodeFilter(string filter)
/// FILTER_EXPR =
/// '(' FILTER_EXPR ')'
/// | TOKEN '=' TOKEN
+ /// | TOKEN '!=' TOKEN
/// | FILTER_EXPR OP FILTER_EXPR
/// | TOKEN
/// OP = '&' | '|'
@@ -210,6 +211,13 @@ private static List ParseFilter(string filter)
isPropAllowed = false;
break;
+ case "!=":
+ operatorStack.Push(OperatorKind.FilterNotEquals);
+
+ isOperatorAllowed = false;
+ isPropAllowed = false;
+ break;
+
default:
expressionStack.Push(new ValueExpression(token));
@@ -311,7 +319,6 @@ private static void ProcessStackOperator(OperatorKind op, Stack FilterOperator.And,
OperatorKind.Or => FilterOperator.Or,
- OperatorKind.FilterEquals => FilterOperator.Equals,
_ => throw ApplicationStateGuard.Unreachable(),
};
@@ -319,6 +326,7 @@ private static void ProcessStackOperator(OperatorKind op, Stack TokenizeFilter(string filter)
break;
+ case '!':
+ if (i + 1 < filter.Length && filter[i + 1] == '=')
+ {
+ if (lastStringTokenBuilder.Length > 0)
+ {
+ yield return lastStringTokenBuilder.ToString();
+ lastStringTokenBuilder.Clear();
+ }
+
+ yield return "!=";
+ i++;
+ }
+ else
+ {
+ goto default;
+ }
+
+ break;
+
default:
lastStringTokenBuilder.Append(Regex.Escape(filter[i].ToString()));
break;
diff --git a/test/UnitTests/Microsoft.Testing.Platform.UnitTests/Requests/TreeNodeFilterTests.cs b/test/UnitTests/Microsoft.Testing.Platform.UnitTests/Requests/TreeNodeFilterTests.cs
index f07a899ae9..f587f02457 100644
--- a/test/UnitTests/Microsoft.Testing.Platform.UnitTests/Requests/TreeNodeFilterTests.cs
+++ b/test/UnitTests/Microsoft.Testing.Platform.UnitTests/Requests/TreeNodeFilterTests.cs
@@ -105,6 +105,42 @@ public void Parameters_PropertyCheck()
Assert.IsFalse(filter.MatchesFilter("/ProjectB.UnitTests", new PropertyBag()));
}
+ [TestMethod]
+ public void Parameters_NegatedPropertyCheck()
+ {
+ TreeNodeFilter filter = new("/*.UnitTests[Tag!=Fast]");
+ Assert.IsFalse(filter.MatchesFilter("/ProjectB.UnitTests", new PropertyBag(new KeyValuePairStringProperty("Tag", "Fast"))));
+ Assert.IsTrue(filter.MatchesFilter("/ProjectB.UnitTests", new PropertyBag(new KeyValuePairStringProperty("Tag", "Slow"))));
+ Assert.IsTrue(filter.MatchesFilter("/ProjectB.UnitTests", new PropertyBag()));
+ }
+
+ [TestMethod]
+ public void Parameters_NegatedPropertyCheckWithMatchAllFilter()
+ {
+ TreeNodeFilter filter = new("/**[Tag!=Fast]");
+ Assert.IsFalse(filter.MatchesFilter("/ProjectB.UnitTests", new PropertyBag(new KeyValuePairStringProperty("Tag", "Fast"))));
+ Assert.IsTrue(filter.MatchesFilter("/ProjectB.UnitTests", new PropertyBag(new KeyValuePairStringProperty("Tag", "Slow"))));
+ Assert.IsTrue(filter.MatchesFilter("/ProjectB.UnitTests", new PropertyBag()));
+ }
+
+ [TestMethod]
+ public void Parameters_NegatedPropertyCheckCombinedWithAnd()
+ {
+ TreeNodeFilter filter = new("/*.UnitTests[(Tag!=Fast)&(Tag!=Slow)]");
+ Assert.IsFalse(filter.MatchesFilter("/ProjectB.UnitTests", new PropertyBag(new KeyValuePairStringProperty("Tag", "Fast"))));
+ Assert.IsFalse(filter.MatchesFilter("/ProjectB.UnitTests", new PropertyBag(new KeyValuePairStringProperty("Tag", "Slow"))));
+ Assert.IsTrue(filter.MatchesFilter("/ProjectB.UnitTests", new PropertyBag()));
+ }
+
+ [TestMethod]
+ public void Parameters_NegatedPropertyCheckCombinedWithOr()
+ {
+ TreeNodeFilter filter = new("/*.UnitTests[(Tag!=Fast)|(Tag!=Slow)]");
+ Assert.IsTrue(filter.MatchesFilter("/ProjectB.UnitTests", new PropertyBag(new KeyValuePairStringProperty("Tag", "Fast"))));
+ Assert.IsTrue(filter.MatchesFilter("/ProjectB.UnitTests", new PropertyBag(new KeyValuePairStringProperty("Tag", "Slow"))));
+ Assert.IsTrue(filter.MatchesFilter("/ProjectB.UnitTests", new PropertyBag()));
+ }
+
[TestMethod]
public void Parameters_DisallowAtStart()
=> Assert.ThrowsException(() => _ = new TreeNodeFilter("/[Tag=Fast]"));