Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,8 @@ internal sealed class OperatorExpression(FilterOperator op, IReadOnlyCollection<
{
public FilterOperator Op { get; } = op;

public IReadOnlyCollection<FilterExpression> SubExpressions { get; } = subExpressions;
public IReadOnlyList<FilterExpression> SubExpressions { get; } =
subExpressions is IReadOnlyList<FilterExpression> readOnlyList
? readOnlyList
: [.. subExpressions];
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
}

/// <summary>
Expand Down Expand Up @@ -503,7 +517,7 @@ public bool MatchesFilter(string testNodeFullPath, PropertyBag filterablePropert
if (currentFragmentIndex >= _filters.Count)
{
Comment thread
abdelghani-moussaid marked this conversation as resolved.
// 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;
Expand Down Expand Up @@ -542,52 +556,147 @@ 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<char> 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<char> 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);
Comment thread
abdelghani-moussaid marked this conversation as resolved.
#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))
{
Comment thread
abdelghani-moussaid marked this conversation as resolved.
return true;
}

currentProp = currentProp.Next;
}
Comment thread
abdelghani-moussaid marked this conversation as resolved.

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 &&
propExpr.Regex.IsMatch(testMetadataProperty.Key) &&
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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,9 @@
<data name="TestHostAdapterInvokerFailedTestSessionErrorMessage" xml:space="preserve">
<value>Test adapter test session failure</value>
</data>
<data name="TreeNodeFilterCannotBeEmptyErrorMessage" xml:space="preserve">
<value>The filter parsed to zero segments and cannot be empty.</value>
</data>
<data name="TreeNodeFilterCannotContainSlashCharacterErrorMessage" xml:space="preserve">
<value>A filter '{0}' should not contain a '/' character</value>
</data>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -934,6 +934,11 @@ Platné hodnoty jsou All, Failed, None. Výchozí hodnota je All.</target>
<target state="translated">Celkem</target>
<note />
</trans-unit>
<trans-unit id="TreeNodeFilterCannotBeEmptyErrorMessage">
<source>The filter parsed to zero segments and cannot be empty.</source>
<target state="new">The filter parsed to zero segments and cannot be empty.</target>
<note />
</trans-unit>
<trans-unit id="TreeNodeFilterCannotContainSlashCharacterErrorMessage">
<source>A filter '{0}' should not contain a '/' character</source>
<target state="translated">Filtr {0} nesmí obsahovat znak /.</target>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -934,6 +934,11 @@ Gültige Werte sind „Alle“, „Fehlgeschlagen“ und „Keine“. Der Standa
<target state="translated">Gesamt</target>
<note />
</trans-unit>
<trans-unit id="TreeNodeFilterCannotBeEmptyErrorMessage">
<source>The filter parsed to zero segments and cannot be empty.</source>
<target state="new">The filter parsed to zero segments and cannot be empty.</target>
<note />
</trans-unit>
<trans-unit id="TreeNodeFilterCannotContainSlashCharacterErrorMessage">
<source>A filter '{0}' should not contain a '/' character</source>
<target state="translated">Ein Filter "{0}" darf kein "/"-Zeichen enthalten</target>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -934,6 +934,11 @@ Los valores válidos son "All", "Failed", "None". El valor predeterminado es "Al
<target state="translated">Total</target>
<note />
</trans-unit>
<trans-unit id="TreeNodeFilterCannotBeEmptyErrorMessage">
<source>The filter parsed to zero segments and cannot be empty.</source>
<target state="new">The filter parsed to zero segments and cannot be empty.</target>
<note />
</trans-unit>
<trans-unit id="TreeNodeFilterCannotContainSlashCharacterErrorMessage">
<source>A filter '{0}' should not contain a '/' character</source>
<target state="translated">Un filtro "{0}" no debe contener un carácter "/".</target>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -934,6 +934,11 @@ Les valeurs valides sont « All », « Failed » et « None ». La valeur
<target state="translated">Total</target>
<note />
</trans-unit>
<trans-unit id="TreeNodeFilterCannotBeEmptyErrorMessage">
<source>The filter parsed to zero segments and cannot be empty.</source>
<target state="new">The filter parsed to zero segments and cannot be empty.</target>
<note />
</trans-unit>
<trans-unit id="TreeNodeFilterCannotContainSlashCharacterErrorMessage">
<source>A filter '{0}' should not contain a '/' character</source>
<target state="translated">Un filtre « {0} » ne doit pas contenir de caractère '/'</target>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -934,6 +934,11 @@ I valori validi sono 'All', 'Failed', 'None'. L'impostazione predefinita è 'All
<target state="translated">Totale</target>
<note />
</trans-unit>
<trans-unit id="TreeNodeFilterCannotBeEmptyErrorMessage">
<source>The filter parsed to zero segments and cannot be empty.</source>
<target state="new">The filter parsed to zero segments and cannot be empty.</target>
<note />
</trans-unit>
<trans-unit id="TreeNodeFilterCannotContainSlashCharacterErrorMessage">
<source>A filter '{0}' should not contain a '/' character</source>
<target state="translated">Un filtro '{0}' non dovrebbe contenere un carattere '/'</target>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -935,6 +935,11 @@ Valid values are 'All', 'Failed', 'None'. Default is 'All'.</source>
<target state="translated">合計</target>
<note />
</trans-unit>
<trans-unit id="TreeNodeFilterCannotBeEmptyErrorMessage">
<source>The filter parsed to zero segments and cannot be empty.</source>
<target state="new">The filter parsed to zero segments and cannot be empty.</target>
<note />
</trans-unit>
<trans-unit id="TreeNodeFilterCannotContainSlashCharacterErrorMessage">
<source>A filter '{0}' should not contain a '/' character</source>
<target state="translated">フィルター '{0}' に '/' 文字を含めることはできません</target>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -934,6 +934,11 @@ Valid values are 'All', 'Failed', 'None'. Default is 'All'.</source>
<target state="translated">합계</target>
<note />
</trans-unit>
<trans-unit id="TreeNodeFilterCannotBeEmptyErrorMessage">
<source>The filter parsed to zero segments and cannot be empty.</source>
<target state="new">The filter parsed to zero segments and cannot be empty.</target>
<note />
</trans-unit>
<trans-unit id="TreeNodeFilterCannotContainSlashCharacterErrorMessage">
<source>A filter '{0}' should not contain a '/' character</source>
<target state="translated">'{0}' 필터는 '/' 문자를 포함하면 안 됨</target>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -934,6 +934,11 @@ Prawidłowe wartości to „All”, „Failed”, „None”. Wartość domyśln
<target state="translated">Łącznie</target>
<note />
</trans-unit>
<trans-unit id="TreeNodeFilterCannotBeEmptyErrorMessage">
<source>The filter parsed to zero segments and cannot be empty.</source>
<target state="new">The filter parsed to zero segments and cannot be empty.</target>
<note />
</trans-unit>
<trans-unit id="TreeNodeFilterCannotContainSlashCharacterErrorMessage">
<source>A filter '{0}' should not contain a '/' character</source>
<target state="translated">Filtr „{0}” nie powinien zawierać znaku „/”</target>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -934,6 +934,11 @@ Os valores válidos são 'All', 'Failed', 'None'. O padrão é 'All'.</target>
<target state="translated">Total</target>
<note />
</trans-unit>
<trans-unit id="TreeNodeFilterCannotBeEmptyErrorMessage">
<source>The filter parsed to zero segments and cannot be empty.</source>
<target state="new">The filter parsed to zero segments and cannot be empty.</target>
<note />
</trans-unit>
<trans-unit id="TreeNodeFilterCannotContainSlashCharacterErrorMessage">
<source>A filter '{0}' should not contain a '/' character</source>
<target state="translated">Um filtro “{0}” não deve conter um caractere '/'</target>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -934,6 +934,11 @@ Valid values are 'All', 'Failed', 'None'. Default is 'All'.</source>
<target state="translated">Всего</target>
<note />
</trans-unit>
<trans-unit id="TreeNodeFilterCannotBeEmptyErrorMessage">
<source>The filter parsed to zero segments and cannot be empty.</source>
<target state="new">The filter parsed to zero segments and cannot be empty.</target>
<note />
</trans-unit>
<trans-unit id="TreeNodeFilterCannotContainSlashCharacterErrorMessage">
<source>A filter '{0}' should not contain a '/' character</source>
<target state="translated">Фильтр "{0}" не должен содержать символ "/"</target>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -934,6 +934,11 @@ Geçerli değerler: 'Tümü', 'Başarısız', 'Yok'. Varsayılan değer: 'Tümü
<target state="translated">Toplam</target>
<note />
</trans-unit>
<trans-unit id="TreeNodeFilterCannotBeEmptyErrorMessage">
<source>The filter parsed to zero segments and cannot be empty.</source>
<target state="new">The filter parsed to zero segments and cannot be empty.</target>
<note />
</trans-unit>
<trans-unit id="TreeNodeFilterCannotContainSlashCharacterErrorMessage">
<source>A filter '{0}' should not contain a '/' character</source>
<target state="translated">'{0}' filtresi, '/' karakteri içermemelidir</target>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -934,6 +934,11 @@ Valid values are 'All', 'Failed', 'None'. Default is 'All'.</source>
<target state="translated">总计</target>
<note />
</trans-unit>
<trans-unit id="TreeNodeFilterCannotBeEmptyErrorMessage">
<source>The filter parsed to zero segments and cannot be empty.</source>
<target state="new">The filter parsed to zero segments and cannot be empty.</target>
<note />
</trans-unit>
<trans-unit id="TreeNodeFilterCannotContainSlashCharacterErrorMessage">
<source>A filter '{0}' should not contain a '/' character</source>
<target state="translated">筛选“{0}”不应包含“/”字符</target>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -934,6 +934,11 @@ Valid values are 'All', 'Failed', 'None'. Default is 'All'.</source>
<target state="translated">總計</target>
<note />
</trans-unit>
<trans-unit id="TreeNodeFilterCannotBeEmptyErrorMessage">
<source>The filter parsed to zero segments and cannot be empty.</source>
<target state="new">The filter parsed to zero segments and cannot be empty.</target>
<note />
</trans-unit>
<trans-unit id="TreeNodeFilterCannotContainSlashCharacterErrorMessage">
<source>A filter '{0}' should not contain a '/' character</source>
<target state="translated">篩選條件 '{0}' 不應包含 '/' 字元</target>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ public void MatchAllFilter_MatchesSubpaths()
[TestMethod]
public void MatchAllFilter_DoNotAllowInMiddleOfFilter() => Assert.ThrowsExactly<ArgumentException>(() => _ = new TreeNodeFilter("/**/Path"));

[TestMethod]
public void EmptyFilter_Invalid() => Assert.ThrowsExactly<ArgumentException>(() => _ = new TreeNodeFilter(string.Empty));

[TestMethod]
public void MatchWildcard_MatchesSubstrings()
{
Expand Down