Skip to content

Commit

Permalink
Fix handling of custom interruption exceptions (fix #670)
Browse files Browse the repository at this point in the history
  • Loading branch information
vmalloc committed Sep 13, 2017
1 parent e7ee593 commit a74d326
Show file tree
Hide file tree
Showing 8 changed files with 49 additions and 21 deletions.
1 change: 1 addition & 0 deletions doc/changelog.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
Changelog
=========

* :bug:`670` Improve handling of interruption exceptions - custom interruption exceptions will now properly cause the session and test to trigger the ``session_interrupt`` and ``test_interrupt`` hooks. Unexpected exceptions like ``SystemExit`` from within tests are now also reported properly instead of silently ignored
* :bug:`668` Properly initialize colorama under Windows
* :bug:`665` Support overriding notifications plugin's ``from_email`` by configuration
* :release:`1.4.2 <13-8-2017>`
Expand Down
3 changes: 2 additions & 1 deletion slash/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from .conf import config
from .core.session import Session
from .reporting.console_reporter import ConsoleReporter
from . import exceptions
from .exceptions import TerminatedException, SlashException
from .exception_handling import handling_exceptions, inhibit_unhandled_exception_traceback, should_inhibit_unhandled_exception_traceback
from .loader import Loader
Expand Down Expand Up @@ -144,7 +145,7 @@ def __exit__(self, exc_type, exc_value, exc_tb):
_logger.error('Unexpected error occurred', exc_info=exc_info)
self.get_reporter().report_error_message('Unexpected error: {}'.format(exc_value))

if isinstance(exc_value, (KeyboardInterrupt, SystemExit, TerminatedException)):
if isinstance(exc_value, exceptions.INTERRUPTION_EXCEPTIONS):
self._interrupted = True

if exc_type is not None:
Expand Down
23 changes: 11 additions & 12 deletions slash/core/result.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from .._compat import OrderedDict, itervalues
from ..ctx import context
from ..exception_handling import capture_sentry_exception
from ..exceptions import FAILURE_EXCEPTION_TYPES
from .. import exceptions
from ..utils.exception_mark import ExceptionMarker
from ..utils.interactive import notify_if_slow_context
from ..utils.python import unpickle
Expand Down Expand Up @@ -82,35 +82,34 @@ def add_exception(self, exc_info=None):
"""
if exc_info is None:
exc_info = sys.exc_info()
exc_class, exc_value, _ = exc_info # pylint: disable=unpacking-non-sequence
_, exc_value, _ = exc_info # pylint: disable=unpacking-non-sequence

if _ADDED_TO_RESULT.is_exception_marked(exc_value):
return

_ADDED_TO_RESULT.mark_exception(exc_value)
if isinstance(exc_value, FAILURE_EXCEPTION_TYPES):
if isinstance(exc_value, exceptions.FAILURE_EXCEPTION_TYPES):
self.add_failure()
elif isinstance(exc_value, context.session.get_skip_exception_types()):
self.add_skip(getattr(exc_value, 'reason', str(exc_value)))
elif issubclass(exc_class, Exception):
#skip keyboardinterrupt and system exit
self.add_error(exc_info=exc_info)
else:
# Assume interrupted
elif isinstance(exc_value, exceptions.INTERRUPTION_EXCEPTIONS):
session_result = context.session.results.global_result
interrupted_test = self.is_interrupted()
interrupted_session = session_result.is_interrupted()

if not self.is_global_result():
# Test was interrupted
interrupted_test = self.is_interrupted()
self.mark_interrupted()
if not interrupted_test and not context.session.has_children():
with notify_if_slow_context(message="Cleaning up test due to interrupt. Please wait..."):
hooks.test_interrupt() # pylint: disable=no-member

if not interrupted_session:
session_result.mark_interrupted()

elif not isinstance(exc_value, GeneratorExit):
#skip keyboardinterrupt and system exit
self.add_error(exc_info=exc_info)
else:
_logger.trace('Ignoring GeneratorExit exception')

def has_errors_or_failures(self):
return bool(self._failures or self._errors)

Expand Down
3 changes: 1 addition & 2 deletions slash/core/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
from .. import ctx, hooks, log, exceptions
from .cleanup_manager import CleanupManager
from ..exception_handling import handling_exceptions
from ..exceptions import INTERRUPTION_EXCEPTIONS
from ..interfaces import Activatable
from ..reporting.null_reporter import NullReporter
from ..utils.id_space import IDSpace
Expand Down Expand Up @@ -111,7 +110,7 @@ def get_started_context(self):
hooks.after_session_start() # pylint: disable=no-member
self._started = True
yield
except INTERRUPTION_EXCEPTIONS:
except exceptions.INTERRUPTION_EXCEPTIONS:
hooks.session_interrupt() # pylint: disable=no-member
raise
finally:
Expand Down
6 changes: 3 additions & 3 deletions slash/exception_handling.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
from .utils.exception_mark import mark_exception, get_exception_mark
from .utils.traceback_proxy import create_traceback_proxy
from . import hooks as trigger_hook
from ._compat import PY2
from . import exceptions
from ._compat import PY2, PYPY
from .ctx import context as slash_context
from .conf import config
from ._compat import PYPY

import functools
import threading
Expand Down Expand Up @@ -130,7 +130,7 @@ def __exit__(self, *exc_info):
handle_exception(exc_info, **self._kwargs)
self._handled.exception = exc_info[1]
skip_types = () if slash_context.session is None else slash_context.session.get_skip_exception_types()
if isinstance(exc_value, skip_types):
if isinstance(exc_value, skip_types) or isinstance(exc_value, exceptions.INTERRUPTION_EXCEPTIONS):
return None
if self._swallow_types and isinstance(exc_value, self._swallow_types):
if PY2:
Expand Down
4 changes: 2 additions & 2 deletions slash/utils/debug.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@

from .. import hooks as trigger_hook
from ..conf import config
from ..exceptions import INTERRUPTION_EXCEPTIONS
from ..ctx import context
from .. import exceptions

import warnings

Expand Down Expand Up @@ -58,7 +58,7 @@ def debug_if_needed(exc_info=None):
return
if isinstance(exc_info[1], context.session.get_skip_exception_types()) and not config.root.debug.debug_skips:
return
if isinstance(exc_info[1], (SystemExit,) + INTERRUPTION_EXCEPTIONS):
if isinstance(exc_info[1], (SystemExit,) + exceptions.INTERRUPTION_EXCEPTIONS):
return

launch_debugger(exc_info)
Expand Down
28 changes: 28 additions & 0 deletions tests/test_interruptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,34 @@ def cleanup():
assert result.session.results.global_result.is_interrupted()


def test_interrupted_with_custom_exception(suite, suite_test, request):

import test

class CustomException(Exception):
pass
test.__interruption_exception__ = CustomException

prev_interruption_exceptions = slash.exceptions.INTERRUPTION_EXCEPTIONS
slash.exceptions.INTERRUPTION_EXCEPTIONS += (CustomException,)

@request.addfinalizer
def cleanup():
del test.__interruption_exception__
slash.exceptions.INTERRUPTION_EXCEPTIONS = prev_interruption_exceptions


suite_test.append_line('import test')
suite_test.append_line('raise test.__interruption_exception__()')
suite_test.expect_interruption()

for t in suite.iter_all_after(suite_test):
t.expect_deselect()

results = suite.run(expect_interruption=True)



@pytest.fixture
def session_interrupt():
callback = Checkpoint()
Expand Down
2 changes: 1 addition & 1 deletion tests/utils/suite_writer/suite.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ def run(self, verify=True, expect_interruption=False, additional_args=(), args=N
if app.interrupted:
assert expect_interruption, 'Unexpectedly interrupted'
else:
assert not expect_interruption, 'KeyboardInterrupt did not happen'
assert not expect_interruption, 'Session was not interrupted as expected'

if captured:
assert len(captured) == 1
Expand Down

0 comments on commit a74d326

Please sign in to comment.