Skip to content

Commit c981c53

Browse files
committed
[lit] Improve test output from lit's internal shell
This patch and D154984 were discussed in <https://discourse.llvm.org/t/rfc-improving-lits-debug-output/72839>. Motivation ---------- D154984 removes the "Script:" section that lit prints along with a test's output, and it makes -v and -a imply -vv. For example, after D154984, the "Script:" section below is never shown, but -v is enough to produce the execution trace following it: ``` Script: -- : 'RUN: at line 1'; echo hello | FileCheck bogus.txt && echo success -- Exit Code: 2 Command Output (stdout): -- $ ":" "RUN: at line 1" $ "echo" "hello" # command output: hello $ "FileCheck" "bogus.txt" # command stderr: Could not open check file 'bogus.txt': No such file or directory error: command failed with exit status: 2 -- ``` In the D154984 review, some reviewers point out that they have been using the "Script:" section for copying and pasting a test's shell commands to a terminal window. The shell commands as printed in the execution trace can be harder to copy and paste for the following reasons: - They drop redirections and break apart RUN lines at `&&`, `|`, etc. - They add `$` at the start of every command, which makes it hard to copy and paste multiple commands in bulk. - Command stdout, stderr, etc. are interleaved with the commands and are not clearly delineated. - They don't always use proper shell quoting. Instead, they blindly enclose all command-line arguments in double quotes. Changes ------- D154984 plus this patch converts the above example into: ``` Exit Code: 2 Command Output (stdout): -- # RUN: at line 1 echo hello | FileCheck bogus-file.txt && echo success # executed command: echo hello # .---command stdout------------ # | hello # `----------------------------- # executed command: FileCheck bogus-file.txt # .---command stderr------------ # | Could not open check file 'bogus-file.txt': No such file or directory # `----------------------------- # error: command failed with exit status: 2 -- ``` Thus, this patch addresses the above issues as follows: - The entire execution trace can be copied and pasted in bulk to a terminal for correct execution of the RUN lines, which are printed intact as they appeared in the original RUN lines except lit substitutions are expanded. Everything else in the execution trace appears in shell comments so it has no effect in a terminal. - Each of the RUN line's commands is repeated (in shell comments) as it executes to show (1) that the command actually executed (e.g., `echo success` above didn't) and (2) what stdout, stderr, non-zero exit status, and output files are associated with the command, if any. Shell quoting in the command is now correct and minimal but is not necessarily the original shell quoting from the RUN line. - The start and end of the contents of stdout, stderr, or an output file is now delineated clearly in the trace. To help produce some of the above output, this patch extends lit's internal shell with a built-in `@echo` command. It's like `echo` except lit suppresses the normal execution trace for `@echo` and just prints its stdout directly. For now, `@echo` isn't documented for use in lit tests. Without this patch, libcxx's custom lit test format tries to parse the stdout from `lit.TestRunner.executeScriptInternal` (which runs lit's internal shell) to extract the stdout and stderr produced by shell commands, and that parse no longer works after the above changes. This patch makes a small adjustment to `lit.TestRunner.executeScriptInternal` so libcxx can just request stdout and stderr without an execution trace. (As a minor drive-by fix that came up in testing: lit's internal `not` command now always produces a numeric exit status and never `True`.) Caveat ------ This patch only makes the above changes for lit's internal shell. In most cases, we do not know how to force external shells (e.g., bash, sh, window's `cmd`) to produce execution traces in the manner we want. To configure a test suite to use lit's internal shell (which is usually better for test portability than external shells anyway), add this to the test suite's `lit.cfg` or other configuration file: ``` config.test_format = lit.formats.ShTest(execute_external=False) ``` Reviewed By: MaskRay, awarzynski Differential Revision: https://reviews.llvm.org/D156954
1 parent 09b6e45 commit c981c53

33 files changed

+1015
-812
lines changed

libcxx/utils/libcxx/test/dsl.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ def programOutput(config, program, args=None):
180180
"Failed to run program, cmd:\n{}\nstderr is:\n{}".format(runcmd, err)
181181
)
182182

183-
return libcxx.test.format._parseLitOutput(out)
183+
return out
184184

185185

186186
@_memoizeExpensiveOperation(

libcxx/utils/libcxx/test/format.py

Lines changed: 2 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -35,39 +35,6 @@ def _checkBaseSubstitutions(substitutions):
3535
for s in ["%{cxx}", "%{compile_flags}", "%{link_flags}", "%{flags}", "%{exec}"]:
3636
assert s in substitutions, "Required substitution {} was not provided".format(s)
3737

38-
def _parseLitOutput(fullOutput):
39-
"""
40-
Parse output of a Lit ShTest to extract the actual output of the contained commands.
41-
42-
This takes output of the form
43-
44-
$ ":" "RUN: at line 11"
45-
$ "echo" "OUTPUT1"
46-
# command output:
47-
OUTPUT1
48-
49-
$ ":" "RUN: at line 12"
50-
$ "echo" "OUTPUT2"
51-
# command output:
52-
OUTPUT2
53-
54-
and returns a string containing
55-
56-
OUTPUT1
57-
OUTPUT2
58-
59-
as-if the commands had been run directly. This is a workaround for the fact
60-
that Lit doesn't let us execute ShTest and retrieve the raw output without
61-
injecting additional Lit output around it.
62-
"""
63-
parsed = ''
64-
for output in re.split('[$]\s*":"\s*"RUN: at line \d+"', fullOutput):
65-
if output: # skip blank lines
66-
commandOutput = re.search("# command output:\n(.+)\n$", output, flags=re.DOTALL)
67-
if commandOutput:
68-
parsed += commandOutput.group(1)
69-
return parsed
70-
7138
def _executeScriptInternal(test, litConfig, commands):
7239
"""
7340
Returns (stdout, stderr, exitCode, timeoutInfo, parsedCommands)
@@ -79,21 +46,12 @@ def _executeScriptInternal(test, litConfig, commands):
7946
_, tmpBase = _getTempPaths(test)
8047
execDir = os.path.dirname(test.getExecPath())
8148
res = lit.TestRunner.executeScriptInternal(
82-
test, litConfig, tmpBase, parsedCommands, execDir
49+
test, litConfig, tmpBase, parsedCommands, execDir, debug=False
8350
)
8451
if isinstance(res, lit.Test.Result): # Handle failure to parse the Lit test
8552
res = ("", res.output, 127, None)
8653
(out, err, exitCode, timeoutInfo) = res
8754

88-
# TODO: As a temporary workaround until https://reviews.llvm.org/D81892 lands, manually
89-
# split any stderr output that is included in stdout. It shouldn't be there, but
90-
# the Lit internal shell conflates stderr and stdout.
91-
conflatedErrorOutput = re.search("(# command stderr:.+$)", out, flags=re.DOTALL)
92-
if conflatedErrorOutput:
93-
conflatedErrorOutput = conflatedErrorOutput.group(0)
94-
out = out[: -len(conflatedErrorOutput)]
95-
err += conflatedErrorOutput
96-
9755
return (out, err, exitCode, timeoutInfo, parsedCommands)
9856

9957

@@ -400,8 +358,7 @@ def _generateGenTest(self, testSuite, pathInSuite, litConfig, localConfig):
400358
raise RuntimeError(f"Error while trying to generate gen test\nstdout:\n{out}\n\nstderr:\n{err}")
401359

402360
# Split the generated output into multiple files and generate one test for each file
403-
parsed = _parseLitOutput(out)
404-
for (subfile, content) in self._splitFile(parsed):
361+
for (subfile, content) in self._splitFile(out):
405362
generatedFile = testSuite.getExecPath(pathInSuite + (subfile, ))
406363
os.makedirs(os.path.dirname(generatedFile), exist_ok=True)
407364
with open(generatedFile, 'w') as f:

llvm/utils/lit/lit/TestRunner.py

Lines changed: 103 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import stat
99
import pathlib
1010
import platform
11+
import shlex
1112
import shutil
1213
import tempfile
1314
import threading
@@ -57,6 +58,14 @@ def __init__(self, command, message):
5758
kPdbgRegex = "%dbg\\(([^)'\"]*)\\)(.*)"
5859

5960

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

6271
"""Mutable shell environment containing things like CWD and env vars.
@@ -340,12 +349,12 @@ def executeBuiltinExport(cmd, shenv):
340349

341350

342351
def executeBuiltinEcho(cmd, shenv):
343-
"""Interpret a redirected echo command"""
352+
"""Interpret a redirected echo or @echo command"""
344353
opened_files = []
345354
stdin, stdout, stderr = processRedirects(cmd, subprocess.PIPE, shenv, opened_files)
346355
if stdin != subprocess.PIPE or stderr != subprocess.PIPE:
347356
raise InternalShellError(
348-
cmd, "stdin and stderr redirects not supported for echo"
357+
cmd, f"stdin and stderr redirects not supported for {cmd.args[0]}"
349358
)
350359

351360
# Some tests have un-redirected echo commands to help debug test failures.
@@ -692,6 +701,7 @@ def _executeShCmd(cmd, shenv, results, timeoutHelper):
692701
"cd": executeBuiltinCd,
693702
"export": executeBuiltinExport,
694703
"echo": executeBuiltinEcho,
704+
"@echo": executeBuiltinEcho,
695705
"mkdir": executeBuiltinMkdir,
696706
"popd": executeBuiltinPopd,
697707
"pushd": executeBuiltinPushd,
@@ -919,7 +929,7 @@ def _executeShCmd(cmd, shenv, results, timeoutHelper):
919929
if res == -signal.SIGINT:
920930
raise KeyboardInterrupt
921931
if proc_not_counts[i] % 2:
922-
res = not res
932+
res = 1 if res == 0 else 0
923933
elif proc_not_counts[i] > 1:
924934
res = 1 if res != 0 else 0
925935

@@ -982,19 +992,58 @@ def _executeShCmd(cmd, shenv, results, timeoutHelper):
982992
return exitCode
983993

984994

985-
def executeScriptInternal(test, litConfig, tmpBase, commands, cwd):
995+
def formatOutput(title, data, limit=None):
996+
if not data.strip():
997+
return ""
998+
if not limit is None and len(data) > limit:
999+
data = data[:limit] + "\n...\n"
1000+
msg = "data was truncated"
1001+
else:
1002+
msg = ""
1003+
ndashes = 30
1004+
out = f"# .---{title}{'-' * (ndashes - 4 - len(title))}\n"
1005+
out += f"# | " + "\n# | ".join(data.splitlines()) + "\n"
1006+
out += f"# `---{msg}{'-' * (ndashes - 4 - len(msg))}\n"
1007+
return out
1008+
1009+
# Normally returns out, err, exitCode, timeoutInfo.
1010+
#
1011+
# If debug is True (the normal lit behavior), err is empty, and out contains an
1012+
# execution trace, including stdout and stderr shown per command executed.
1013+
#
1014+
# If debug is False (set by some custom lit test formats that call this
1015+
# function), out contains only stdout from the script, err contains only stderr
1016+
# from the script, and there is no execution trace.
1017+
def executeScriptInternal(test, litConfig, tmpBase, commands, cwd,
1018+
debug=True):
9861019
cmds = []
9871020
for i, ln in enumerate(commands):
1021+
# Within lit, we try to always add '%dbg(...)' to command lines in order
1022+
# to maximize debuggability. However, custom lit test formats might not
1023+
# always add it, so add a generic debug message in that case.
9881024
match = re.match(kPdbgRegex, ln)
9891025
if match:
1026+
dbg = match.group(1)
9901027
command = match.group(2)
991-
ln = commands[i] = match.expand(": '\\1'; \\2" if command else ": '\\1'")
1028+
else:
1029+
dbg = "command line"
1030+
command = ln
1031+
if debug:
1032+
ln = f"@echo '# {dbg}' "
1033+
if command:
1034+
ln += f"&& @echo {shlex.quote(command.lstrip())} && {command}"
1035+
else:
1036+
ln += "has no command after substitutions"
1037+
else:
1038+
ln = command
9921039
try:
9931040
cmds.append(
9941041
ShUtil.ShParser(ln, litConfig.isWindows, test.config.pipefail).parse()
9951042
)
9961043
except:
997-
return lit.Test.Result(Test.FAIL, "shell parser error on: %r" % ln)
1044+
return lit.Test.Result(
1045+
Test.FAIL, f"shell parser error on {dbg}: {command.lstrip()}\n"
1046+
)
9981047

9991048
cmd = cmds[0]
10001049
for c in cmds[1:]:
@@ -1014,8 +1063,42 @@ def executeScriptInternal(test, litConfig, tmpBase, commands, cwd):
10141063

10151064
out = err = ""
10161065
for i, result in enumerate(results):
1017-
# Write the command line run.
1018-
out += "$ %s\n" % (" ".join('"%s"' % s for s in result.command.args),)
1066+
if not debug:
1067+
out += result.stdout
1068+
err += result.stderr
1069+
continue
1070+
1071+
# The purpose of an "@echo" command is merely to add a debugging message
1072+
# directly to lit's output. It is used internally by lit's internal
1073+
# shell and is not currently documented for use in lit tests. However,
1074+
# if someone misuses it (e.g., both "echo" and "@echo" complain about
1075+
# stdin redirection), produce the normal execution trace to facilitate
1076+
# debugging.
1077+
if (
1078+
result.command.args[0] == "@echo"
1079+
and result.exitCode == 0
1080+
and not result.stderr
1081+
and not result.outputFiles
1082+
and not result.timeoutReached
1083+
):
1084+
out += result.stdout
1085+
continue
1086+
1087+
# Write the command line that was run. Properly quote it. Leading
1088+
# "!" commands should not be quoted as that would indicate they are not
1089+
# the builtins.
1090+
out += "# executed command: "
1091+
nLeadingBangs = next(
1092+
(i for i, cmd in enumerate(result.command.args) if cmd != "!"),
1093+
len(result.command.args),
1094+
)
1095+
out += "! " * nLeadingBangs
1096+
out += " ".join(
1097+
shlex.quote(str(s))
1098+
for i, s in enumerate(result.command.args)
1099+
if i >= nLeadingBangs
1100+
)
1101+
out += "\n"
10191102

10201103
# If nothing interesting happened, move on.
10211104
if (
@@ -1030,22 +1113,16 @@ def executeScriptInternal(test, litConfig, tmpBase, commands, cwd):
10301113

10311114
# Add the command output, if redirected.
10321115
for (name, path, data) in result.outputFiles:
1033-
if data.strip():
1034-
out += "# redirected output from %r:\n" % (name,)
1035-
data = to_string(data.decode("utf-8", errors="replace"))
1036-
if len(data) > 1024:
1037-
out += data[:1024] + "\n...\n"
1038-
out += "note: data was truncated\n"
1039-
else:
1040-
out += data
1041-
out += "\n"
1042-
1116+
data = to_string(data.decode("utf-8", errors="replace"))
1117+
out += formatOutput(
1118+
f"redirected output from '{name}'", data, limit=1024
1119+
)
10431120
if result.stdout.strip():
1044-
out += "# command output:\n%s\n" % (result.stdout,)
1121+
out += formatOutput("command stdout", result.stdout)
10451122
if result.stderr.strip():
1046-
out += "# command stderr:\n%s\n" % (result.stderr,)
1123+
out += formatOutput("command stderr", result.stderr)
10471124
if not result.stdout.strip() and not result.stderr.strip():
1048-
out += "note: command had no output on stdout or stderr\n"
1125+
out += "# note: command had no output on stdout or stderr\n"
10491126

10501127
# Show the error conditions:
10511128
if result.exitCode != 0:
@@ -1055,9 +1132,9 @@ def executeScriptInternal(test, litConfig, tmpBase, commands, cwd):
10551132
codeStr = hex(int(result.exitCode & 0xFFFFFFFF)).rstrip("L")
10561133
else:
10571134
codeStr = str(result.exitCode)
1058-
out += "error: command failed with exit status: %s\n" % (codeStr,)
1135+
out += "# error: command failed with exit status: %s\n" % (codeStr,)
10591136
if litConfig.maxIndividualTestTime > 0 and result.timeoutReached:
1060-
out += "error: command reached timeout: %s\n" % (
1137+
out += "# error: command reached timeout: %s\n" % (
10611138
str(result.timeoutReached),
10621139
)
10631140

@@ -1833,13 +1910,7 @@ def _handleCommand(cls, line_number, line, output, keyword):
18331910
if not output or not output[-1].add_continuation(line_number, keyword, line):
18341911
if output is None:
18351912
output = []
1836-
pdbg = "%dbg({keyword} at line {line_number})".format(
1837-
keyword=keyword, line_number=line_number
1838-
)
1839-
assert re.match(
1840-
kPdbgRegex + "$", pdbg
1841-
), "kPdbgRegex expected to match actual %dbg usage"
1842-
line = "{pdbg} {real_command}".format(pdbg=pdbg, real_command=line)
1913+
line = buildPdbgCommand(f"{keyword} at line {line_number}", line)
18431914
output.append(CommandDirective(line_number, line_number, keyword, line))
18441915
return output
18451916

@@ -2104,6 +2175,8 @@ def executeShTest(
21042175
return lit.Test.Result(Test.UNSUPPORTED, "Test is unsupported")
21052176

21062177
script = list(preamble_commands)
2178+
script = [buildPdbgCommand(f"preamble command line", ln) for ln in script]
2179+
21072180
parsed = parseIntegratedTestScript(test, require_script=not script)
21082181
if isinstance(parsed, lit.Test.Result):
21092182
return parsed

llvm/utils/lit/tests/Inputs/lit-opts/lit.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,4 @@ config.suffixes = [".txt"]
55
config.test_format = lit.formats.ShTest()
66
config.test_source_root = None
77
config.test_exec_root = None
8-
config.substitutions.append(("%var", lit_config.params.get("var", "")))
8+
config.substitutions.append(("%var", lit_config.params.get("var", "default")))

llvm/utils/lit/tests/Inputs/shtest-define/examples/param-subst.txt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,21 +14,21 @@
1414
; REDEFINE: %{cflags} = -triple x86_64-apple-darwin10.6.0 -fopenmp-simd
1515
; REDEFINE: %{fcflags} = -check-prefix=SIMD
1616
; RUN: %{check}
17-
; CHECK: %clang_cc1 -verify -fopenmp -fopenmp-version=51 -triple x86_64-apple-darwin10.6.0 -fopenmp-simd -emit-llvm -o - {{.*}} | FileCheck -check-prefix=SIMD {{.*}}
17+
; CHECK:# | %clang_cc1 -verify -fopenmp -fopenmp-version=51 -triple x86_64-apple-darwin10.6.0 -fopenmp-simd -emit-llvm -o - {{.*}} | FileCheck -check-prefix=SIMD {{.*}}
1818

1919
; REDEFINE: %{cflags} = -triple x86_64-unknown-linux-gnu -fopenmp-simd
2020
; REDEFINE: %{fcflags} = -check-prefix=SIMD
2121
; RUN: %{check}
22-
; CHECK: %clang_cc1 -verify -fopenmp -fopenmp-version=51 -triple x86_64-unknown-linux-gnu -fopenmp-simd -emit-llvm -o - {{.*}} | FileCheck -check-prefix=SIMD {{.*}}
22+
; CHECK:# | %clang_cc1 -verify -fopenmp -fopenmp-version=51 -triple x86_64-unknown-linux-gnu -fopenmp-simd -emit-llvm -o - {{.*}} | FileCheck -check-prefix=SIMD {{.*}}
2323

2424
; REDEFINE: %{cflags} = -triple x86_64-apple-darwin10.6.0
2525
; REDEFINE: %{fcflags} = -check-prefix=NO-SIMD
2626
; RUN: %{check}
27-
; CHECK: %clang_cc1 -verify -fopenmp -fopenmp-version=51 -triple x86_64-apple-darwin10.6.0 -emit-llvm -o - {{.*}} | FileCheck -check-prefix=NO-SIMD {{.*}}
27+
; CHECK:# | %clang_cc1 -verify -fopenmp -fopenmp-version=51 -triple x86_64-apple-darwin10.6.0 -emit-llvm -o - {{.*}} | FileCheck -check-prefix=NO-SIMD {{.*}}
2828

2929
; REDEFINE: %{cflags} = -triple x86_64-unknown-linux-gnu
3030
; REDEFINE: %{fcflags} = -check-prefix=NO-SIMD
3131
; RUN: %{check}
32-
; CHECK: %clang_cc1 -verify -fopenmp -fopenmp-version=51 -triple x86_64-unknown-linux-gnu -emit-llvm -o - {{.*}} | FileCheck -check-prefix=NO-SIMD {{.*}}
32+
; CHECK:# | %clang_cc1 -verify -fopenmp -fopenmp-version=51 -triple x86_64-unknown-linux-gnu -emit-llvm -o - {{.*}} | FileCheck -check-prefix=NO-SIMD {{.*}}
3333

3434
; CHECK: Passed: 1

llvm/utils/lit/tests/Inputs/shtest-define/expansion-order.txt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
#
88
# REDEFINE: %{global:greeting}=Hello
99
# RUN: %{global:echo}
10-
# CHECK: GLOBAL: Hello World
10+
# CHECK:# | GLOBAL: Hello World
1111

1212
# We can redefine the test suite config's substitutions multiple times. Again,
1313
# the expansion order remains the same (%{global:echo} before %{global:greeting}
@@ -17,7 +17,7 @@
1717
# REDEFINE: %{global:greeting}=Goodbye %{global:what}
1818
# REDEFINE: %{global:what}=Sleep
1919
# RUN: %{global:echo}
20-
# CHECK: GLOBAL: Goodbye Sleep Sleep
20+
# CHECK:# | GLOBAL: Goodbye Sleep Sleep
2121

2222
# A new local substitution is prepended to the substitution list so that it can
2323
# depend on all substitutions that were defined previously, including those from
@@ -26,7 +26,7 @@
2626
# DEFINE: %{local:greeting}=Hey %{global:what}
2727
# DEFINE: %{local:echo}=echo "LOCAL: %{local:greeting} %{global:what}"
2828
# RUN: %{local:echo}
29-
# CHECK: LOCAL: Hey Sleep Sleep
29+
# CHECK:# | LOCAL: Hey Sleep Sleep
3030

3131
# As for substitutions from the test suite config, redefining local
3232
# substitutions should not change the expansion order. Again, the expansion
@@ -36,6 +36,6 @@
3636
# REDEFINE: %{local:greeting}=So Long %{global:what}
3737
# REDEFINE: %{global:what}=World
3838
# RUN: %{local:echo}
39-
# CHECK: LOCAL: So Long World World
39+
# CHECK:# | LOCAL: So Long World World
4040

4141
# CHECK: Passed: 1

0 commit comments

Comments
 (0)