diff --git a/openmc/executor.py b/openmc/executor.py index 75094e7388f..44ed1b80b1f 100644 --- a/openmc/executor.py +++ b/openmc/executor.py @@ -92,6 +92,36 @@ def _process_CLI_arguments(volume=False, geometry_debug=False, particles=None, return args +def _get_error_message(output): + """Get the error message from the output of a failed OpenMC run + + Parameters + ---------- + output : str + Standard output and standard error of the OpenMC executable + + Returns + ------- + str + Error message with whitespace simplified + + """ + if 'ERROR: ' in output: + _, _, error_msg = output.partition('ERROR: ') + elif 'what()' in output: + _, _, error_msg = output.partition('what(): ') + elif 'terminating due to uncaught exception of type ' in output: + # libc++ (e.g. macOS) reports "...of type std::runtime_error: msg" + _, _, error_msg = output.partition( + 'terminating due to uncaught exception of type ') + _, sep, error_msg = error_msg.partition(': ') + if not sep or not error_msg.strip(): + error_msg = 'OpenMC aborted unexpectedly.' + else: + error_msg = 'OpenMC aborted unexpectedly.' + return ' '.join(error_msg.split()) + + def _run(args, output, cwd): # Launch a subprocess p = subprocess.Popen(args, cwd=cwd, stdout=subprocess.PIPE, @@ -113,17 +143,7 @@ def _run(args, output, cwd): # Raise an exception if return status is non-zero if p.returncode != 0: - # Get error message from output and simplify whitespace - output = ''.join(lines) - if 'ERROR: ' in output: - _, _, error_msg = output.partition('ERROR: ') - elif 'what()' in output: - _, _, error_msg = output.partition('what(): ') - else: - error_msg = 'OpenMC aborted unexpectedly.' - error_msg = ' '.join(error_msg.split()) - - raise RuntimeError(error_msg) + raise RuntimeError(_get_error_message(''.join(lines))) def plot_geometry(output=True, openmc_exec='openmc', cwd='.', path_input=None): diff --git a/tests/unit_tests/test_executor.py b/tests/unit_tests/test_executor.py new file mode 100644 index 00000000000..0460b10c27f --- /dev/null +++ b/tests/unit_tests/test_executor.py @@ -0,0 +1,43 @@ +import io +import subprocess + +import openmc +from openmc.executor import _get_error_message +import pytest + + +@pytest.mark.parametrize('output, message', [ + # OpenMC's own error messages + (' ERROR: Could not add nuclide', 'Could not add nuclide'), + # libstdc++ (e.g. Linux) + ("terminate called after throwing an instance of 'std::runtime_error'\n" + " what(): Could not add nuclide", 'Could not add nuclide'), + # libc++ (e.g. macOS) + ('libc++abi: terminating due to uncaught exception of type ' + 'std::runtime_error: Could not add nuclide', 'Could not add nuclide'), + # libc++ with an exception that has no message + ('libc++abi: terminating due to uncaught exception of type int', + 'OpenMC aborted unexpectedly.'), + # Any other failure + ('Segmentation fault', 'OpenMC aborted unexpectedly.'), +]) +def test_error_message_from_output(output, message): + """The error message is recovered from each way OpenMC can fail.""" + assert _get_error_message(output) == message + + +def test_failed_run_raises_error_message(monkeypatch): + """A failed run raises a RuntimeError with the message from its output.""" + class FailedProcess: + def __init__(self, *args, **kwargs): + self.stdout = io.StringIO( + 'libc++abi: terminating due to uncaught exception of type ' + 'std::runtime_error: Could not add nuclide\n') + self.returncode = 1 + + def poll(self): + return self.returncode + + monkeypatch.setattr(subprocess, 'Popen', FailedProcess) + with pytest.raises(RuntimeError, match='^Could not add nuclide$'): + openmc.plot_geometry(output=False)