-
Notifications
You must be signed in to change notification settings - Fork 323
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
Changes from 7 commits
3059910
9657101
57ef9c5
2d032d2
f36ee5d
9f92a13
94a9011
8181492
42caef2
7af070d
e16762f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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; | ||
|
@@ -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 | ||
|
@@ -168,16 +168,16 @@ 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). | ||
//// below parsing doesn't error out on pattern (), so explicitly search for that (empty parethesis). | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit nit nit: might want to undo the changes to this comment (remove the extra |
||
var invalidInput = Regex.Match(filterString, @"\(\s*\)"); | ||
if (invalidInput.Success) | ||
{ | ||
throw new FormatException(string.Format(CultureInfo.CurrentCulture, CommonResources.TestCaseFilterFormatException, CommonResources.EmptyParenthesis)); | ||
} | ||
|
||
var tokens = Regex.Split(filterString, filterExpressionSeperatorString); | ||
var tokens = FilterHelpers.TokenizeFilterExpressionString(filterString); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: Delete not required fields |
||
var operatorStack = new Stack<Operator>(); | ||
var filterStack = new Stack<FilterExpression>(); | ||
|
||
|
@@ -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) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,231 @@ | ||
// 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can this be internal? LUT has dependency on There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 = '\\'; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: |
||
private static readonly char[] SpecialCharacters = { '\\', '(', ')', '&', '|', '=', '!', '~' }; | ||
|
||
/// <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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 (SpecialCharacters.Contains(currentChar)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we use |
||
{ | ||
builder.Append(EscapePrefix); | ||
} | ||
builder.Append(currentChar); | ||
} | ||
|
||
return builder.ToString(); | ||
} | ||
|
||
/// <summary> | ||
/// Converts any escaped characters in the input filter string. | ||
/// </summary> | ||
/// <param name="str">The input string that contains the text to convert.</param> | ||
/// <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 || !SpecialCharacters.Contains(currentChar = str[i])) | ||
{ | ||
// "\" should be followed by a special character. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Show the position and character in error message and log same error message to EqtTrace. |
||
throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Resources.TestCaseFilterEscapeException, str)); | ||
} | ||
} | ||
|
||
// Strictly speaking, string to be unescaped shouldn't contain any of the special characters, | ||
// other than being part of escape sequence, but we will ignore that to avoid additional overhead. | ||
|
||
builder.Append(currentChar); | ||
} | ||
|
||
return builder.ToString(); | ||
} | ||
|
||
internal static IEnumerable<string> TokenizeFilterExpressionString(string str) | ||
{ | ||
if (str == null) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use string.IsNullOrEmpty() instead everywhere. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is to ensure the exact same behavior as before (with regex). we might be able to do further adjustment in |
||
{ | ||
throw new ArgumentNullException(nameof(str)); | ||
} | ||
|
||
StringBuilder tokenBuilder = new StringBuilder(); | ||
|
||
var last = '\0'; | ||
for (int i = 0; i < str.Length; ++i) | ||
{ | ||
var current = str[i]; | ||
|
||
if (last == EscapePrefix) | ||
{ | ||
// Don't check if `current` is one of the special characters here. | ||
// Instead, we blindly let any character follows '\' pass though and | ||
// relies on `FiltetrHelpers.Unescape` to report such errors. | ||
tokenBuilder.Append(current); | ||
|
||
if (current == EscapePrefix) | ||
{ | ||
// We just encountered "\\" (escaped '\'), this will set last to '\0' | ||
// so the next char will not be treated as a suffix of escape sequence. | ||
current = '\0'; | ||
} | ||
} | ||
else | ||
{ | ||
switch (current) | ||
{ | ||
case '(': | ||
case ')': | ||
case '&': | ||
case '|': | ||
if (tokenBuilder.Length > 0) | ||
{ | ||
yield return tokenBuilder.ToString(); | ||
tokenBuilder.Clear(); | ||
} | ||
yield return current.ToString(); | ||
break; | ||
|
||
default: | ||
tokenBuilder.Append(current); | ||
break; | ||
} | ||
} | ||
|
||
last = current; | ||
} | ||
|
||
if (tokenBuilder.Length > 0) | ||
{ | ||
yield return tokenBuilder.ToString(); | ||
} | ||
} | ||
|
||
internal static IEnumerable<string> TokenizeFilterConditionString(string str) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: Please add documentation for these as well. |
||
{ | ||
if (str == null) | ||
{ | ||
throw new ArgumentNullException(nameof(str)); | ||
} | ||
|
||
StringBuilder tokenBuilder = new StringBuilder(); | ||
|
||
var last = '\0'; | ||
for (int i = 0; i < str.Length; ++i) | ||
{ | ||
var current = str[i]; | ||
|
||
if (last == FilterHelpers.EscapePrefix) | ||
{ | ||
// Don't check if `current` is one of the special characters here. | ||
// Instead, we blindly let any character follows '\' pass though and | ||
// relies on `FiltetrHelpers.Unescape` to report such errors. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: pass through*/ FilterHelpers* (Same in the other method) |
||
tokenBuilder.Append(current); | ||
|
||
if (current == FilterHelpers.EscapePrefix) | ||
{ | ||
// We just encountered "\\" (escaped '\'), this will set last to '\0' | ||
// so the next char will not be treated as a suffix of escape sequence. | ||
current = '\0'; | ||
} | ||
} | ||
else | ||
{ | ||
switch (current) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just put a comment telling the allowed special tokens, eg "=", "!=", "!", "~" There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When do we expect just "!" ? Can you share an example, also add to tests. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We don't expect '!', but it's not handled in tokenizer. Instead, an exception is thrown in |
||
{ | ||
case '=': | ||
if (tokenBuilder.Length > 0) | ||
{ | ||
yield return tokenBuilder.ToString(); | ||
tokenBuilder.Clear(); | ||
} | ||
yield return "="; | ||
break; | ||
|
||
case '!': | ||
if (tokenBuilder.Length > 0) | ||
{ | ||
yield return tokenBuilder.ToString(); | ||
tokenBuilder.Clear(); | ||
} | ||
// Determine if this is a "!=" or just a single "!". | ||
var next = str[i + 1]; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This will cause There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good catch! Fixed |
||
if (next == '=') | ||
{ | ||
++i; | ||
current = next; | ||
yield return "!="; | ||
} | ||
else | ||
{ | ||
yield return "!"; | ||
} | ||
break; | ||
|
||
case '~': | ||
if (tokenBuilder.Length > 0) | ||
{ | ||
yield return tokenBuilder.ToString(); | ||
tokenBuilder.Clear(); | ||
} | ||
yield return "~"; | ||
break; | ||
|
||
default: | ||
tokenBuilder.Append(current); | ||
break; | ||
} | ||
} | ||
last = current; | ||
} | ||
|
||
if (tokenBuilder.Length > 0) | ||
{ | ||
yield return tokenBuilder.ToString(); | ||
} | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Delete this please