diff --git a/src/Platform/Microsoft.Testing.Platform/Requests/TreeNodeFilter/OperatorExpression.cs b/src/Platform/Microsoft.Testing.Platform/Requests/TreeNodeFilter/OperatorExpression.cs index cf5409e595..8f3fd2c1ba 100644 --- a/src/Platform/Microsoft.Testing.Platform/Requests/TreeNodeFilter/OperatorExpression.cs +++ b/src/Platform/Microsoft.Testing.Platform/Requests/TreeNodeFilter/OperatorExpression.cs @@ -8,5 +8,8 @@ internal sealed class OperatorExpression(FilterOperator op, IReadOnlyCollection< { public FilterOperator Op { get; } = op; - public IReadOnlyCollection SubExpressions { get; } = 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 da7178a629..4b37c97372 100644 --- a/src/Platform/Microsoft.Testing.Platform/Requests/TreeNodeFilter/TreeNodeFilter.cs +++ b/src/Platform/Microsoft.Testing.Platform/Requests/TreeNodeFilter/TreeNodeFilter.cs @@ -26,7 +26,21 @@ internal TreeNodeFilter(string filter) { Filter = filter ?? throw new ArgumentNullException(nameof(filter)); _filters = ParseFilter(filter); - ContainsPropertyFilters = _filters.Any(HasPropertyFilterExpression); + + if (_filters.Count == 0) + { + throw new ArgumentException(PlatformResources.TreeNodeFilterCannotBeEmptyErrorMessage, nameof(filter)); + } + + ContainsPropertyFilters = false; + for (int i = 0; i < _filters.Count; i++) + { + if (HasPropertyFilterExpression(_filters[i])) + { + ContainsPropertyFilters = true; + break; + } + } } /// @@ -503,7 +517,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 +556,123 @@ private static bool MatchFilterPattern( int endFragmentIndex, PropertyBag properties) { - string str = testNodeFullPath[startFragmentIndex..endFragmentIndex]; - return MatchFilterPattern(filterExpression, str, 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) - => filterExpression switch +#else + private static bool MatchFilterPattern( + FilterExpression filterExpression, + ReadOnlySpan testNodeFragment, + PropertyBag properties) +#endif + { + 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: +#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.Count; 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.Count; 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 }: + PropertyBag.Property? currentProp = properties._property; + while (currentProp is not null) + { + if (IsMatchingProperty(currentProp.Current, propExpr, valueExpr)) + { + return true; + } + + currentProp = currentProp.Next; + } + + return false; + + case OperatorExpression { Op: FilterOperator.Or, SubExpressions: var subExprs }: + for (int i = 0; i < subExprs.Count; i++) + { + if (MatchProperties(subExprs[i], properties)) + { + return true; + } + } + + return false; + + case OperatorExpression { Op: FilterOperator.And, SubExpressions: var subExprs }: + for (int i = 0; i < subExprs.Count; 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 +680,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.Count; i++) + { + if (HasPropertyFilterExpression(op.SubExpressions[i])) + { + return true; + } + } + } + + return false; + } } diff --git a/src/Platform/Microsoft.Testing.Platform/Resources/PlatformResources.resx b/src/Platform/Microsoft.Testing.Platform/Resources/PlatformResources.resx index 2b1b96918f..aba899e904 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 eb50d2d177..b2875275b3 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 95a59bc488..b0388b2b0f 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 7498efda2d..50855c9f3a 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 84f1baa049..5d346c968e 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 7dcc50f3cc..9997cec659 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 fd6c736980..92f347750f 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 ddbc83c020..e51915645b 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 3f4afe566f..b0fcd5fb99 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 6e7cd587d1..29677a454b 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 e6e8098163..57a67e909f 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 2a70594081..8b9bd17c3b 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 51121cef90..0ef63663e1 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 266c6d62bb..4426cf4e4b 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}' 不應包含 '/' 字元 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() {