diff options
author | ederag <edera@gmx.fr> | 2018-02-18 10:11:33 +0100 |
---|---|---|
committer | Gábor Bernát <gaborjbernat@gmail.com> | 2018-02-18 09:11:33 +0000 |
commit | 3ed55fb0dac65cbeb11072e713ba73a754398377 (patch) | |
tree | b1e5accc86a207a233ea355842f539a803bc06fd | |
parent | 13beb8b09946a5f09721e4a3bfa0c488ccb4fecd (diff) | |
download | tox-git-3ed55fb0dac65cbeb11072e713ba73a754398377.tar.gz |
Hint for possible signal upon InvocationError (#766)
* Display exitcode upon InvocationError.
Issue #290.
Co-authored-by: Daniel Hahler <git@thequod.de>
* hint for exitcode > 128
* add changelog fragment
* use keyword argument instead of try/except
* add documentation
* hints on the potential signal raised + better tests
* pep8 naming: exitcode => exit_code
* rename test_InvocationError => test_invocation_error
* exit code + signal name
* terser note
* remove blank line
* add changelog fragment
* remove "python" from expected_command_arg (fix appveyor)
-rw-r--r-- | changelog/766.feature.rst | 1 | ||||
-rw-r--r-- | doc/example/general.rst | 10 | ||||
-rw-r--r-- | setup.py | 1 | ||||
-rw-r--r-- | tests/test_result.py | 34 | ||||
-rw-r--r-- | tests/test_z_cmdline.py | 34 | ||||
-rw-r--r-- | tox.ini | 8 | ||||
-rw-r--r-- | tox/__init__.py | 39 |
7 files changed, 101 insertions, 26 deletions
diff --git a/changelog/766.feature.rst b/changelog/766.feature.rst new file mode 100644 index 00000000..5d50ca0a --- /dev/null +++ b/changelog/766.feature.rst @@ -0,0 +1 @@ +Hint for possible signal upon ``InvocationError``, on posix systems - by @ederag and @asottile. diff --git a/doc/example/general.rst b/doc/example/general.rst index 684d143c..b27b5ba2 100644 --- a/doc/example/general.rst +++ b/doc/example/general.rst @@ -222,13 +222,17 @@ If the command starts with ``pytest`` or ``python setup.py test`` for instance, then the `pytest exit codes`_ are relevant. On unix systems, there are some rather `common exit codes`_. -This is why for exit codes larger than 128, an additional hint is given: +This is why for exit codes larger than 128, +if a signal with number equal to ``<exit code> - 128`` is found +in the :py:mod:`signal` module, an additional hint is given: .. code-block:: shell ERROR: InvocationError for command - '<command defined in tox.ini>' (exited with code 139) - Note: On unix systems, an exit code larger than 128 often means a fatal error (e.g. 139=128+11: segmentation fault) + '<command>' (exited with code 139) + Note: this might indicate a fatal error signal (139 - 128 = 11: SIGSEGV) + +where ``<command>`` is the command defined in ``tox.ini``, with quotes removed. The signal numbers (e.g. 11 for a segmentation fault) can be found in the "Standard signals" section of the `signal man page`_. @@ -53,6 +53,7 @@ def main(): 'virtualenv>=1.11.2'], extras_require={'testing': ['pytest >= 3.0.0', 'pytest-cov', + 'pytest-mock', 'pytest-timeout', 'pytest-xdist'], 'docs': ['sphinx >= 1.6.3, < 2', diff --git a/tests/test_result.py b/tests/test_result.py index e771e1d4..515fb7e8 100644 --- a/tests/test_result.py +++ b/tests/test_result.py @@ -1,3 +1,5 @@ +import os +import signal import socket import sys @@ -69,3 +71,35 @@ def test_get_commandlog(pkg): assert envlog.dict["setup"] setuplog2 = replog.get_envlog("py36").get_commandlog("setup") assert setuplog2.list == setuplog.list + + +@pytest.mark.parametrize('exit_code', [None, 0, 5, 128 + signal.SIGTERM, 1234]) +@pytest.mark.parametrize('os_name', ['posix', 'nt']) +def test_invocation_error(exit_code, os_name, mocker, monkeypatch): + monkeypatch.setattr(os, 'name', value=os_name) + mocker.spy(tox, '_exit_code_str') + if exit_code is None: + exception = tox.exception.InvocationError("<command>") + else: + exception = tox.exception.InvocationError("<command>", exit_code) + result = str(exception) + # check that mocker works, + # because it will be our only test in test_z_cmdline.py::test_exit_code + # need the mocker.spy above + assert tox._exit_code_str.call_count == 1 + assert tox._exit_code_str.call_args == mocker.call('InvocationError', "<command>", exit_code) + if exit_code is None: + needle = "(exited with code" + assert needle not in result + else: + needle = "(exited with code %d)" % exit_code + assert needle in result + note = ("Note: this might indicate a fatal error signal") + if (os_name == 'posix') and (exit_code == 128 + signal.SIGTERM): + assert note in result + number = signal.SIGTERM + name = "SIGTERM" + signal_str = "({} - 128 = {}: {})".format(exit_code, number, name) + assert signal_str in result + else: + assert note not in result diff --git a/tests/test_z_cmdline.py b/tests/test_z_cmdline.py index c1e09ae7..5eefab3b 100644 --- a/tests/test_z_cmdline.py +++ b/tests/test_z_cmdline.py @@ -906,18 +906,26 @@ def test_tox_cmdline(monkeypatch): tox.cmdline(['caller_script', '--help']) -@pytest.mark.parametrize('exitcode', [0, 5, 129]) -def test_exitcode(initproj, cmd, exitcode): - tox_ini_content = "[testenv:foo]\ncommands=python -c 'import sys; sys.exit(%d)'" % exitcode +@pytest.mark.parametrize('exit_code', [0, 6]) +def test_exit_code(initproj, cmd, exit_code, mocker): + """ Check for correct InvocationError, with exit code, + except for zero exit code """ + mocker.spy(tox, '_exit_code_str') + tox_ini_content = "[testenv:foo]\ncommands=python -c 'import sys; sys.exit(%d)'" % exit_code initproj("foo", filedefs={'tox.ini': tox_ini_content}) - result = cmd() - if exitcode: - needle = "(exited with code %d)" % exitcode - assert any(needle in line for line in result.outlines) - if exitcode > 128: - needle = ("Note: On unix systems, an exit code larger than 128 " - "often means a fatal error (e.g. 139=128+11: segmentation fault)") - assert any(needle in line for line in result.outlines) + cmd() + if exit_code: + # need mocker.spy above + assert tox._exit_code_str.call_count == 1 + (args, kwargs) = tox._exit_code_str.call_args + assert kwargs == {} + (call_error_name, call_command, call_exit_code) = args + assert call_error_name == 'InvocationError' + # quotes are removed in result.out + # do not include "python" as it is changed to python.EXE by appveyor + expected_command_arg = ' -c import sys; sys.exit(%d)' % exit_code + assert expected_command_arg in call_command + assert call_exit_code == exit_code else: - needle = "(exited with code" - assert all(needle not in line for line in result.outlines) + # need mocker.spy above + assert tox._exit_code_str.call_count == 0 @@ -56,6 +56,14 @@ deps = codecov skip_install = True commands = codecov --file "{toxworkdir}/coverage.xml" +[testenv:exit_code] +# to see how the InvocationError is displayed, use +# PYTHONPATH=.:$PYTHONPATH python3 -m tox -e exit_code +basepython = python3.6 +description = commands with several exit codes +skip_install = True +commands = python3.6 -c "import sys; sys.exit(139)" + [testenv:pra] passenv = * description = "personal release assistant" - see HOWTORELEASE.rst diff --git a/tox/__init__.py b/tox/__init__.py index 25b468a1..df61d71e 100644 --- a/tox/__init__.py +++ b/tox/__init__.py @@ -1,3 +1,6 @@ +import os +import signal + from pkg_resources import DistributionNotFound from pkg_resources import get_distribution @@ -11,6 +14,28 @@ except DistributionNotFound: __version__ = '0.0.0.dev0' +# separate function because pytest-mock `spy` does not work on Exceptions +# can use neither a class method nor a static because of +# https://bugs.python.org/issue23078 +# even a normal method failed with +# TypeError: descriptor '__getattribute__' requires a 'BaseException' object but received a 'type' +def _exit_code_str(exception_name, command, exit_code): + """ string representation for an InvocationError, with exit code """ + str_ = "%s for command %s" % (exception_name, command) + if exit_code is not None: + str_ += " (exited with code %d)" % (exit_code) + if (os.name == 'posix') and (exit_code > 128): + signals = {number: name + for name, number in vars(signal).items() + if name.startswith("SIG")} + number = exit_code - 128 + name = signals.get(number) + if name: + str_ += ("\nNote: this might indicate a fatal error signal " + "({} - 128 = {}: {})".format(number+128, number, name)) + return str_ + + class exception: class Error(Exception): def __str__(self): @@ -34,19 +59,13 @@ class exception: class InvocationError(Error): """ an error while invoking a script. """ - def __init__(self, command, exitcode=None): - super(exception.Error, self).__init__(command, exitcode) + def __init__(self, command, exit_code=None): + super(exception.Error, self).__init__(command, exit_code) self.command = command - self.exitcode = exitcode + self.exit_code = exit_code def __str__(self): - str_ = "%s for command %s" % (self.__class__.__name__, self.command) - if self.exitcode: - str_ += " (exited with code %d)" % (self.exitcode) - if self.exitcode > 128: - str_ += ("\nNote: On unix systems, an exit code larger than 128 " - "often means a fatal error (e.g. 139=128+11: segmentation fault)") - return str_ + return _exit_code_str(self.__class__.__name__, self.command, self.exit_code) class MissingFile(Error): """ an error while invoking a script. """ |