Index: llvm/docs/TestingGuide.rst =================================================================== --- llvm/docs/TestingGuide.rst +++ llvm/docs/TestingGuide.rst @@ -612,6 +612,13 @@ Example: ``Windows %errc_ENOENT: no such file or directory`` +``%if feature {} %else {}`` + + Conditional substitution: if ``feature`` is available it expands to + ````, otherwise it expands to ````. + ``%else {}`` is optional and treated like ``%else {}`` + if not present. + **LLVM-specific substitutions:** ``%shlibext`` Index: llvm/utils/lit/lit/TestRunner.py =================================================================== --- llvm/utils/lit/lit/TestRunner.py +++ llvm/utils/lit/lit/TestRunner.py @@ -48,7 +48,10 @@ # This regex captures ARG. ARG must not contain a right parenthesis, which # terminates %dbg. ARG must not contain quotes, in which ARG might be enclosed # during expansion. -kPdbgRegex = '%dbg\\(([^)\'"]*)\\)' +# +# COMMAND that follows %dbg(ARG) is also captured. COMMAND can be +# empty as a result of conditinal substitution. +kPdbgRegex = '%dbg\\(([^)\'"]*)\\)(.*)' class ShellEnvironment(object): @@ -899,7 +902,11 @@ def executeScriptInternal(test, litConfig, tmpBase, commands, cwd): cmds = [] for i, ln in enumerate(commands): - ln = commands[i] = re.sub(kPdbgRegex, ": '\\1'; ", ln) + match = re.match(kPdbgRegex, ln) + if match: + command = match.group(2) + ln = commands[i] = \ + match.expand(": '\\1'; \\2" if command else ": '\\1'") try: cmds.append(ShUtil.ShParser(ln, litConfig.isWindows, test.config.pipefail).parse()) @@ -987,7 +994,12 @@ f = open(script, mode, **open_kwargs) if isWin32CMDEXE: for i, ln in enumerate(commands): - commands[i] = re.sub(kPdbgRegex, "echo '\\1' > nul && ", ln) + match = re.match(kPdbgRegex, ln) + if match: + command = match.group(2) + commands[i] = \ + match.expand("echo '\\1' > nul && " if command + else "echo '\\1' > nul") if litConfig.echo_all_commands: f.write('@echo on\n') else: @@ -995,7 +1007,11 @@ f.write('\n@if %ERRORLEVEL% NEQ 0 EXIT\n'.join(commands)) else: for i, ln in enumerate(commands): - commands[i] = re.sub(kPdbgRegex, ": '\\1'; ", ln) + match = re.match(kPdbgRegex, ln) + if match: + command = match.group(2) + commands[i] = match.expand(": '\\1'; \\2" if command + else ": '\\1'") if test.config.pipefail: f.write(b'set -o pipefail;' if mode == 'wb' else 'set -o pipefail;') if litConfig.echo_all_commands: @@ -1179,7 +1195,7 @@ def _caching_re_compile(r): return re.compile(r) -def applySubstitutions(script, substitutions, recursion_limit=None): +def applySubstitutions(script, substitutions, conditions={}, recursion_limit=None): """ Apply substitutions to the script. Allow full regular expression syntax. Replace each matching occurrence of regular expression pattern a with @@ -1192,15 +1208,148 @@ `None` (the default), no recursive substitution is performed at all. """ - # We use #_MARKER_# to hide %% while we do the other substitutions. + # We use #_MARKERN_# to hide %%, %{ and %} while we do the other + # substitutions. def escape(ln): - return _caching_re_compile('%%').sub('#_MARKER_#', ln) - + return _caching_re_compile('%%').sub('#_MARKER0_#', ln) def unescape(ln): - return _caching_re_compile('#_MARKER_#').sub('%', ln) + return _caching_re_compile('#_MARKER0_#').sub('%', ln) + + def escapeBraces(ln): + repl = { '{': '#_MARKER1_#', '}': '#_MARKER2_#' } + return _caching_re_compile('%(\{|\})').sub( + lambda match: repl[match.group(1)], ln) + + def unescapeBraces(ln): + repl = { '#_MARKER1_#': '{', '#_MARKER2_#': '}' } + return _caching_re_compile('#_MARKER[1-2]_#').sub( + lambda match: repl[match.group()], ln) + + def substituteIfElse(ln): + if ln.find('%if ') == -1: + return ln + + def tryParseIfCond(ln): + # space is important to not conflict with other (possible) + # substitutions + if not ln.startswith('%if '): + return None, ln + ln = ln[4:] + + # stop at '{', but treat '{{' (syntax for regex match) as + # part of the condition expression. + match = _caching_re_compile('[^{]{[^{]').search(ln) + if not match: + raise ValueError("malformed %if substitution") + cond = ln[:match.end() - 2] + + # eat '{' as well + ln = ln[match.end() - 1:] + return cond, ln + + def tryParseElse(ln): + match = _caching_re_compile('^\s*%else\s*{').search(ln) + if not match: + return False, ln + return True, ln[match.end():] + + def tryParseEnd(ln): + if ln.startswith('}'): + return True, ln[1:] + return False, ln + + def parseText(ln): + # parse everything until %if or '}' + match = _caching_re_compile('(.*?)(?:%if|})').search(ln) + if not match: + # there is no terminating %if or }, so treat the whole + # line as text + return ln, '' + text_end = match.end(1) + assert text_end > 0 + return ln[:text_end], ln[text_end:] + + def evalExprList(expr_list): + result = '' + # expr is either or a plain string or an '%if..%else' + # expression (a list); evaluate and concatenate all items + # into a single string. + for expr in expr_list: + if not isinstance(expr, list): + assert isinstance(expr, str) + result += expr + continue + + assert expr[0] is token_if + cond = expr[1] + branch_if = expr[2] + branch_else = expr[3] if len(expr) > 3 else '' + if BooleanExpression.evaluate(cond, conditions): + result += branch_if + else: + result += branch_else + + return result + + # escape '%{' and '%}' to avoid matching them as begin/end + ln = escapeBraces(ln) + + # Each level of the stack is a list of strings and + # lists. Lists represent %if..%else expressions. For example: + # + # RUN: foo %if feature {if_branch} %else {else_branch} + # Stack: [['foo', [token_if, 'feature', 'if_branch', 'else_branch']]] + # + stack = [[]] + token_if = object() + + while len(ln): + # %if starts a new scope ('if' branch). Push '%if cond' to + # the stack and create a new stack level for the body. + cond, ln = tryParseIfCond(ln) + if cond: + stack[-1].append([token_if, cond]) + stack.append([]) + continue + + # %else also starts a new scope for the 'else' branch. + found_else, ln = tryParseElse(ln) + if found_else: + current_expr = stack[-1][-1] + if current_expr[0] is not token_if: + raise ValueError("unexpected '%else'") + stack.append([]) + continue + + # '}' terminates the current scope. Evaluate everything on + # the current stack level and push the result to the last + # expression of the higher stack level. + found_end, ln = tryParseEnd(ln) + if found_end: + result = evalExprList(stack.pop()) + + # push the result to the outer %if or %else branch + outer_expr = stack[-1][-1] + if not outer_expr or not isinstance(outer_expr, list): + raise ValueError("unexpected '}'") + outer_expr.append(result) + continue + + # The rest is handled as plain text. Parse it until the + # first non-text element: '%if' or '}' + text, ln = parseText(ln) + stack[-1].append(text) + + # We parsed everything, the line is now empty. We're supposed + # to have a single stack level with only top-most expressions. + if len(stack) != 1: + raise ValueError("unballanced '%if' expession") + ln = evalExprList(stack.pop()) + return unescapeBraces(ln) def processLine(ln): # Apply substitutions + ln = substituteIfElse(escape(ln)) for a,b in substitutions: if kIsWindows: b = b.replace("\\","\\\\") @@ -1610,7 +1759,8 @@ substitutions = list(extra_substitutions) substitutions += getDefaultSubstitutions(test, tmpDir, tmpBase, normalize_slashes=useExternalSh) - script = applySubstitutions(script, substitutions, + conditions = { feature: True for feature in test.config.available_features } + script = applySubstitutions(script, substitutions, conditions, recursion_limit=test.config.recursiveExpansionLimit) return _runShTest(test, litConfig, useExternalSh, script, tmpBase) Index: llvm/utils/lit/tests/Inputs/shtest-if-else/lit.cfg =================================================================== --- /dev/null +++ llvm/utils/lit/tests/Inputs/shtest-if-else/lit.cfg @@ -0,0 +1,7 @@ +import lit.formats +config.name = 'shtest-if-else' +config.test_format = lit.formats.ShTest() +config.test_source_root = None +config.test_exec_root = None +config.suffixes = ['.txt'] +config.available_features.add("feature") Index: llvm/utils/lit/tests/Inputs/shtest-if-else/test.txt =================================================================== --- /dev/null +++ llvm/utils/lit/tests/Inputs/shtest-if-else/test.txt @@ -0,0 +1,84 @@ +# CHECK: -- Testing:{{.*}} +# CHECK-NEXT: PASS: shtest-if-else :: test.txt (1 of 1) +# CHECK-NEXT: Script: +# CHECK-NEXT: -- + +# RUN: %if feature { echo "test-1" } +# CHECK-NEXT: {{^.*'RUN}}: at line [[#@LINE-1]]'; echo "test-1" + +# If %else is not present it is treated like %else {}. Empty commands +# are ignored. +# +# RUN: %if nofeature { echo "fail" } +# CHECK-NEXT: {{^.*'RUN}}: at line [[#@LINE-1]]' +# CHECK-NOT: fail + +# RUN: %if nofeature { echo "fail" } %else { echo "test-2" } +# CHECK-NEXT: {{^.*'RUN}}: at line [[#@LINE-1]]'; echo "test-2" + +# Spaces inside curly braces are not ignored +# +# RUN: echo test-%if feature { 3 } %else { fail }-test +# RUN: echo test-%if feature { 4 4 } %else { fail }-test +# CHECK-NEXT: {{^.*'RUN}}: at line [[#@LINE-2]]'; echo test- 3 -test +# CHECK-NEXT: {{^.*'RUN}}: at line [[#@LINE-2]]'; echo test- 4 4 -test + +# Escape line breaks for multi-line expressions +# +# RUN: %if feature \ +# RUN: { echo \ +# RUN: "test-5" \ +# RUN: } +# CHECK-NEXT: {{^.*'RUN}}: at line [[#@LINE-4]]'; echo "test-5" + +# RUN: %if nofeature \ +# RUN: { echo "fail" } \ +# RUN: %else \ +# RUN: { echo "test-6" } +# CHECK-NEXT: {{^.*'RUN}}: at line [[#@LINE-4]]'; echo "test-6" + +# RUN: echo "test%if feature {} %else {}-7" +# CHECK-NEXT: {{^.*'RUN}}: at line [[#@LINE-1]]'; echo "test-7" + +# Escape %if +# +# RUN: echo %%if feature { echo "test-8" } +# CHECK-NEXT: {{^.*'RUN}}: at line [[#@LINE-1]]'; echo %if feature { echo "test-8" } + +# Nested expressions are supported: +# +# RUN: echo %if feature { %if feature { %if nofeature {"fail"} %else {"test-9"} } } +# CHECK-NEXT: {{^.*'RUN}}: at line [[#@LINE-1]]'; echo "test-9" + +# Binary expression evaluation and regex match can be used as +# conditions. +# +# RUN: echo %if feature && !nofeature { "test-10" } +# RUN: echo %if feature && nofeature { "fail" } %else { "test-11" } +# RUN: echo %if {{fea.+}} { "test-12" } %else { "fail" } +# CHECK-NEXT: {{^.*'RUN}}: at line [[#@LINE-3]]'; echo "test-10" +# CHECK-NEXT: {{^.*'RUN}}: at line [[#@LINE-3]]'; echo "test-11" +# CHECK-NEXT: {{^.*'RUN}}: at line [[#@LINE-3]]'; echo "test-12" + +# Spaces between %if and %else are ignored. If there is no %else - +# space after %if {...} is not ignored. +# +# RUN: echo XX %if feature {YY} ZZ +# RUN: echo AA %if feature {BB} %else {CC} DD +# RUN: echo AA %if nofeature {BB} %else {CC} DD +# CHECK-NEXT: {{^.*'RUN}}: at line [[#@LINE-3]]'; echo XX YY ZZ +# CHECK-NEXT: {{^.*'RUN}}: at line [[#@LINE-3]]'; echo AA BB DD +# CHECK-NEXT: {{^.*'RUN}}: at line [[#@LINE-3]]'; echo AA CC DD + +# '{' and '}' should be escaped as '%{' and '%}' +# +# RUN: %if feature {echo %{%}} +# CHECK-NEXT: {{^.*'RUN}}: at line [[#@LINE-1]]'; echo {} + +# Spaces are not required +# +# RUN: echo %if feature{"ok"}%else{"fail"} +# CHECK-NEXT: {{^.*'RUN}}: at line [[#@LINE-1]]'; echo "ok" + +# CHECK-NEXT: -- +# CHECK-NEXT: Exit Code: 0 Index: llvm/utils/lit/tests/shtest-if-else.py =================================================================== --- /dev/null +++ llvm/utils/lit/tests/shtest-if-else.py @@ -0,0 +1 @@ +# RUN: %{lit} -v --show-all %{inputs}/shtest-if-else/test.txt | FileCheck %{inputs}/shtest-if-else/test.txt --match-full-lines