Add new function options to conditions in test runner (#611)
authorJuan Hoyos <juan.hoyos@microsoft.com>
Thu, 14 Nov 2019 19:40:42 +0000 (11:40 -0800)
committerGitHub <noreply@github.com>
Thu, 14 Nov 2019 19:40:42 +0000 (11:40 -0800)
Adds the following options to the test helper condition attributes:

* Contains(string, string)
* StartsWith(string, string)
* EndsWith(string, string)

And adds argument validation that throws exception - to prevent any more tests to be skipped silently.

src/Microsoft.Diagnostics.TestHelpers/TestConfiguration.cs

index d1dfbba4fee4d47da75777ee58701834c58f22f2..30c287694017cd62222021d36dd8bad7744dff88 100644 (file)
@@ -171,51 +171,146 @@ namespace Microsoft.Diagnostics.TestHelpers
             return templates;
         }
 
+        // Currently we only support single function clauses as follows
+        //  Exists('<string: file or directory name>')
+        //  StartsWith('<string>', '<string: prefix>')
+        //  EndsWith('<string>', '<string: postfix>')
+        //  Contains('<string>', '<string: substring>')
+        //  '<string>' == '<string>'
+        //  '<string>' != '<string>'
+        // strings support variable embedding with $(<var_name>). e.g Exists('$(PropsFile)')
         bool EvaluateConditional(Dictionary<string, string> config, XElement node)
         {
+            void ValidateAndResolveParameters(string funcName, int expectedParamCount, List<string> paramList)
+            {
+                if (paramList.Count != expectedParamCount)
+                {
+                    throw new InvalidDataException($"Expected {expectedParamCount} arguments for {funcName} in condition");
+                }
+
+                for (int i = 0; i < paramList.Count; i++)
+                {
+                    paramList[i] = ResolveProperties(config, paramList[i]);
+                }
+            }
+
             foreach (XAttribute attr in node.Attributes("Condition"))
             {
                 string conditionText = attr.Value;
+                bool isNegative = conditionText.Length > 0 && conditionText[0] == '!';
 
                 // Check if Exists('<directory or file>')
-                const string existsKeyword = "Exists('";
-                int existsStartIndex = conditionText.IndexOf(existsKeyword);
-                if (existsStartIndex != -1)
+                const string existsKeyword = "Exists";
+                if (TryGetParametersForFunction(conditionText, existsKeyword, out List<string> paramList))
+                {
+                    ValidateAndResolveParameters(existsKeyword, 1, paramList);
+                    bool exists = Directory.Exists(paramList[0]) || File.Exists(paramList[0]);
+                    return isNegative ? !exists : exists;
+                }
+
+                // Check if StartsWith('string', 'prefix')
+                const string startsWithKeyword = "StartsWith";
+                if (TryGetParametersForFunction(conditionText, startsWithKeyword, out paramList))
                 {
-                    bool not = (existsStartIndex > 0) && (conditionText[existsStartIndex - 1] == '!');
+                    ValidateAndResolveParameters(startsWithKeyword, 2, paramList);
+                    bool isPrefix = paramList[0].StartsWith(paramList[1]);
+                    return isNegative ? !isPrefix : isPrefix;
+                }
 
-                    existsStartIndex += existsKeyword.Length;
-                    int existsEndIndex = conditionText.IndexOf("')", existsStartIndex);
-                    Assert.NotEqual(-1, existsEndIndex);
+                // Check if EndsWith('string', 'postfix')
+                const string endsWithKeyword = "EndsWith";
+                if (TryGetParametersForFunction(conditionText, endsWithKeyword, out paramList))
+                {
+                    ValidateAndResolveParameters(endsWithKeyword, 2, paramList);
+                    bool isPostfix = paramList[0].EndsWith(paramList[1]);
+                    return isNegative ? !isPostfix : isPostfix;
+                }
 
-                    string path = conditionText.Substring(existsStartIndex, existsEndIndex - existsStartIndex);
-                    path = Path.GetFullPath(ResolveProperties(config, path));
-                    bool exists = Directory.Exists(path) || File.Exists(path);
-                    return not ? !exists : exists;
+                // Check if Contains('string', 'substring')
+                const string containsKeyword = "Contains";
+                if (TryGetParametersForFunction(conditionText, containsKeyword, out paramList))
+                {
+                    ValidateAndResolveParameters(containsKeyword, 2, paramList);
+                    bool isInString = paramList[0].Contains(paramList[1]);
+                    return isNegative ? !isInString : isInString;
                 }
-                else
+
+                // Check if equals and not equals
+                bool isEquals = conditionText.Contains("==");
+                bool isDifferent = conditionText.Contains("!=");
+                
+                if (isEquals == isDifferent) 
                 {
-                    // Check if equals and not equals
-                    string[] parts = conditionText.Split("==");
-                    bool equal;
+                    throw new InvalidDataException($"Unknown condition type in {conditionText}. See TestRunConfiguration.EvaluateConditional for supported values.");
+                }
+
+                string[] parts = isEquals ? conditionText.Split("==") : conditionText.Split("!=");
+
+                // Resolve any config values in the condition
+                string leftValue = ResolveProperties(config, parts[0]).Trim();
+                string rightValue = ResolveProperties(config, parts[1]).Trim();
+
+                // Now do the simple string comparison of the left/right sides of the condition
+                return isEquals ? leftValue == rightValue : leftValue != rightValue;
+            }
+            return true;
+        }
+
+        private bool TryGetParametersForFunction(string expression, string targetFunctionName, out List<string> exprParams)
+        {
+            int functionKeyworkIndex = expression.IndexOf($"{targetFunctionName}(");
+            if (functionKeyworkIndex == -1) {
+                exprParams = null;
+                return false;
+            }
 
-                    if (parts.Length == 2)
+            if (functionKeyworkIndex != 0 || functionKeyworkIndex >= 1 && expression[0] != '!' || !expression.EndsWith(')'))
+            {
+                throw new InvalidDataException($"Condition {expression} malformed. Currently only single-function conditions are supported.");
+            }
+
+            exprParams = new List<string>();
+            bool isWithinString = false;
+            bool expectDelimiter = false;
+            int curParsingIndex = functionKeyworkIndex + targetFunctionName.Length + 1;
+            StringBuilder resolvedValue = new StringBuilder();
+
+            // Account for the trailing parenthesis.
+            while(curParsingIndex + 1 < expression.Length)
+            {
+                char currentChar = expression[curParsingIndex];
+                // toggle string nesting on ', except if scaped
+                if (currentChar == '\'' && !(curParsingIndex > 0 && expression[curParsingIndex - 1] == '\\'))
+                {
+                    if (isWithinString)
                     {
-                        equal = true;
+                        exprParams.Add(resolvedValue.ToString());
+                        expectDelimiter = true;
                     }
-                    else
+
+                    isWithinString = !isWithinString;
+                }
+                else if (isWithinString)
+                {
+                    resolvedValue.Append(currentChar);
+                }
+                else if (currentChar == ',')
+                {
+                    if (!expectDelimiter)
                     {
-                        parts = conditionText.Split("!=");
-                        Assert.Equal(2, parts.Length);
-                        equal = false;
+                        throw new InvalidDataException($"Unexpected comma found within {expression}");
                     }
-                    // Resolve any config values in the condition
-                    string leftValue = ResolveProperties(config, parts[0]).Trim();
-                    string rightValue = ResolveProperties(config, parts[1]).Trim();
-
-                    // Now do the simple string comparison of the left/right sides of the condition
-                    return equal ? leftValue == rightValue : leftValue != rightValue;
+                    expectDelimiter = false;
+                }
+                else if (!Char.IsWhiteSpace(currentChar))
+                {
+                    throw new InvalidDataException($"Non whitespace, non comma value found outside of string within: {expression}");
                 }
+                curParsingIndex++;
+            }
+
+            if (isWithinString) {
+                throw new InvalidDataException($"Non-terminated string detected within {expression}");
             }
             return true;
         }