diff --git a/bash_kernel/exit_code_checker.py b/bash_kernel/exit_code_checker.py new file mode 100644 index 0000000..e4ad4a0 --- /dev/null +++ b/bash_kernel/exit_code_checker.py @@ -0,0 +1,12 @@ +import re + +exit_code_format = "BASHKERNEL_RETCODE<<<{}>>>" +exit_code_re = re.compile(exit_code_format.format("([0-9]+)")) +exit_code_printf_format = exit_code_format.format("%d") +exit_code_command_checker = f'{{ printf "{exit_code_printf_format}" $?; }} 2>/dev/null' + + +def get_last_exit_code(bashwrapper): + output = bashwrapper.run_command(exit_code_command_checker) + matches = exit_code_re.findall(output) + return int(matches[0]) diff --git a/bash_kernel/kernel.py b/bash_kernel/kernel.py index 3b9cf44..debdcf7 100644 --- a/bash_kernel/kernel.py +++ b/bash_kernel/kernel.py @@ -16,6 +16,7 @@ version_pat = re.compile(r'version (\d+(\.\d+)+)') from .display import (extract_contents, build_cmds) +from .exit_code_checker import get_last_exit_code class IREPLWrapper(replwrap.REPLWrapper): """A subclass of REPLWrapper that gives incremental output @@ -224,7 +225,7 @@ def do_execute(self, code, silent, store_history=True, return {'status': 'abort', 'execution_count': self.execution_count} try: - exitcode = int(self.bashwrapper.run_command('{ echo $?; } 2>/dev/null').rstrip().split("\r\n")[0]) + exitcode = get_last_exit_code(self.bashwrapper) except Exception as exc: exitcode = 1 diff --git a/tests/test_exit_code_checker.py b/tests/test_exit_code_checker.py new file mode 100644 index 0000000..67ffe75 --- /dev/null +++ b/tests/test_exit_code_checker.py @@ -0,0 +1,63 @@ +import os +import pexpect +from bash_kernel.kernel import IREPLWrapper +from bash_kernel.exit_code_checker import get_last_exit_code +import pytest + + +def test_get_last_exit_code_command_not_found(bashwrapper): + bashwrapper.run_command("this-command-does-not-exist") + assert get_last_exit_code(bashwrapper) == 127 + + +def test_get_last_exit_code_success(bashwrapper): + bashwrapper.run_command("echo") + assert get_last_exit_code(bashwrapper) == 0 + + +def test_get_last_exit_code_command_not_found_conda(mock_conda_bashwrapper): + mock_conda_bashwrapper.run_command("this-command-does-not-exist") + assert get_last_exit_code(mock_conda_bashwrapper) == 127 + + +def test_get_last_exit_code_success_conda(mock_conda_bashwrapper): + mock_conda_bashwrapper.run_command("echo") + assert get_last_exit_code(mock_conda_bashwrapper) == 0 + + +@pytest.fixture(scope="session") +def mock_conda_bashwrapper(bashwrapper): + class FakeCondaWrapper: + def __init__(self): + self.child = bashwrapper + + def run_command(self, cmd): + out = self.child.run_command(cmd) + # Simulating the effect of a conda env + # on the output of a wrapper + return out + "(condaenv)" + + return FakeCondaWrapper() + + +# From BashKernel._start_bash +@pytest.fixture(scope="session") +def bashwrapper(): + unique_prompt = "PROMPT_ASDFASFAS" + bashrc = os.path.join(os.path.dirname(pexpect.__file__), "bashrc.sh") + child = pexpect.spawn( + "bash", + ["--rcfile", bashrc], + echo=False, + encoding="utf-8", + codec_errors="replace", + ) + + ps1 = unique_prompt + "\[\]" + ">" + ps2 = unique_prompt + "\[\]" + "+" + prompt_change = "PS1='{0}' PS2='{1}' PROMPT_COMMAND=''".format(ps1, ps2) + # Using IREPLWrapper to get incremental output + bashwrapper = IREPLWrapper( + child, "\$", prompt_change, unique_prompt, extra_init_cmd="export PAGER=cat" + ) + return bashwrapper