diff --git a/llvm/docs/TestingGuide.rst b/llvm/docs/TestingGuide.rst index 04d9ccc5ba98..4bbc0972cdaf 100644 --- a/llvm/docs/TestingGuide.rst +++ b/llvm/docs/TestingGuide.rst @@ -612,6 +612,13 @@ RUN lines: 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`` diff --git a/llvm/utils/lit/lit/TestRunner.py b/llvm/utils/lit/lit/TestRunner.py index d3e655a324e4..de711b4eec2a 100644 --- a/llvm/utils/lit/lit/TestRunner.py +++ b/llvm/utils/lit/lit/TestRunner.py @@ -48,7 +48,10 @@ kDevNull = "/dev/null" # 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 _executeShCmd(cmd, shenv, results, timeoutHelper): 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 @@ def executeScript(test, litConfig, tmpBase, commands, cwd): 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 @@ def executeScript(test, litConfig, tmpBase, commands, cwd): 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,8 @@ def _memoize(f): 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 @@ -1193,14 +1210,103 @@ def applySubstitutions(script, substitutions, recursion_limit=None): """ # We use #_MARKER_# to hide %% while we do the other substitutions. - def escape(ln): + def escapePercents(ln): return _caching_re_compile('%%').sub('#_MARKER_#', ln) - def unescape(ln): + def unescapePercents(ln): return _caching_re_compile('#_MARKER_#').sub('%', ln) + def substituteIfElse(ln): + # early exit to avoid wasting time on lines without + # conditional substitutions + 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 '%{' + match = _caching_re_compile('%{').search(ln) + if not match: + raise ValueError("'%{' is missing for %if substitution") + cond = ln[:match.start()] + + # eat '%{' as well + ln = ln[match.end():] + return cond, ln + + def tryParseElse(ln): + match = _caching_re_compile('^\s*%else\s*(%{)?').search(ln) + if not match: + return False, ln + if not match.group(1): + raise ValueError("'%{' is missing for %else substitution") + return True, ln[match.end():] + + def tryParseEnd(ln): + if ln.startswith('%}'): + return True, ln[2:] + return False, ln + + def parseText(ln, isNested): + # parse everything until %if, or %} if we're parsing a + # nested expression. + match = _caching_re_compile( + '(.*?)(?:%if|%})' if isNested else '(.*?)(?:%if)').search(ln) + if not match: + # there is no terminating pattern, so treat the whole + # line as text + return ln, '' + text_end = match.end(1) + return ln[:text_end], ln[text_end:] + + def parseRecursive(ln, isNested): + result = '' + while len(ln): + if isNested: + found_end, _ = tryParseEnd(ln) + if found_end: + break + + # %if cond %{ branch_if %} %else %{ branch_else %} + cond, ln = tryParseIfCond(ln) + if cond: + branch_if, ln = parseRecursive(ln, isNested=True) + found_end, ln = tryParseEnd(ln) + if not found_end: + raise ValueError("'%}' is missing for %if substitution") + + branch_else = '' + found_else, ln = tryParseElse(ln) + if found_else: + branch_else, ln = parseRecursive(ln, isNested=True) + found_end, ln = tryParseEnd(ln) + if not found_end: + raise ValueError("'%}' is missing for %else substitution") + + if BooleanExpression.evaluate(cond, conditions): + result += branch_if + else: + result += branch_else + continue + + # The rest is handled as plain text. + text, ln = parseText(ln, isNested) + result += text + + return result, ln + + result, ln = parseRecursive(ln, isNested=False) + assert len(ln) == 0 + return result + def processLine(ln): # Apply substitutions + ln = substituteIfElse(escapePercents(ln)) for a,b in substitutions: if kIsWindows: b = b.replace("\\","\\\\") @@ -1211,7 +1317,7 @@ def applySubstitutions(script, substitutions, recursion_limit=None): # short-lived, since the set of substitutions is fairly small, and # since thrashing has such bad consequences, not bounding the cache # seems reasonable. - ln = _caching_re_compile(a).sub(str(b), escape(ln)) + ln = _caching_re_compile(a).sub(str(b), escapePercents(ln)) # Strip the trailing newline and any extra whitespace. return ln.strip() @@ -1235,7 +1341,7 @@ def applySubstitutions(script, substitutions, recursion_limit=None): process = processLine if recursion_limit is None else processLineToFixedPoint - return [unescape(process(ln)) for ln in script] + return [unescapePercents(process(ln)) for ln in script] class ParserKind(object): @@ -1610,7 +1716,8 @@ def executeShTest(test, litConfig, useExternalSh, 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) diff --git a/llvm/utils/lit/tests/Inputs/shtest-if-else/lit.cfg b/llvm/utils/lit/tests/Inputs/shtest-if-else/lit.cfg new file mode 100644 index 000000000000..b2243df51c20 --- /dev/null +++ b/llvm/utils/lit/tests/Inputs/shtest-if-else/lit.cfg @@ -0,0 +1,8 @@ +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') +config.substitutions.append(('%{sub}', 'ok')) diff --git a/llvm/utils/lit/tests/Inputs/shtest-if-else/test-neg1.txt b/llvm/utils/lit/tests/Inputs/shtest-if-else/test-neg1.txt new file mode 100644 index 000000000000..ce748526979b --- /dev/null +++ b/llvm/utils/lit/tests/Inputs/shtest-if-else/test-neg1.txt @@ -0,0 +1,3 @@ +# CHECK: ValueError: '%{' is missing for %if substitution +# +# RUN: %if feature echo "test-1" diff --git a/llvm/utils/lit/tests/Inputs/shtest-if-else/test-neg2.txt b/llvm/utils/lit/tests/Inputs/shtest-if-else/test-neg2.txt new file mode 100644 index 000000000000..ae7ad887a06f --- /dev/null +++ b/llvm/utils/lit/tests/Inputs/shtest-if-else/test-neg2.txt @@ -0,0 +1,3 @@ +# CHECK: ValueError: '%}' is missing for %if substitution +# +# RUN: %if feature %{ echo diff --git a/llvm/utils/lit/tests/Inputs/shtest-if-else/test-neg3.txt b/llvm/utils/lit/tests/Inputs/shtest-if-else/test-neg3.txt new file mode 100644 index 000000000000..ed6594c23827 --- /dev/null +++ b/llvm/utils/lit/tests/Inputs/shtest-if-else/test-neg3.txt @@ -0,0 +1,3 @@ +# CHECK: ValueError: '%{' is missing for %else substitution +# +# RUN: %if feature %{ echo %} %else fail diff --git a/llvm/utils/lit/tests/Inputs/shtest-if-else/test-neg4.txt b/llvm/utils/lit/tests/Inputs/shtest-if-else/test-neg4.txt new file mode 100644 index 000000000000..0ee85f2df2ed --- /dev/null +++ b/llvm/utils/lit/tests/Inputs/shtest-if-else/test-neg4.txt @@ -0,0 +1,3 @@ +# CHECK: ValueError: '%}' is missing for %else substitution +# +# RUN: %if feature %{ echo %} %else %{ fail diff --git a/llvm/utils/lit/tests/Inputs/shtest-if-else/test.txt b/llvm/utils/lit/tests/Inputs/shtest-if-else/test.txt new file mode 100644 index 000000000000..805a74de3a7e --- /dev/null +++ b/llvm/utils/lit/tests/Inputs/shtest-if-else/test.txt @@ -0,0 +1,92 @@ +# 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 +# RUN: echo test-%if nofeature %{ fail %} %else %{ 5 5 %}-test +# CHECK-NEXT: {{^.*'RUN}}: at line [[#@LINE-3]]'; echo test- 3 -test +# CHECK-NEXT: {{^.*'RUN}}: at line [[#@LINE-3]]'; echo test- 4 4 -test +# CHECK-NEXT: {{^.*'RUN}}: at line [[#@LINE-3]]'; echo test- 5 5 -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. Without %if..%else context '%{' and '%}' are treated +# literally. +# +# 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 '}' can be used without escaping +# +# 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" + +# Substitutions with braces are handled correctly +# +# RUN: echo %{sub} %if feature%{test-%{sub}%}%else%{"fail"%} +# CHECK-NEXT: {{^.*'RUN}}: at line [[#@LINE-1]]'; echo ok test-ok + +# CHECK-NEXT: -- +# CHECK-NEXT: Exit Code: 0 diff --git a/llvm/utils/lit/tests/shtest-if-else.py b/llvm/utils/lit/tests/shtest-if-else.py new file mode 100644 index 000000000000..aaf94a6e2437 --- /dev/null +++ b/llvm/utils/lit/tests/shtest-if-else.py @@ -0,0 +1,14 @@ +# RUN: %{lit} -v --show-all %{inputs}/shtest-if-else/test.txt \ +# RUN: | FileCheck %{inputs}/shtest-if-else/test.txt --match-full-lines + +# RUN: not %{lit} -v --show-all %{inputs}/shtest-if-else/test-neg1.txt 2>&1 \ +# RUN: | FileCheck %{inputs}/shtest-if-else/test-neg1.txt + +# RUN: not %{lit} -v --show-all %{inputs}/shtest-if-else/test-neg2.txt 2>&1 \ +# RUN: | FileCheck %{inputs}/shtest-if-else/test-neg2.txt + +# RUN: not %{lit} -v --show-all %{inputs}/shtest-if-else/test-neg3.txt 2>&1 \ +# RUN: | FileCheck %{inputs}/shtest-if-else/test-neg3.txt + +# RUN: not %{lit} -v --show-all %{inputs}/shtest-if-else/test-neg4.txt 2>&1 \ +# RUN: | FileCheck %{inputs}/shtest-if-else/test-neg4.txt