Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 10 additions & 10 deletions easybuild/tools/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@
]


RunResult = namedtuple('RunResult', ('output', 'exit_code', 'stderr'))
RunResult = namedtuple('RunResult', ('cmd', 'exit_code', 'output', 'stderr', 'work_dir'))


def run_cmd_cache(func):
Expand Down Expand Up @@ -142,9 +142,9 @@ def run(cmd, fail_on_error=True, split_stderr=False, stdin=None, env=None,
raise NotImplementedError

if isinstance(cmd, str):
cmd_msg = cmd.strip()
cmd_str = cmd.strip()
elif isinstance(cmd, list):
cmd_msg = ' '.join(cmd)
cmd_str = ' '.join(cmd)
else:
raise EasyBuildError(f"Unknown command type ('{type(cmd)}'): {cmd}")

Expand All @@ -164,15 +164,15 @@ def run(cmd, fail_on_error=True, split_stderr=False, stdin=None, env=None,
if not in_dry_run and build_option('extended_dry_run'):
if not hidden:
silent = build_option('silent')
msg = f" running command \"{cmd_msg}\"\n"
msg = f" running command \"{cmd_str}\"\n"
msg += f" (in {work_dir})"
dry_run_msg(msg, silent=silent)

return RunResult(output='', exit_code=0, stderr=None)
return RunResult(cmd=cmd_str, exit_code=0, output='', stderr=None, work_dir=work_dir)

start_time = datetime.now()
if not hidden:
cmd_trace_msg(cmd_msg, start_time, work_dir, stdin, cmd_out_fp)
cmd_trace_msg(cmd_str, start_time, work_dir, stdin, cmd_out_fp)

if stdin:
# 'input' value fed to subprocess.run must be a byte sequence
Expand All @@ -185,21 +185,21 @@ def run(cmd, fail_on_error=True, split_stderr=False, stdin=None, env=None,

stderr = subprocess.PIPE if split_stderr else subprocess.STDOUT

_log.info(f"Running command '{cmd_msg}' in {work_dir}")
_log.info(f"Running command '{cmd_str}' in {work_dir}")
proc = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=stderr, check=fail_on_error,
env=env, input=stdin, shell=shell, executable=executable)

# return output as a regular string rather than a byte sequence (and non-UTF-8 characters get stripped out)
output = proc.stdout.decode('utf-8', 'ignore')
stderr_output = proc.stderr.decode('utf-8', 'ignore') if split_stderr else None

res = RunResult(output=output, exit_code=proc.returncode, stderr=stderr_output)
res = RunResult(cmd=cmd_str, exit_code=proc.returncode, output=output, stderr=stderr_output, work_dir=work_dir)

if split_stderr:
log_msg = f"Command '{cmd_msg}' exited with exit code {res.exit_code}, "
log_msg = f"Command '{cmd_str}' exited with exit code {res.exit_code}, "
log_msg += f"with stdout:\n{res.output}\nstderr:\n{res.stderr}"
else:
log_msg = f"Command '{cmd_msg}' exited with exit code {res.exit_code} and output:\n{res.output}"
log_msg = f"Command '{cmd_str}' exited with exit code {res.exit_code} and output:\n{res.output}"
_log.info(log_msg)

if not hidden:
Expand Down
37 changes: 26 additions & 11 deletions test/framework/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,8 +166,11 @@ def test_run_basic(self):
res = run("echo hello")
self.assertEqual(res.output, "hello\n")
# no reason echo hello could fail
self.assertEqual(res.cmd, "echo hello")
self.assertEqual(res.exit_code, 0)
self.assertEqual(type(res.output), str)
self.assertTrue(isinstance(res.output, str))
self.assertEqual(res.stderr, None)
self.assertTrue(res.work_dir and isinstance(res.work_dir, str))

# test running command that emits non-UTF-8 characters
# this is constructed to reproduce errors like:
Expand All @@ -181,9 +184,11 @@ def test_run_basic(self):

with self.mocked_stdout_stderr():
res = run(cmd)
self.assertEqual(res.cmd, cmd)
self.assertEqual(res.exit_code, 0)
self.assertTrue(res.output.startswith('foo ') and res.output.endswith(' bar'))
self.assertEqual(type(res.output), str)
self.assertTrue(isinstance(res.output, str))
self.assertTrue(res.work_dir and isinstance(res.work_dir, str))

def test_run_cmd_log(self):
"""Test logging of executed commands."""
Expand Down Expand Up @@ -761,37 +766,47 @@ def test_run_cmd_cache(self):

def test_run_cache(self):
"""Test caching for run"""

cmd = "ulimit -u"
with self.mocked_stdout_stderr():
res = run("ulimit -u")
res = run(cmd)
first_out = res.output
self.assertEqual(res.exit_code, 0)

with self.mocked_stdout_stderr():
res = run("ulimit -u")
res = run(cmd)
cached_out = res.output
self.assertEqual(res.exit_code, 0)
self.assertEqual(first_out, cached_out)

# inject value into cache to check whether executing command again really returns cached value
with self.mocked_stdout_stderr():
cached_res = RunResult(output="123456", exit_code=123, stderr=None)
run.update_cache({("ulimit -u", None): cached_res})
res = run("ulimit -u")
cached_res = RunResult(cmd=cmd, output="123456", exit_code=123, stderr=None, work_dir='/test_ulimit')
run.update_cache({(cmd, None): cached_res})
res = run(cmd)
self.assertEqual(res.cmd, cmd)
self.assertEqual(res.exit_code, 123)
self.assertEqual(res.output, "123456")
self.assertEqual(res.stderr, None)
self.assertEqual(res.work_dir, '/test_ulimit')

# also test with command that uses stdin
cmd = "cat"
with self.mocked_stdout_stderr():
res = run("cat", stdin='foo')
res = run(cmd, stdin='foo')
self.assertEqual(res.exit_code, 0)
self.assertEqual(res.output, 'foo')

# inject different output for cat with 'foo' as stdin to check whether cached value is used
with self.mocked_stdout_stderr():
cached_res = RunResult(output="bar", exit_code=123, stderr=None)
run.update_cache({('cat', 'foo'): cached_res})
res = run("cat", stdin='foo')
cached_res = RunResult(cmd=cmd, output="bar", exit_code=123, stderr=None, work_dir='/test_cat')
run.update_cache({(cmd, 'foo'): cached_res})
res = run(cmd, stdin='foo')
self.assertEqual(res.cmd, cmd)
self.assertEqual(res.exit_code, 123)
self.assertEqual(res.output, 'bar')
self.assertEqual(res.stderr, None)
self.assertEqual(res.work_dir, '/test_cat')

run.clear_cache()

Expand Down
5 changes: 3 additions & 2 deletions test/framework/systemtools.py
Original file line number Diff line number Diff line change
Expand Up @@ -340,7 +340,7 @@ def mocked_run(cmd, **kwargs):
"ulimit -u": '40',
}
if cmd in known_cmds:
return RunResult(output=known_cmds[cmd], exit_code=0, stderr=None)
return RunResult(cmd=cmd, exit_code=0, output=known_cmds[cmd], stderr=None, work_dir=os.getcwd())
else:
return run(cmd, **kwargs)

Expand Down Expand Up @@ -774,7 +774,8 @@ def test_gcc_version_darwin(self):
"""Test getting gcc version (mocked for Darwin)."""
st.get_os_type = lambda: st.DARWIN
out = "Apple LLVM version 7.0.0 (clang-700.1.76)"
st.run = lambda *args, **kwargs: RunResult(output=out, exit_code=0, stderr=None)
mocked_run_res = RunResult(cmd="gcc --version", exit_code=0, output=out, stderr=None, work_dir=os.getcwd())
st.run = lambda *args, **kwargs: mocked_run_res
self.assertEqual(get_gcc_version(), None)

def test_glibc_version_native(self):
Expand Down