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 @@ +