Skip to content

Commit f223022

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 1495d51 commit f223022

34 files changed

+1012
-812
lines changed

libcxx/utils/libcxx/test/dsl.py

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

189-
return libcxx.test.format._parseLitOutput(out)
189+
return out
190190

191191

192192
@_memoizeExpensiveOperation(

libcxx/utils/libcxx/test/format.py

Lines changed: 3 additions & 46 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,9 +358,8 @@ 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):
405-
generatedFile = testSuite.getExecPath(pathInSuite + (subfile, ))
361+
for subfile, content in self._splitFile(out):
362+
generatedFile = testSuite.getExecPath(pathInSuite + (subfile,))
406363
os.makedirs(os.path.dirname(generatedFile), exist_ok=True)
407364
with open(generatedFile, 'w') as f:
408365
f.write(content)

llvm/docs/CommandGuide/lit.rst

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -96,10 +96,9 @@ OUTPUT OPTIONS
9696

9797
Each command is printed before it is executed. This can be valuable for
9898
debugging test failures, as the last printed command is the one that failed.
99-
Moreover, :program:`lit` inserts a no-op command (``:`` in the case of bash)
100-
with argument ``'RUN: at line N'`` before each command pipeline, and those
101-
no-op commands are also printed to help you locate the source line of the
102-
failed command.
99+
Moreover, :program:`lit` inserts ``'RUN: at line N'`` before each
100+
command pipeline in the output to help you locate the source line of
101+
the failed command.
103102

104103
.. option:: -vv, --echo-all-commands
105104

llvm/utils/lit/lit/TestRunner.py

Lines changed: 94 additions & 23 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
@@ -348,12 +349,12 @@ def executeBuiltinExport(cmd, shenv):
348349

349350

350351
def executeBuiltinEcho(cmd, shenv):
351-
"""Interpret a redirected echo command"""
352+
"""Interpret a redirected echo or @echo command"""
352353
opened_files = []
353354
stdin, stdout, stderr = processRedirects(cmd, subprocess.PIPE, shenv, opened_files)
354355
if stdin != subprocess.PIPE or stderr != subprocess.PIPE:
355356
raise InternalShellError(
356-
cmd, "stdin and stderr redirects not supported for echo"
357+
cmd, f"stdin and stderr redirects not supported for {cmd.args[0]}"
357358
)
358359

359360
# Some tests have un-redirected echo commands to help debug test failures.
@@ -700,6 +701,7 @@ def _executeShCmd(cmd, shenv, results, timeoutHelper):
700701
"cd": executeBuiltinCd,
701702
"export": executeBuiltinExport,
702703
"echo": executeBuiltinEcho,
704+
"@echo": executeBuiltinEcho,
703705
"mkdir": executeBuiltinMkdir,
704706
"popd": executeBuiltinPopd,
705707
"pushd": executeBuiltinPushd,
@@ -927,7 +929,7 @@ def _executeShCmd(cmd, shenv, results, timeoutHelper):
927929
if res == -signal.SIGINT:
928930
raise KeyboardInterrupt
929931
if proc_not_counts[i] % 2:
930-
res = not res
932+
res = 1 if res == 0 else 0
931933
elif proc_not_counts[i] > 1:
932934
res = 1 if res != 0 else 0
933935

@@ -990,19 +992,60 @@ def _executeShCmd(cmd, shenv, results, timeoutHelper):
990992
return exitCode
991993

992994

993-
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+
# fmt: off
1005+
out = f"# .---{title}{'-' * (ndashes - 4 - len(title))}\n"
1006+
out += f"# | " + "\n# | ".join(data.splitlines()) + "\n"
1007+
out += f"# `---{msg}{'-' * (ndashes - 4 - len(msg))}\n"
1008+
# fmt: on
1009+
return out
1010+
1011+
1012+
# Normally returns out, err, exitCode, timeoutInfo.
1013+
#
1014+
# If debug is True (the normal lit behavior), err is empty, and out contains an
1015+
# execution trace, including stdout and stderr shown per command executed.
1016+
#
1017+
# If debug is False (set by some custom lit test formats that call this
1018+
# function), out contains only stdout from the script, err contains only stderr
1019+
# from the script, and there is no execution trace.
1020+
def executeScriptInternal(test, litConfig, tmpBase, commands, cwd, debug=True):
9941021
cmds = []
9951022
for i, ln in enumerate(commands):
1023+
# Within lit, we try to always add '%dbg(...)' to command lines in order
1024+
# to maximize debuggability. However, custom lit test formats might not
1025+
# always add it, so add a generic debug message in that case.
9961026
match = re.fullmatch(kPdbgRegex, ln)
9971027
if match:
1028+
dbg = match.group(1)
9981029
command = match.group(2)
999-
ln = commands[i] = match.expand(": '\\1'; \\2" if command else ": '\\1'")
1030+
else:
1031+
dbg = "command line"
1032+
command = ln
1033+
if debug:
1034+
ln = f"@echo '# {dbg}' "
1035+
if command:
1036+
ln += f"&& @echo {shlex.quote(command.lstrip())} && {command}"
1037+
else:
1038+
ln += "has no command after substitutions"
1039+
else:
1040+
ln = command
10001041
try:
10011042
cmds.append(
10021043
ShUtil.ShParser(ln, litConfig.isWindows, test.config.pipefail).parse()
10031044
)
10041045
except:
1005-
return lit.Test.Result(Test.FAIL, "shell parser error on: %r" % ln)
1046+
return lit.Test.Result(
1047+
Test.FAIL, f"shell parser error on {dbg}: {command.lstrip()}\n"
1048+
)
10061049

10071050
cmd = cmds[0]
10081051
for c in cmds[1:]:
@@ -1022,8 +1065,42 @@ def executeScriptInternal(test, litConfig, tmpBase, commands, cwd):
10221065

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

10281105
# If nothing interesting happened, move on.
10291106
if (
@@ -1038,22 +1115,14 @@ def executeScriptInternal(test, litConfig, tmpBase, commands, cwd):
10381115

10391116
# Add the command output, if redirected.
10401117
for (name, path, data) in result.outputFiles:
1041-
if data.strip():
1042-
out += "# redirected output from %r:\n" % (name,)
1043-
data = to_string(data.decode("utf-8", errors="replace"))
1044-
if len(data) > 1024:
1045-
out += data[:1024] + "\n...\n"
1046-
out += "note: data was truncated\n"
1047-
else:
1048-
out += data
1049-
out += "\n"
1050-
1118+
data = to_string(data.decode("utf-8", errors="replace"))
1119+
out += formatOutput(f"redirected output from '{name}'", data, limit=1024)
10511120
if result.stdout.strip():
1052-
out += "# command output:\n%s\n" % (result.stdout,)
1121+
out += formatOutput("command stdout", result.stdout)
10531122
if result.stderr.strip():
1054-
out += "# command stderr:\n%s\n" % (result.stderr,)
1123+
out += formatOutput("command stderr", result.stderr)
10551124
if not result.stdout.strip() and not result.stderr.strip():
1056-
out += "note: command had no output on stdout or stderr\n"
1125+
out += "# note: command had no output on stdout or stderr\n"
10571126

10581127
# Show the error conditions:
10591128
if result.exitCode != 0:
@@ -1063,9 +1132,9 @@ def executeScriptInternal(test, litConfig, tmpBase, commands, cwd):
10631132
codeStr = hex(int(result.exitCode & 0xFFFFFFFF)).rstrip("L")
10641133
else:
10651134
codeStr = str(result.exitCode)
1066-
out += "error: command failed with exit status: %s\n" % (codeStr,)
1135+
out += "# error: command failed with exit status: %s\n" % (codeStr,)
10671136
if litConfig.maxIndividualTestTime > 0 and result.timeoutReached:
1068-
out += "error: command reached timeout: %s\n" % (
1137+
out += "# error: command reached timeout: %s\n" % (
10691138
str(result.timeoutReached),
10701139
)
10711140

@@ -2117,6 +2186,8 @@ def executeShTest(
21172186
return lit.Test.Result(Test.UNSUPPORTED, "Test is unsupported")
21182187

21192188
script = list(preamble_commands)
2189+
script = [buildPdbgCommand(f"preamble command line", ln) for ln in script]
2190+
21202191
parsed = parseIntegratedTestScript(test, require_script=not script)
21212192
if isinstance(parsed, lit.Test.Result):
21222193
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)