diff --git a/nipype/pipeline/engine/nodes.py b/nipype/pipeline/engine/nodes.py index 20fe360504..b1f2a662b2 100644 --- a/nipype/pipeline/engine/nodes.py +++ b/nipype/pipeline/engine/nodes.py @@ -750,9 +750,25 @@ def _run_command(self, execute, copyfiles=True): ) if exc_tb: - raise NodeExecutionError( - f"Exception raised while executing Node {self.name}.\n\n{result.runtime.traceback}" - ) + runtime = result.runtime + + def _tab(text): + from textwrap import indent + + if not text: + return "" + return indent(text, '\t') + + msg = f"Exception raised while executing Node {self.name}.\n\n" + if hasattr(runtime, 'cmdline'): + msg += ( + f"Cmdline:\n{_tab(runtime.cmdline)}\n" + f"Stdout:\n{_tab(runtime.stdout)}\n" + f"Stderr:\n{_tab(runtime.stderr)}\n" + ) + # Always pass along the traceback + msg += f"Traceback:\n{_tab(runtime.traceback)}" + raise NodeExecutionError(msg) return result diff --git a/nipype/pipeline/engine/tests/test_nodes.py b/nipype/pipeline/engine/tests/test_nodes.py index f5e2d5016c..44da004620 100644 --- a/nipype/pipeline/engine/tests/test_nodes.py +++ b/nipype/pipeline/engine/tests/test_nodes.py @@ -7,6 +7,7 @@ from .... import config from ....interfaces import utility as niu +from ....interfaces import base as nib from ... import engine as pe from ..utils import merge_dict from .test_base import EngineTestInterface @@ -334,3 +335,45 @@ def _producer(num=1, deadly_num=7): wf.base_dir = os.path.abspath("./test_output") with pytest.raises(RuntimeError): wf.run(plugin="MultiProc") + + +class FailCommandLine(nib.CommandLine): + input_spec = nib.CommandLineInputSpec + output_spec = nib.TraitedSpec + _cmd = 'nipype-node-execution-fail' + + +def test_NodeExecutionError(tmp_path, monkeypatch): + import stat + + monkeypatch.chdir(tmp_path) + + # create basic executable and add to PATH + exebin = tmp_path / 'bin' + exebin.mkdir() + exe = exebin / 'nipype-node-execution-fail' + exe.write_text('#!/bin/bash\necho "Running"\necho "This should fail" >&2\nexit 1') + exe.chmod(exe.stat().st_mode | stat.S_IEXEC) + monkeypatch.setenv("PATH", str(exe.parent.absolute()), prepend=os.pathsep) + + # Test with cmdline interface + cmd = pe.Node(FailCommandLine(), name="cmd-fail", base_dir='cmd') + with pytest.raises(pe.nodes.NodeExecutionError) as exc: + cmd.run() + error_msg = str(exc.value) + + for attr in ("Cmdline:", "Stdout:", "Stderr:", "Traceback:"): + assert attr in error_msg + assert "This should fail" in error_msg + + # Test with function interface + def fail(): + raise Exception("Functions can fail too") + + func = pe.Node(niu.Function(function=fail), name='func-fail', base_dir='func') + with pytest.raises(pe.nodes.NodeExecutionError) as exc: + func.run() + error_msg = str(exc.value) + assert "Traceback:" in error_msg + assert "Cmdline:" not in error_msg + assert "Functions can fail too" in error_msg