diff --git a/doc/changelog.rst b/doc/changelog.rst index 946b286d..aedf39f7 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -1,6 +1,9 @@ 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>` * :bug:`-` Add ``current_config`` property to plugins * :release:`1.4.1 <9-8-2017>` diff --git a/slash/app.py b/slash/app.py index 2d39f2db..296c9963 100644 --- a/slash/app.py +++ b/slash/app.py @@ -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 @@ -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: diff --git a/slash/core/result.py b/slash/core/result.py index c85737e3..1bf71c72 100644 --- a/slash/core/result.py +++ b/slash/core/result.py @@ -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 @@ -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) diff --git a/slash/core/session.py b/slash/core/session.py index be4f92cc..5e2e1d5c 100644 --- a/slash/core/session.py +++ b/slash/core/session.py @@ -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 @@ -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: diff --git a/slash/exception_handling.py b/slash/exception_handling.py index 15a46422..2f372a85 100644 --- a/slash/exception_handling.py +++ b/slash/exception_handling.py @@ -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 @@ -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: diff --git a/slash/frontend/main.py b/slash/frontend/main.py index 1df2bfcb..ae3a8eea 100644 --- a/slash/frontend/main.py +++ b/slash/frontend/main.py @@ -2,6 +2,8 @@ from __future__ import print_function import argparse import contextlib + +import colorama import logbook # pylint: disable=F0401 import sys @@ -60,7 +62,11 @@ def _setup_logging_context(args): #### For use with entry_points/console_scripts def main_entry_point(): - sys.exit(main()) + colorama.init() + try: + sys.exit(main()) + finally: + colorama.deinit() if __name__ == "__main__": main_entry_point() diff --git a/slash/plugins/builtin/notifications.py b/slash/plugins/builtin/notifications.py index 71c8f726..8250b966 100644 --- a/slash/plugins/builtin/notifications.py +++ b/slash/plugins/builtin/notifications.py @@ -99,6 +99,7 @@ def __init__(self, *args, **kwargs): self._add_notifier(self._nma_notifier, 'nma', {'api_key': None, 'enabled': True}) self._add_notifier(self._pushbullet_notifier, 'pushbullet', {'api_key': None, 'enabled': True}) self._add_notifier(self._email_notifier, 'email', { + 'from_email': 'Slash ', 'smtp_server': None, 'to_list': [] // Cmdline(append='--email-to', metavar='ADDRESS'), 'cc_list': [] @@ -157,7 +158,7 @@ def _pushbullet_notifier(self, message): def _email_notifier(self, message): email_config = config.root.plugin_config.notifications.email email_kwargs = { - 'from_email': 'Slash ', + 'from_email': email_config.from_email, 'subject': message.get_title(), 'body': message.get_html_message(), 'smtp_server': email_config.smtp_server, diff --git a/slash/utils/debug.py b/slash/utils/debug.py index 31642759..fb1bb5ad 100644 --- a/slash/utils/debug.py +++ b/slash/utils/debug.py @@ -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 @@ -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) diff --git a/tests/test_interruptions.py b/tests/test_interruptions.py index 90cf9c0b..4052bf28 100644 --- a/tests/test_interruptions.py +++ b/tests/test_interruptions.py @@ -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() diff --git a/tests/utils/suite_writer/suite.py b/tests/utils/suite_writer/suite.py index ee195f6d..40038d7b 100644 --- a/tests/utils/suite_writer/suite.py +++ b/tests/utils/suite_writer/suite.py @@ -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