Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support to escape/unescape testcase filter strings #1627

Merged
merged 11 commits into from
Jun 5, 2018
Next Next commit
Add support to escape/unescape testcase filter strings
  • Loading branch information
genlu committed May 31, 2018
commit 3059910e2b69dad3aac54f88cba3402b265bb5d1
6 changes: 3 additions & 3 deletions src/Microsoft.TestPlatform.Common/Filtering/Condition.cs
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ internal Condition(string name, Operation operation, string value)
/// </summary>
internal bool Evaluate(Func<string, Object> propertyValueProvider)
{
ValidateArg.NotNull(propertyValueProvider, "propertyValueProvider");
ValidateArg.NotNull(propertyValueProvider, nameof(propertyValueProvider));
var result = false;
var multiValue = this.GetPropertyValue(propertyValueProvider);
switch (this.Operation)
Expand Down Expand Up @@ -174,7 +174,7 @@ internal static Condition Parse(string conditionString)
{
// If only parameter values is passed, create condition with default property name,
// default operation and given condition string as parameter value.
return new Condition(Condition.DefaultPropertyName, Condition.DefaultOperation, conditionString.Trim());
return new Condition(Condition.DefaultPropertyName, Condition.DefaultOperation, FilterHelpers.Unescape(conditionString.Trim()));
}

if (parts.Length != 3)
Expand All @@ -192,7 +192,7 @@ internal static Condition Parse(string conditionString)
}

Operation operation = GetOperator(parts[1]);
Condition condition = new Condition(parts[0], operation, parts[2]);
Condition condition = new Condition(parts[0], operation, FilterHelpers.Unescape(parts[2]));
return condition;
}

Expand Down
10 changes: 5 additions & 5 deletions src/Microsoft.TestPlatform.Common/Filtering/FilterExpression.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,8 @@ internal class FilterExpression

private FilterExpression(FilterExpression left, FilterExpression right, bool areJoinedByAnd)
{
ValidateArg.NotNull(left, "left");
ValidateArg.NotNull(right, "right");
ValidateArg.NotNull(left, nameof(left));
ValidateArg.NotNull(right, nameof(right));

this.left = left;
this.right = right;
Expand All @@ -62,7 +62,7 @@ private FilterExpression(FilterExpression left, FilterExpression right, bool are

private FilterExpression(Condition condition)
{
ValidateArg.NotNull(condition, "condition");
ValidateArg.NotNull(condition, nameof(condition));
this.condition = condition;
}
#endregion
Expand Down Expand Up @@ -168,7 +168,7 @@ internal string[] ValidForProperties(IEnumerable<string> properties, Func<string
/// </summary>
internal static FilterExpression Parse(string filterString, out FastFilter fastFilter)
{
ValidateArg.NotNull(filterString, "filterString");
ValidateArg.NotNull(filterString, nameof(filterString));

// below parsing doesn't error out on pattern (), so explicitly search for that (empty parethesis).
var invalidInput = Regex.Match(filterString, @"\(\s*\)");
Expand Down Expand Up @@ -283,7 +283,7 @@ internal static FilterExpression Parse(string filterString, out FastFilter fastF
/// <returns> True if evaluation is successful. </returns>
internal bool Evaluate(Func<string, Object> propertyValueProvider)
{
ValidateArg.NotNull(propertyValueProvider, "propertyValueProvider");
ValidateArg.NotNull(propertyValueProvider, nameof(propertyValueProvider));

bool filterResult = false;
if (null != this.condition)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public class FilterExpressionWrapper
/// </summary>
public FilterExpressionWrapper(string filterString, FilterOptions options)
{
ValidateArg.NotNullOrEmpty(filterString, "filterString");
ValidateArg.NotNullOrEmpty(filterString, nameof(filterString));

this.FilterString = filterString;
this.FilterOptions = options;
Expand Down Expand Up @@ -120,7 +120,7 @@ public string[] ValidForProperties(IEnumerable<String> supportedProperties, Func
/// </summary>
public bool Evaluate(Func<string, Object> propertyValueProvider)
{
ValidateArg.NotNull(propertyValueProvider, "propertyValueProvider");
ValidateArg.NotNull(propertyValueProvider, nameof(propertyValueProvider));

if (UseFastFilter)
{
Expand Down
104 changes: 104 additions & 0 deletions src/Microsoft.TestPlatform.Common/Filtering/FilterHelpers.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

namespace Microsoft.VisualStudio.TestPlatform.Common.Filtering
{
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using Microsoft.VisualStudio.TestPlatform.Common.Resources;

public static class FilterHelpers
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this be internal? LUT has dependency on Microsoft.TestPlatform.Common?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, made this public for LUT. LUT has dependency on TranslationLayer, which pulls in Common as well.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it. However not all clients(VS code, Rider,..) uses TranslationLayer. We should rename this to FilterEscapeHelper and move this to OM?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess the tokenizer code should remain private to FilterExpression/Condition in this case, don't see a point of exposing them.

{
private const char EscapePrefix = '%';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use \(backslash) as escape character convert ( ( -> \(, & -> \&,etc..) and write the our own parsing logic rather than depending upon regex? To make easy to construct the filter from CLI?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with you that this is hard to use in CLI scenario. As you can see from the out-of-sync comment you pointed out below, I actually started with exactly what you suggested, but quickly realized that it would involve a bigger change, which I was reluctant to do this late and I really want this fix for preview 3, as it's a serious regression for LUT. Let me give this a try today and see how it goes.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

private static readonly Dictionary<char, char> LookUpMap =
new Dictionary<char, char>()
{
{ '%', '0'},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this exhaustive list of special characters we expect ? If not, this won't scale as we can add just 2 more with current design as %9 will be the last supported value.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know if there's plan to expand operator/operation support for filter, but we can always use "%a", "%b", etc

{ '(', '1'},
{ ')', '2'},
{ '&', '3'},
{ '|', '4'},
{ '=', '5'},
{ '!', '6'},
{ '~', '7'},
};

private static readonly Dictionary<char, char> ReverseLookUpMap = LookUpMap.ToDictionary(kvp => kvp.Value, kvp => kvp.Key);
private static readonly char[] SpecialCharacters = LookUpMap.Keys.ToArray();

/// <summary>
/// Escapes a set of special characters for filter (%, (, ), &, |, =, !, ~) by replacing them with their escape sequences.
/// </summary>
/// <param name="str">The input string that contains the text to convert.</param>
/// <returns>A string of characters with special characters converted to their escaped form.</returns>
public static string Escape(string str)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am guessing. we are just going to replicate this code on the IDE side. If yes, it is problematic, maintenance issues and what not.
Also, we would need to document this for other consumers of TP to use this escaping logic

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I made the helper class public, so it can be used by other components. But I'm not sure if this is the right place for public helpers.

{
if (str == null)
{
throw new ArgumentNullException(nameof(str));
}

if (str.IndexOfAny(SpecialCharacters) < 0)
{
return str;
}

var builder = new StringBuilder();
for (int i = 0; i < str.Length; ++i)
{
var currentChar = str[i];
if (LookUpMap.TryGetValue(currentChar, out var escaped))
{
builder.Append(EscapePrefix);
currentChar = escaped;
}
builder.Append(currentChar);
}

return builder.ToString();
}

/// <summary>
/// Converts any escaped characters in the input filter string.
/// </summary>
/// <param name="str"></param>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: doc for str

/// <returns>A filter string of characters with any escaped characters converted to their unescaped form.</returns>
public static string Unescape(string str)
{
if (str == null)
{
throw new ArgumentNullException(nameof(str));
}

if (str.IndexOf(EscapePrefix) < 0)
{
return str;
}

var builder = new StringBuilder();
for (int i = 0; i < str.Length; ++i)
{
var currentChar = str[i];
if (currentChar == EscapePrefix)
{
if (++i == str.Length || !ReverseLookUpMap.TryGetValue(currentChar = str[i], out var specialChar))
{
// "\" should be followed by a valid escape seq. i.e. '0' - '7'.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: % should be followed.

throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Resources.TestCaseFilterEscapeException, str));
}
currentChar = specialChar;
}

// Strictly speaking, string to be unescaped shouldn't contain any of the special characters (except '%'),
// but we will ignore that to avoid additional overhead.

builder.Append(currentChar);
}

return builder.ToString();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public class TestCaseFilterExpression : ITestCaseFilterExpression
/// </summary>
public TestCaseFilterExpression(FilterExpressionWrapper filterWrapper)
{
ValidateArg.NotNull(filterWrapper, "filterWrapper");
ValidateArg.NotNull(filterWrapper, nameof(filterWrapper));
this.filterWrapper = filterWrapper;
this.validForMatch = string.IsNullOrEmpty(filterWrapper.ParseError);
}
Expand Down Expand Up @@ -62,8 +62,8 @@ public string[] ValidForProperties(IEnumerable<String> supportedProperties, Func
/// </summary>
public bool MatchTestCase(TestCase testCase, Func<string, Object> propertyValueProvider)
{
ValidateArg.NotNull(testCase, "testCase");
ValidateArg.NotNull(propertyValueProvider, "propertyValueProvider");
ValidateArg.NotNull(testCase, nameof(testCase));
ValidateArg.NotNull(propertyValueProvider, nameof(propertyValueProvider));
if (!this.validForMatch)
{
return false;
Expand Down
59 changes: 32 additions & 27 deletions src/Microsoft.TestPlatform.Common/Resources/Resources.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading