From 37f557175c344e9146c3a328ab35660ea2d819ff Mon Sep 17 00:00:00 2001 From: Kavya Wadhwa <72140379+kavyawadhwa134@users.noreply.github.com> Date: Wed, 14 Oct 2026 19:28:08 +0530 Subject: [PATCH 1/3] Recognize libc++ uncaught exception messages in executor --- openmc/executor.py | 5 +++++ tests/unit_tests/test_executor.py | 27 +++++++++++++++++++++++++++ 2 files changed, 32 insertions(+) create mode 100644 tests/unit_tests/test_executor.py diff --git a/openmc/executor.py b/openmc/executor.py index 75094e7388f..1c6d8edf114 100644 --- a/openmc/executor.py +++ b/openmc/executor.py @@ -119,6 +119,11 @@ def _run(args, output, cwd): _, _, 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 ') + _, _, error_msg = error_msg.partition(': ') else: error_msg = 'OpenMC aborted unexpectedly.' error_msg = ' '.join(error_msg.split()) diff --git a/tests/unit_tests/test_executor.py b/tests/unit_tests/test_executor.py new file mode 100644 index 00000000000..01ac82e16a5 --- /dev/null +++ b/tests/unit_tests/test_executor.py @@ -0,0 +1,27 @@ +import os +import sys + +import openmc +import pytest + + +@pytest.mark.parametrize('output', [ + # OpenMC's own error messages + ' ERROR: Could not add nuclide', + # libstdc++ (e.g. Linux) + "terminate called after throwing an instance of 'std::runtime_error'\n" + " what(): Could not add nuclide", + # libc++ (e.g. macOS) + 'libc++abi: terminating due to uncaught exception of type ' + 'std::runtime_error: Could not add nuclide', +]) +def test_error_message_from_output(run_in_tmpdir, output): + """The error message is recovered from each way OpenMC can fail.""" + # Stand-in for the openmc executable that prints the output and fails + fake_openmc = os.path.abspath('fake_openmc') + with open(fake_openmc, 'w') as f: + f.write(f'#!{sys.executable}\nimport sys\nprint({output!r})\nsys.exit(1)\n') + os.chmod(fake_openmc, 0o755) + + with pytest.raises(RuntimeError, match='^Could not add nuclide$'): + openmc.plot_geometry(output=False, openmc_exec=fake_openmc) From 146c1a9b167380b3537b0fc876c39aeb4c8759fc Mon Sep 17 00:00:00 2001 From: Kavya Wadhwa <72140379+kavyawadhwa134@users.noreply.github.com> Date: Thu, 15 Oct 2026 10:10:13 +0530 Subject: [PATCH 2/3] Fall back to the abort message for libc++ errors without a message --- openmc/executor.py | 4 +++- tests/unit_tests/test_executor.py | 30 +++++++++++++++++------------- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/openmc/executor.py b/openmc/executor.py index 1c6d8edf114..7b5fe2f53ac 100644 --- a/openmc/executor.py +++ b/openmc/executor.py @@ -123,7 +123,9 @@ def _run(args, output, cwd): # libc++ (e.g. macOS) reports "...of type std::runtime_error: msg" _, _, error_msg = output.partition( 'terminating due to uncaught exception of type ') - _, _, error_msg = error_msg.partition(': ') + _, 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.' error_msg = ' '.join(error_msg.split()) diff --git a/tests/unit_tests/test_executor.py b/tests/unit_tests/test_executor.py index 01ac82e16a5..3388fc53b0a 100644 --- a/tests/unit_tests/test_executor.py +++ b/tests/unit_tests/test_executor.py @@ -1,27 +1,31 @@ -import os +from pathlib import Path import sys import openmc import pytest -@pytest.mark.parametrize('output', [ +@pytest.mark.parametrize('output, message', [ # OpenMC's own error messages - ' ERROR: Could not add nuclide', + (' 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", + ("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', + ('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.'), ]) -def test_error_message_from_output(run_in_tmpdir, output): +def test_error_message_from_output(run_in_tmpdir, output, message): """The error message is recovered from each way OpenMC can fail.""" # Stand-in for the openmc executable that prints the output and fails - fake_openmc = os.path.abspath('fake_openmc') - with open(fake_openmc, 'w') as f: + fake_openmc = Path.cwd() / 'fake_openmc' + with fake_openmc.open('w') as f: f.write(f'#!{sys.executable}\nimport sys\nprint({output!r})\nsys.exit(1)\n') - os.chmod(fake_openmc, 0o755) + fake_openmc.chmod(0o755) - with pytest.raises(RuntimeError, match='^Could not add nuclide$'): - openmc.plot_geometry(output=False, openmc_exec=fake_openmc) + with pytest.raises(RuntimeError) as excinfo: + openmc.plot_geometry(output=False, openmc_exec=str(fake_openmc)) + assert str(excinfo.value) == message From 32533e26661d47bd6462407a096a43026883cff1 Mon Sep 17 00:00:00 2001 From: Kavya Wadhwa <72140379+kavyawadhwa134@users.noreply.github.com> Date: Thu, 15 Oct 2026 20:06:54 +0530 Subject: [PATCH 3/3] Factor out error message parsing and test it directly --- openmc/executor.py | 49 +++++++++++++++++++------------ tests/unit_tests/test_executor.py | 34 ++++++++++++++------- 2 files changed, 54 insertions(+), 29 deletions(-) diff --git a/openmc/executor.py b/openmc/executor.py index 7b5fe2f53ac..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,24 +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(): ') - 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.' - 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 index 3388fc53b0a..0460b10c27f 100644 --- a/tests/unit_tests/test_executor.py +++ b/tests/unit_tests/test_executor.py @@ -1,7 +1,8 @@ -from pathlib import Path -import sys +import io +import subprocess import openmc +from openmc.executor import _get_error_message import pytest @@ -17,15 +18,26 @@ # 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(run_in_tmpdir, output, message): +def test_error_message_from_output(output, message): """The error message is recovered from each way OpenMC can fail.""" - # Stand-in for the openmc executable that prints the output and fails - fake_openmc = Path.cwd() / 'fake_openmc' - with fake_openmc.open('w') as f: - f.write(f'#!{sys.executable}\nimport sys\nprint({output!r})\nsys.exit(1)\n') - fake_openmc.chmod(0o755) + assert _get_error_message(output) == message - with pytest.raises(RuntimeError) as excinfo: - openmc.plot_geometry(output=False, openmc_exec=str(fake_openmc)) - assert str(excinfo.value) == 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)