Skip to content

Commit

Permalink
Add support to escape/unescape testcase filter strings (#1627)
Browse files Browse the repository at this point in the history
* Add support to escape/unescape testcase filter strings

* Add a filter escape test

* Change escape sequences to be backslash as prefix plus escaped character.

* Remove pref tests

* Move FilterHelper to ObjectModel

* Revert unintentional change to resource

* Address review comments
  • Loading branch information
genlu authored and smadala committed Jun 5, 2018
1 parent a92a6be commit e1d4b3b
Show file tree
Hide file tree
Showing 25 changed files with 697 additions and 41 deletions.
117 changes: 99 additions & 18 deletions src/Microsoft.TestPlatform.Common/Filtering/Condition.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ namespace Microsoft.VisualStudio.TestPlatform.Common.Filtering
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Text.RegularExpressions;

using System.Text;
using Microsoft.VisualStudio.TestPlatform.ObjectModel;
using Microsoft.VisualStudio.TestPlatform.ObjectModel.Utilities;
using CommonResources = Microsoft.VisualStudio.TestPlatform.Common.Resources.Resources;

internal enum Operation
Expand Down Expand Up @@ -40,11 +40,6 @@ internal enum Operator
internal class Condition
{
#region Fields
/// <summary>
/// String seperator used for parsing input string of format '<propertyName>Operation<propertyValue>'
/// ! is not a valid operation, but is required to filter the invalid patterns.
/// </summary>
private static string propertyNameValueSeperatorString = @"(\!\=)|(\=)|(\~)|(\!)";

/// <summary>
/// Default property name which will be used when filter has only property value.
Expand Down Expand Up @@ -99,7 +94,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 @@ -155,10 +150,7 @@ internal bool Evaluate(Func<string, Object> propertyValueProvider)
break;
}
return result;
}



}

/// <summary>
/// Returns a condition object after parsing input string of format '<propertyName>Operation<propertyValue>'
Expand All @@ -169,12 +161,13 @@ internal static Condition Parse(string conditionString)
{
ThrownFormatExceptionForInvalidCondition(conditionString);
}
string[] parts = Regex.Split(conditionString, propertyNameValueSeperatorString);

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(Condition.DefaultPropertyName, Condition.DefaultOperation, conditionString.Trim());
return new Condition(Condition.DefaultPropertyName, Condition.DefaultOperation, FilterHelper.Unescape(conditionString.Trim()));
}

if (parts.Length != 3)
Expand All @@ -192,16 +185,15 @@ 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, FilterHelper.Unescape(parts[2]));
return condition;
}

private static void ThrownFormatExceptionForInvalidCondition(string conditionString)
{
throw new FormatException(string.Format(CultureInfo.CurrentCulture, CommonResources.TestCaseFilterFormatException,
string.Format(CultureInfo.CurrentCulture, CommonResources.InvalidCondition, conditionString)));
}

}

/// <summary>
/// Check if condition validates any property in properties.
Expand Down Expand Up @@ -281,7 +273,96 @@ private string[] GetPropertyValue(Func<string, Object> propertyValueProvider)
}

return null;
}
}

internal static IEnumerable<string> TokenizeFilterConditionString(string str)
{
if (str == null)
{
throw new ArgumentNullException(nameof(str));
}

return TokenizeFilterConditionStringWorker(str);

IEnumerable<string> TokenizeFilterConditionStringWorker(string s)
{
StringBuilder tokenBuilder = new StringBuilder();

var last = '\0';
for (int i = 0; i < s.Length; ++i)
{
var current = s[i];

if (last == FilterHelper.EscapeCharacter)
{
// Don't check if `current` is one of the special characters here.
// Instead, we blindly let any character follows '\' pass though and
// relies on `FilterHelpers.Unescape` to report such errors.
tokenBuilder.Append(current);

if (current == FilterHelper.EscapeCharacter)
{
// We just encountered double backslash (i.e. escaped '\'), therefore set `last` to '\0'
// so the second '\' (i.e. current) will not be treated as the prefix of escape sequence
// in next iteration.
current = '\0';
}
}
else
{
switch (current)
{
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 = i + 1;
if (next < s.Length && s[next] == '=')
{
i = next;
current = '=';
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();
}
}
}
}
}
86 changes: 73 additions & 13 deletions src/Microsoft.TestPlatform.Common/Filtering/FilterExpression.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@ namespace Microsoft.VisualStudio.TestPlatform.Common.Filtering
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;

using Microsoft.VisualStudio.TestPlatform.ObjectModel;
using Microsoft.VisualStudio.TestPlatform.ObjectModel.Utilities;
using CommonResources = Microsoft.VisualStudio.TestPlatform.Common.Resources.Resources;

/// <summary>
Expand All @@ -22,12 +24,6 @@ namespace Microsoft.VisualStudio.TestPlatform.Common.Filtering
/// </summary>
internal class FilterExpression
{

/// <summary>
/// Seperator string to seperate various tokens in input string.
/// </summary>
private const string filterExpressionSeperatorString = @"(\&)|(\|)|(\()|(\))";

/// <summary>
/// Condition, if expression is conditional expression.
/// </summary>
Expand All @@ -52,8 +48,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 +58,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,16 +164,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).
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 = TokenizeFilterExpressionString(filterString);
var operatorStack = new Stack<Operator>();
var filterStack = new Stack<FilterExpression>();

Expand Down Expand Up @@ -283,7 +279,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 All @@ -305,6 +301,70 @@ internal bool Evaluate(Func<string, Object> propertyValueProvider)
}
}
return filterResult;
}

internal static IEnumerable<string> TokenizeFilterExpressionString(string str)
{
if (str == null)
{
throw new ArgumentNullException(nameof(str));
}

return TokenizeFilterExpressionStringHelper(str);

IEnumerable<string> TokenizeFilterExpressionStringHelper(string s)
{
StringBuilder tokenBuilder = new StringBuilder();

var last = '\0';
for (int i = 0; i < s.Length; ++i)
{
var current = s[i];

if (last == FilterHelper.EscapeCharacter)
{
// Don't check if `current` is one of the special characters here.
// Instead, we blindly let any character follows '\' pass though and
// relies on `FilterHelpers.Unescape` to report such errors.
tokenBuilder.Append(current);

if (current == FilterHelper.EscapeCharacter)
{
// 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();
}
}
}
}
}
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
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

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

Original file line number Diff line number Diff line change
Expand Up @@ -344,4 +344,7 @@
<data name="MissingLoggerAttributes" xml:space="preserve">
<value>Invalid settings '{0}'. Expected atleast one of the XmlAttribute among friendlyName, uri and assemblyQualifiedName.</value>
</data>
<data name="TestCaseFilterEscapeException" xml:space="preserve">
<value>Filter string '{0}' includes unrecognized escape sequence.</value>
</data>
</root>
Original file line number Diff line number Diff line change
Expand Up @@ -612,6 +612,11 @@
<target state="translated">Neplatné nastavení {0}. Očekával se alespoň jeden XmlAttribute: popisný název (friendlyName), identifikátor URI (uri) nebo kvalifikovaný název sestavení (assemblyQualifiedName).</target>
<note />
</trans-unit>
<trans-unit id="TestCaseFilterEscapeException">
<source>Filter string '{0}' includes unrecognized escape sequence.</source>
<target state="new">Filter string '{0}' includes unrecognized escape sequence.</target>
<note></note>
</trans-unit>
</body>
</file>
</xliff>
Loading

0 comments on commit e1d4b3b

Please sign in to comment.