Skip to content

Commit 9e739fd

Browse files
authored
[lit] Fix some issues from --per-test-coverage (#65242)
D154280 (landed in 64d1954 in July, 2023) implements `--per-test-coverage` (which can also be specified via `lit_config.per_test_coverage`). However, it has a few issues, which the current patch addresses: 1. D154280 implements `--per-test-coverage` only for the case that lit is configured to use an external shell. The current patch extends the implementation to lit's internal shell. 2. In the case that lit is configured to use an external shell, regardless of whether `--per-test-coverage` is actually specified, D154280 causes `%dbg(RUN: at line N)` to be expanded in RUN lines early and in a manner that is specific to sh-like shells. As a result, later code in lit that expands it in a shell-specific manner is useless as there's nothing left to expand. The current patch cleans up the implementation to avoid useless code. 3. Because of issue 2, D154280 corrupts support for windows `cmd` as an external shell (effectively comments out all RUN lines with `:`). The current patch happens to fix that particular corruption by addressing issue 2. However, D122569 (landed in 1041a96 in April, 2022) had already broken support for windows `cmd` as an external shell (discards RUN lines when expanding `%dbg(RUN: at line N)`). The current patch does not attempt to fix that bug. For further details, see the PR discussion of the current patch. The current patch addresses the above issues by implementing `--per-test-coverage` before selecting the shell (internal or external) and by leaving `%dbg(RUN: at line N)` unexpanded there. Thus, it is expanded later in a shell-specific manner, as before D154280. This patch introduces `buildPdbgCommand` into lit's implementation to encapsulate the process of building (or rebuilding in the case of the `--per-test-coverage` implementation) a full `%dbg(RUN: at line N) cmd` line and asserting that the result matches `kPdbgRegex`. It also cleans up that and all other uses of `kPdbgRegex` to operate on the full line with `re.fullmatch` not `re.match`. This change better reflects the intention in every case, but it is expected to be NFC because `kPdbgRegex` ends in `.*` and thus avoids the difference between `re.fullmatch` and `re.match`. The only caveat is that `.*` does not match newlines, but RUN lines cannot contain newlines currently, so this caveat currently shouldn't matter in practice. The original `--per-test-coverage` implementation avoided accumulating `export LLVM_PROFILE_FILE={profile}` insertions across retries (due to `ALLOW_RETRIES`) by skipping the insertion if `%dbg(RUN: at line N)` was not present and thus had already been expanded. However, the current patch makes sure the insertions also happen for commands without `%dbg(RUN: at line N)`, such as preamble commands or some commands from other lit test formats. Thus, the current patch implements a different mechanism to avoid accumulating those insertions (see code comments).
1 parent c7d65e4 commit 9e739fd

File tree

8 files changed

+110
-40
lines changed

8 files changed

+110
-40
lines changed

llvm/utils/lit/lit/TestRunner.py

Lines changed: 40 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,14 @@ def __init__(self, command, message):
5757
kPdbgRegex = "%dbg\\(([^)'\"]*)\\)(.*)"
5858

5959

60+
def buildPdbgCommand(msg, cmd):
61+
res = f"%dbg({msg}) {cmd}"
62+
assert re.fullmatch(
63+
kPdbgRegex, res
64+
), f"kPdbgRegex expected to match actual %dbg usage: {res}"
65+
return res
66+
67+
6068
class ShellEnvironment(object):
6169

6270
"""Mutable shell environment containing things like CWD and env vars.
@@ -985,7 +993,7 @@ def _executeShCmd(cmd, shenv, results, timeoutHelper):
985993
def executeScriptInternal(test, litConfig, tmpBase, commands, cwd):
986994
cmds = []
987995
for i, ln in enumerate(commands):
988-
match = re.match(kPdbgRegex, ln)
996+
match = re.fullmatch(kPdbgRegex, ln)
989997
if match:
990998
command = match.group(2)
991999
ln = commands[i] = match.expand(": '\\1'; \\2" if command else ": '\\1'")
@@ -1067,25 +1075,10 @@ def executeScriptInternal(test, litConfig, tmpBase, commands, cwd):
10671075
def executeScript(test, litConfig, tmpBase, commands, cwd):
10681076
bashPath = litConfig.getBashPath()
10691077
isWin32CMDEXE = litConfig.isWindows and not bashPath
1070-
coverage_index = 0 # Counter for coverage file index
10711078
script = tmpBase + ".script"
10721079
if isWin32CMDEXE:
10731080
script += ".bat"
10741081

1075-
# Set unique LLVM_PROFILE_FILE for each run command
1076-
for j, ln in enumerate(commands):
1077-
match = re.match(kPdbgRegex, ln)
1078-
if match:
1079-
command = match.group(2)
1080-
commands[j] = match.expand(": '\\1'; \\2" if command else ": '\\1'")
1081-
if litConfig.per_test_coverage:
1082-
# Extract the test case name from the test object
1083-
test_case_name = test.path_in_suite[-1]
1084-
test_case_name = test_case_name.rsplit(".", 1)[0] # Remove the file extension
1085-
llvm_profile_file = f"{test_case_name}{coverage_index}.profraw"
1086-
commands[j] = f"export LLVM_PROFILE_FILE={llvm_profile_file} && {commands[j]}"
1087-
coverage_index += 1
1088-
10891082
# Write script file
10901083
mode = "w"
10911084
open_kwargs = {}
@@ -1096,7 +1089,7 @@ def executeScript(test, litConfig, tmpBase, commands, cwd):
10961089
f = open(script, mode, **open_kwargs)
10971090
if isWin32CMDEXE:
10981091
for i, ln in enumerate(commands):
1099-
match = re.match(kPdbgRegex, ln)
1092+
match = re.fullmatch(kPdbgRegex, ln)
11001093
if match:
11011094
command = match.group(2)
11021095
commands[i] = match.expand(
@@ -1109,7 +1102,7 @@ def executeScript(test, litConfig, tmpBase, commands, cwd):
11091102
f.write("\n@if %ERRORLEVEL% NEQ 0 EXIT\n".join(commands))
11101103
else:
11111104
for i, ln in enumerate(commands):
1112-
match = re.match(kPdbgRegex, ln)
1105+
match = re.fullmatch(kPdbgRegex, ln)
11131106
if match:
11141107
command = match.group(2)
11151108
commands[i] = match.expand(": '\\1'; \\2" if command else ": '\\1'")
@@ -1837,13 +1830,7 @@ def _handleCommand(cls, line_number, line, output, keyword):
18371830
if not output or not output[-1].add_continuation(line_number, keyword, line):
18381831
if output is None:
18391832
output = []
1840-
pdbg = "%dbg({keyword} at line {line_number})".format(
1841-
keyword=keyword, line_number=line_number
1842-
)
1843-
assert re.match(
1844-
kPdbgRegex + "$", pdbg
1845-
), "kPdbgRegex expected to match actual %dbg usage"
1846-
line = "{pdbg} {real_command}".format(pdbg=pdbg, real_command=line)
1833+
line = buildPdbgCommand(f"{keyword} at line {line_number}", line)
18471834
output.append(CommandDirective(line_number, line_number, keyword, line))
18481835
return output
18491836

@@ -2048,6 +2035,27 @@ def parseIntegratedTestScript(test, additional_parsers=[], require_script=True):
20482035

20492036
def _runShTest(test, litConfig, useExternalSh, script, tmpBase):
20502037
def runOnce(execdir):
2038+
# Set unique LLVM_PROFILE_FILE for each run command
2039+
if litConfig.per_test_coverage:
2040+
# Extract the test case name from the test object, and remove the
2041+
# file extension.
2042+
test_case_name = test.path_in_suite[-1]
2043+
test_case_name = test_case_name.rsplit(".", 1)[0]
2044+
coverage_index = 0 # Counter for coverage file index
2045+
for i, ln in enumerate(script):
2046+
match = re.fullmatch(kPdbgRegex, ln)
2047+
if match:
2048+
dbg = match.group(1)
2049+
command = match.group(2)
2050+
else:
2051+
command = ln
2052+
profile = f"{test_case_name}{coverage_index}.profraw"
2053+
coverage_index += 1
2054+
command = f"export LLVM_PROFILE_FILE={profile}; {command}"
2055+
if match:
2056+
command = buildPdbgCommand(dbg, command)
2057+
script[i] = command
2058+
20512059
if useExternalSh:
20522060
res = executeScript(test, litConfig, tmpBase, script, execdir)
20532061
else:
@@ -2071,7 +2079,14 @@ def runOnce(execdir):
20712079
# Re-run failed tests up to test.allowed_retries times.
20722080
execdir = os.path.dirname(test.getExecPath())
20732081
attempts = test.allowed_retries + 1
2082+
scriptInit = script
20742083
for i in range(attempts):
2084+
# runOnce modifies script, but applying the modifications again to the
2085+
# result can corrupt script, so we restore the original upon a retry.
2086+
# A cleaner solution would be for runOnce to encapsulate operating on a
2087+
# copy of script, but we actually want it to modify the original script
2088+
# so we can print the modified version under "Script:" below.
2089+
script = scriptInit[:]
20752090
res = runOnce(execdir)
20762091
if isinstance(res, lit.Test.Result):
20772092
return res

llvm/utils/lit/tests/Inputs/per-test-coverage-by-lit-cfg/lit.cfg

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ import os
33

44
config.name = "per-test-coverage-by-lit-cfg"
55
config.suffixes = [".py"]
6-
config.test_format = lit.formats.ShTest(execute_external=True)
6+
config.test_format = lit.formats.ShTest(
7+
execute_external=eval(lit_config.params.get("execute_external")),
8+
preamble_commands=["%{python} %s | FileCheck -DINDEX=0 %s"]
9+
)
710
lit_config.per_test_coverage = True
811
config.substitutions.append(("%{python}", '"%s"' % (sys.executable)))
Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# Check that the environment variable is set correctly
2-
# RUN: %{python} %s | FileCheck %s
2+
# RUN: %{python} %s | FileCheck -DINDEX=1 %s
3+
# RUN: %{python} %s | FileCheck -DINDEX=2 %s
34

45
# Python script to read the environment variable
56
# and print its value
@@ -8,4 +9,4 @@
89
llvm_profile_file = os.environ.get('LLVM_PROFILE_FILE')
910
print(llvm_profile_file)
1011

11-
# CHECK: per-test-coverage-by-lit-cfg0.profraw
12+
# CHECK: per-test-coverage-by-lit-cfg[[INDEX]].profraw

llvm/utils/lit/tests/Inputs/per-test-coverage/lit.cfg

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import os
33

44
config.name = "per-test-coverage"
55
config.suffixes = [".py"]
6-
config.test_format = lit.formats.ShTest(execute_external=True)
6+
config.test_format = lit.formats.ShTest(
7+
execute_external=eval(lit_config.params.get("execute_external")),
8+
preamble_commands=["%{python} %s | FileCheck -DINDEX=0 %s"]
9+
)
710
config.substitutions.append(("%{python}", '"%s"' % (sys.executable)))
8-
Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# Check that the environment variable is set correctly
2-
# RUN: %{python} %s | FileCheck %s
2+
# RUN: %{python} %s | FileCheck -DINDEX=1 %s
3+
# RUN: %{python} %s | FileCheck -DINDEX=2 %s
34

45
# Python script to read the environment variable
56
# and print its value
@@ -8,4 +9,4 @@
89
llvm_profile_file = os.environ.get('LLVM_PROFILE_FILE')
910
print(llvm_profile_file)
1011

11-
# CHECK: per-test-coverage0.profraw
12+
# CHECK: per-test-coverage[[INDEX]].profraw

llvm/utils/lit/tests/allow-retries.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,15 @@
3939
# RUN: rm -f %t.counter
4040
# RUN: %{lit} %{inputs}/test_retry_attempts/test.py -Dcounter=%t.counter -Dpython=%{python} | FileCheck --check-prefix=CHECK-TEST6 %s
4141
# CHECK-TEST6: Passed With Retry: 1
42+
43+
# This test checks that --per-test-coverage doesn't accumulate inserted
44+
# LLVM_PROFILE_FILE= commands across retries.
45+
#
46+
# RUN: rm -f %t.counter
47+
# RUN: %{lit} -a %{inputs}/test_retry_attempts/test.py --per-test-coverage\
48+
# RUN: -Dcounter=%t.counter -Dpython=%{python} | \
49+
# RUN: FileCheck --check-prefix=CHECK-TEST7 %s
50+
# CHECK-TEST7: Command Output (stdout):
51+
# CHECK-TEST7: LLVM_PROFILE_FILE=
52+
# CHECK-TEST7-NOT: LLVM_PROFILE_FILE=
53+
# CHECK-TEST7: Passed With Retry: 1
Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,24 @@
11
# Test if lit_config.per_test_coverage in lit.cfg sets individual test case coverage.
22

3-
# RUN: %{lit} -a -v %{inputs}/per-test-coverage-by-lit-cfg/per-test-coverage-by-lit-cfg.py \
4-
# RUN: | FileCheck -match-full-lines %s
5-
#
6-
# CHECK: PASS: per-test-coverage-by-lit-cfg :: per-test-coverage-by-lit-cfg.py ({{[^)]*}})
3+
# RUN: %{lit} -a -vv -Dexecute_external=False \
4+
# RUN: %{inputs}/per-test-coverage-by-lit-cfg/per-test-coverage-by-lit-cfg.py | \
5+
# RUN: FileCheck -DOUT=stdout %s
6+
7+
# RUN: %{lit} -a -vv -Dexecute_external=True \
8+
# RUN: %{inputs}/per-test-coverage-by-lit-cfg/per-test-coverage-by-lit-cfg.py | \
9+
# RUN: FileCheck -DOUT=stderr %s
10+
11+
# CHECK: {{^}}PASS: per-test-coverage-by-lit-cfg :: per-test-coverage-by-lit-cfg.py ({{[^)]*}})
12+
# CHECK: Command Output ([[OUT]]):
13+
# CHECK-NEXT: --
14+
# CHECK: export
15+
# CHECK: LLVM_PROFILE_FILE=per-test-coverage-by-lit-cfg0.profraw
16+
# CHECK: per-test-coverage-by-lit-cfg.py
17+
# CHECK: {{RUN}}: at line 2
18+
# CHECK: export
19+
# CHECK: LLVM_PROFILE_FILE=per-test-coverage-by-lit-cfg1.profraw
20+
# CHECK: per-test-coverage-by-lit-cfg.py
21+
# CHECK: {{RUN}}: at line 3
22+
# CHECK: export
23+
# CHECK: LLVM_PROFILE_FILE=per-test-coverage-by-lit-cfg2.profraw
24+
# CHECK: per-test-coverage-by-lit-cfg.py
Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,24 @@
11
# Test LLVM_PROFILE_FILE is set when --per-test-coverage is passed to command line.
22

3-
# RUN: %{lit} -a -v --per-test-coverage %{inputs}/per-test-coverage/per-test-coverage.py \
4-
# RUN: | FileCheck -match-full-lines %s
5-
#
6-
# CHECK: PASS: per-test-coverage :: per-test-coverage.py ({{[^)]*}})
3+
# RUN: %{lit} -a -vv --per-test-coverage -Dexecute_external=False \
4+
# RUN: %{inputs}/per-test-coverage/per-test-coverage.py | \
5+
# RUN: FileCheck -DOUT=stdout %s
6+
7+
# RUN: %{lit} -a -vv --per-test-coverage -Dexecute_external=True \
8+
# RUN: %{inputs}/per-test-coverage/per-test-coverage.py | \
9+
# RUN: FileCheck -DOUT=stderr %s
10+
11+
# CHECK: {{^}}PASS: per-test-coverage :: per-test-coverage.py ({{[^)]*}})
12+
# CHECK: Command Output ([[OUT]]):
13+
# CHECK-NEXT: --
14+
# CHECK: export
15+
# CHECK: LLVM_PROFILE_FILE=per-test-coverage0.profraw
16+
# CHECK: per-test-coverage.py
17+
# CHECK: {{RUN}}: at line 2
18+
# CHECK: export
19+
# CHECK: LLVM_PROFILE_FILE=per-test-coverage1.profraw
20+
# CHECK: per-test-coverage.py
21+
# CHECK: {{RUN}}: at line 3
22+
# CHECK: export
23+
# CHECK: LLVM_PROFILE_FILE=per-test-coverage2.profraw
24+
# CHECK: per-test-coverage.py

0 commit comments

Comments
 (0)