From 40d474063c79157104a8db916f802ad05fbe10aa Mon Sep 17 00:00:00 2001 From: Abdelghani Moussaid <20841568+abdelghani-moussaid@users.noreply.github.com> Date: Tue, 5 May 2026 22:16:56 +0100 Subject: [PATCH 1/7] perf: eliminate LINQ closures and heap allocations in TreeNodeFilter hot paths --- .../TreeNodeFilter/OperatorExpression.cs | 2 +- .../Requests/TreeNodeFilter/TreeNodeFilter.cs | 154 ++++++++++++++---- 2 files changed, 119 insertions(+), 37 deletions(-) diff --git a/src/Platform/Microsoft.Testing.Platform/Requests/TreeNodeFilter/OperatorExpression.cs b/src/Platform/Microsoft.Testing.Platform/Requests/TreeNodeFilter/OperatorExpression.cs index cf5409e595..801d7dc2ed 100644 --- a/src/Platform/Microsoft.Testing.Platform/Requests/TreeNodeFilter/OperatorExpression.cs +++ b/src/Platform/Microsoft.Testing.Platform/Requests/TreeNodeFilter/OperatorExpression.cs @@ -8,5 +8,5 @@ internal sealed class OperatorExpression(FilterOperator op, IReadOnlyCollection< { public FilterOperator Op { get; } = op; - public IReadOnlyCollection SubExpressions { get; } = subExpressions; + public FilterExpression[] SubExpressions { get; } = [.. subExpressions]; } diff --git a/src/Platform/Microsoft.Testing.Platform/Requests/TreeNodeFilter/TreeNodeFilter.cs b/src/Platform/Microsoft.Testing.Platform/Requests/TreeNodeFilter/TreeNodeFilter.cs index da7178a629..67806305ad 100644 --- a/src/Platform/Microsoft.Testing.Platform/Requests/TreeNodeFilter/TreeNodeFilter.cs +++ b/src/Platform/Microsoft.Testing.Platform/Requests/TreeNodeFilter/TreeNodeFilter.cs @@ -1,6 +1,5 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. - using Microsoft.Testing.Platform.Extensions.Messages; using Microsoft.Testing.Platform.Helpers; using Microsoft.Testing.Platform.Resources; @@ -26,7 +25,15 @@ internal TreeNodeFilter(string filter) { Filter = filter ?? throw new ArgumentNullException(nameof(filter)); _filters = ParseFilter(filter); - ContainsPropertyFilters = _filters.Any(HasPropertyFilterExpression); + ContainsPropertyFilters = false; + for (int i = 0; i < _filters.Count; i++) + { + if (HasPropertyFilterExpression(_filters[i])) + { + ContainsPropertyFilters = true; + break; + } + } } /// @@ -290,9 +297,9 @@ private static void ValidateExpression(FilterExpression expr, bool isMatchAllAll { switch (expr) { - case OperatorExpression { Op: FilterOperator.Not, SubExpressions: var subexprsNot } when subexprsNot.Count != 1: - case OperatorExpression { Op: FilterOperator.And, SubExpressions.Count: < 2 }: - case OperatorExpression { Op: FilterOperator.Or, SubExpressions.Count: < 2 }: + case OperatorExpression { Op: FilterOperator.Not, SubExpressions: var subexprsNot } when subexprsNot.Length != 1: + case OperatorExpression { Op: FilterOperator.And, SubExpressions.Length: < 2 }: + case OperatorExpression { Op: FilterOperator.Or, SubExpressions.Length: < 2 }: throw ApplicationStateGuard.Unreachable(); case OperatorExpression opExpr: @@ -503,7 +510,7 @@ public bool MatchesFilter(string testNodeFullPath, PropertyBag filterablePropert if (currentFragmentIndex >= _filters.Count) { // Note: The regex for ** is .*.*, so we match against such a value expression. - FilterExpression lastFilter = _filters.Last(); + FilterExpression lastFilter = _filters[_filters.Count - 1]; if (lastFilter is ValueAndPropertyExpression valueAndPropertyExpression) { lastFilter = valueAndPropertyExpression.Value; @@ -542,45 +549,103 @@ private static bool MatchFilterPattern( int endFragmentIndex, PropertyBag properties) { - string str = testNodeFullPath[startFragmentIndex..endFragmentIndex]; - return MatchFilterPattern(filterExpression, str, properties); + ReadOnlySpan fragment = testNodeFullPath.AsSpan(startFragmentIndex, endFragmentIndex - startFragmentIndex); + return MatchFilterPattern(filterExpression, fragment, properties); } private static bool MatchFilterPattern( FilterExpression filterExpression, - string testNodeFragment, + ReadOnlySpan testNodeFragment, PropertyBag properties) - => filterExpression switch + { + switch (filterExpression) { - ValueExpression vExpr => vExpr.Regex.IsMatch(testNodeFragment), - OperatorExpression { Op: FilterOperator.Or, SubExpressions: var subexprs } - => subexprs.Any(expr => MatchFilterPattern(expr, testNodeFragment, properties)), - OperatorExpression { Op: FilterOperator.And, SubExpressions: var subexprs } - => subexprs.All(expr => MatchFilterPattern(expr, testNodeFragment, properties)), - OperatorExpression { Op: FilterOperator.Not, SubExpressions: var subexprs } - => !MatchFilterPattern(subexprs.Single(), testNodeFragment, properties), - ValueAndPropertyExpression { Value: var valueExpr, Properties: var propExpr } - => MatchFilterPattern(valueExpr, testNodeFragment, properties) - && MatchProperties(propExpr, properties), - NopExpression => true, - _ => throw ApplicationStateGuard.Unreachable(), - }; + case ValueExpression vExpr: + return vExpr.Regex.IsMatch(testNodeFragment); + + case OperatorExpression { Op: FilterOperator.Or, SubExpressions: var subexprs }: + for (int i = 0; i < subexprs.Length; i++) + { + if (MatchFilterPattern(subexprs[i], testNodeFragment, properties)) + { + return true; + } + } + + return false; + + case OperatorExpression { Op: FilterOperator.And, SubExpressions: var subexprs }: + for (int i = 0; i < subexprs.Length; i++) + { + if (!MatchFilterPattern(subexprs[i], testNodeFragment, properties)) + { + return false; + } + } + + return true; + + case OperatorExpression { Op: FilterOperator.Not, SubExpressions: var subexprs }: + return !MatchFilterPattern(subexprs[0], testNodeFragment, properties); + + case ValueAndPropertyExpression { Value: var valueExpr, Properties: var propExpr }: + return MatchFilterPattern(valueExpr, testNodeFragment, properties) + && MatchProperties(propExpr, properties); + + case NopExpression: + return true; + + default: + throw ApplicationStateGuard.Unreachable(); + } + } private static bool MatchProperties( FilterExpression propertyExpr, PropertyBag properties) - => propertyExpr switch + { + switch (propertyExpr) { - PropertyExpression { PropertyName: var propExpr, Value: var valueExpr } - => properties.AsEnumerable().Any(prop => IsMatchingProperty(prop, propExpr, valueExpr)), - OperatorExpression { Op: FilterOperator.Or, SubExpressions: var subExprs } - => subExprs.Any(expr => MatchProperties(expr, properties)), - OperatorExpression { Op: FilterOperator.And, SubExpressions: var subExprs } - => subExprs.All(expr => MatchProperties(expr, properties)), - OperatorExpression { Op: FilterOperator.Not, SubExpressions: var subExprs } - => !MatchProperties(subExprs.Single(), properties), - _ => throw ApplicationStateGuard.Unreachable(), - }; + case PropertyExpression { PropertyName: var propExpr, Value: var valueExpr }: + foreach (IProperty prop in properties) + { + if (IsMatchingProperty(prop, propExpr, valueExpr)) + { + return true; + } + } + + return false; + + case OperatorExpression { Op: FilterOperator.Or, SubExpressions: var subExprs }: + for (int i = 0; i < subExprs.Length; i++) + { + if (MatchProperties(subExprs[i], properties)) + { + return true; + } + } + + return false; + + case OperatorExpression { Op: FilterOperator.And, SubExpressions: var subExprs }: + for (int i = 0; i < subExprs.Length; i++) + { + if (!MatchProperties(subExprs[i], properties)) + { + return false; + } + } + + return true; + + case OperatorExpression { Op: FilterOperator.Not, SubExpressions: var subExprs }: + return !MatchProperties(subExprs[0], properties); + + default: + throw ApplicationStateGuard.Unreachable(); + } + } private static bool IsMatchingProperty(IProperty prop, ValueExpression propExpr, ValueExpression valueExpr) => prop is TestMetadataProperty testMetadataProperty && @@ -588,6 +653,23 @@ private static bool IsMatchingProperty(IProperty prop, ValueExpression propExpr, valueExpr.Regex.IsMatch(testMetadataProperty.Value); private static bool HasPropertyFilterExpression(FilterExpression expression) - => expression is ValueAndPropertyExpression || - (expression is OperatorExpression op && op.SubExpressions.Any(HasPropertyFilterExpression)); + { + if (expression is ValueAndPropertyExpression) + { + return true; + } + + if (expression is OperatorExpression op) + { + for (int i = 0; i < op.SubExpressions.Length; i++) + { + if (HasPropertyFilterExpression(op.SubExpressions[i])) + { + return true; + } + } + } + + return false; + } } From 97eeb11d386953b7aab322ab9a6d2152a2794700 Mon Sep 17 00:00:00 2001 From: Abdelghani Moussaid <20841568+abdelghani-moussaid@users.noreply.github.com> Date: Tue, 5 May 2026 23:12:56 +0100 Subject: [PATCH 2/7] fix: resolve netstandard2.0 Span compatibility and array mutation --- .../TreeNodeFilter/OperatorExpression.cs | 2 +- .../Requests/TreeNodeFilter/TreeNodeFilter.cs | 27 +++++++++++++------ 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/src/Platform/Microsoft.Testing.Platform/Requests/TreeNodeFilter/OperatorExpression.cs b/src/Platform/Microsoft.Testing.Platform/Requests/TreeNodeFilter/OperatorExpression.cs index 801d7dc2ed..a4dcb8b6d9 100644 --- a/src/Platform/Microsoft.Testing.Platform/Requests/TreeNodeFilter/OperatorExpression.cs +++ b/src/Platform/Microsoft.Testing.Platform/Requests/TreeNodeFilter/OperatorExpression.cs @@ -8,5 +8,5 @@ internal sealed class OperatorExpression(FilterOperator op, IReadOnlyCollection< { public FilterOperator Op { get; } = op; - public FilterExpression[] SubExpressions { get; } = [.. subExpressions]; + public IReadOnlyList SubExpressions { get; } = [.. subExpressions]; } diff --git a/src/Platform/Microsoft.Testing.Platform/Requests/TreeNodeFilter/TreeNodeFilter.cs b/src/Platform/Microsoft.Testing.Platform/Requests/TreeNodeFilter/TreeNodeFilter.cs index 67806305ad..dde8872e09 100644 --- a/src/Platform/Microsoft.Testing.Platform/Requests/TreeNodeFilter/TreeNodeFilter.cs +++ b/src/Platform/Microsoft.Testing.Platform/Requests/TreeNodeFilter/TreeNodeFilter.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. + using Microsoft.Testing.Platform.Extensions.Messages; using Microsoft.Testing.Platform.Helpers; using Microsoft.Testing.Platform.Resources; @@ -297,9 +298,9 @@ private static void ValidateExpression(FilterExpression expr, bool isMatchAllAll { switch (expr) { - case OperatorExpression { Op: FilterOperator.Not, SubExpressions: var subexprsNot } when subexprsNot.Length != 1: - case OperatorExpression { Op: FilterOperator.And, SubExpressions.Length: < 2 }: - case OperatorExpression { Op: FilterOperator.Or, SubExpressions.Length: < 2 }: + case OperatorExpression { Op: FilterOperator.Not, SubExpressions: var subexprsNot } when subexprsNot.Count != 1: + case OperatorExpression { Op: FilterOperator.And, SubExpressions.Count: < 2 }: + case OperatorExpression { Op: FilterOperator.Or, SubExpressions.Count: < 2 }: throw ApplicationStateGuard.Unreachable(); case OperatorExpression opExpr: @@ -509,6 +510,11 @@ public bool MatchesFilter(string testNodeFullPath, PropertyBag filterablePropert if (currentFragmentIndex >= _filters.Count) { + if (_filters.Count == 0) + { + return false; + } + // Note: The regex for ** is .*.*, so we match against such a value expression. FilterExpression lastFilter = _filters[_filters.Count - 1]; if (lastFilter is ValueAndPropertyExpression valueAndPropertyExpression) @@ -561,10 +567,15 @@ private static bool MatchFilterPattern( switch (filterExpression) { case ValueExpression vExpr: +#if NETSTANDARD2_0 + // Fallback for netstandard2.0 which doesn't support Span in Regex + return vExpr.Regex.IsMatch(testNodeFragment.ToString()); +#else return vExpr.Regex.IsMatch(testNodeFragment); +#endif case OperatorExpression { Op: FilterOperator.Or, SubExpressions: var subexprs }: - for (int i = 0; i < subexprs.Length; i++) + for (int i = 0; i < subexprs.Count; i++) { if (MatchFilterPattern(subexprs[i], testNodeFragment, properties)) { @@ -575,7 +586,7 @@ private static bool MatchFilterPattern( return false; case OperatorExpression { Op: FilterOperator.And, SubExpressions: var subexprs }: - for (int i = 0; i < subexprs.Length; i++) + for (int i = 0; i < subexprs.Count; i++) { if (!MatchFilterPattern(subexprs[i], testNodeFragment, properties)) { @@ -618,7 +629,7 @@ private static bool MatchProperties( return false; case OperatorExpression { Op: FilterOperator.Or, SubExpressions: var subExprs }: - for (int i = 0; i < subExprs.Length; i++) + for (int i = 0; i < subExprs.Count; i++) { if (MatchProperties(subExprs[i], properties)) { @@ -629,7 +640,7 @@ private static bool MatchProperties( return false; case OperatorExpression { Op: FilterOperator.And, SubExpressions: var subExprs }: - for (int i = 0; i < subExprs.Length; i++) + for (int i = 0; i < subExprs.Count; i++) { if (!MatchProperties(subExprs[i], properties)) { @@ -661,7 +672,7 @@ private static bool HasPropertyFilterExpression(FilterExpression expression) if (expression is OperatorExpression op) { - for (int i = 0; i < op.SubExpressions.Length; i++) + for (int i = 0; i < op.SubExpressions.Count; i++) { if (HasPropertyFilterExpression(op.SubExpressions[i])) { From 11a52441f5d0df4567d255ad2fcce01d015f8e00 Mon Sep 17 00:00:00 2001 From: Abdelghani Moussaid <20841568+abdelghani-moussaid@users.noreply.github.com> Date: Tue, 5 May 2026 23:23:46 +0100 Subject: [PATCH 3/7] fix: restore InvalidOperationException parity --- .../Requests/TreeNodeFilter/TreeNodeFilter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Platform/Microsoft.Testing.Platform/Requests/TreeNodeFilter/TreeNodeFilter.cs b/src/Platform/Microsoft.Testing.Platform/Requests/TreeNodeFilter/TreeNodeFilter.cs index dde8872e09..499ee5777f 100644 --- a/src/Platform/Microsoft.Testing.Platform/Requests/TreeNodeFilter/TreeNodeFilter.cs +++ b/src/Platform/Microsoft.Testing.Platform/Requests/TreeNodeFilter/TreeNodeFilter.cs @@ -512,7 +512,7 @@ public bool MatchesFilter(string testNodeFullPath, PropertyBag filterablePropert { if (_filters.Count == 0) { - return false; + throw new InvalidOperationException(); } // Note: The regex for ** is .*.*, so we match against such a value expression. From 61d72037ccdbd6ba2539d9a38e80c20ccd755309 Mon Sep 17 00:00:00 2001 From: Abdelghani Moussaid <20841568+abdelghani-moussaid@users.noreply.github.com> Date: Tue, 5 May 2026 23:57:58 +0100 Subject: [PATCH 4/7] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../Requests/TreeNodeFilter/TreeNodeFilter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Platform/Microsoft.Testing.Platform/Requests/TreeNodeFilter/TreeNodeFilter.cs b/src/Platform/Microsoft.Testing.Platform/Requests/TreeNodeFilter/TreeNodeFilter.cs index 499ee5777f..ee064ac051 100644 --- a/src/Platform/Microsoft.Testing.Platform/Requests/TreeNodeFilter/TreeNodeFilter.cs +++ b/src/Platform/Microsoft.Testing.Platform/Requests/TreeNodeFilter/TreeNodeFilter.cs @@ -512,7 +512,7 @@ public bool MatchesFilter(string testNodeFullPath, PropertyBag filterablePropert { if (_filters.Count == 0) { - throw new InvalidOperationException(); + throw new InvalidOperationException("Filter parsed to zero segments."); } // Note: The regex for ** is .*.*, so we match against such a value expression. From 177cf429b138f887d2134e8f957a8f775311a37d Mon Sep 17 00:00:00 2001 From: Abdelghani Moussaid <20841568+abdelghani-moussaid@users.noreply.github.com> Date: Wed, 6 May 2026 00:07:54 +0100 Subject: [PATCH 5/7] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../Requests/TreeNodeFilter/OperatorExpression.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Platform/Microsoft.Testing.Platform/Requests/TreeNodeFilter/OperatorExpression.cs b/src/Platform/Microsoft.Testing.Platform/Requests/TreeNodeFilter/OperatorExpression.cs index a4dcb8b6d9..34e61ccc11 100644 --- a/src/Platform/Microsoft.Testing.Platform/Requests/TreeNodeFilter/OperatorExpression.cs +++ b/src/Platform/Microsoft.Testing.Platform/Requests/TreeNodeFilter/OperatorExpression.cs @@ -8,5 +8,7 @@ internal sealed class OperatorExpression(FilterOperator op, IReadOnlyCollection< { public FilterOperator Op { get; } = op; - public IReadOnlyList SubExpressions { get; } = [.. subExpressions]; + public IReadOnlyList SubExpressions { get; } = subExpressions is IReadOnlyList readOnlyList + ? readOnlyList + : [.. subExpressions]; } From a32be941c12ab6296ca0b15004a017873ac3034c Mon Sep 17 00:00:00 2001 From: Abdelghani Moussaid <20841568+abdelghani-moussaid@users.noreply.github.com> Date: Wed, 6 May 2026 17:25:42 +0100 Subject: [PATCH 6/7] Optimize TreeNodeFilter allocations and improve validation - Move empty filter validation to TreeNodeFilter constructor to fail fast with a localized ArgumentException. - Optimize MatchProperties by replacing the foreach loop with a direct linked-list walk, eliminating IEnumerator boxing allocations. - Expose OperatorExpression.SubExpressions as IReadOnlyList and reuse the collection when possible to avoid array copy allocations. - Implement ReadOnlySpan fast-path for filter fragment matching with conditional #if fallback for netstandard2.0. - Resolve merge conflicts. --- .../TreeNodeFilter/OperatorExpression.cs | 7 ++-- .../Requests/TreeNodeFilter/TreeNodeFilter.cs | 36 +++++++++++++++---- .../Resources/PlatformResources.resx | 3 ++ .../Resources/xlf/PlatformResources.cs.xlf | 5 +++ .../Resources/xlf/PlatformResources.de.xlf | 5 +++ .../Resources/xlf/PlatformResources.es.xlf | 5 +++ .../Resources/xlf/PlatformResources.fr.xlf | 5 +++ .../Resources/xlf/PlatformResources.it.xlf | 5 +++ .../Resources/xlf/PlatformResources.ja.xlf | 5 +++ .../Resources/xlf/PlatformResources.ko.xlf | 5 +++ .../Resources/xlf/PlatformResources.pl.xlf | 5 +++ .../Resources/xlf/PlatformResources.pt-BR.xlf | 5 +++ .../Resources/xlf/PlatformResources.ru.xlf | 5 +++ .../Resources/xlf/PlatformResources.tr.xlf | 5 +++ .../xlf/PlatformResources.zh-Hans.xlf | 5 +++ .../xlf/PlatformResources.zh-Hant.xlf | 5 +++ 16 files changed, 101 insertions(+), 10 deletions(-) diff --git a/src/Platform/Microsoft.Testing.Platform/Requests/TreeNodeFilter/OperatorExpression.cs b/src/Platform/Microsoft.Testing.Platform/Requests/TreeNodeFilter/OperatorExpression.cs index 34e61ccc11..8f3fd2c1ba 100644 --- a/src/Platform/Microsoft.Testing.Platform/Requests/TreeNodeFilter/OperatorExpression.cs +++ b/src/Platform/Microsoft.Testing.Platform/Requests/TreeNodeFilter/OperatorExpression.cs @@ -8,7 +8,8 @@ internal sealed class OperatorExpression(FilterOperator op, IReadOnlyCollection< { public FilterOperator Op { get; } = op; - public IReadOnlyList SubExpressions { get; } = subExpressions is IReadOnlyList readOnlyList - ? readOnlyList - : [.. subExpressions]; + public IReadOnlyList SubExpressions { get; } = + subExpressions is IReadOnlyList readOnlyList + ? readOnlyList + : [.. subExpressions]; } diff --git a/src/Platform/Microsoft.Testing.Platform/Requests/TreeNodeFilter/TreeNodeFilter.cs b/src/Platform/Microsoft.Testing.Platform/Requests/TreeNodeFilter/TreeNodeFilter.cs index ee064ac051..f47992561d 100644 --- a/src/Platform/Microsoft.Testing.Platform/Requests/TreeNodeFilter/TreeNodeFilter.cs +++ b/src/Platform/Microsoft.Testing.Platform/Requests/TreeNodeFilter/TreeNodeFilter.cs @@ -26,6 +26,12 @@ internal TreeNodeFilter(string filter) { Filter = filter ?? throw new ArgumentNullException(nameof(filter)); _filters = ParseFilter(filter); + + if (_filters.Count == 0) + { + throw new ArgumentException(PlatformResources.TreeNodeFilterCannotBeEmptyErrorMessage, nameof(filter)); + } + ContainsPropertyFilters = false; for (int i = 0; i < _filters.Count; i++) { @@ -510,11 +516,6 @@ public bool MatchesFilter(string testNodeFullPath, PropertyBag filterablePropert if (currentFragmentIndex >= _filters.Count) { - if (_filters.Count == 0) - { - throw new InvalidOperationException("Filter parsed to zero segments."); - } - // Note: The regex for ** is .*.*, so we match against such a value expression. FilterExpression lastFilter = _filters[_filters.Count - 1]; if (lastFilter is ValueAndPropertyExpression valueAndPropertyExpression) @@ -555,14 +556,26 @@ private static bool MatchFilterPattern( int endFragmentIndex, PropertyBag properties) { +#if NETSTANDARD2_0 + string fragment = testNodeFullPath.Substring(startFragmentIndex, endFragmentIndex - startFragmentIndex); + return MatchFilterPattern(filterExpression, fragment, properties); +#else ReadOnlySpan fragment = testNodeFullPath.AsSpan(startFragmentIndex, endFragmentIndex - startFragmentIndex); return MatchFilterPattern(filterExpression, fragment, properties); +#endif } +#if NETSTANDARD2_0 + private static bool MatchFilterPattern( + FilterExpression filterExpression, + string testNodeFragment, + PropertyBag properties) +#else private static bool MatchFilterPattern( FilterExpression filterExpression, ReadOnlySpan testNodeFragment, PropertyBag properties) +#endif { switch (filterExpression) { @@ -618,12 +631,21 @@ private static bool MatchProperties( switch (propertyExpr) { case PropertyExpression { PropertyName: var propExpr, Value: var valueExpr }: - foreach (IProperty prop in properties) + if (properties._testNodeStateProperty is not null && + IsMatchingProperty(properties._testNodeStateProperty, propExpr, valueExpr)) { - if (IsMatchingProperty(prop, propExpr, valueExpr)) + return true; + } + + PropertyBag.Property? currentProp = properties._property; + while (currentProp is not null) + { + if (IsMatchingProperty(currentProp.Current, propExpr, valueExpr)) { return true; } + + currentProp = currentProp.Next; } return false; diff --git a/src/Platform/Microsoft.Testing.Platform/Resources/PlatformResources.resx b/src/Platform/Microsoft.Testing.Platform/Resources/PlatformResources.resx index 3a423b2c31..e4b3b3dc68 100644 --- a/src/Platform/Microsoft.Testing.Platform/Resources/PlatformResources.resx +++ b/src/Platform/Microsoft.Testing.Platform/Resources/PlatformResources.resx @@ -192,6 +192,9 @@ Test adapter test session failure + + The filter parsed to zero segments and cannot be empty. + A filter '{0}' should not contain a '/' character diff --git a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.cs.xlf b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.cs.xlf index acfebfe360..6e70c65525 100644 --- a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.cs.xlf +++ b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.cs.xlf @@ -934,6 +934,11 @@ Platné hodnoty jsou All, Failed, None. Výchozí hodnota je All. Celkem + + The filter parsed to zero segments and cannot be empty. + The filter parsed to zero segments and cannot be empty. + + A filter '{0}' should not contain a '/' character Filtr {0} nesmí obsahovat znak /. diff --git a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.de.xlf b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.de.xlf index 380dedbbef..c15e03d8da 100644 --- a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.de.xlf +++ b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.de.xlf @@ -934,6 +934,11 @@ Gültige Werte sind „Alle“, „Fehlgeschlagen“ und „Keine“. Der Standa Gesamt + + The filter parsed to zero segments and cannot be empty. + The filter parsed to zero segments and cannot be empty. + + A filter '{0}' should not contain a '/' character Ein Filter "{0}" darf kein "/"-Zeichen enthalten diff --git a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.es.xlf b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.es.xlf index d0716d0420..d8481120cb 100644 --- a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.es.xlf +++ b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.es.xlf @@ -934,6 +934,11 @@ Los valores válidos son "All", "Failed", "None". El valor predeterminado es "Al Total + + The filter parsed to zero segments and cannot be empty. + The filter parsed to zero segments and cannot be empty. + + A filter '{0}' should not contain a '/' character Un filtro "{0}" no debe contener un carácter "/". diff --git a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.fr.xlf b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.fr.xlf index 3db0991d7d..4a909cb89a 100644 --- a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.fr.xlf +++ b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.fr.xlf @@ -934,6 +934,11 @@ Les valeurs valides sont « All », « Failed » et « None ». La valeur Total + + The filter parsed to zero segments and cannot be empty. + The filter parsed to zero segments and cannot be empty. + + A filter '{0}' should not contain a '/' character Un filtre « {0} » ne doit pas contenir de caractère '/' diff --git a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.it.xlf b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.it.xlf index 362b6d78c5..42c817d3d9 100644 --- a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.it.xlf +++ b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.it.xlf @@ -934,6 +934,11 @@ I valori validi sono 'All', 'Failed', 'None'. L'impostazione predefinita è 'All Totale + + The filter parsed to zero segments and cannot be empty. + The filter parsed to zero segments and cannot be empty. + + A filter '{0}' should not contain a '/' character Un filtro '{0}' non dovrebbe contenere un carattere '/' diff --git a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.ja.xlf b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.ja.xlf index edb8293ce1..2b46da32c2 100644 --- a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.ja.xlf +++ b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.ja.xlf @@ -935,6 +935,11 @@ Valid values are 'All', 'Failed', 'None'. Default is 'All'. 合計 + + The filter parsed to zero segments and cannot be empty. + The filter parsed to zero segments and cannot be empty. + + A filter '{0}' should not contain a '/' character フィルター '{0}' に '/' 文字を含めることはできません diff --git a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.ko.xlf b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.ko.xlf index f1bc6bad67..4dec751ee0 100644 --- a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.ko.xlf +++ b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.ko.xlf @@ -934,6 +934,11 @@ Valid values are 'All', 'Failed', 'None'. Default is 'All'. 합계 + + The filter parsed to zero segments and cannot be empty. + The filter parsed to zero segments and cannot be empty. + + A filter '{0}' should not contain a '/' character '{0}' 필터는 '/' 문자를 포함하면 안 됨 diff --git a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.pl.xlf b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.pl.xlf index 6770601624..d73d267f61 100644 --- a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.pl.xlf +++ b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.pl.xlf @@ -934,6 +934,11 @@ Prawidłowe wartości to „All”, „Failed”, „None”. Wartość domyśln Łącznie + + The filter parsed to zero segments and cannot be empty. + The filter parsed to zero segments and cannot be empty. + + A filter '{0}' should not contain a '/' character Filtr „{0}” nie powinien zawierać znaku „/” diff --git a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.pt-BR.xlf b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.pt-BR.xlf index efed783190..cad4e7d0f0 100644 --- a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.pt-BR.xlf +++ b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.pt-BR.xlf @@ -934,6 +934,11 @@ Os valores válidos são 'All', 'Failed', 'None'. O padrão é 'All'. Total + + The filter parsed to zero segments and cannot be empty. + The filter parsed to zero segments and cannot be empty. + + A filter '{0}' should not contain a '/' character Um filtro “{0}” não deve conter um caractere '/' diff --git a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.ru.xlf b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.ru.xlf index 5b084435a5..fd1b9a290f 100644 --- a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.ru.xlf +++ b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.ru.xlf @@ -934,6 +934,11 @@ Valid values are 'All', 'Failed', 'None'. Default is 'All'. Всего + + The filter parsed to zero segments and cannot be empty. + The filter parsed to zero segments and cannot be empty. + + A filter '{0}' should not contain a '/' character Фильтр "{0}" не должен содержать символ "/" diff --git a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.tr.xlf b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.tr.xlf index 178258a8a7..a5396c8041 100644 --- a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.tr.xlf +++ b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.tr.xlf @@ -934,6 +934,11 @@ Geçerli değerler: 'Tümü', 'Başarısız', 'Yok'. Varsayılan değer: 'Tümü Toplam + + The filter parsed to zero segments and cannot be empty. + The filter parsed to zero segments and cannot be empty. + + A filter '{0}' should not contain a '/' character '{0}' filtresi, '/' karakteri içermemelidir diff --git a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.zh-Hans.xlf b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.zh-Hans.xlf index 6ada95bb40..9c8e0c2984 100644 --- a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.zh-Hans.xlf +++ b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.zh-Hans.xlf @@ -934,6 +934,11 @@ Valid values are 'All', 'Failed', 'None'. Default is 'All'. 总计 + + The filter parsed to zero segments and cannot be empty. + The filter parsed to zero segments and cannot be empty. + + A filter '{0}' should not contain a '/' character 筛选“{0}”不应包含“/”字符 diff --git a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.zh-Hant.xlf b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.zh-Hant.xlf index a0eecd0d78..06ea1f654b 100644 --- a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.zh-Hant.xlf +++ b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.zh-Hant.xlf @@ -934,6 +934,11 @@ Valid values are 'All', 'Failed', 'None'. Default is 'All'. 總計 + + The filter parsed to zero segments and cannot be empty. + The filter parsed to zero segments and cannot be empty. + + A filter '{0}' should not contain a '/' character 篩選條件 '{0}' 不應包含 '/' 字元 From ee4a617902e39aad5fc4e627c4998112c6740713 Mon Sep 17 00:00:00 2001 From: Abdelghani Moussaid <20841568+abdelghani-moussaid@users.noreply.github.com> Date: Thu, 7 May 2026 16:21:12 +0100 Subject: [PATCH 7/7] Clean up TreeNodeFilter and add unit tests - Remove Dead Code: Removed the unreachable _testNodeStateProperty branch in TreeNodeFilter.MatchProperties. - Add Unit Test: Added EmptyFilter_Invalid to TreeNodeFilterTests.cs to verify that passing an empty string to TreeNodeFilter correctly throws an ArgumentException. --- .../Requests/TreeNodeFilter/TreeNodeFilter.cs | 6 ------ .../Requests/TreeNodeFilterTests.cs | 3 +++ 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/Platform/Microsoft.Testing.Platform/Requests/TreeNodeFilter/TreeNodeFilter.cs b/src/Platform/Microsoft.Testing.Platform/Requests/TreeNodeFilter/TreeNodeFilter.cs index f47992561d..4b37c97372 100644 --- a/src/Platform/Microsoft.Testing.Platform/Requests/TreeNodeFilter/TreeNodeFilter.cs +++ b/src/Platform/Microsoft.Testing.Platform/Requests/TreeNodeFilter/TreeNodeFilter.cs @@ -631,12 +631,6 @@ private static bool MatchProperties( switch (propertyExpr) { case PropertyExpression { PropertyName: var propExpr, Value: var valueExpr }: - if (properties._testNodeStateProperty is not null && - IsMatchingProperty(properties._testNodeStateProperty, propExpr, valueExpr)) - { - return true; - } - PropertyBag.Property? currentProp = properties._property; while (currentProp is not null) { diff --git a/test/UnitTests/Microsoft.Testing.Platform.UnitTests/Requests/TreeNodeFilterTests.cs b/test/UnitTests/Microsoft.Testing.Platform.UnitTests/Requests/TreeNodeFilterTests.cs index 78f7406778..ae385f9abf 100644 --- a/test/UnitTests/Microsoft.Testing.Platform.UnitTests/Requests/TreeNodeFilterTests.cs +++ b/test/UnitTests/Microsoft.Testing.Platform.UnitTests/Requests/TreeNodeFilterTests.cs @@ -31,6 +31,9 @@ public void MatchAllFilter_MatchesSubpaths() [TestMethod] public void MatchAllFilter_DoNotAllowInMiddleOfFilter() => Assert.ThrowsExactly(() => _ = new TreeNodeFilter("/**/Path")); + [TestMethod] + public void EmptyFilter_Invalid() => Assert.ThrowsExactly(() => _ = new TreeNodeFilter(string.Empty)); + [TestMethod] public void MatchWildcard_MatchesSubstrings() {