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;
}