Skip to content

Commit 1041a96

Browse files
committed
[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
1 parent 26a0d53 commit 1041a96

File tree

9 files changed

+250
-10
lines changed

9 files changed

+250
-10
lines changed

llvm/docs/TestingGuide.rst

+7
Original file line numberDiff line numberDiff line change
@@ -612,6 +612,13 @@ RUN lines:
612612

613613
Example: ``Windows %errc_ENOENT: no such file or directory``
614614

615+
``%if feature %{<if branch>%} %else %{<else branch>%}``
616+
617+
Conditional substitution: if ``feature`` is available it expands to
618+
``<if branch>``, otherwise it expands to ``<else branch>``.
619+
``%else %{<else branch>%}`` is optional and treated like ``%else %{%}``
620+
if not present.
621+
615622
**LLVM-specific substitutions:**
616623

617624
``%shlibext``

llvm/utils/lit/lit/TestRunner.py

+117-10
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,10 @@ def __init__(self, command, message):
4848
# This regex captures ARG. ARG must not contain a right parenthesis, which
4949
# terminates %dbg. ARG must not contain quotes, in which ARG might be enclosed
5050
# during expansion.
51-
kPdbgRegex = '%dbg\\(([^)\'"]*)\\)'
51+
#
52+
# COMMAND that follows %dbg(ARG) is also captured. COMMAND can be
53+
# empty as a result of conditinal substitution.
54+
kPdbgRegex = '%dbg\\(([^)\'"]*)\\)(.*)'
5255

5356
class ShellEnvironment(object):
5457

@@ -899,7 +902,11 @@ def _executeShCmd(cmd, shenv, results, timeoutHelper):
899902
def executeScriptInternal(test, litConfig, tmpBase, commands, cwd):
900903
cmds = []
901904
for i, ln in enumerate(commands):
902-
ln = commands[i] = re.sub(kPdbgRegex, ": '\\1'; ", ln)
905+
match = re.match(kPdbgRegex, ln)
906+
if match:
907+
command = match.group(2)
908+
ln = commands[i] = \
909+
match.expand(": '\\1'; \\2" if command else ": '\\1'")
903910
try:
904911
cmds.append(ShUtil.ShParser(ln, litConfig.isWindows,
905912
test.config.pipefail).parse())
@@ -987,15 +994,24 @@ def executeScript(test, litConfig, tmpBase, commands, cwd):
987994
f = open(script, mode, **open_kwargs)
988995
if isWin32CMDEXE:
989996
for i, ln in enumerate(commands):
990-
commands[i] = re.sub(kPdbgRegex, "echo '\\1' > nul && ", ln)
997+
match = re.match(kPdbgRegex, ln)
998+
if match:
999+
command = match.group(2)
1000+
commands[i] = \
1001+
match.expand("echo '\\1' > nul && " if command
1002+
else "echo '\\1' > nul")
9911003
if litConfig.echo_all_commands:
9921004
f.write('@echo on\n')
9931005
else:
9941006
f.write('@echo off\n')
9951007
f.write('\n@if %ERRORLEVEL% NEQ 0 EXIT\n'.join(commands))
9961008
else:
9971009
for i, ln in enumerate(commands):
998-
commands[i] = re.sub(kPdbgRegex, ": '\\1'; ", ln)
1010+
match = re.match(kPdbgRegex, ln)
1011+
if match:
1012+
command = match.group(2)
1013+
commands[i] = match.expand(": '\\1'; \\2" if command
1014+
else ": '\\1'")
9991015
if test.config.pipefail:
10001016
f.write(b'set -o pipefail;' if mode == 'wb' else 'set -o pipefail;')
10011017
if litConfig.echo_all_commands:
@@ -1179,7 +1195,8 @@ def memoized(x):
11791195
def _caching_re_compile(r):
11801196
return re.compile(r)
11811197

1182-
def applySubstitutions(script, substitutions, recursion_limit=None):
1198+
def applySubstitutions(script, substitutions, conditions={},
1199+
recursion_limit=None):
11831200
"""
11841201
Apply substitutions to the script. Allow full regular expression syntax.
11851202
Replace each matching occurrence of regular expression pattern a with
@@ -1193,14 +1210,103 @@ def applySubstitutions(script, substitutions, recursion_limit=None):
11931210
"""
11941211

11951212
# We use #_MARKER_# to hide %% while we do the other substitutions.
1196-
def escape(ln):
1213+
def escapePercents(ln):
11971214
return _caching_re_compile('%%').sub('#_MARKER_#', ln)
11981215

1199-
def unescape(ln):
1216+
def unescapePercents(ln):
12001217
return _caching_re_compile('#_MARKER_#').sub('%', ln)
12011218

1219+
def substituteIfElse(ln):
1220+
# early exit to avoid wasting time on lines without
1221+
# conditional substitutions
1222+
if ln.find('%if ') == -1:
1223+
return ln
1224+
1225+
def tryParseIfCond(ln):
1226+
# space is important to not conflict with other (possible)
1227+
# substitutions
1228+
if not ln.startswith('%if '):
1229+
return None, ln
1230+
ln = ln[4:]
1231+
1232+
# stop at '%{'
1233+
match = _caching_re_compile('%{').search(ln)
1234+
if not match:
1235+
raise ValueError("'%{' is missing for %if substitution")
1236+
cond = ln[:match.start()]
1237+
1238+
# eat '%{' as well
1239+
ln = ln[match.end():]
1240+
return cond, ln
1241+
1242+
def tryParseElse(ln):
1243+
match = _caching_re_compile('^\s*%else\s*(%{)?').search(ln)
1244+
if not match:
1245+
return False, ln
1246+
if not match.group(1):
1247+
raise ValueError("'%{' is missing for %else substitution")
1248+
return True, ln[match.end():]
1249+
1250+
def tryParseEnd(ln):
1251+
if ln.startswith('%}'):
1252+
return True, ln[2:]
1253+
return False, ln
1254+
1255+
def parseText(ln, isNested):
1256+
# parse everything until %if, or %} if we're parsing a
1257+
# nested expression.
1258+
match = _caching_re_compile(
1259+
'(.*?)(?:%if|%})' if isNested else '(.*?)(?:%if)').search(ln)
1260+
if not match:
1261+
# there is no terminating pattern, so treat the whole
1262+
# line as text
1263+
return ln, ''
1264+
text_end = match.end(1)
1265+
return ln[:text_end], ln[text_end:]
1266+
1267+
def parseRecursive(ln, isNested):
1268+
result = ''
1269+
while len(ln):
1270+
if isNested:
1271+
found_end, _ = tryParseEnd(ln)
1272+
if found_end:
1273+
break
1274+
1275+
# %if cond %{ branch_if %} %else %{ branch_else %}
1276+
cond, ln = tryParseIfCond(ln)
1277+
if cond:
1278+
branch_if, ln = parseRecursive(ln, isNested=True)
1279+
found_end, ln = tryParseEnd(ln)
1280+
if not found_end:
1281+
raise ValueError("'%}' is missing for %if substitution")
1282+
1283+
branch_else = ''
1284+
found_else, ln = tryParseElse(ln)
1285+
if found_else:
1286+
branch_else, ln = parseRecursive(ln, isNested=True)
1287+
found_end, ln = tryParseEnd(ln)
1288+
if not found_end:
1289+
raise ValueError("'%}' is missing for %else substitution")
1290+
1291+
if BooleanExpression.evaluate(cond, conditions):
1292+
result += branch_if
1293+
else:
1294+
result += branch_else
1295+
continue
1296+
1297+
# The rest is handled as plain text.
1298+
text, ln = parseText(ln, isNested)
1299+
result += text
1300+
1301+
return result, ln
1302+
1303+
result, ln = parseRecursive(ln, isNested=False)
1304+
assert len(ln) == 0
1305+
return result
1306+
12021307
def processLine(ln):
12031308
# Apply substitutions
1309+
ln = substituteIfElse(escapePercents(ln))
12041310
for a,b in substitutions:
12051311
if kIsWindows:
12061312
b = b.replace("\\","\\\\")
@@ -1211,7 +1317,7 @@ def processLine(ln):
12111317
# short-lived, since the set of substitutions is fairly small, and
12121318
# since thrashing has such bad consequences, not bounding the cache
12131319
# seems reasonable.
1214-
ln = _caching_re_compile(a).sub(str(b), escape(ln))
1320+
ln = _caching_re_compile(a).sub(str(b), escapePercents(ln))
12151321

12161322
# Strip the trailing newline and any extra whitespace.
12171323
return ln.strip()
@@ -1235,7 +1341,7 @@ def processLineToFixedPoint(ln):
12351341

12361342
process = processLine if recursion_limit is None else processLineToFixedPoint
12371343

1238-
return [unescape(process(ln)) for ln in script]
1344+
return [unescapePercents(process(ln)) for ln in script]
12391345

12401346

12411347
class ParserKind(object):
@@ -1610,7 +1716,8 @@ def executeShTest(test, litConfig, useExternalSh,
16101716
substitutions = list(extra_substitutions)
16111717
substitutions += getDefaultSubstitutions(test, tmpDir, tmpBase,
16121718
normalize_slashes=useExternalSh)
1613-
script = applySubstitutions(script, substitutions,
1719+
conditions = { feature: True for feature in test.config.available_features }
1720+
script = applySubstitutions(script, substitutions, conditions,
16141721
recursion_limit=test.config.recursiveExpansionLimit)
16151722

16161723
return _runShTest(test, litConfig, useExternalSh, script, tmpBase)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import lit.formats
2+
config.name = 'shtest-if-else'
3+
config.test_format = lit.formats.ShTest()
4+
config.test_source_root = None
5+
config.test_exec_root = None
6+
config.suffixes = ['.txt']
7+
config.available_features.add('feature')
8+
config.substitutions.append(('%{sub}', 'ok'))
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# CHECK: ValueError: '%{' is missing for %if substitution
2+
#
3+
# RUN: %if feature echo "test-1"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# CHECK: ValueError: '%}' is missing for %if substitution
2+
#
3+
# RUN: %if feature %{ echo
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# CHECK: ValueError: '%{' is missing for %else substitution
2+
#
3+
# RUN: %if feature %{ echo %} %else fail
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# CHECK: ValueError: '%}' is missing for %else substitution
2+
#
3+
# RUN: %if feature %{ echo %} %else %{ fail
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
# CHECK: -- Testing:{{.*}}
2+
# CHECK-NEXT: PASS: shtest-if-else :: test.txt (1 of 1)
3+
# CHECK-NEXT: Script:
4+
# CHECK-NEXT: --
5+
6+
# RUN: %if feature %{ echo "test-1" %}
7+
# CHECK-NEXT: {{^.*'RUN}}: at line [[#@LINE-1]]'; echo "test-1"
8+
9+
# If %else is not present it is treated like %else %{%}. Empty commands
10+
# are ignored.
11+
#
12+
# RUN: %if nofeature %{ echo "fail" %}
13+
# CHECK-NEXT: {{^.*'RUN}}: at line [[#@LINE-1]]'
14+
# CHECK-NOT: fail
15+
16+
# RUN: %if nofeature %{ echo "fail" %} %else %{ echo "test-2" %}
17+
# CHECK-NEXT: {{^.*'RUN}}: at line [[#@LINE-1]]'; echo "test-2"
18+
19+
# Spaces inside curly braces are not ignored
20+
#
21+
# RUN: echo test-%if feature %{ 3 %} %else %{ fail %}-test
22+
# RUN: echo test-%if feature %{ 4 4 %} %else %{ fail %}-test
23+
# RUN: echo test-%if nofeature %{ fail %} %else %{ 5 5 %}-test
24+
# CHECK-NEXT: {{^.*'RUN}}: at line [[#@LINE-3]]'; echo test- 3 -test
25+
# CHECK-NEXT: {{^.*'RUN}}: at line [[#@LINE-3]]'; echo test- 4 4 -test
26+
# CHECK-NEXT: {{^.*'RUN}}: at line [[#@LINE-3]]'; echo test- 5 5 -test
27+
28+
# Escape line breaks for multi-line expressions
29+
#
30+
# RUN: %if feature \
31+
# RUN: %{ echo \
32+
# RUN: "test-5" \
33+
# RUN: %}
34+
# CHECK-NEXT: {{^.*'RUN}}: at line [[#@LINE-4]]'; echo "test-5"
35+
36+
# RUN: %if nofeature \
37+
# RUN: %{ echo "fail" %} \
38+
# RUN: %else \
39+
# RUN: %{ echo "test-6" %}
40+
# CHECK-NEXT: {{^.*'RUN}}: at line [[#@LINE-4]]'; echo "test-6"
41+
42+
# RUN: echo "test%if feature %{%} %else %{%}-7"
43+
# CHECK-NEXT: {{^.*'RUN}}: at line [[#@LINE-1]]'; echo "test-7"
44+
45+
# Escape %if. Without %if..%else context '%{' and '%}' are treated
46+
# literally.
47+
#
48+
# RUN: echo %%if feature %{ echo "test-8" %}
49+
# CHECK-NEXT: {{^.*'RUN}}: at line [[#@LINE-1]]'; echo %if feature %{ echo "test-8" %}
50+
51+
# Nested expressions are supported:
52+
#
53+
# RUN: echo %if feature %{ %if feature %{ %if nofeature %{"fail"%} %else %{"test-9"%} %} %}
54+
# CHECK-NEXT: {{^.*'RUN}}: at line [[#@LINE-1]]'; echo "test-9"
55+
56+
# Binary expression evaluation and regex match can be used as
57+
# conditions.
58+
#
59+
# RUN: echo %if feature && !nofeature %{ "test-10" %}
60+
# RUN: echo %if feature && nofeature %{ "fail" %} %else %{ "test-11" %}
61+
# RUN: echo %if {{fea.+}} %{ "test-12" %} %else %{ "fail" %}
62+
# CHECK-NEXT: {{^.*'RUN}}: at line [[#@LINE-3]]'; echo "test-10"
63+
# CHECK-NEXT: {{^.*'RUN}}: at line [[#@LINE-3]]'; echo "test-11"
64+
# CHECK-NEXT: {{^.*'RUN}}: at line [[#@LINE-3]]'; echo "test-12"
65+
66+
# Spaces between %if and %else are ignored. If there is no %else -
67+
# space after %if %{...%} is not ignored.
68+
#
69+
# RUN: echo XX %if feature %{YY%} ZZ
70+
# RUN: echo AA %if feature %{BB%} %else %{CC%} DD
71+
# RUN: echo AA %if nofeature %{BB%} %else %{CC%} DD
72+
# CHECK-NEXT: {{^.*'RUN}}: at line [[#@LINE-3]]'; echo XX YY ZZ
73+
# CHECK-NEXT: {{^.*'RUN}}: at line [[#@LINE-3]]'; echo AA BB DD
74+
# CHECK-NEXT: {{^.*'RUN}}: at line [[#@LINE-3]]'; echo AA CC DD
75+
76+
# '{' and '}' can be used without escaping
77+
#
78+
# RUN: %if feature %{echo {}%}
79+
# CHECK-NEXT: {{^.*'RUN}}: at line [[#@LINE-1]]'; echo {}
80+
81+
# Spaces are not required
82+
#
83+
# RUN: echo %if feature%{"ok"%}%else%{"fail"%}
84+
# CHECK-NEXT: {{^.*'RUN}}: at line [[#@LINE-1]]'; echo "ok"
85+
86+
# Substitutions with braces are handled correctly
87+
#
88+
# RUN: echo %{sub} %if feature%{test-%{sub}%}%else%{"fail"%}
89+
# CHECK-NEXT: {{^.*'RUN}}: at line [[#@LINE-1]]'; echo ok test-ok
90+
91+
# CHECK-NEXT: --
92+
# CHECK-NEXT: Exit Code: 0
+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# RUN: %{lit} -v --show-all %{inputs}/shtest-if-else/test.txt \
2+
# RUN: | FileCheck %{inputs}/shtest-if-else/test.txt --match-full-lines
3+
4+
# RUN: not %{lit} -v --show-all %{inputs}/shtest-if-else/test-neg1.txt 2>&1 \
5+
# RUN: | FileCheck %{inputs}/shtest-if-else/test-neg1.txt
6+
7+
# RUN: not %{lit} -v --show-all %{inputs}/shtest-if-else/test-neg2.txt 2>&1 \
8+
# RUN: | FileCheck %{inputs}/shtest-if-else/test-neg2.txt
9+
10+
# RUN: not %{lit} -v --show-all %{inputs}/shtest-if-else/test-neg3.txt 2>&1 \
11+
# RUN: | FileCheck %{inputs}/shtest-if-else/test-neg3.txt
12+
13+
# RUN: not %{lit} -v --show-all %{inputs}/shtest-if-else/test-neg4.txt 2>&1 \
14+
# RUN: | FileCheck %{inputs}/shtest-if-else/test-neg4.txt

0 commit comments

Comments
 (0)