diff --git a/news/failover-script.rst b/news/failover-script.rst new file mode 100644 index 000000000..2eacb1eef --- /dev/null +++ b/news/failover-script.rst @@ -0,0 +1,14 @@ +**Added:** None + +**Changed:** None + +**Deprecated:** None + +**Removed:** None + +**Fixed:** + +* Fixed an issue that xonsh would fail over to external shells when + running .xsh script which raises exceptions. + +**Security:** None diff --git a/tests/scripts/raise.xsh b/tests/scripts/raise.xsh new file mode 100644 index 000000000..3bf1881e8 --- /dev/null +++ b/tests/scripts/raise.xsh @@ -0,0 +1 @@ +raise Exception('oh no') diff --git a/tests/test_main.py b/tests/test_main.py index 6c6e075e7..a3bc3ab7e 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -9,6 +9,7 @@ import sys import xonsh.main import pytest +from tools import TEST_DIR def Shell(*args, **kwargs): @@ -91,7 +92,7 @@ def test_xonsh_failback(shell, monkeypatch): assert failback_checker == ['/bin/xshell', '/bin/xshell'] -def test_xonsh_failback_non_interactive(shell, monkeypatch): +def test_xonsh_failback_single(shell, monkeypatch): class FakeFailureError(Exception): pass @@ -103,3 +104,17 @@ def test_xonsh_failback_non_interactive(shell, monkeypatch): with pytest.raises(FakeFailureError): xonsh.main.main() + + +def test_xonsh_failback_script_from_file(shell, monkeypatch): + checker = [] + def mocked_execlp(f, *args): + checker.append(f) + monkeypatch.setattr(os, 'execlp', mocked_execlp) + + script = os.path.join(TEST_DIR, 'scripts', 'raise.xsh') + monkeypatch.setattr(sys, 'argv', ['xonsh', script]) + monkeypatch.setattr(sys, 'stderr', open(os.devnull, 'w')) + with pytest.raises(Exception): + xonsh.main.main() + assert len(checker) == 0 diff --git a/tests/tools.py b/tests/tools.py index 4542f0117..66dfd4fef 100644 --- a/tests/tools.py +++ b/tests/tools.py @@ -24,6 +24,7 @@ ON_DARWIN = (platform.system() == 'Darwin') ON_WINDOWS = (platform.system() == 'Windows') ON_CONDA = True in [conda in pytest.__file__ for conda in ['anaconda', 'miniconda']] +TEST_DIR = os.path.dirname(__file__) # pytest skip decorators skip_if_py34 = pytest.mark.skipif(VER_MAJOR_MINOR < VER_3_5, reason="Py3.5+ only test") diff --git a/xonsh/main.py b/xonsh/main.py index 65e5d1227..ac067721d 100644 --- a/xonsh/main.py +++ b/xonsh/main.py @@ -253,17 +253,11 @@ def premain(argv=None): return args -def _failback_to_other_shells(argv, err): - args = None - try: - args = premain(argv) - except Exception: - pass +def _failback_to_other_shells(args, err): # only failback for interactive shell; if we cannot tell, treat it # as an interactive one for safe. if hasattr(args, 'mode') and args.mode != XonshMode.interactive: raise err - foreign_shell = None shells_file = '/etc/shells' if not os.path.exists(shells_file): @@ -294,15 +288,16 @@ def _failback_to_other_shells(argv, err): def main(argv=None): + args = None try: - return main_xonsh(argv) + args = premain(argv) + return main_xonsh(args) except Exception as err: - _failback_to_other_shells(argv, err) + _failback_to_other_shells(args, err) -def main_xonsh(argv=None): +def main_xonsh(args): """Main entry point for xonsh cli.""" - args = premain(argv) events.on_post_init.fire() env = builtins.__xonsh_env__ shell = builtins.__xonsh_shell__