diff --git a/CHANGELOG.md b/CHANGELOG.md index 34a3ddd..69e3ae5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## 0.4.4 + +- Add 'lode_runner.plugins.Suppressor' plugin. Allows suppress any exceptions in tearDown-methods + +- Add optional parameter '--xunit-dump-suite-output'. If enabled drops TestSuite-level sysout/syserr to XUnit report. + ## 0.4.3 - Add XUnit plugin 'lode_runner.plugins.ClassSkipper'. Allows skip TestClasses with no setUpClass calls diff --git a/lode_runner/core.py b/lode_runner/core.py index 79b05f1..ea502ed 100644 --- a/lode_runner/core.py +++ b/lode_runner/core.py @@ -1,3 +1,6 @@ +# coding: utf-8 + +import logging from unittest import suite from nose.core import TextTestResult, TextTestRunner, TestProgram @@ -7,6 +10,9 @@ from nose.failure import Failure +log = logging.getLogger('lode_runner.core') + + class ResultProxy(NoseResultProxy): def assertMyTest(self, test): pass @@ -49,11 +55,20 @@ def __init__(self, *args, **kwargs): super(TestLoader, self).__init__(*args, **kwargs) self.suiteClass = ContextSuiteFactory(self.config, resultProxy=ResultProxyFactory(config=self.config)) + def makeTest(self, obj, parent=None): + if getattr(self.config, "suppressTearDownExceptions", False): + obj_to_wrap = obj if isinstance(obj, type) else parent + if obj_to_wrap: + self._wrap_with_suppressor(obj_to_wrap) + + return super(TestLoader, self).makeTest(obj, parent) + def loadTestsFromTestCase(self, testCaseClass): """Return a suite of all tests cases contained in testCaseClass""" if issubclass(testCaseClass, suite.TestSuite): raise TypeError("Test cases should not be derived from TestSuite." " Maybe you meant to derive from TestCase?") + test_case_names = self.getTestCaseNames(testCaseClass) if not test_case_names and hasattr(testCaseClass, 'runTest'): test_case_names = ['runTest'] @@ -72,6 +87,18 @@ def loadTestsFromModule(self, module, path=None, discovered=False): return plugin_tests return super(TestLoader, self).loadTestsFromModule(module, path=None, discovered=False) + def _wrap_with_suppressor(self, obj): + try: + from lode_runner.plugins.suppressor import suppress_exceptions + except ImportError: + log.exception('Error wrapping with lode_runner.plugins.suppressor ()') + return + + names_list = ['tearDown'] + list(self.suiteClass.suiteClass.classTeardown) + methods_to_wrap = [getattr(obj, _name) for _name in names_list if hasattr(obj, _name)] + for _method in methods_to_wrap: + setattr(obj, _method.__name__, suppress_exceptions(_method)) + class LodeTestResult(TextTestResult): pass @@ -113,9 +140,10 @@ def plugins(): from lode_runner.plugins.initializer import Initializer from lode_runner.plugins.failer import Failer from lode_runner.plugins.class_skipper import ClassSkipper + from lode_runner.plugins.suppressor import Suppressor plugs = [ - Dataprovider, Xunit, MultiProcess, TestId, Initializer, Failer, ClassSkipper + Dataprovider, Xunit, MultiProcess, TestId, Initializer, Failer, ClassSkipper, Suppressor ] from nose.plugins import builtin diff --git a/lode_runner/plugins/suppressor.py b/lode_runner/plugins/suppressor.py new file mode 100644 index 0000000..5140439 --- /dev/null +++ b/lode_runner/plugins/suppressor.py @@ -0,0 +1,41 @@ +# coding: utf-8 + +import logging +from functools import wraps +from nose.plugins import Plugin + +log = logging.getLogger('lode.plugins.suppressor') + + +def suppress_exceptions(func): + @wraps(func) + def wrapped_tear_down(*args, **kwargs): + try: + return func(*args, **kwargs) + except Exception: + log.exception('Suppressed error in {} {}'.format(func.__name__, func)) + + return wrapped_tear_down + + +class Suppressor(Plugin): + """ + Suppressor plugin: if enabled - suppress all exceptions in tearDown-like methods. + """ + name = 'suppressor' + enabled = True + + def options(self, parser, env): + Plugin.options(self, parser, env) + parser.add_option( + '--suppress-teardown-exceptions', + action='store_true', + default=False, + help="Suppress any exceptions in tearDown/tearDownClass-like methods" + ) + + def configure(self, options, conf): + if not options.suppress_teardown_exceptions: + return + + conf.suppressTearDownExceptions = options.suppress_teardown_exceptions diff --git a/lode_runner/plugins/xunit.py b/lode_runner/plugins/xunit.py index 4b26cb0..0c7b004 100644 --- a/lode_runner/plugins/xunit.py +++ b/lode_runner/plugins/xunit.py @@ -5,14 +5,9 @@ from xml.etree import ElementTree -try: - from StringIO import StringIO -except ImportError: - from io import StringIO - from lode_runner.plugins import force_unicode_decorator -from nose.plugins.xunit import Xunit, force_unicode +from nose.plugins.xunit import Xunit as NoseXunit, force_unicode from nose.plugins.base import Plugin @@ -30,23 +25,67 @@ def __init__(self): 'skipped': 0}) -class Xunit(Xunit): +class Xunit(NoseXunit): + _suite_stdout = None + _suite_stderr = None + + def options(self, parser, env): + parser.add_option( + '--xunit-dump-suite-output', action='store_true', default=False, + help="If enabled, will dump suite-level sys-out and sys-err to XUnit report" + ) + super(Xunit, self).options(parser, env) + def configure(self, options, config): """Configures the xunit plugin.""" Plugin.configure(self, options, config) self.config = config - if self.enabled: - if hasattr(options, 'multiprocess_workers') and options.multiprocess_workers: - if multiprocessing.current_process().name == 'MainProcess': - Xunit.mp_context = MultiprocessContext() - self.stats = Xunit.mp_context.stats - self.errorlist = Xunit.mp_context.error_list - self.xunit_testsuite_name = options.xunit_testsuite_name - else: - super(Xunit, self).configure(options, config) + if not self.enabled: + return + + self.xunit_dump_suite_output = options.xunit_dump_suite_output + + if hasattr(options, 'multiprocess_workers') and options.multiprocess_workers: + if multiprocessing.current_process().name == 'MainProcess': + Xunit.mp_context = MultiprocessContext() + self.stats = Xunit.mp_context.stats + self.errorlist = Xunit.mp_context.error_list + self.xunit_testsuite_name = options.xunit_testsuite_name + else: + super(Xunit, self).configure(options, config) self.error_report_filename = options.xunit_file + def _dump_suite_output(self): + if not self.xunit_dump_suite_output: + return + + _captured_stdout = self._getCapturedStdout() + if _captured_stdout: + self._suite_stdout = _captured_stdout + + _captured_stderr = self._getCapturedStderr() + if _captured_stderr: + self._suite_stderr = _captured_stderr + + self._endCapture() + + def beforeTest(self, test): + """Initializes a timer before starting a test.""" + test.id = force_unicode_decorator(test.id) + self._dump_suite_output() + super(Xunit, self).beforeTest(test) + + def afterTest(self, test): + super(Xunit, self).afterTest(test) + + if self.xunit_dump_suite_output: + self._startCapture() + + def stopContext(self, context): + self._dump_suite_output() + super(Xunit, self).stopContext(context) + def report(self, stream): """Writes an Xunit-formatted XML file @@ -65,13 +104,16 @@ def report(self, stream): }) errors = [force_unicode(error) for error in self.errorlist] [testsuite.append(ElementTree.fromstring(error.encode("utf-8"))) for error in errors] + + if self.xunit_dump_suite_output: + if self._suite_stderr: + testsuite.append(ElementTree.fromstring(self._suite_stderr)) + + if self._suite_stdout: + testsuite.append(ElementTree.fromstring(self._suite_stdout)) + ElementTree.ElementTree(testsuite).write(self.error_report_filename, encoding="utf-8", xml_declaration=True) if self.config.verbosity > 1: stream.writeln("-" * 70) stream.writeln("XML: {}".format(os.path.abspath(self.error_report_filename))) - - def beforeTest(self, test): - """Initializes a timer before starting a test.""" - test.id = force_unicode_decorator(test.id) - super(Xunit, self).beforeTest(test) diff --git a/setup.py b/setup.py index df98bb7..9a3bf8c 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ setup( name='lode_runner', url='https://github.com/2gis/lode_runner', - version='0.4.3', + version='0.4.4', description='Nosetests runner plugins package', long_description='', author='Igor Pavlov', @@ -26,7 +26,8 @@ 'testid = lode_runner.plugins.testid:TestId', 'initializer = lode_runner.plugins.initializer:Initializer', 'failer = lode_runner.plugins.failer:Failer', - 'class_skipper = lode_runner.plugins.class_skipper:ClassSkipper' + 'class_skipper = lode_runner.plugins.class_skipper:ClassSkipper', + 'suppressor = lode_runner.plugins.suppressor:Suppressor' ] }, classifiers=[