diff --git a/Directory.Build.targets b/Directory.Build.targets
index f0c700ee2e..8311c1a9cf 100644
--- a/Directory.Build.targets
+++ b/Directory.Build.targets
@@ -4,7 +4,11 @@
<_NetFrameworkHostedCompilersVersion Condition="'$(_NetFrameworkHostedCompilersVersion)' == ''">4.11.0-3.24280.3
-
+
+
+ $(DefineConstants);IS_VSTEST_REPO
+
+
@@ -22,7 +26,7 @@
-
+
None
diff --git a/TestPlatform.slnx b/TestPlatform.slnx
index d7036c6a12..11d17f9705 100644
--- a/TestPlatform.slnx
+++ b/TestPlatform.slnx
@@ -75,6 +75,7 @@
+
@@ -115,6 +116,7 @@
+
diff --git a/eng/common/tools.sh b/eng/common/tools.sh
old mode 100644
new mode 100755
diff --git a/eng/verify-nupkgs.ps1 b/eng/verify-nupkgs.ps1
index c6417b4f10..087ee04c2f 100644
--- a/eng/verify-nupkgs.ps1
+++ b/eng/verify-nupkgs.ps1
@@ -30,6 +30,7 @@ function Verify-Nuget-Packages {
"Microsoft.TestPlatform.TestHost" = 64
"Microsoft.TestPlatform.TranslationLayer" = 175
"Microsoft.TestPlatform.Internal.Uwp" = 39
+ "Microsoft.TestPlatform.Filter.Source" = 13
}
$packageDirectory = Resolve-Path "$PSScriptRoot/../artifacts/packages/$configuration"
diff --git a/src/Microsoft.TestPlatform.Common/Microsoft.TestPlatform.Common.csproj b/src/Microsoft.TestPlatform.Common/Microsoft.TestPlatform.Common.csproj
index d117107a34..b40ed6c227 100644
--- a/src/Microsoft.TestPlatform.Common/Microsoft.TestPlatform.Common.csproj
+++ b/src/Microsoft.TestPlatform.Common/Microsoft.TestPlatform.Common.csproj
@@ -24,6 +24,13 @@
true
+
+
+
+
+
+
+
True
diff --git a/src/Microsoft.TestPlatform.Common/Filtering/Condition.cs b/src/Microsoft.TestPlatform.Filter.Source/Condition.cs
similarity index 87%
rename from src/Microsoft.TestPlatform.Common/Filtering/Condition.cs
rename to src/Microsoft.TestPlatform.Filter.Source/Condition.cs
index b0477c6022..f931d9a0c5 100644
--- a/src/Microsoft.TestPlatform.Common/Filtering/Condition.cs
+++ b/src/Microsoft.TestPlatform.Filter.Source/Condition.cs
@@ -1,20 +1,36 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+#nullable enable
+
using System;
using System.Collections.Generic;
-using System.Diagnostics.CodeAnalysis;
using System.Globalization;
+#if IS_VSTEST_REPO
+using System.Diagnostics.CodeAnalysis;
+#endif
using System.Linq;
using System.Text;
+#if !IS_VSTEST_REPO
+using Microsoft.CodeAnalysis;
+#endif
+
+#if IS_VSTEST_REPO
using Microsoft.VisualStudio.TestPlatform.ObjectModel;
+#endif
+
using Microsoft.VisualStudio.TestPlatform.ObjectModel.Utilities;
-using CommonResources = Microsoft.VisualStudio.TestPlatform.Common.Resources.Resources;
+#if IS_VSTEST_REPO
+using static Microsoft.VisualStudio.TestPlatform.Common.Resources.Resources;
+#endif
namespace Microsoft.VisualStudio.TestPlatform.Common.Filtering;
+#if !IS_VSTEST_REPO
+[Embedded]
+#endif
internal enum Operation
{
Equal,
@@ -28,6 +44,9 @@ internal enum Operation
/// Precedence(And) > Precedence(Or)
/// Precedence of OpenBrace and CloseBrace operators is not used, instead parsing code takes care of same.
///
+#if !IS_VSTEST_REPO
+[Embedded]
+#endif
internal enum Operator
{
None,
@@ -40,6 +59,9 @@ internal enum Operator
///
/// Represents a condition in filter expression.
///
+#if !IS_VSTEST_REPO
+[Embedded]
+#endif
internal sealed class Condition
{
///
@@ -52,6 +74,14 @@ internal sealed class Condition
///
public const Operation DefaultOperation = Operation.Contains;
+#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.";
+
+ private const string InvalidCondition = "Error: Invalid Condition '{0}'";
+
+ private const string InvalidOperator = "Error: Invalid operator '{0}'";
+#endif
+
internal Condition(string name, Operation operation, string value)
{
Name = name;
@@ -97,8 +127,10 @@ private bool EvaluateContainsOperation(string[]? multiValue)
{
foreach (string propertyValue in multiValue)
{
+#if IS_VSTEST_REPO
TPDebug.Assert(null != propertyValue, "PropertyValue can not be null.");
- if (propertyValue.IndexOf(Value, StringComparison.OrdinalIgnoreCase) != -1)
+#endif
+ if (propertyValue!.IndexOf(Value, StringComparison.OrdinalIgnoreCase) != -1)
{
return true;
}
@@ -113,7 +145,9 @@ private bool EvaluateContainsOperation(string[]? multiValue)
///
internal bool Evaluate(Func propertyValueProvider)
{
+#if IS_VSTEST_REPO
ValidateArg.NotNull(propertyValueProvider, nameof(propertyValueProvider));
+#endif
var multiValue = GetPropertyValue(propertyValueProvider);
var result = Operation switch
{
@@ -136,17 +170,21 @@ internal bool Evaluate(Func propertyValueProvider)
///
internal static Condition Parse(string? conditionString)
{
+#if IS_VSTEST_REPO
if (conditionString.IsNullOrWhiteSpace())
+#else
+ if (string.IsNullOrWhiteSpace(conditionString))
+#endif
{
ThrownFormatExceptionForInvalidCondition(conditionString);
}
- var parts = TokenizeFilterConditionString(conditionString).ToArray();
+ var parts = TokenizeFilterConditionString(conditionString!).ToArray();
if (parts.Length == 1)
{
// If only parameter values is passed, create condition with default property name,
// default operation and given condition string as parameter value.
- return new Condition(DefaultPropertyName, DefaultOperation, FilterHelper.Unescape(conditionString.Trim()));
+ return new Condition(DefaultPropertyName, DefaultOperation, FilterHelper.Unescape(conditionString!.Trim()));
}
if (parts.Length != 3)
@@ -156,7 +194,11 @@ internal static Condition Parse(string? conditionString)
for (int index = 0; index < 3; index++)
{
+#if IS_VSTEST_REPO
if (parts[index].IsNullOrWhiteSpace())
+#else
+ if (string.IsNullOrWhiteSpace(parts[index]))
+#endif
{
ThrownFormatExceptionForInvalidCondition(conditionString);
}
@@ -168,17 +210,23 @@ internal static Condition Parse(string? conditionString)
return condition;
}
+#if IS_VSTEST_REPO
[DoesNotReturn]
+#endif
private static void ThrownFormatExceptionForInvalidCondition(string? conditionString)
{
- throw new FormatException(string.Format(CultureInfo.CurrentCulture, CommonResources.TestCaseFilterFormatException,
- string.Format(CultureInfo.CurrentCulture, CommonResources.InvalidCondition, conditionString)));
+ throw new FormatException(string.Format(CultureInfo.CurrentCulture, TestCaseFilterFormatException,
+ string.Format(CultureInfo.CurrentCulture, InvalidCondition, conditionString)));
}
///
/// Check if condition validates any property in properties.
///
+#if IS_VSTEST_REPO
internal bool ValidForProperties(IEnumerable properties, Func? propertyProvider)
+#else
+ internal bool ValidForProperties(IEnumerable properties)
+#endif
{
bool valid = false;
@@ -186,15 +234,18 @@ internal bool ValidForProperties(IEnumerable properties, Func? propertyProvider)
{
bool valid = true;
@@ -215,6 +266,7 @@ private bool ValidForContainsOperation(Func? propertyProv
}
return valid;
}
+#endif
///
/// Return Operation corresponding to the operationString
@@ -227,7 +279,7 @@ private static Operation GetOperator(string operationString)
"!=" => Operation.NotEqual,
"~" => Operation.Contains,
"!~" => Operation.NotContains,
- _ => throw new FormatException(string.Format(CultureInfo.CurrentCulture, CommonResources.TestCaseFilterFormatException, string.Format(CultureInfo.CurrentCulture, CommonResources.InvalidOperator, operationString))),
+ _ => throw new FormatException(string.Format(CultureInfo.CurrentCulture, TestCaseFilterFormatException, string.Format(CultureInfo.CurrentCulture, InvalidOperator, operationString))),
};
}
diff --git a/src/Microsoft.TestPlatform.Filter.Source/EmbeddedAttribute.cs b/src/Microsoft.TestPlatform.Filter.Source/EmbeddedAttribute.cs
new file mode 100644
index 0000000000..c142b46bac
--- /dev/null
+++ b/src/Microsoft.TestPlatform.Filter.Source/EmbeddedAttribute.cs
@@ -0,0 +1,8 @@
+//
+#nullable enable
+namespace Microsoft.CodeAnalysis
+{
+ internal sealed partial class EmbeddedAttribute : global::System.Attribute
+ {
+ }
+}
diff --git a/src/Microsoft.TestPlatform.Common/Filtering/FastFilter.cs b/src/Microsoft.TestPlatform.Filter.Source/FastFilter.cs
similarity index 85%
rename from src/Microsoft.TestPlatform.Common/Filtering/FastFilter.cs
rename to src/Microsoft.TestPlatform.Filter.Source/FastFilter.cs
index 1566671d36..379f3addf4 100644
--- a/src/Microsoft.TestPlatform.Common/Filtering/FastFilter.cs
+++ b/src/Microsoft.TestPlatform.Filter.Source/FastFilter.cs
@@ -1,22 +1,40 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+#nullable enable
+
using System;
using System.Collections.Generic;
+#if IS_VSTEST_REPO
using System.Collections.Immutable;
-using System.Globalization;
+#endif
using System.Linq;
using System.Text.RegularExpressions;
+#if !IS_VSTEST_REPO
+using Microsoft.CodeAnalysis;
+#endif
+
+#if IS_VSTEST_REPO
using Microsoft.VisualStudio.TestPlatform.ObjectModel;
+#endif
namespace Microsoft.VisualStudio.TestPlatform.Common.Filtering;
+#if !IS_VSTEST_REPO
+[Embedded]
+#endif
internal sealed class FastFilter
{
+#if IS_VSTEST_REPO
internal FastFilter(ImmutableDictionary> filterProperties, Operation filterOperation, Operator filterOperator)
+#else
+ internal FastFilter(Dictionary> filterProperties, Operation filterOperation, Operator filterOperator)
+#endif
{
+#if IS_VSTEST_REPO
ValidateArg.NotNullOrEmpty(filterProperties, nameof(filterProperties));
+#endif
FilterProperties = filterProperties;
@@ -24,10 +42,18 @@ internal FastFilter(ImmutableDictionary> filterProperties,
(filterOperation != Operation.Equal || filterOperator != Operator.Or && filterOperator != Operator.None)
&& (filterOperation == Operation.NotEqual && (filterOperator == Operator.And || filterOperator == Operator.None)
? true
- : throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Resources.Resources.FastFilterException)));
+#if IS_VSTEST_REPO
+ : throw new ArgumentException(Resources.Resources.FastFilterException));
+#else
+ : throw new ArgumentException("An error occurred while creating Fast filter."));
+#endif
}
+#if IS_VSTEST_REPO
internal ImmutableDictionary> FilterProperties { get; }
+#else
+ internal Dictionary> FilterProperties { get; }
+#endif
internal bool IsFilteredOutWhenMatched { get; }
@@ -49,7 +75,9 @@ internal FastFilter(ImmutableDictionary> filterProperties,
internal bool Evaluate(Func propertyValueProvider)
{
+#if IS_VSTEST_REPO
ValidateArg.NotNull(propertyValueProvider, nameof(propertyValueProvider));
+#endif
bool matched = false;
foreach (var name in FilterProperties.Keys)
@@ -86,12 +114,14 @@ internal bool Evaluate(Func propertyValueProvider)
/// For matching, returns the result of matching, null if no match found. For replacement, returns the result of replacement.
private string? ApplyRegex(string value)
{
+#if IS_VSTEST_REPO
TPDebug.Assert(PropertyValueRegex != null);
+#endif
string? result = null;
if (PropertyValueRegexReplacement == null)
{
- var match = PropertyValueRegex.Match(value);
+ var match = PropertyValueRegex!.Match(value);
if (match.Success)
{
result = match.Value;
@@ -99,7 +129,7 @@ internal bool Evaluate(Func propertyValueProvider)
}
else
{
- result = PropertyValueRegex.Replace(value, PropertyValueRegexReplacement);
+ result = PropertyValueRegex!.Replace(value, PropertyValueRegexReplacement);
}
return result;
}
@@ -134,7 +164,12 @@ internal sealed class Builder
private bool _conditionEncountered;
private Operation _fastFilterOperation;
+
+#if IS_VSTEST_REPO
private readonly ImmutableDictionary.Builder>.Builder _filterDictionaryBuilder = ImmutableDictionary.CreateBuilder.Builder>(StringComparer.OrdinalIgnoreCase);
+#else
+ private readonly Dictionary> _filterDictionaryBuilder = new Dictionary>(StringComparer.OrdinalIgnoreCase);
+#endif
private bool _containsValidFilter = true;
@@ -201,7 +236,11 @@ private void AddProperty(string name, string value)
{
if (!_filterDictionaryBuilder.TryGetValue(name, out var values))
{
+#if IS_VSTEST_REPO
values = ImmutableHashSet.CreateBuilder(StringComparer.OrdinalIgnoreCase);
+#else
+ values = new HashSet(StringComparer.OrdinalIgnoreCase);
+#endif
_filterDictionaryBuilder.Add(name, values);
}
@@ -212,7 +251,11 @@ private void AddProperty(string name, string value)
{
return ContainsValidFilter
? new FastFilter(
+#if IS_VSTEST_REPO
_filterDictionaryBuilder.ToImmutableDictionary(kvp => kvp.Key, kvp => (ISet)_filterDictionaryBuilder[kvp.Key].ToImmutable()),
+#else
+ _filterDictionaryBuilder,
+#endif
_fastFilterOperation,
_fastFilterOperator)
: null;
diff --git a/src/Microsoft.TestPlatform.Common/Filtering/FilterExpression.cs b/src/Microsoft.TestPlatform.Filter.Source/FilterExpression.cs
similarity index 88%
rename from src/Microsoft.TestPlatform.Common/Filtering/FilterExpression.cs
rename to src/Microsoft.TestPlatform.Filter.Source/FilterExpression.cs
index 88102867d4..66925feddf 100644
--- a/src/Microsoft.TestPlatform.Common/Filtering/FilterExpression.cs
+++ b/src/Microsoft.TestPlatform.Filter.Source/FilterExpression.cs
@@ -1,6 +1,8 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+#nullable enable
+
using System;
using System.Collections.Generic;
using System.Diagnostics;
@@ -9,10 +11,19 @@
using System.Text;
using System.Text.RegularExpressions;
+#if !IS_VSTEST_REPO
+using Microsoft.CodeAnalysis;
+#endif
+
+#if IS_VSTEST_REPO
using Microsoft.VisualStudio.TestPlatform.ObjectModel;
+#endif
+
using Microsoft.VisualStudio.TestPlatform.ObjectModel.Utilities;
-using CommonResources = Microsoft.VisualStudio.TestPlatform.Common.Resources.Resources;
+#if IS_VSTEST_REPO
+using static Microsoft.VisualStudio.TestPlatform.Common.Resources.Resources;
+#endif
namespace Microsoft.VisualStudio.TestPlatform.Common.Filtering;
@@ -23,8 +34,25 @@ namespace Microsoft.VisualStudio.TestPlatform.Common.Filtering;
/// Equality Operators: =, !=
/// Parenthesis (, ) for grouping.
///
+#if !IS_VSTEST_REPO
+[Embedded]
+#endif
internal sealed class FilterExpression
{
+#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.";
+
+ private const string MissingOperand = "Error: Missing operand";
+
+ private const string MissingCloseParenthesis = "Error: Missing ')'";
+
+ private const string EmptyParenthesis = "Error: Empty parenthesis ( )";
+
+ private const string MissingOpenParenthesis = "Error: Missing '('";
+
+ private const string MissingOperator = "Missing Operator '|' or '&'";
+#endif
+
///
/// Condition, if expression is conditional expression.
///
@@ -78,7 +106,7 @@ private static void ProcessOperator(Stack filterStack, Operato
{
if (filterStack.Count < 2)
{
- throw new FormatException(string.Format(CultureInfo.CurrentCulture, CommonResources.TestCaseFilterFormatException, CommonResources.MissingOperand));
+ throw new FormatException(string.Format(CultureInfo.CurrentCulture, TestCaseFilterFormatException, MissingOperand));
}
var filterRight = filterStack.Pop();
@@ -90,7 +118,7 @@ private static void ProcessOperator(Stack filterStack, Operato
{
if (filterStack.Count < 2)
{
- throw new FormatException(string.Format(CultureInfo.CurrentCulture, CommonResources.TestCaseFilterFormatException, CommonResources.MissingOperand));
+ throw new FormatException(string.Format(CultureInfo.CurrentCulture, TestCaseFilterFormatException, MissingOperand));
}
var filterRight = filterStack.Pop();
@@ -100,12 +128,12 @@ private static void ProcessOperator(Stack filterStack, Operato
}
else if (op == Operator.OpenBrace)
{
- throw new FormatException(string.Format(CultureInfo.CurrentCulture, CommonResources.TestCaseFilterFormatException, CommonResources.MissingCloseParenthesis));
+ throw new FormatException(string.Format(CultureInfo.CurrentCulture, TestCaseFilterFormatException, MissingCloseParenthesis));
}
else
{
Debug.Fail("ProcessOperator called for Unexpected operator.");
- throw new FormatException(string.Format(CultureInfo.CurrentCulture, CommonResources.TestCaseFilterFormatException, string.Empty));
+ throw new FormatException(string.Format(CultureInfo.CurrentCulture, TestCaseFilterFormatException, string.Empty));
}
}
@@ -113,7 +141,11 @@ private static void ProcessOperator(Stack filterStack, Operato
/// True, if filter is valid for given set of properties.
/// When False, invalidProperties would contain properties making filter invalid.
///
+#if IS_VSTEST_REPO
internal string[]? ValidForProperties(IEnumerable? properties, Func? propertyProvider)
+#else
+ internal string[]? ValidForProperties(IEnumerable? properties)
+#endif
{
properties ??= [];
@@ -122,7 +154,11 @@ private static void ProcessOperator(Stack filterStack, Operato
// Only the leaves have a condition value.
if (current._condition != null)
{
+#if IS_VSTEST_REPO
var valid = current._condition.ValidForProperties(properties, propertyProvider);
+#else
+ var valid = current._condition.ValidForProperties(properties);
+#endif
// If it's not valid will add it to the function's return array.
return !valid ? [current._condition.Name] : null;
}
@@ -150,13 +186,15 @@ private static void ProcessOperator(Stack filterStack, Operato
///
internal static FilterExpression Parse(string filterString, out FastFilter? fastFilter)
{
+#if IS_VSTEST_REPO
ValidateArg.NotNull(filterString, nameof(filterString));
+#endif
// Below parsing doesn't error out on pattern (), so explicitly search for that (empty parenthesis).
var invalidInput = Regex.Match(filterString, @"\(\s*\)");
if (invalidInput.Success)
{
- throw new FormatException(string.Format(CultureInfo.CurrentCulture, CommonResources.TestCaseFilterFormatException, CommonResources.EmptyParenthesis));
+ throw new FormatException(string.Format(CultureInfo.CurrentCulture, TestCaseFilterFormatException, EmptyParenthesis));
}
var tokens = TokenizeFilterExpressionString(filterString);
@@ -170,7 +208,11 @@ internal static FilterExpression Parse(string filterString, out FastFilter? fast
foreach (var inputToken in tokens)
{
var token = inputToken.Trim();
+#if IS_VSTEST_REPO
if (token.IsNullOrEmpty())
+#else
+ if (string.IsNullOrEmpty(token))
+#endif
{
// ignore empty tokens
continue;
@@ -218,7 +260,7 @@ internal static FilterExpression Parse(string filterString, out FastFilter? fast
// If stack is empty at any time, than matching OpenBrace is missing from the expression.
if (operatorStack.Count == 0)
{
- throw new FormatException(string.Format(CultureInfo.CurrentCulture, CommonResources.TestCaseFilterFormatException, CommonResources.MissingOpenParenthesis));
+ throw new FormatException(string.Format(CultureInfo.CurrentCulture, TestCaseFilterFormatException, MissingOpenParenthesis));
}
Operator temp = operatorStack.Pop();
@@ -227,7 +269,7 @@ internal static FilterExpression Parse(string filterString, out FastFilter? fast
ProcessOperator(filterStack, temp);
if (operatorStack.Count == 0)
{
- throw new FormatException(string.Format(CultureInfo.CurrentCulture, CommonResources.TestCaseFilterFormatException, CommonResources.MissingOpenParenthesis));
+ throw new FormatException(string.Format(CultureInfo.CurrentCulture, TestCaseFilterFormatException, MissingOpenParenthesis));
}
temp = operatorStack.Pop();
}
@@ -253,7 +295,7 @@ internal static FilterExpression Parse(string filterString, out FastFilter? fast
if (filterStack.Count != 1)
{
- throw new FormatException(string.Format(CultureInfo.CurrentCulture, CommonResources.TestCaseFilterFormatException, CommonResources.MissingOperator));
+ throw new FormatException(string.Format(CultureInfo.CurrentCulture, TestCaseFilterFormatException, MissingOperator));
}
fastFilter = fastFilterBuilder.ToFastFilter();
@@ -300,7 +342,7 @@ private T IterateFilterExpression(Func, T> getNode
}
while (filterStack.Count > 0);
- TPDebug.Assert(result.Count == 1, "Result stack should have one element at the end.");
+ Debug.Assert(result.Count == 1, "Result stack should have one element at the end.");
return result.Peek();
}
///
@@ -310,7 +352,9 @@ private T IterateFilterExpression(Func, T> getNode
/// True if evaluation is successful.
internal bool Evaluate(Func propertyValueProvider)
{
+#if IS_VSTEST_REPO
ValidateArg.NotNull(propertyValueProvider, nameof(propertyValueProvider));
+#endif
return IterateFilterExpression((current, result) =>
{
@@ -332,7 +376,9 @@ internal bool Evaluate(Func propertyValueProvider)
internal static IEnumerable TokenizeFilterExpressionString(string str)
{
+#if IS_VSTEST_REPO
ValidateArg.NotNull(str, nameof(str));
+#endif
return TokenizeFilterExpressionStringHelper(str);
static IEnumerable TokenizeFilterExpressionStringHelper(string s)
diff --git a/src/Microsoft.TestPlatform.Common/Filtering/FilterExpressionWrapper.cs b/src/Microsoft.TestPlatform.Filter.Source/FilterExpressionWrapper.cs
similarity index 82%
rename from src/Microsoft.TestPlatform.Common/Filtering/FilterExpressionWrapper.cs
rename to src/Microsoft.TestPlatform.Filter.Source/FilterExpressionWrapper.cs
index f532ce1a17..f52b0880d2 100644
--- a/src/Microsoft.TestPlatform.Common/Filtering/FilterExpressionWrapper.cs
+++ b/src/Microsoft.TestPlatform.Filter.Source/FilterExpressionWrapper.cs
@@ -1,20 +1,36 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+#nullable enable
+
using System;
using System.Collections.Generic;
+#if IS_VSTEST_REPO
using System.Diagnostics.CodeAnalysis;
using System.Text.RegularExpressions;
+#endif
+
+#if !IS_VSTEST_REPO
+using Microsoft.CodeAnalysis;
+#endif
+#if IS_VSTEST_REPO
using Microsoft.VisualStudio.TestPlatform.ObjectModel;
+
using Microsoft.VisualStudio.TestPlatform.ObjectModel.Client;
+#endif
namespace Microsoft.VisualStudio.TestPlatform.Common.Filtering;
///
/// Class holds information related to filtering criteria.
///
+#if IS_VSTEST_REPO
public class FilterExpressionWrapper
+#else
+[Embedded]
+internal sealed class FilterExpressionWrapper
+#endif
{
///
/// FilterExpression corresponding to filter criteria
@@ -29,12 +45,21 @@ public class FilterExpressionWrapper
///
/// Initializes FilterExpressionWrapper with given filterString and options.
///
+#if IS_VSTEST_REPO
public FilterExpressionWrapper(string filterString, FilterOptions? options)
+#else
+ public FilterExpressionWrapper(string filterString)
+#endif
{
+#if IS_VSTEST_REPO
ValidateArg.NotNullOrEmpty(filterString, nameof(filterString));
+#endif
FilterString = filterString;
+
+#if IS_VSTEST_REPO
FilterOptions = options;
+#endif
try
{
@@ -48,6 +73,7 @@ public FilterExpressionWrapper(string filterString, FilterOptions? options)
// Property value regex is only supported for fast filter,
// so we ignore it if no fast filter is constructed.
+#if IS_VSTEST_REPO
// TODO: surface an error message to user.
var regexString = options?.FilterRegEx;
if (!regexString.IsNullOrEmpty())
@@ -56,6 +82,7 @@ public FilterExpressionWrapper(string filterString, FilterOptions? options)
FastFilter.PropertyValueRegex = new Regex(regexString, RegexOptions.Compiled);
FastFilter.PropertyValueRegexReplacement = options.FilterRegExReplacement;
}
+#endif
}
}
@@ -70,6 +97,7 @@ public FilterExpressionWrapper(string filterString, FilterOptions? options)
}
}
+#if IS_VSTEST_REPO
///
/// Initializes FilterExpressionWrapper with given filterString.
///
@@ -77,8 +105,11 @@ public FilterExpressionWrapper(string filterString)
: this(filterString, null)
{
}
+#endif
+#if IS_VSTEST_REPO
[MemberNotNullWhen(true, nameof(FastFilter))]
+#endif
private bool UseFastFilter => FastFilter != null;
///
@@ -86,10 +117,12 @@ public FilterExpressionWrapper(string filterString)
///
public string FilterString { get; }
+#if IS_VSTEST_REPO
///
/// User specified additional filter options.
///
public FilterOptions? FilterOptions { get; }
+#endif
///
/// Parsing error (if any), when parsing 'FilterString' with built-in parser.
@@ -99,20 +132,30 @@ public FilterExpressionWrapper(string filterString)
///
/// Validate if underlying filter expression is valid for given set of supported properties.
///
+#if IS_VSTEST_REPO
public string[]? ValidForProperties(IEnumerable? supportedProperties, Func? propertyProvider)
+#else
+ public string[]? ValidForProperties(IEnumerable? supportedProperties)
+#endif
=> UseFastFilter
- ? FastFilter.ValidForProperties(supportedProperties)
+ ? FastFilter!.ValidForProperties(supportedProperties)
+#if IS_VSTEST_REPO
: _filterExpression?.ValidForProperties(supportedProperties, propertyProvider);
+#else
+ : _filterExpression?.ValidForProperties(supportedProperties);
+#endif
///
/// Evaluate filterExpression with given propertyValueProvider.
///
public bool Evaluate(Func propertyValueProvider)
{
+#if IS_VSTEST_REPO
ValidateArg.NotNull(propertyValueProvider, nameof(propertyValueProvider));
+#endif
return UseFastFilter
- ? FastFilter.Evaluate(propertyValueProvider)
+ ? FastFilter!.Evaluate(propertyValueProvider)
: _filterExpression != null && _filterExpression.Evaluate(propertyValueProvider);
}
}
diff --git a/src/Microsoft.TestPlatform.ObjectModel/Utilities/FilterHelper.cs b/src/Microsoft.TestPlatform.Filter.Source/FilterHelper.cs
similarity index 84%
rename from src/Microsoft.TestPlatform.ObjectModel/Utilities/FilterHelper.cs
rename to src/Microsoft.TestPlatform.Filter.Source/FilterHelper.cs
index f2f6239863..25c72a0e8c 100644
--- a/src/Microsoft.TestPlatform.ObjectModel/Utilities/FilterHelper.cs
+++ b/src/Microsoft.TestPlatform.Filter.Source/FilterHelper.cs
@@ -1,19 +1,38 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+#nullable enable
+
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
+#if !IS_VSTEST_REPO
+using Microsoft.CodeAnalysis;
+#endif
+
+#if IS_VSTEST_REPO
+using static Microsoft.VisualStudio.TestPlatform.ObjectModel.Resources.Resources;
+#endif
+
namespace Microsoft.VisualStudio.TestPlatform.ObjectModel.Utilities;
+#if IS_VSTEST_REPO
public static class FilterHelper
+#else
+[Embedded]
+internal static class FilterHelper
+#endif
{
public const char EscapeCharacter = '\\';
private static readonly char[] SpecialCharacters = ['\\', '(', ')', '&', '|', '=', '!', '~'];
private static readonly HashSet SpecialCharactersSet = new(SpecialCharacters);
+#if !IS_VSTEST_REPO
+ private const string TestCaseFilterEscapeException = "Filter string '{0}' includes unrecognized escape sequence.";
+#endif
+
///
/// Escapes a set of special characters for filter (%, (, ), &, |, =, !, ~) by replacing them with their escape sequences.
///
@@ -21,7 +40,10 @@ public static class FilterHelper
/// A string of characters with special characters converted to their escaped form.
public static string Escape(string str)
{
+#if IS_VSTEST_REPO
ValidateArg.NotNull(str, nameof(str));
+#endif
+
if (str.IndexOfAny(SpecialCharacters) < 0)
{
return str;
@@ -48,7 +70,10 @@ public static string Escape(string str)
/// A filter string of characters with any escaped characters converted to their un-escaped form.
public static string Unescape(string str)
{
+#if IS_VSTEST_REPO
ValidateArg.NotNull(str, nameof(str));
+#endif
+
if (str.IndexOf(EscapeCharacter) < 0)
{
return str;
@@ -63,7 +88,7 @@ public static string Unescape(string str)
if (++i == str.Length || !SpecialCharactersSet.Contains(currentChar = str[i]))
{
// "\" should be followed by a special character.
- throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Resources.Resources.TestCaseFilterEscapeException, str));
+ throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, TestCaseFilterEscapeException, str));
}
}
diff --git a/src/Microsoft.TestPlatform.Filter.Source/Microsoft.TestPlatform.Filter.Source.csproj b/src/Microsoft.TestPlatform.Filter.Source/Microsoft.TestPlatform.Filter.Source.csproj
new file mode 100644
index 0000000000..9ab9f38517
--- /dev/null
+++ b/src/Microsoft.TestPlatform.Filter.Source/Microsoft.TestPlatform.Filter.Source.csproj
@@ -0,0 +1,18 @@
+
+
+
+ netstandard2.0
+ true
+ true
+
+
+
+ true
+ Microsoft.TestPlatform.Filter.Source.nuspec
+ $(OutputPath)
+ Microsoft.TestPlatform.Filter.Source
+ vstest visual-studio unittest testplatform mstest microsoft test testing filter
+ The Microsoft Test Platform Filter Implementation.
+
+
+
diff --git a/src/Microsoft.TestPlatform.Filter.Source/Microsoft.TestPlatform.Filter.Source.nuspec b/src/Microsoft.TestPlatform.Filter.Source/Microsoft.TestPlatform.Filter.Source.nuspec
new file mode 100644
index 0000000000..444696c464
--- /dev/null
+++ b/src/Microsoft.TestPlatform.Filter.Source/Microsoft.TestPlatform.Filter.Source.nuspec
@@ -0,0 +1,19 @@
+
+
+
+ $CommonMetadataElements$
+ README.md
+
+
+
+ $CommonFileElements$
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Microsoft.TestPlatform.Filter.Source/README.md b/src/Microsoft.TestPlatform.Filter.Source/README.md
new file mode 100644
index 0000000000..7607aa9c75
--- /dev/null
+++ b/src/Microsoft.TestPlatform.Filter.Source/README.md
@@ -0,0 +1,13 @@
+# Microsoft.TestPlatform.Filter.Source
+
+Provides source code for filter implementation. This package is a development dependency.
+
+- The source code in this package has some code under IS_VSTEST_REPO conditions. These are intended to be used only by VSTest code base itself. Consumers of this package MUST NOT define IS_VSTEST_REPO.
+- The only intended supported usage of this package is the following:
+
+ ```csharp
+ var wrapper = new FilterExpressionWrapper(vstestFilterString);
+ var expression = new TestCaseFilterExpression(wrapper);
+
+ bool match = expression.MatchTestCase(propertyProviderGoesHere);
+ ```
diff --git a/src/Microsoft.TestPlatform.Common/Filtering/TestCaseFilterExpression.cs b/src/Microsoft.TestPlatform.Filter.Source/TestCaseFilterExpression.cs
similarity index 77%
rename from src/Microsoft.TestPlatform.Common/Filtering/TestCaseFilterExpression.cs
rename to src/Microsoft.TestPlatform.Filter.Source/TestCaseFilterExpression.cs
index 401332903b..b312cc5607 100644
--- a/src/Microsoft.TestPlatform.Common/Filtering/TestCaseFilterExpression.cs
+++ b/src/Microsoft.TestPlatform.Filter.Source/TestCaseFilterExpression.cs
@@ -1,18 +1,31 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+#nullable enable
+
using System;
using System.Collections.Generic;
+#if !IS_VSTEST_REPO
+using Microsoft.CodeAnalysis;
+#endif
+
+#if IS_VSTEST_REPO
using Microsoft.VisualStudio.TestPlatform.ObjectModel;
using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter;
+#endif
namespace Microsoft.VisualStudio.TestPlatform.Common.Filtering;
///
/// Implements ITestCaseFilterExpression, providing test case filtering functionality.
///
+#if IS_VSTEST_REPO
public class TestCaseFilterExpression : ITestCaseFilterExpression
+#else
+[Embedded]
+internal sealed class TestCaseFilterExpression
+#endif
{
private readonly FilterExpressionWrapper _filterWrapper;
@@ -29,7 +42,11 @@ public class TestCaseFilterExpression : ITestCaseFilterExpression
public TestCaseFilterExpression(FilterExpressionWrapper filterWrapper)
{
_filterWrapper = filterWrapper ?? throw new ArgumentNullException(nameof(filterWrapper));
+#if IS_VSTEST_REPO
_validForMatch = filterWrapper.ParseError.IsNullOrEmpty();
+#else
+ _validForMatch = string.IsNullOrEmpty(filterWrapper.ParseError);
+#endif
}
///
@@ -40,11 +57,19 @@ public TestCaseFilterExpression(FilterExpressionWrapper filterWrapper)
///
/// Validate if underlying filter expression is valid for given set of supported properties.
///
+#if IS_VSTEST_REPO
public string[]? ValidForProperties(IEnumerable? supportedProperties, Func propertyProvider)
+#else
+ public string[]? ValidForProperties(IEnumerable? supportedProperties)
+#endif
{
if (_validForMatch)
{
+#if IS_VSTEST_REPO
return _filterWrapper.ValidForProperties(supportedProperties, propertyProvider);
+#else
+ return _filterWrapper.ValidForProperties(supportedProperties);
+#endif
}
return null;
@@ -53,11 +78,16 @@ public TestCaseFilterExpression(FilterExpressionWrapper filterWrapper)
///
/// Match test case with filter criteria.
///
+#if IS_VSTEST_REPO
public bool MatchTestCase(TestCase testCase, Func propertyValueProvider)
+#else
+ public bool MatchTestCase(Func propertyValueProvider)
+#endif
{
+#if IS_VSTEST_REPO
ValidateArg.NotNull(testCase, nameof(testCase));
ValidateArg.NotNull(propertyValueProvider, nameof(propertyValueProvider));
-
+#endif
if (_validForMatch)
{
return _filterWrapper.Evaluate(propertyValueProvider);
diff --git a/src/Microsoft.TestPlatform.ObjectModel/Microsoft.TestPlatform.ObjectModel.csproj b/src/Microsoft.TestPlatform.ObjectModel/Microsoft.TestPlatform.ObjectModel.csproj
index 1675d4cb00..33872067fb 100644
--- a/src/Microsoft.TestPlatform.ObjectModel/Microsoft.TestPlatform.ObjectModel.csproj
+++ b/src/Microsoft.TestPlatform.ObjectModel/Microsoft.TestPlatform.ObjectModel.csproj
@@ -34,8 +34,7 @@
-
+
@@ -45,6 +44,9 @@
Resources\CommonResources.resx
+
+
+
True
diff --git a/test/Microsoft.TestPlatform.Filter.Source.UnitTests/FilterExpressionWrapperTests.cs b/test/Microsoft.TestPlatform.Filter.Source.UnitTests/FilterExpressionWrapperTests.cs
new file mode 100644
index 0000000000..6efe5f711b
--- /dev/null
+++ b/test/Microsoft.TestPlatform.Filter.Source.UnitTests/FilterExpressionWrapperTests.cs
@@ -0,0 +1,155 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+using System.Collections.Generic;
+
+using Microsoft.VisualStudio.TestPlatform.Common.Filtering;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace Microsoft.TestPlatform.Filter.Source.UnitTests;
+
+[TestClass]
+public class FilterExpressionWrapperTests
+{
+ [TestMethod]
+ public void ConstructorShouldSetFilterString()
+ {
+ var filterString = "FullyQualifiedName=Test1";
+ var wrapper = new FilterExpressionWrapper(filterString);
+ Assert.AreEqual(filterString, wrapper.FilterString);
+ }
+
+ [TestMethod]
+ public void ParseErrorShouldBeNullForValidFilter()
+ {
+ var wrapper = new FilterExpressionWrapper("Name=Test1");
+ Assert.IsNull(wrapper.ParseError);
+ }
+
+ [TestMethod]
+ public void ParseErrorShouldBeSetForInvalidFilter()
+ {
+ var wrapper = new FilterExpressionWrapper("(Name=Test1");
+ Assert.IsNotNull(wrapper.ParseError);
+ }
+
+ [TestMethod]
+ public void EvaluateShouldReturnTrueWhenPropertyMatches()
+ {
+ var wrapper = new FilterExpressionWrapper("Name=Test1");
+
+ bool result = wrapper.Evaluate(prop => prop == "Name" ? "Test1" : null);
+
+ Assert.IsTrue(result);
+ }
+
+ [TestMethod]
+ public void EvaluateShouldReturnFalseWhenPropertyDoesNotMatch()
+ {
+ var wrapper = new FilterExpressionWrapper("Name=Test1");
+
+ bool result = wrapper.Evaluate(prop => prop == "Name" ? "Test2" : null);
+
+ Assert.IsFalse(result);
+ }
+
+ [TestMethod]
+ public void EvaluateShouldReturnFalseWhenFilterHasParseError()
+ {
+ var wrapper = new FilterExpressionWrapper("(Name=Test1");
+
+ bool result = wrapper.Evaluate(prop => "Test1");
+
+ Assert.IsFalse(result);
+ }
+
+ [TestMethod]
+ public void EvaluateShouldSupportAndOperator()
+ {
+ var wrapper = new FilterExpressionWrapper("Name=Test1&Category=Unit");
+
+ bool result = wrapper.Evaluate(prop => prop switch
+ {
+ "Name" => "Test1",
+ "Category" => "Unit",
+ _ => null,
+ });
+
+ Assert.IsTrue(result);
+ }
+
+ [TestMethod]
+ public void EvaluateShouldSupportOrOperator()
+ {
+ var wrapper = new FilterExpressionWrapper("Name=Test1|Name=Test2");
+
+ bool matchFirst = wrapper.Evaluate(prop => prop == "Name" ? "Test1" : null);
+ bool matchSecond = wrapper.Evaluate(prop => prop == "Name" ? "Test2" : null);
+ bool matchNeither = wrapper.Evaluate(prop => prop == "Name" ? "Test3" : null);
+
+ Assert.IsTrue(matchFirst);
+ Assert.IsTrue(matchSecond);
+ Assert.IsFalse(matchNeither);
+ }
+
+ [TestMethod]
+ public void EvaluateShouldSupportNotEqualOperator()
+ {
+ var wrapper = new FilterExpressionWrapper("Name!=Test1");
+
+ bool result = wrapper.Evaluate(prop => prop == "Name" ? "Test2" : null);
+
+ Assert.IsTrue(result);
+ }
+
+ [TestMethod]
+ public void EvaluateShouldSupportContainsOperator()
+ {
+ var wrapper = new FilterExpressionWrapper("Name~Test");
+
+ bool result = wrapper.Evaluate(prop => prop == "Name" ? "MyTestMethod" : null);
+
+ Assert.IsTrue(result);
+ }
+
+ [TestMethod]
+ public void ValidForPropertiesShouldReturnNullWhenAllPropertiesAreSupported()
+ {
+ var wrapper = new FilterExpressionWrapper("Name=Test1");
+ var supportedProperties = new List { "Name" };
+
+ string[]? invalidProperties = wrapper.ValidForProperties(supportedProperties);
+
+ Assert.IsNull(invalidProperties);
+ }
+
+ [TestMethod]
+ public void ValidForPropertiesShouldReturnUnsupportedPropertyNames()
+ {
+ var wrapper = new FilterExpressionWrapper("UnknownProperty=Value");
+ var supportedProperties = new List { "Name", "Category" };
+
+ string[]? invalidProperties = wrapper.ValidForProperties(supportedProperties);
+
+ Assert.IsNotNull(invalidProperties);
+ Assert.HasCount(1, invalidProperties);
+ Assert.AreEqual("UnknownProperty", invalidProperties[0]);
+ }
+
+ [TestMethod]
+ public void FastFilterShouldBeCreatedForSimpleEqualityFilter()
+ {
+ var wrapper = new FilterExpressionWrapper("Name=Test1");
+
+ Assert.IsNotNull(wrapper.FastFilter);
+ }
+
+ [TestMethod]
+ public void FastFilterShouldBeNullForComplexFilter()
+ {
+ var wrapper = new FilterExpressionWrapper("Name=Test1&Name=Test2");
+
+ Assert.IsNull(wrapper.FastFilter);
+ }
+}
diff --git a/test/Microsoft.TestPlatform.Filter.Source.UnitTests/FilterHelperTests.cs b/test/Microsoft.TestPlatform.Filter.Source.UnitTests/FilterHelperTests.cs
new file mode 100644
index 0000000000..bf1aa85ccc
--- /dev/null
+++ b/test/Microsoft.TestPlatform.Filter.Source.UnitTests/FilterHelperTests.cs
@@ -0,0 +1,112 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+
+using Microsoft.VisualStudio.TestPlatform.ObjectModel.Utilities;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace Microsoft.TestPlatform.Filter.Source.UnitTests;
+
+[TestClass]
+public class FilterHelperTests
+{
+ [TestMethod]
+ public void EscapeShouldReturnOriginalStringWhenNoSpecialCharacters()
+ {
+ var input = "TestMethod";
+ Assert.AreEqual(input, FilterHelper.Escape(input));
+ }
+
+ [TestMethod]
+ public void EscapeShouldEscapeOpenParenthesis()
+ {
+ Assert.AreEqual(@"Test\(Method", FilterHelper.Escape("Test(Method"));
+ }
+
+ [TestMethod]
+ public void EscapeShouldEscapeCloseParenthesis()
+ {
+ Assert.AreEqual(@"Test\)Method", FilterHelper.Escape("Test)Method"));
+ }
+
+ [TestMethod]
+ public void EscapeShouldEscapeAmpersand()
+ {
+ Assert.AreEqual(@"Test\&Method", FilterHelper.Escape("Test&Method"));
+ }
+
+ [TestMethod]
+ public void EscapeShouldEscapePipe()
+ {
+ Assert.AreEqual(@"Test\|Method", FilterHelper.Escape("Test|Method"));
+ }
+
+ [TestMethod]
+ public void EscapeShouldEscapeEqualSign()
+ {
+ Assert.AreEqual(@"Test\=Method", FilterHelper.Escape("Test=Method"));
+ }
+
+ [TestMethod]
+ public void EscapeShouldEscapeExclamationMark()
+ {
+ Assert.AreEqual(@"Test\!Method", FilterHelper.Escape("Test!Method"));
+ }
+
+ [TestMethod]
+ public void EscapeShouldEscapeTilde()
+ {
+ Assert.AreEqual(@"Test\~Method", FilterHelper.Escape("Test~Method"));
+ }
+
+ [TestMethod]
+ public void EscapeShouldEscapeBackslash()
+ {
+ Assert.AreEqual(@"Test\\Method", FilterHelper.Escape(@"Test\Method"));
+ }
+
+ [TestMethod]
+ public void EscapeShouldEscapeMultipleSpecialCharacters()
+ {
+ Assert.AreEqual(@"Test\(A\|B\)", FilterHelper.Escape("Test(A|B)"));
+ }
+
+ [TestMethod]
+ public void UnescapeShouldReturnOriginalStringWhenNoEscapeCharacters()
+ {
+ var input = "TestMethod";
+ Assert.AreEqual(input, FilterHelper.Unescape(input));
+ }
+
+ [TestMethod]
+ public void UnescapeShouldUnescapeOpenParenthesis()
+ {
+ Assert.AreEqual("Test(Method", FilterHelper.Unescape(@"Test\(Method"));
+ }
+
+ [TestMethod]
+ public void UnescapeShouldUnescapeCloseParenthesis()
+ {
+ Assert.AreEqual("Test)Method", FilterHelper.Unescape(@"Test\)Method"));
+ }
+
+ [TestMethod]
+ public void UnescapeShouldUnescapeBackslash()
+ {
+ Assert.AreEqual(@"Test\Method", FilterHelper.Unescape(@"Test\\Method"));
+ }
+
+ [TestMethod]
+ public void UnescapeShouldThrowOnInvalidEscapeSequence()
+ {
+ Assert.ThrowsExactly(() => FilterHelper.Unescape(@"Test\AMethod"));
+ }
+
+ [TestMethod]
+ public void EscapeAndUnescapeRoundtrip()
+ {
+ var input = "TestClass(\"param\").Method(1.5)";
+ Assert.AreEqual(input, FilterHelper.Unescape(FilterHelper.Escape(input)));
+ }
+}
diff --git a/test/Microsoft.TestPlatform.Filter.Source.UnitTests/Microsoft.TestPlatform.Filter.Source.UnitTests.csproj b/test/Microsoft.TestPlatform.Filter.Source.UnitTests/Microsoft.TestPlatform.Filter.Source.UnitTests.csproj
new file mode 100644
index 0000000000..f7671614aa
--- /dev/null
+++ b/test/Microsoft.TestPlatform.Filter.Source.UnitTests/Microsoft.TestPlatform.Filter.Source.UnitTests.csproj
@@ -0,0 +1,28 @@
+
+
+
+ true
+ $(NetCurrent);net48
+ Exe
+ true
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/Microsoft.TestPlatform.Filter.Source.UnitTests/TestCaseFilterExpressionTests.cs b/test/Microsoft.TestPlatform.Filter.Source.UnitTests/TestCaseFilterExpressionTests.cs
new file mode 100644
index 0000000000..bf8ddd901f
--- /dev/null
+++ b/test/Microsoft.TestPlatform.Filter.Source.UnitTests/TestCaseFilterExpressionTests.cs
@@ -0,0 +1,133 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+using System.Collections.Generic;
+
+using Microsoft.VisualStudio.TestPlatform.Common.Filtering;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace Microsoft.TestPlatform.Filter.Source.UnitTests;
+
+[TestClass]
+public class TestCaseFilterExpressionTests
+{
+ [TestMethod]
+ public void ConstructorShouldThrowForNullFilterWrapper()
+ {
+ Assert.ThrowsExactly(() => new TestCaseFilterExpression(null!));
+ }
+
+ [TestMethod]
+ public void TestCaseFilterValueShouldMatchFilterString()
+ {
+ var filterString = "Name=Test1";
+ var wrapper = new FilterExpressionWrapper(filterString);
+ var expression = new TestCaseFilterExpression(wrapper);
+
+ Assert.AreEqual(filterString, expression.TestCaseFilterValue);
+ }
+
+ [TestMethod]
+ public void MatchTestCaseShouldReturnTrueWhenPropertyMatches()
+ {
+ var wrapper = new FilterExpressionWrapper("Name=Test1");
+ var expression = new TestCaseFilterExpression(wrapper);
+
+ bool result = expression.MatchTestCase(prop => prop == "Name" ? "Test1" : null);
+
+ Assert.IsTrue(result);
+ }
+
+ [TestMethod]
+ public void MatchTestCaseShouldReturnFalseWhenPropertyDoesNotMatch()
+ {
+ var wrapper = new FilterExpressionWrapper("Name=Test1");
+ var expression = new TestCaseFilterExpression(wrapper);
+
+ bool result = expression.MatchTestCase(prop => prop == "Name" ? "Test2" : null);
+
+ Assert.IsFalse(result);
+ }
+
+ [TestMethod]
+ public void MatchTestCaseShouldReturnFalseWhenFilterHasParseError()
+ {
+ var wrapper = new FilterExpressionWrapper("(Name=Test1");
+ var expression = new TestCaseFilterExpression(wrapper);
+
+ bool result = expression.MatchTestCase(prop => "Test1");
+
+ Assert.IsFalse(result);
+ }
+
+ [TestMethod]
+ public void ValidForPropertiesShouldReturnNullWhenAllPropertiesAreSupported()
+ {
+ var wrapper = new FilterExpressionWrapper("Name=Test1");
+ var expression = new TestCaseFilterExpression(wrapper);
+ var supportedProperties = new List { "Name" };
+
+ string[]? invalidProperties = expression.ValidForProperties(supportedProperties);
+
+ Assert.IsNull(invalidProperties);
+ }
+
+ [TestMethod]
+ public void ValidForPropertiesShouldReturnUnsupportedPropertyNames()
+ {
+ var wrapper = new FilterExpressionWrapper("UnknownProperty=Value");
+ var expression = new TestCaseFilterExpression(wrapper);
+ var supportedProperties = new List { "Name", "Category" };
+
+ string[]? invalidProperties = expression.ValidForProperties(supportedProperties);
+
+ Assert.IsNotNull(invalidProperties);
+ Assert.HasCount(1, invalidProperties);
+ Assert.AreEqual("UnknownProperty", invalidProperties[0]);
+ }
+
+ [TestMethod]
+ public void ValidForPropertiesShouldReturnNullWhenFilterHasParseError()
+ {
+ var wrapper = new FilterExpressionWrapper("(Name=Test1");
+ var expression = new TestCaseFilterExpression(wrapper);
+ var supportedProperties = new List { "Name" };
+
+ // When filter has parse error, ValidForProperties returns null
+ string[]? invalidProperties = expression.ValidForProperties(supportedProperties);
+
+ Assert.IsNull(invalidProperties);
+ }
+
+ [TestMethod]
+ public void MatchTestCaseShouldSupportOrOperator()
+ {
+ var wrapper = new FilterExpressionWrapper("Name=Test1|Name=Test2");
+ var expression = new TestCaseFilterExpression(wrapper);
+
+ bool matchFirst = expression.MatchTestCase(prop => prop == "Name" ? "Test1" : null);
+ bool matchSecond = expression.MatchTestCase(prop => prop == "Name" ? "Test2" : null);
+ bool matchNeither = expression.MatchTestCase(prop => prop == "Name" ? "Test3" : null);
+
+ Assert.IsTrue(matchFirst);
+ Assert.IsTrue(matchSecond);
+ Assert.IsFalse(matchNeither);
+ }
+
+ [TestMethod]
+ public void MatchTestCaseShouldSupportAndOperator()
+ {
+ var wrapper = new FilterExpressionWrapper("Name=Test1&Category=Unit");
+ var expression = new TestCaseFilterExpression(wrapper);
+
+ bool result = expression.MatchTestCase(prop => prop switch
+ {
+ "Name" => "Test1",
+ "Category" => "Unit",
+ _ => null,
+ });
+
+ Assert.IsTrue(result);
+ }
+}
diff --git a/test/Microsoft.TestPlatform.Library.IntegrationTests/FilterSourceIntegrationTests.cs b/test/Microsoft.TestPlatform.Library.IntegrationTests/FilterSourceIntegrationTests.cs
new file mode 100644
index 0000000000..a4d78efa44
--- /dev/null
+++ b/test/Microsoft.TestPlatform.Library.IntegrationTests/FilterSourceIntegrationTests.cs
@@ -0,0 +1,36 @@
+// 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.TestPlatform.TestUtilities;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace Microsoft.TestPlatform.Library.IntegrationTests;
+
+///
+/// Integration tests for the Microsoft.TestPlatform.Filter.Source NuGet package.
+/// The test asset FilterSourcePackageConsumerTests references the locally-built package,
+/// which embeds the filter source files as contentFiles. Running those tests through
+/// vstest.console end-to-end verifies that the package compiles and executes correctly.
+///
+[TestClass]
+public class FilterSourceIntegrationTests : AcceptanceTestBase
+{
+ [TestMethod]
+ [NetFullTargetFrameworkDataSource(useDesktopRunner: false)]
+ [NetCoreTargetFrameworkDataSource(useDesktopRunner: false)]
+ public void FilterSourcePackage_AllTestsPass(RunnerInfo runnerInfo)
+ {
+ SetTestEnvironment(_testEnvironment, runnerInfo);
+
+ var testAssembly = GetAssetFullPath("FilterSourcePackageConsumerTests.dll");
+ var arguments = PrepareArguments(testAssembly, null, string.Empty, FrameworkArgValue,
+ runnerInfo.InIsolationValue, resultsDirectory: TempDirectory.Path);
+
+ InvokeVsTest(arguments);
+
+ // 7 FilterExpressionWrapper tests + 7 TestCaseFilterExpression tests = 14 total
+ ValidateSummaryStatus(14, 0, 0);
+ ExitCodeEquals(0);
+ }
+}
+
diff --git a/test/TestAssets/FilterSourcePackageConsumerTests/FilterSourcePackageConsumerTests.cs b/test/TestAssets/FilterSourcePackageConsumerTests/FilterSourcePackageConsumerTests.cs
new file mode 100644
index 0000000000..7143959b39
--- /dev/null
+++ b/test/TestAssets/FilterSourcePackageConsumerTests/FilterSourcePackageConsumerTests.cs
@@ -0,0 +1,152 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+// The Microsoft.TestPlatform.Filter.Source package embeds its source files into this project
+// as contentFiles. FilterExpressionWrapper and TestCaseFilterExpression are compiled as
+// internal sealed types in this assembly (non-IS_VSTEST_REPO form). These tests verify that
+// the package works correctly when consumed as a NuGet package.
+
+using System.Collections.Generic;
+
+using Microsoft.VisualStudio.TestPlatform.Common.Filtering;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace FilterSourcePackageConsumerTests;
+
+[TestClass]
+public class FilterExpressionWrapperPackageTests
+{
+ [TestMethod]
+ public void EvaluateShouldReturnTrueForMatchingFilter()
+ {
+ var wrapper = new FilterExpressionWrapper("Name=Test1");
+ bool result = wrapper.Evaluate(p => p == "Name" ? "Test1" : null);
+ Assert.IsTrue(result);
+ }
+
+ [TestMethod]
+ public void EvaluateShouldReturnFalseForNonMatchingFilter()
+ {
+ var wrapper = new FilterExpressionWrapper("Name=Test1");
+ bool result = wrapper.Evaluate(p => p == "Name" ? "Test2" : null);
+ Assert.IsFalse(result);
+ }
+
+ [TestMethod]
+ public void EvaluateShouldReturnFalseWhenFilterHasParseError()
+ {
+ var wrapper = new FilterExpressionWrapper("(Name=Test1");
+ bool result = wrapper.Evaluate(p => "Test1");
+ Assert.IsFalse(result);
+ }
+
+ [TestMethod]
+ public void EvaluateShouldSupportAndOperator()
+ {
+ var wrapper = new FilterExpressionWrapper("Name=Test1&Category=Unit");
+ bool result = wrapper.Evaluate(p => p switch
+ {
+ "Name" => "Test1",
+ "Category" => "Unit",
+ _ => null,
+ });
+ Assert.IsTrue(result);
+ }
+
+ [TestMethod]
+ public void EvaluateShouldSupportOrOperator()
+ {
+ var wrapper = new FilterExpressionWrapper("Name=Test1|Name=Test2");
+ Assert.IsTrue(wrapper.Evaluate(p => p == "Name" ? "Test1" : null));
+ Assert.IsTrue(wrapper.Evaluate(p => p == "Name" ? "Test2" : null));
+ Assert.IsFalse(wrapper.Evaluate(p => p == "Name" ? "Test3" : null));
+ }
+
+ [TestMethod]
+ public void ValidForPropertiesShouldReturnNullForSupportedProperties()
+ {
+ var wrapper = new FilterExpressionWrapper("Name=Test1");
+ string[]? invalid = wrapper.ValidForProperties(new List { "Name" });
+ Assert.IsNull(invalid);
+ }
+
+ [TestMethod]
+ public void ValidForPropertiesShouldReturnUnsupportedPropertyNames()
+ {
+ var wrapper = new FilterExpressionWrapper("UnknownProp=Value");
+ string[]? invalid = wrapper.ValidForProperties(new List { "Name" });
+ Assert.IsNotNull(invalid);
+ Assert.AreEqual(1, invalid.Length);
+ Assert.AreEqual("UnknownProp", invalid[0]);
+ }
+}
+
+[TestClass]
+public class TestCaseFilterExpressionPackageTests
+{
+ [TestMethod]
+ public void MatchTestCaseShouldReturnTrueForMatchingFilter()
+ {
+ var wrapper = new FilterExpressionWrapper("Name=Test1");
+ var expression = new TestCaseFilterExpression(wrapper);
+ bool result = expression.MatchTestCase(p => p == "Name" ? "Test1" : null);
+ Assert.IsTrue(result);
+ }
+
+ [TestMethod]
+ public void MatchTestCaseShouldReturnFalseForNonMatchingFilter()
+ {
+ var wrapper = new FilterExpressionWrapper("Name=Test1");
+ var expression = new TestCaseFilterExpression(wrapper);
+ bool result = expression.MatchTestCase(p => p == "Name" ? "Test2" : null);
+ Assert.IsFalse(result);
+ }
+
+ [TestMethod]
+ public void MatchTestCaseShouldReturnFalseWhenFilterHasParseError()
+ {
+ var wrapper = new FilterExpressionWrapper("(Name=Test1");
+ var expression = new TestCaseFilterExpression(wrapper);
+ bool result = expression.MatchTestCase(p => "Test1");
+ Assert.IsFalse(result);
+ }
+
+ [TestMethod]
+ public void MatchTestCaseShouldSupportOrOperator()
+ {
+ var wrapper = new FilterExpressionWrapper("Name=Test1|Name=Test2");
+ var expression = new TestCaseFilterExpression(wrapper);
+ Assert.IsTrue(expression.MatchTestCase(p => p == "Name" ? "Test1" : null));
+ Assert.IsTrue(expression.MatchTestCase(p => p == "Name" ? "Test2" : null));
+ Assert.IsFalse(expression.MatchTestCase(p => p == "Name" ? "Test3" : null));
+ }
+
+ [TestMethod]
+ public void TestCaseFilterValueShouldReturnOriginalFilterString()
+ {
+ var filterString = "Name=Test1";
+ var wrapper = new FilterExpressionWrapper(filterString);
+ var expression = new TestCaseFilterExpression(wrapper);
+ Assert.AreEqual(filterString, expression.TestCaseFilterValue);
+ }
+
+ [TestMethod]
+ public void ValidForPropertiesShouldReturnNullForSupportedProperties()
+ {
+ var wrapper = new FilterExpressionWrapper("Name=Test1");
+ var expression = new TestCaseFilterExpression(wrapper);
+ string[] invalid = expression.ValidForProperties(new List { "Name" });
+ Assert.IsNull(invalid);
+ }
+
+ [TestMethod]
+ public void ValidForPropertiesShouldReturnUnsupportedPropertyNames()
+ {
+ var wrapper = new FilterExpressionWrapper("UnknownProp=Value");
+ var expression = new TestCaseFilterExpression(wrapper);
+ string[] invalid = expression.ValidForProperties(new List { "Name" });
+ Assert.IsNotNull(invalid);
+ Assert.AreEqual(1, invalid.Length);
+ Assert.AreEqual("UnknownProp", invalid[0]);
+ }
+}
diff --git a/test/TestAssets/FilterSourcePackageConsumerTests/FilterSourcePackageConsumerTests.csproj b/test/TestAssets/FilterSourcePackageConsumerTests/FilterSourcePackageConsumerTests.csproj
new file mode 100644
index 0000000000..39975dc4f0
--- /dev/null
+++ b/test/TestAssets/FilterSourcePackageConsumerTests/FilterSourcePackageConsumerTests.csproj
@@ -0,0 +1,19 @@
+
+
+
+
+ $(TestProjectTargetFrameworks)
+
+
+
+
+
+
+
+
+
+
diff --git a/test/TestAssets/TestAssets.slnx b/test/TestAssets/TestAssets.slnx
index 42ca210f20..83551a9a06 100644
--- a/test/TestAssets/TestAssets.slnx
+++ b/test/TestAssets/TestAssets.slnx
@@ -46,6 +46,7 @@
+