Skip to content

Commit bb9f41f

Browse files
authored
Merge pull request #4666 from boegel/run_shell_cmd_cmd.sh_script
include path to `cmd.sh` script in output generated by `run_shell_cmd` when a command fails + use colors: red for ERROR line, yellow for path to output files + `cmd.sh` script
2 parents 8fc631d + aa39c6d commit bb9f41f

File tree

5 files changed

+100
-42
lines changed

5 files changed

+100
-42
lines changed

easybuild/tools/output.py

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
"""
3434
import functools
3535
from collections import OrderedDict
36+
import sys
3637

3738
from easybuild.tools.build_log import EasyBuildError
3839
from easybuild.tools.config import OUTPUT_STYLE_RICH, build_option, get_output_style
@@ -328,9 +329,7 @@ def print_checks(checks_data):
328329

329330
if use_rich():
330331
console = Console()
331-
# don't use console.print, which causes SyntaxError in Python 2
332-
console_print = getattr(console, 'print') # noqa: B009
333-
console_print('')
332+
console.print('')
334333

335334
for section in checks_data:
336335
section_checks = checks_data[section]
@@ -382,11 +381,25 @@ def print_checks(checks_data):
382381
lines.append('')
383382

384383
if use_rich():
385-
console_print(table)
384+
console.print(table)
386385
else:
387386
print('\n'.join(lines))
388387

389388

389+
def print_error(error_msg, rich_highlight=True):
390+
"""
391+
Print error message, using a Rich Console instance if possible.
392+
Newlines before/after message are automatically added.
393+
394+
:param rich_highlight: boolean indicating whether automatic highlighting by Rich should be enabled
395+
"""
396+
if use_rich():
397+
console = Console(stderr=True)
398+
console.print('\n\n' + error_msg + '\n', highlight=rich_highlight)
399+
else:
400+
sys.stderr.write('\n' + error_msg + '\n\n')
401+
402+
390403
# this constant must be defined at the end, since functions used as values need to be defined
391404
PROGRESS_BAR_TYPES = {
392405
PROGRESS_BAR_DOWNLOAD_ALL: download_all_progress_bar,

easybuild/tools/run.py

Lines changed: 41 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,6 @@
4545
import shutil
4646
import string
4747
import subprocess
48-
import sys
4948
import tempfile
5049
import time
5150
from collections import namedtuple
@@ -68,6 +67,7 @@
6867
from easybuild.tools.build_log import dry_run_msg, print_msg, time_str_since
6968
from easybuild.tools.config import build_option
7069
from easybuild.tools.hooks import RUN_SHELL_CMD, load_hooks, run_hook
70+
from easybuild.tools.output import COLOR_RED, COLOR_YELLOW, colorize, print_error
7171
from easybuild.tools.utilities import trace_msg
7272

7373

@@ -86,7 +86,20 @@
8686
)
8787

8888
RunShellCmdResult = namedtuple('RunShellCmdResult', ('cmd', 'exit_code', 'output', 'stderr', 'work_dir',
89-
'out_file', 'err_file', 'thread_id', 'task_id'))
89+
'out_file', 'err_file', 'cmd_sh', 'thread_id', 'task_id'))
90+
RunShellCmdResult.__doc__ = """A namedtuple that represents the result of a call to run_shell_cmd,
91+
with the following fields:
92+
- cmd: the command that was executed;
93+
- exit_code: the exit code of the command (zero if it was successful, non-zero if not);
94+
- output: output of the command (stdout+stderr combined, only stdout if stderr was caught separately);
95+
- stderr: stderr output produced by the command, if caught separately (None otherwise);
96+
- work_dir: the working directory of the command;
97+
- out_file: path to file with output of command (stdout+stderr combined, only stdout if stderr was caught separately);
98+
- err_file: path to file with stderr output of command, if caught separately (None otherwise);
99+
- cmd_sh: path to script to set up interactive shell with environment in which command was executed;
100+
- thread_id: thread ID of command that was executed (None unless asynchronous mode was enabled for running command);
101+
- task_id: task ID of command, if it was specified (None otherwise);
102+
"""
90103

91104

92105
class RunShellCmdError(BaseException):
@@ -101,6 +114,7 @@ def __init__(self, cmd_result, caller_info, *args, **kwargs):
101114
self.out_file = cmd_result.out_file
102115
self.stderr = cmd_result.stderr
103116
self.err_file = cmd_result.err_file
117+
self.cmd_sh = cmd_result.cmd_sh
104118

105119
self.caller_info = caller_info
106120

@@ -112,33 +126,36 @@ def print(self):
112126
Report failed shell command for this RunShellCmdError instance
113127
"""
114128

115-
def pad_4_spaces(msg):
116-
return ' ' * 4 + msg
129+
def pad_4_spaces(msg, color=None):
130+
padded_msg = ' ' * 4 + msg
131+
if color:
132+
return colorize(padded_msg, color)
133+
else:
134+
return padded_msg
135+
136+
caller_file_name, caller_line_nr, caller_function_name = self.caller_info
137+
called_from_info = f"'{caller_function_name}' function in {caller_file_name} (line {caller_line_nr})"
117138

118139
error_info = [
119-
'',
120-
"ERROR: Shell command failed!",
140+
colorize("ERROR: Shell command failed!", COLOR_RED),
121141
pad_4_spaces(f"full command -> {self.cmd}"),
122142
pad_4_spaces(f"exit code -> {self.exit_code}"),
143+
pad_4_spaces(f"called from -> {called_from_info}"),
123144
pad_4_spaces(f"working directory -> {self.work_dir}"),
124145
]
125146

126147
if self.out_file is not None:
127148
# if there's no separate file for error/warnings, then out_file includes both stdout + stderr
128149
out_info_msg = "output (stdout + stderr)" if self.err_file is None else "output (stdout) "
129-
error_info.append(pad_4_spaces(f"{out_info_msg} -> {self.out_file}"))
150+
error_info.append(pad_4_spaces(f"{out_info_msg} -> {self.out_file}", color=COLOR_YELLOW))
130151

131152
if self.err_file is not None:
132-
error_info.append(pad_4_spaces(f"error/warnings (stderr) -> {self.err_file}"))
153+
error_info.append(pad_4_spaces(f"error/warnings (stderr) -> {self.err_file}", color=COLOR_YELLOW))
133154

134-
caller_file_name, caller_line_nr, caller_function_name = self.caller_info
135-
called_from_info = f"'{caller_function_name}' function in {caller_file_name} (line {caller_line_nr})"
136-
error_info.extend([
137-
pad_4_spaces(f"called from -> {called_from_info}"),
138-
'',
139-
])
155+
if self.cmd_sh is not None:
156+
error_info.append(pad_4_spaces(f"interactive shell script -> {self.cmd_sh}", color=COLOR_YELLOW))
140157

141-
sys.stderr.write('\n'.join(error_info) + '\n')
158+
print_error('\n'.join(error_info), rich_highlight=False)
142159

143160

144161
def raise_run_shell_cmd_error(cmd_res):
@@ -254,6 +271,8 @@ def create_cmd_scripts(cmd_str, work_dir, env, tmpdir, out_file, err_file):
254271
]))
255272
os.chmod(cmd_fp, 0o775)
256273

274+
return cmd_fp
275+
257276

258277
def _answer_question(stdout, proc, qa_patterns, qa_wait_patterns):
259278
"""
@@ -430,9 +449,9 @@ def to_cmd_str(cmd):
430449
else:
431450
cmd_err_fp = None
432451

433-
create_cmd_scripts(cmd_str, work_dir, env, tmpdir, cmd_out_fp, cmd_err_fp)
452+
cmd_sh = create_cmd_scripts(cmd_str, work_dir, env, tmpdir, cmd_out_fp, cmd_err_fp)
434453
else:
435-
tmpdir, cmd_out_fp, cmd_err_fp = None, None, None
454+
tmpdir, cmd_out_fp, cmd_err_fp, cmd_sh = None, None, None, None
436455

437456
interactive_msg = 'interactive ' if interactive else ''
438457

@@ -445,7 +464,8 @@ def to_cmd_str(cmd):
445464
dry_run_msg(msg, silent=silent)
446465

447466
return RunShellCmdResult(cmd=cmd_str, exit_code=0, output='', stderr=None, work_dir=work_dir,
448-
out_file=cmd_out_fp, err_file=cmd_err_fp, thread_id=thread_id, task_id=task_id)
467+
out_file=cmd_out_fp, err_file=cmd_err_fp, cmd_sh=cmd_sh,
468+
thread_id=thread_id, task_id=task_id)
449469

450470
start_time = datetime.now()
451471
if not hidden:
@@ -574,8 +594,9 @@ def to_cmd_str(cmd):
574594
except IOError as err:
575595
raise EasyBuildError(f"Failed to dump command output to temporary file: {err}")
576596

577-
res = RunShellCmdResult(cmd=cmd_str, exit_code=proc.returncode, output=output, stderr=stderr, work_dir=work_dir,
578-
out_file=cmd_out_fp, err_file=cmd_err_fp, thread_id=thread_id, task_id=task_id)
597+
res = RunShellCmdResult(cmd=cmd_str, exit_code=proc.returncode, output=output, stderr=stderr,
598+
work_dir=work_dir, out_file=cmd_out_fp, err_file=cmd_err_fp, cmd_sh=cmd_sh,
599+
thread_id=thread_id, task_id=task_id)
579600

580601
# always log command output
581602
cmd_name = cmd_str.split(' ')[0]

test/framework/output.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
from easybuild.tools.build_log import EasyBuildError
3636
from easybuild.tools.config import build_option, get_output_style, update_build_option
3737
from easybuild.tools.output import PROGRESS_BAR_EXTENSIONS, PROGRESS_BAR_TYPES
38-
from easybuild.tools.output import DummyRich, colorize, get_progress_bar, show_progress_bars
38+
from easybuild.tools.output import DummyRich, colorize, get_progress_bar, print_error, show_progress_bars
3939
from easybuild.tools.output import start_progress_bar, status_bar, stop_progress_bar, update_progress_bar, use_rich
4040

4141
try:
@@ -139,6 +139,26 @@ def test_colorize(self):
139139

140140
self.assertErrorRegex(EasyBuildError, "Unknown color: nosuchcolor", colorize, 'test', 'nosuchcolor')
141141

142+
def test_print_error(self):
143+
"""
144+
Test print_error function
145+
"""
146+
msg = "This is yellow: " + colorize("a banana", color='yellow')
147+
self.mock_stderr(True)
148+
self.mock_stdout(True)
149+
print_error(msg)
150+
stderr = self.get_stderr()
151+
stdout = self.get_stdout()
152+
self.mock_stderr(False)
153+
self.mock_stdout(False)
154+
self.assertEqual(stdout, '')
155+
if HAVE_RICH:
156+
# when using Rich, message printed to stderr won't have funny terminal escape characters for the color
157+
expected = '\n\nThis is yellow: a banana\n\n'
158+
else:
159+
expected = '\nThis is yellow: \x1b[1;33ma banana\x1b[0m\n\n'
160+
self.assertEqual(stderr, expected)
161+
142162
def test_get_progress_bar(self):
143163
"""
144164
Test get_progress_bar.

test/framework/run.py

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -491,12 +491,14 @@ def handler(signum, _):
491491
# check error reporting output
492492
stderr = stderr.getvalue()
493493
patterns = [
494-
r"^ERROR: Shell command failed!",
495-
r"^\s+full command\s* -> kill -9 \$\$",
496-
r"^\s+exit code\s* -> -9",
497-
r"^\s+working directory\s* -> " + work_dir,
498-
r"^\s+called from\s* -> 'test_run_shell_cmd_fail' function in .*/test/.*/run.py \(line [0-9]+\)",
499-
r"^\s+output \(stdout \+ stderr\)\s* -> .*/run-shell-cmd-output/kill-.*/out.txt",
494+
r"ERROR: Shell command failed!",
495+
r"\s+full command\s* -> kill -9 \$\$",
496+
r"\s+exit code\s* -> -9",
497+
r"\s+working directory\s* -> " + work_dir,
498+
r"\s+called from\s* -> 'test_run_shell_cmd_fail' function in "
499+
r"(.|\n)*/test/(.|\n)*/run.py \(line [0-9]+\)",
500+
r"\s+output \(stdout \+ stderr\)\s* -> (.|\n)*/run-shell-cmd-output/kill-(.|\n)*/out.txt",
501+
r"\s+interactive shell script\s* -> (.|\n)*/run-shell-cmd-output/kill-(.|\n)*/cmd.sh",
500502
]
501503
for pattern in patterns:
502504
regex = re.compile(pattern, re.M)
@@ -526,13 +528,15 @@ def handler(signum, _):
526528
# check error reporting output
527529
stderr = stderr.getvalue()
528530
patterns = [
529-
r"^ERROR: Shell command failed!",
530-
r"^\s+full command\s+ -> kill -9 \$\$",
531-
r"^\s+exit code\s+ -> -9",
532-
r"^\s+working directory\s+ -> " + work_dir,
533-
r"^\s+called from\s+ -> 'test_run_shell_cmd_fail' function in .*/test/.*/run.py \(line [0-9]+\)",
534-
r"^\s+output \(stdout\)\s+ -> .*/run-shell-cmd-output/kill-.*/out.txt",
535-
r"^\s+error/warnings \(stderr\)\s+ -> .*/run-shell-cmd-output/kill-.*/err.txt",
531+
r"ERROR: Shell command failed!",
532+
r"\s+full command\s+ -> kill -9 \$\$",
533+
r"\s+exit code\s+ -> -9",
534+
r"\s+working directory\s+ -> " + work_dir,
535+
r"\s+called from\s+ -> 'test_run_shell_cmd_fail' function in "
536+
r"(.|\n)*/test/(.|\n)*/run.py \(line [0-9]+\)",
537+
r"\s+output \(stdout\)\s+ -> (.|\n)*/run-shell-cmd-output/kill-(.|\n)*/out.txt",
538+
r"\s+error/warnings \(stderr\)\s+ -> (.|\n)*/run-shell-cmd-output/kill-(.|\n)*/err.txt",
539+
r"\s+interactive shell script\s* -> (.|\n)*/run-shell-cmd-output/kill-(.|\n)*/cmd.sh",
536540
]
537541
for pattern in patterns:
538542
regex = re.compile(pattern, re.M)
@@ -1383,7 +1387,7 @@ def test_run_shell_cmd_cache(self):
13831387
with self.mocked_stdout_stderr():
13841388
cached_res = RunShellCmdResult(cmd=cmd, output="123456", exit_code=123, stderr=None,
13851389
work_dir='/test_ulimit', out_file='/tmp/foo.out', err_file=None,
1386-
thread_id=None, task_id=None)
1390+
cmd_sh='/tmp/cmd.sh', thread_id=None, task_id=None)
13871391
run_shell_cmd.update_cache({(cmd, None): cached_res})
13881392
res = run_shell_cmd(cmd)
13891393
self.assertEqual(res.cmd, cmd)
@@ -1403,7 +1407,7 @@ def test_run_shell_cmd_cache(self):
14031407
with self.mocked_stdout_stderr():
14041408
cached_res = RunShellCmdResult(cmd=cmd, output="bar", exit_code=123, stderr=None,
14051409
work_dir='/test_cat', out_file='/tmp/cat.out', err_file=None,
1406-
thread_id=None, task_id=None)
1410+
cmd_sh='/tmp/cmd.sh', thread_id=None, task_id=None)
14071411
run_shell_cmd.update_cache({(cmd, 'foo'): cached_res})
14081412
res = run_shell_cmd(cmd, stdin='foo')
14091413
self.assertEqual(res.cmd, cmd)

test/framework/systemtools.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -341,7 +341,7 @@ def mocked_run_shell_cmd(cmd, **kwargs):
341341
}
342342
if cmd in known_cmds:
343343
return RunShellCmdResult(cmd=cmd, exit_code=0, output=known_cmds[cmd], stderr=None, work_dir=os.getcwd(),
344-
out_file=None, err_file=None, thread_id=None, task_id=None)
344+
out_file=None, err_file=None, cmd_sh=None, thread_id=None, task_id=None)
345345
else:
346346
return run_shell_cmd(cmd, **kwargs)
347347

@@ -774,7 +774,7 @@ def test_gcc_version_darwin(self):
774774
out = "Apple LLVM version 7.0.0 (clang-700.1.76)"
775775
cwd = os.getcwd()
776776
mocked_run_res = RunShellCmdResult(cmd="gcc --version", exit_code=0, output=out, stderr=None, work_dir=cwd,
777-
out_file=None, err_file=None, thread_id=None, task_id=None)
777+
out_file=None, err_file=None, cmd_sh=None, thread_id=None, task_id=None)
778778
st.run_shell_cmd = lambda *args, **kwargs: mocked_run_res
779779
self.assertEqual(get_gcc_version(), None)
780780

0 commit comments

Comments
 (0)