summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorederag <edera@gmx.fr>2018-02-18 10:11:33 +0100
committerGábor Bernát <gaborjbernat@gmail.com>2018-02-18 09:11:33 +0000
commit3ed55fb0dac65cbeb11072e713ba73a754398377 (patch)
treeb1e5accc86a207a233ea355842f539a803bc06fd
parent13beb8b09946a5f09721e4a3bfa0c488ccb4fecd (diff)
downloadtox-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.rst1
-rw-r--r--doc/example/general.rst10
-rw-r--r--setup.py1
-rw-r--r--tests/test_result.py34
-rw-r--r--tests/test_z_cmdline.py34
-rw-r--r--tox.ini8
-rw-r--r--tox/__init__.py39
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`_.
diff --git a/setup.py b/setup.py
index 6fc8eb6a..52cd3c47 100644
--- a/setup.py
+++ b/setup.py
@@ -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
diff --git a/tox.ini b/tox.ini
index a8879699..38609004 100644
--- a/tox.ini
+++ b/tox.ini
@@ -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. """