Skip to content

Commit 31d0c59

Browse files
authored
Merge pull request #3865 from boegel/check_async_cmd
add check_async_cmd function to facilitate checking on asynchronously running commands
2 parents e358782 + bdf1e20 commit 31d0c59

File tree

2 files changed

+106
-5
lines changed

2 files changed

+106
-5
lines changed

easybuild/tools/run.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,47 @@ def run_cmd(cmd, log_ok=True, log_all=False, simple=False, inp=None, regexp=True
248248
regexp=regexp, stream_output=stream_output, trace=trace)
249249

250250

251+
def check_async_cmd(proc, cmd, owd, start_time, cmd_log, fail_on_error=True, output_read_size=1024, output=''):
252+
"""
253+
Check status of command that was started asynchronously.
254+
255+
:param proc: subprocess.Popen instance representing asynchronous command
256+
:param cmd: command being run
257+
:param owd: original working directory
258+
:param start_time: start time of command (datetime instance)
259+
:param cmd_log: log file to print command output to
260+
:param fail_on_error: raise EasyBuildError when command exited with an error
261+
:param output_read_size: number of bytes to read from output
262+
:param output: already collected output for this command
263+
264+
:result: dict value with result of the check (boolean 'done', 'exit_code', 'output')
265+
"""
266+
# use small read size, to avoid waiting for a long time until sufficient output is produced
267+
if output_read_size:
268+
if not isinstance(output_read_size, int) or output_read_size < 0:
269+
raise EasyBuildError("Number of output bytes to read should be a positive integer value (or zero)")
270+
add_out = get_output_from_process(proc, read_size=output_read_size)
271+
_log.debug("Additional output from asynchronous command '%s': %s" % (cmd, add_out))
272+
output += add_out
273+
274+
exit_code = proc.poll()
275+
if exit_code is None:
276+
_log.debug("Asynchronous command '%s' still running..." % cmd)
277+
done = False
278+
else:
279+
_log.debug("Asynchronous command '%s' completed!", cmd)
280+
output, _ = complete_cmd(proc, cmd, owd, start_time, cmd_log, output=output,
281+
simple=False, trace=False, log_ok=fail_on_error)
282+
done = True
283+
284+
res = {
285+
'done': done,
286+
'exit_code': exit_code,
287+
'output': output,
288+
}
289+
return res
290+
291+
251292
def complete_cmd(proc, cmd, owd, start_time, cmd_log, log_ok=True, log_all=False, simple=False,
252293
regexp=True, stream_output=None, trace=True, output=''):
253294
"""

test/framework/run.py

Lines changed: 65 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747
import easybuild.tools.utilities
4848
from easybuild.tools.build_log import EasyBuildError, init_logging, stop_logging
4949
from easybuild.tools.filetools import adjust_permissions, read_file, write_file
50-
from easybuild.tools.run import check_log_for_errors, complete_cmd, get_output_from_process
50+
from easybuild.tools.run import check_async_cmd, check_log_for_errors, complete_cmd, get_output_from_process
5151
from easybuild.tools.run import parse_log_for_error, run_cmd, run_cmd_qa
5252
from easybuild.tools.config import ERROR, IGNORE, WARN
5353

@@ -575,7 +575,8 @@ def test_run_cmd_async(self):
575575

576576
os.environ['TEST'] = 'test123'
577577

578-
cmd_info = run_cmd("sleep 2; echo $TEST", asynchronous=True)
578+
test_cmd = "echo 'sleeping...'; sleep 2; echo $TEST"
579+
cmd_info = run_cmd(test_cmd, asynchronous=True)
579580
proc = cmd_info[0]
580581

581582
# change value of $TEST to check that command is completed with correct environment
@@ -585,18 +586,51 @@ def test_run_cmd_async(self):
585586
ec = proc.poll()
586587
self.assertEqual(ec, None)
587588

589+
# wait until command is done
588590
while ec is None:
589591
time.sleep(1)
590592
ec = proc.poll()
591593

592594
out, ec = complete_cmd(*cmd_info, simple=False)
593595
self.assertEqual(ec, 0)
594-
self.assertEqual(out, 'test123\n')
596+
self.assertEqual(out, 'sleeping...\ntest123\n')
597+
598+
# also test use of check_async_cmd function
599+
os.environ['TEST'] = 'test123'
600+
cmd_info = run_cmd(test_cmd, asynchronous=True)
601+
602+
# first check, only read first 12 output characters
603+
# (otherwise we'll be waiting until command is completed)
604+
res = check_async_cmd(*cmd_info, output_read_size=12)
605+
self.assertEqual(res, {'done': False, 'exit_code': None, 'output': 'sleeping...\n'})
606+
607+
# 2nd check with default output size (1024) gets full output
608+
res = check_async_cmd(*cmd_info, output=res['output'])
609+
self.assertEqual(res, {'done': True, 'exit_code': 0, 'output': 'sleeping...\ntest123\n'})
610+
611+
# check asynchronous running of failing command
612+
error_test_cmd = "echo 'FAIL!' >&2; exit 123"
613+
cmd_info = run_cmd(error_test_cmd, asynchronous=True)
614+
error_pattern = 'cmd ".*" exited with exit code 123'
615+
self.assertErrorRegex(EasyBuildError, error_pattern, check_async_cmd, *cmd_info)
616+
617+
cmd_info = run_cmd(error_test_cmd, asynchronous=True)
618+
res = check_async_cmd(*cmd_info, fail_on_error=False)
619+
self.assertEqual(res, {'done': True, 'exit_code': 123, 'output': "FAIL!\n"})
595620

596621
# also test with a command that produces a lot of output,
597622
# since that tends to lock up things unless we frequently grab some output...
598-
cmd = "echo start; for i in $(seq 1 50); do sleep 0.1; for j in $(seq 1000); do echo foo; done; done; echo done"
599-
cmd_info = run_cmd(cmd, asynchronous=True)
623+
verbose_test_cmd = ';'.join([
624+
"echo start",
625+
"for i in $(seq 1 50)",
626+
"do sleep 0.1",
627+
"for j in $(seq 1000)",
628+
"do echo foo",
629+
"done",
630+
"done",
631+
"echo done",
632+
])
633+
cmd_info = run_cmd(verbose_test_cmd, asynchronous=True)
600634
proc = cmd_info[0]
601635

602636
output = ''
@@ -613,6 +647,32 @@ def test_run_cmd_async(self):
613647
self.assertTrue(out.startswith('start\n'))
614648
self.assertTrue(out.endswith('\ndone\n'))
615649

650+
# also test use of check_async_cmd on verbose test command
651+
cmd_info = run_cmd(verbose_test_cmd, asynchronous=True)
652+
653+
error_pattern = r"Number of output bytes to read should be a positive integer value \(or zero\)"
654+
self.assertErrorRegex(EasyBuildError, error_pattern, check_async_cmd, *cmd_info, output_read_size=-1)
655+
self.assertErrorRegex(EasyBuildError, error_pattern, check_async_cmd, *cmd_info, output_read_size='foo')
656+
657+
# with output_read_size set to 0, no output is read yet, only status of command is checked
658+
res = check_async_cmd(*cmd_info, output_read_size=0)
659+
self.assertEqual(res['done'], False)
660+
self.assertEqual(res['exit_code'], None)
661+
self.assertEqual(res['output'], '')
662+
663+
res = check_async_cmd(*cmd_info)
664+
self.assertEqual(res['done'], False)
665+
self.assertEqual(res['exit_code'], None)
666+
self.assertTrue(res['output'].startswith('start\n'))
667+
self.assertFalse(res['output'].endswith('\ndone\n'))
668+
# keep checking until command is complete
669+
while not res['done']:
670+
res = check_async_cmd(*cmd_info, output=res['output'])
671+
self.assertEqual(res['done'], True)
672+
self.assertEqual(res['exit_code'], 0)
673+
self.assertTrue(res['output'].startswith('start\n'))
674+
self.assertTrue(res['output'].endswith('\ndone\n'))
675+
616676
def test_check_log_for_errors(self):
617677
fd, logfile = tempfile.mkstemp(suffix='.log', prefix='eb-test-')
618678
os.close(fd)

0 commit comments

Comments
 (0)