[lit] Support %if ... %else syntax for RUN lines

This syntax allows to modify RUN lines based on features
available. For example:

    RUN: ... | FileCheck %s --check-prefix=%if windows %{CHECK-W%} %else %{CHECK-NON-W%}
    CHECK-W: ...
    CHECK-NON-W: ...

The whole command can be put under %if ... %else:

    RUN: %if tool_available %{ %tool %} %else %{ true %}

or:

    RUN: %if tool_available %{ %tool %}

If tool_available feature is missing, we'll have an empty command in
this RUN line.  LIT used to emit an error for empty commands, but now
it treats such commands as nop in all cases.

Multi-line expressions are also supported:

    RUN: %if tool_available %{ \
    RUN:   %tool               \
    RUN: %} %else %{           \
    RUN:   true                \
    RUN: %}

Background and motivation:
D121727 [NVPTX] Integrate ptxas to LIT tests
https://reviews.llvm.org/D121727

Differential Revision: https://reviews.llvm.org/D122569
This commit is contained in:
Andrew Savonichev 2022-04-27 19:36:54 +03:00
parent 26a0d53b15
commit 1041a9642b
9 changed files with 250 additions and 10 deletions

View File

@ -612,6 +612,13 @@ RUN lines:
Example: ``Windows %errc_ENOENT: no such file or directory``
``%if feature %{<if branch>%} %else %{<else branch>%}``
Conditional substitution: if ``feature`` is available it expands to
``<if branch>``, otherwise it expands to ``<else branch>``.
``%else %{<else branch>%}`` is optional and treated like ``%else %{%}``
if not present.
**LLVM-specific substitutions:**
``%shlibext``

View File

@ -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)

View File

@ -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'))

View File

@ -0,0 +1,3 @@
# CHECK: ValueError: '%{' is missing for %if substitution
#
# RUN: %if feature echo "test-1"

View File

@ -0,0 +1,3 @@
# CHECK: ValueError: '%}' is missing for %if substitution
#
# RUN: %if feature %{ echo

View File

@ -0,0 +1,3 @@
# CHECK: ValueError: '%{' is missing for %else substitution
#
# RUN: %if feature %{ echo %} %else fail

View File

@ -0,0 +1,3 @@
# CHECK: ValueError: '%}' is missing for %else substitution
#
# RUN: %if feature %{ echo %} %else %{ fail

View File

@ -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

View File

@ -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