From 868ce889f1b6cf6423fdd56fbc90058c2f4895d8 Mon Sep 17 00:00:00 2001 From: kumar Date: Mon, 10 Oct 2011 09:38:34 -0500 Subject: Adds --cover-xml/--cover-xml-file options. Issue 311 --- CHANGELOG | 2 ++ README.txt | 16 ++++++++++++++-- nose/plugins/cover.py | 16 ++++++++++++++++ nosetests.1 | 17 ++++++++++++++++- 4 files changed, 48 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 61cf858..2023b9c 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,7 @@ 1.1.3 +- Adds :option:`--cover-xml` and :option:`--cover-xml-file` (#311). + Patch by Timothée Peignier. - Adds support for :option:`--cover-branches` (related to #370). Patch by Timothée Peignier. - Fixed Unicode issue on Python 3.1 with coverage (#442) diff --git a/README.txt b/README.txt index 1b578da..f8a057f 100644 --- a/README.txt +++ b/README.txt @@ -107,7 +107,7 @@ In addition to passing command-line options, you may also put configuration options in your project's *setup.cfg* file, or a .noserc or nose.cfg file in your home directory. In any of these standard .ini-style config files, you put your nosetests configuration in a -``[nosetests]`` section. Options are the same as on the command line, +"[nosetests]" section. Options are the same as on the command line, with the -- prefix removed. For options that are simple switches, you must supply a value: @@ -117,7 +117,7 @@ must supply a value: All configuration files that are found will be loaded and their options combined. You can override the standard config file loading -with the ``-c`` option. +with the "-c" option. Using Plugins @@ -353,6 +353,18 @@ Options Produce HTML coverage information in dir +--cover-branches + + Include branch coverage in coverage report [NOSE_COVER_BRANCHES] + +--cover-xml + + Produce XML coverage information + +--cover-xml-file=FILE + + Produce XML coverage information in file + --pdb Drop into debugger on errors diff --git a/nose/plugins/cover.py b/nose/plugins/cover.py index 8be1fe2..21545bb 100644 --- a/nose/plugins/cover.py +++ b/nose/plugins/cover.py @@ -113,6 +113,15 @@ class Coverage(Plugin): dest="cover_branches", help="Include branch coverage in coverage report " "[NOSE_COVER_BRANCHES]") + parser.add_option("--cover-xml", action="store_true", + default=env.get('NOSE_COVER_XML'), + dest="cover_xml", + help="Produce XML coverage information") + parser.add_option("--cover-xml-file", action="store", + default=env.get('NOSE_COVER_XML_FILE', 'coverage.xml'), + dest="cover_xml_file", + metavar="FILE", + help="Produce XML coverage information in file") def configure(self, options, config): """ @@ -149,6 +158,10 @@ class Coverage(Plugin): self.coverHtmlDir = options.cover_html_dir log.debug('Will put HTML coverage report in %s', self.coverHtmlDir) self.coverBranches = options.cover_branches + self.coverXmlFile = None + if options.cover_xml: + self.coverXmlFile = options.cover_xml_file + log.debug('Will put XML coverage report in %s', self.coverHtmlFile) if self.enabled: self.status['active'] = True @@ -182,6 +195,9 @@ class Coverage(Plugin): self.coverInstance.html_report(modules, self.coverHtmlDir) else: self.report_html(modules) + if self.coverXmlFile: + log.debug("Generating XML coverage report") + self.coverInstance.xml_report(modules, self.coverXmlFile) def report_html(self, modules): if not os.path.exists(self.coverHtmlDir): diff --git a/nosetests.1 b/nosetests.1 index bdb9ef2..36982b7 100644 --- a/nosetests.1 +++ b/nosetests.1 @@ -328,6 +328,21 @@ Produce HTML coverage information Produce HTML coverage information in dir +.TP +\fB\-\-cover\-branches\fR\fR +Include branch coverage in coverage report [NOSE_COVER_BRANCHES] + + +.TP +\fB\-\-cover\-xml\fR\fR +Produce XML coverage information + + +.TP +\fB\-\-cover\-xml\-file\fR\fR=FILE +Produce XML coverage information in file + + .TP \fB\-\-pdb\fR\fR Drop into debugger on errors @@ -476,5 +491,5 @@ jpellerin+nose@gmail.com .SH COPYRIGHT LGPL -.\" Generated by docutils manpage writer on 2011-08-05 10:25. +.\" Generated by docutils manpage writer on 2011-10-10 09:32. .\" -- cgit v1.2.1 From 7eb66b631fd32f5c40b870118fc1a1fd2a575869 Mon Sep 17 00:00:00 2001 From: Heng Liu Date: Wed, 26 Oct 2011 15:11:39 -0700 Subject: fixes Issue 462: class fixture are not properly handled in multiprocess. added functional test coverage. fixes Issue 465: properly tearDown multiprocess functional tests so all tests will run. --- .../test_multiprocessing/support/class.py | 14 +++++++++ .../test_multiprocessing/test_class.py | 33 ++++++++++++++++++++++ .../test_multiprocessing/test_nameerror.py | 2 ++ .../test_multiprocessing/test_process_timeout.py | 3 ++ nose/plugins/multiprocess.py | 6 ++-- 5 files changed, 56 insertions(+), 2 deletions(-) create mode 100644 functional_tests/test_multiprocessing/support/class.py create mode 100644 functional_tests/test_multiprocessing/test_class.py diff --git a/functional_tests/test_multiprocessing/support/class.py b/functional_tests/test_multiprocessing/support/class.py new file mode 100644 index 0000000..905bcdf --- /dev/null +++ b/functional_tests/test_multiprocessing/support/class.py @@ -0,0 +1,14 @@ +class TestFunctionalTest(object): + counter = 0 + @classmethod + def setup_class(cls): + cls.counter += 1 + @classmethod + def teardown_class(cls): + cls.counter -= 1 + def _run(self): + assert self.counter==1 + def test1(self): + self._run() + def test2(self): + self._run() diff --git a/functional_tests/test_multiprocessing/test_class.py b/functional_tests/test_multiprocessing/test_class.py new file mode 100644 index 0000000..1e36ba6 --- /dev/null +++ b/functional_tests/test_multiprocessing/test_class.py @@ -0,0 +1,33 @@ +import os +import unittest + +from nose.plugins import PluginTester +from nose.plugins.skip import SkipTest +from nose.plugins.multiprocess import MultiProcess + + +support = os.path.join(os.path.dirname(__file__), 'support') + + +def setup(): + try: + import multiprocessing + if 'active' in MultiProcess.status: + raise SkipTest("Multiprocess plugin is active. Skipping tests of " + "plugin itself.") + except ImportError: + raise SkipTest("multiprocessing module not available") + + +#test case for #462 +class TestClassFixture(PluginTester, unittest.TestCase): + activate = '--processes=1' + plugins = [MultiProcess()] + suitepath = os.path.join(support, 'class.py') + + def runTest(self): + assert str(self.output).strip().endswith('OK') + assert 'Ran 2 tests' in self.output + def tearDown(self): + MultiProcess.status.pop('active') + diff --git a/functional_tests/test_multiprocessing/test_nameerror.py b/functional_tests/test_multiprocessing/test_nameerror.py index f73d02b..d3d7210 100644 --- a/functional_tests/test_multiprocessing/test_nameerror.py +++ b/functional_tests/test_multiprocessing/test_nameerror.py @@ -28,4 +28,6 @@ class TestMPNameError(PluginTester, unittest.TestCase): print str(self.output) assert 'NameError' in self.output assert "'undefined_variable' is not defined" in self.output + def tearDown(self): + MultiProcess.status.pop('active') diff --git a/functional_tests/test_multiprocessing/test_process_timeout.py b/functional_tests/test_multiprocessing/test_process_timeout.py index 535ecdb..27e0584 100644 --- a/functional_tests/test_multiprocessing/test_process_timeout.py +++ b/functional_tests/test_multiprocessing/test_process_timeout.py @@ -35,3 +35,6 @@ class TestMPTimeoutPass(TestMPTimeout): def runTest(self): assert "TimedOutException: 'timeout.test_timeout'" not in self.output assert str(self.output).strip().endswith('OK') + def tearDown(self): + MultiProcess.status.pop('active') + diff --git a/nose/plugins/multiprocess.py b/nose/plugins/multiprocess.py index 260cbf8..604752a 100644 --- a/nose/plugins/multiprocess.py +++ b/nose/plugins/multiprocess.py @@ -573,7 +573,7 @@ class MultiProcessTestRunner(TextTestRunner): for batch in self.nextBatch(case): yield batch - def checkCanSplit(self, context, fixt): + def checkCanSplit(context, fixt): """ Callback that we use to check whether the fixtures found in a context or ancestor are ones we care about. @@ -587,6 +587,7 @@ class MultiProcessTestRunner(TextTestRunner): if getattr(context, '_multiprocess_can_split_', False): return False return True + checkCanSplit = staticmethod(checkCanSplit) def sharedFixtures(self, case): context = getattr(case, 'context', None) @@ -755,7 +756,8 @@ class NoSharedFixtureContextSuite(ContextSuite): return try: localtests = [test for test in self._tests] - if len(localtests) > 1 and self.testQueue is not None: + if (not self.hasFixtures(MultiProcessTestRunner.checkCanSplit) + and len(localtests) > 1 and self.testQueue is not None): log.debug("queue %d tests"%len(localtests)) for test in localtests: if isinstance(test.test,nose.failure.Failure): -- cgit v1.2.1 From ec9eb1d2ffb2ea897fac5b80c9d941b3845cc6a0 Mon Sep 17 00:00:00 2001 From: Heng Liu Date: Wed, 26 Oct 2011 16:13:20 -0700 Subject: properly distribute tests under shared fixtures to multiprocess worker this is a regression introduced when fixing Issue 462, but there is no test coverage for this issue. added a functional test case to make sure it's working as expected --- .../support/concurrent_shared/__init__.py | 6 + .../support/concurrent_shared/test.py | 11 ++ .../test_multiprocessing/test_concurrent_shared.py | 32 ++++++ nose/plugins/multiprocess.py | 128 ++++++++++----------- 4 files changed, 110 insertions(+), 67 deletions(-) create mode 100644 functional_tests/test_multiprocessing/support/concurrent_shared/__init__.py create mode 100644 functional_tests/test_multiprocessing/support/concurrent_shared/test.py create mode 100644 functional_tests/test_multiprocessing/test_concurrent_shared.py diff --git a/functional_tests/test_multiprocessing/support/concurrent_shared/__init__.py b/functional_tests/test_multiprocessing/support/concurrent_shared/__init__.py new file mode 100644 index 0000000..c557001 --- /dev/null +++ b/functional_tests/test_multiprocessing/support/concurrent_shared/__init__.py @@ -0,0 +1,6 @@ +counter=[0] +_multiprocess_shared_ = True +def setup_package(): + counter[0] += 1 +def teardown_package(): + counter[0] -= 1 diff --git a/functional_tests/test_multiprocessing/support/concurrent_shared/test.py b/functional_tests/test_multiprocessing/support/concurrent_shared/test.py new file mode 100644 index 0000000..5bc21f6 --- /dev/null +++ b/functional_tests/test_multiprocessing/support/concurrent_shared/test.py @@ -0,0 +1,11 @@ +#from . import counter +from time import sleep +#_multiprocess_can_split_ = True +class Test1(object): + def test1(self): + sleep(1) + pass +class Test2(object): + def test2(self): + sleep(1) + pass diff --git a/functional_tests/test_multiprocessing/test_concurrent_shared.py b/functional_tests/test_multiprocessing/test_concurrent_shared.py new file mode 100644 index 0000000..cfb36c4 --- /dev/null +++ b/functional_tests/test_multiprocessing/test_concurrent_shared.py @@ -0,0 +1,32 @@ +import os +import unittest + +from nose.plugins import PluginTester +from nose.plugins.skip import SkipTest +from nose.plugins.multiprocess import MultiProcess + + +support = os.path.join(os.path.dirname(__file__), 'support') + + +def setup(): + try: + import multiprocessing + if 'active' in MultiProcess.status: + raise SkipTest("Multiprocess plugin is active. Skipping tests of " + "plugin itself.") + except ImportError: + raise SkipTest("multiprocessing module not available") + + +class TestConcurrentShared(PluginTester, unittest.TestCase): + activate = '--processes=2' + plugins = [MultiProcess()] + suitepath = os.path.join(support, 'concurrent_shared') + + def runTest(self): + assert 'Ran 2 tests in 1.' in self.output, "make sure two tests use 1.x seconds (no more than 2 seconsd)" + assert str(self.output).strip().endswith('OK') + def tearDown(self): + MultiProcess.status.pop('active') + diff --git a/nose/plugins/multiprocess.py b/nose/plugins/multiprocess.py index 604752a..fc4235b 100644 --- a/nose/plugins/multiprocess.py +++ b/nose/plugins/multiprocess.py @@ -254,35 +254,7 @@ class MultiProcessTestRunner(TextTestRunner): self.loaderClass = kw.pop('loaderClass', loader.defaultTestLoader) super(MultiProcessTestRunner, self).__init__(**kw) - def run(self, test): - """ - Execute the test (which may be a test suite). If the test is a suite, - distribute it out among as many processes as have been configured, at - as fine a level as is possible given the context fixtures defined in - the suite or any sub-suites. - - """ - log.debug("%s.run(%s) (%s)", self, test, os.getpid()) - wrapper = self.config.plugins.prepareTest(test) - if wrapper is not None: - test = wrapper - - # plugins can decorate or capture the output stream - wrapped = self.config.plugins.setOutputStream(self.stream) - if wrapped is not None: - self.stream = wrapped - - testQueue = Queue() - resultQueue = Queue() - tasks = [] - completed = [] - workers = [] - to_teardown = [] - shouldStop = Event() - - result = self._makeResult() - start = time.time() - + def collect(self, test, testQueue, tasks, to_teardown, result): # dispatch and collect results # put indexes only on queue because tests aren't picklable for case in self.nextBatch(test): @@ -308,16 +280,51 @@ class MultiProcessTestRunner(TextTestRunner): result.addError(case, sys.exc_info()) else: to_teardown.append(case) - for _t in case: - test_addr = self.addtask(testQueue,tasks,_t) - log.debug("Queued shared-fixture test %s (%s) to %s", - len(tasks), test_addr, testQueue) + if case.factory: + ancestors=case.factory.context.get(case, []) + for an in ancestors[:2]: + #log.debug('reset ancestor %s', an) + if getattr(an, '_multiprocess_shared_', False): + an._multiprocess_can_split_=True + #an._multiprocess_shared_=False + self.collect(case, testQueue, tasks, to_teardown, result) else: test_addr = self.addtask(testQueue,tasks,case) log.debug("Queued test %s (%s) to %s", len(tasks), test_addr, testQueue) + def run(self, test): + """ + Execute the test (which may be a test suite). If the test is a suite, + distribute it out among as many processes as have been configured, at + as fine a level as is possible given the context fixtures defined in + the suite or any sub-suites. + + """ + log.debug("%s.run(%s) (%s)", self, test, os.getpid()) + wrapper = self.config.plugins.prepareTest(test) + if wrapper is not None: + test = wrapper + + # plugins can decorate or capture the output stream + wrapped = self.config.plugins.setOutputStream(self.stream) + if wrapped is not None: + self.stream = wrapped + + testQueue = Queue() + resultQueue = Queue() + tasks = [] + completed = [] + workers = [] + to_teardown = [] + shouldStop = Event() + + result = self._makeResult() + start = time.time() + + self.collect(test, testQueue, tasks, to_teardown, result) + log.debug("Starting %s workers", self.config.multiprocess_workers) for i in range(self.config.multiprocess_workers): currentaddr = Value('c',bytes_('')) @@ -755,40 +762,27 @@ class NoSharedFixtureContextSuite(ContextSuite): result.addError(self, self._exc_info()) return try: - localtests = [test for test in self._tests] - if (not self.hasFixtures(MultiProcessTestRunner.checkCanSplit) - and len(localtests) > 1 and self.testQueue is not None): - log.debug("queue %d tests"%len(localtests)) - for test in localtests: - if isinstance(test.test,nose.failure.Failure): - # proably failed in the generator, so execute directly - # to get the exception - test(orig) - else: - MultiProcessTestRunner.addtask(self.testQueue, - self.tasks, test) - else: - for test in localtests: - if (isinstance(test,nose.case.Test) - and self.arg is not None): - test.test.arg = self.arg - else: - test.arg = self.arg - test.testQueue = self.testQueue - test.tasks = self.tasks - if result.shouldStop: - log.debug("stopping") - break - # each nose.case.Test will create its own result proxy - # so the cases need the original result, to avoid proxy - # chains - try: - test(orig) - except KeyboardInterrupt,e: - err = (TimedOutException,TimedOutException(str(test)), - sys.exc_info()[2]) - test.config.plugins.addError(test,err) - orig.addError(test,err) + for test in self._tests: + if (isinstance(test,nose.case.Test) + and self.arg is not None): + test.test.arg = self.arg + else: + test.arg = self.arg + test.testQueue = self.testQueue + test.tasks = self.tasks + if result.shouldStop: + log.debug("stopping") + break + # each nose.case.Test will create its own result proxy + # so the cases need the original result, to avoid proxy + # chains + try: + test(orig) + except KeyboardInterrupt,e: + err = (TimedOutException,TimedOutException(str(test)), + sys.exc_info()[2]) + test.config.plugins.addError(test,err) + orig.addError(test,err) finally: self.has_run = True try: -- cgit v1.2.1 From 823bbf6712244723400ac491242d557f7d21e818 Mon Sep 17 00:00:00 2001 From: Heng Liu Date: Wed, 2 Nov 2011 10:26:30 -0700 Subject: update CHANGELOG to list fixes for issue #462 --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index 2023b9c..f58e1b4 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -5,6 +5,7 @@ - Adds support for :option:`--cover-branches` (related to #370). Patch by Timothée Peignier. - Fixed Unicode issue on Python 3.1 with coverage (#442) +- fixed class level fixture handling in multiprocessing plugin 1.1.2 -- cgit v1.2.1 From 24308b0db2b0583d9270fea5f1be464965a3fac5 Mon Sep 17 00:00:00 2001 From: Heng Liu Date: Wed, 2 Nov 2011 16:32:58 -0700 Subject: streamline multiprocessing tests consolidated checking of multiprocessing python module availability into package setup created a base class for all multiprocessing plugin tests to do cleanups --- functional_tests/test_multiprocessing/__init__.py | 31 ++++++++++++++++++++++ .../test_multiprocessing/support/timeout.py | 6 +++++ .../test_multiprocessing/test_class.py | 24 ++--------------- .../test_multiprocessing/test_concurrent_shared.py | 26 +++--------------- .../test_multiprocessing/test_nameerror.py | 26 +++--------------- .../test_multiprocessing/test_process_timeout.py | 29 ++++---------------- 6 files changed, 50 insertions(+), 92 deletions(-) create mode 100644 functional_tests/test_multiprocessing/__init__.py diff --git a/functional_tests/test_multiprocessing/__init__.py b/functional_tests/test_multiprocessing/__init__.py new file mode 100644 index 0000000..856b0ad --- /dev/null +++ b/functional_tests/test_multiprocessing/__init__.py @@ -0,0 +1,31 @@ +import os +from unittest import TestCase + +from nose.plugins import PluginTester +from nose.plugins.skip import SkipTest +from nose.plugins.multiprocess import MultiProcess + +support = os.path.join(os.path.dirname(__file__), 'support') + +def setup(): + try: + import multiprocessing + if 'active' in MultiProcess.status: + raise SkipTest("Multiprocess plugin is active. Skipping tests of " + "plugin itself.") + except ImportError: + raise SkipTest("multiprocessing module not available") + +class MPTestBase(PluginTester, TestCase): + processes = 1 + activate = '--processes=1' + plugins = [MultiProcess()] + suitepath = os.path.join(support, 'timeout.py') + + def __init__(self, *args, **kwargs): + self.activate = '--processes=%d' % self.processes + PluginTester.__init__(self) + TestCase.__init__(self, *args, **kwargs) + + def tearDown(self): + MultiProcess.status.pop('active') diff --git a/functional_tests/test_multiprocessing/support/timeout.py b/functional_tests/test_multiprocessing/support/timeout.py index 52dce12..480c859 100644 --- a/functional_tests/test_multiprocessing/support/timeout.py +++ b/functional_tests/test_multiprocessing/support/timeout.py @@ -1,6 +1,12 @@ +#make sure all tests in this file are dispatched to the same subprocess +def setup(): + pass def test_timeout(): "this test *should* fail when process-timeout=1" from time import sleep sleep(2) +# check timeout will not prevent remaining tests dispatched to the same subprocess to continue to run +def test_pass(): + pass diff --git a/functional_tests/test_multiprocessing/test_class.py b/functional_tests/test_multiprocessing/test_class.py index 1e36ba6..6f42ac3 100644 --- a/functional_tests/test_multiprocessing/test_class.py +++ b/functional_tests/test_multiprocessing/test_class.py @@ -1,33 +1,13 @@ import os -import unittest -from nose.plugins import PluginTester -from nose.plugins.skip import SkipTest -from nose.plugins.multiprocess import MultiProcess - - -support = os.path.join(os.path.dirname(__file__), 'support') - - -def setup(): - try: - import multiprocessing - if 'active' in MultiProcess.status: - raise SkipTest("Multiprocess plugin is active. Skipping tests of " - "plugin itself.") - except ImportError: - raise SkipTest("multiprocessing module not available") +from . import support, MPTestBase #test case for #462 -class TestClassFixture(PluginTester, unittest.TestCase): - activate = '--processes=1' - plugins = [MultiProcess()] +class TestClassFixture(MPTestBase): suitepath = os.path.join(support, 'class.py') def runTest(self): assert str(self.output).strip().endswith('OK') assert 'Ran 2 tests' in self.output - def tearDown(self): - MultiProcess.status.pop('active') diff --git a/functional_tests/test_multiprocessing/test_concurrent_shared.py b/functional_tests/test_multiprocessing/test_concurrent_shared.py index cfb36c4..9974bfd 100644 --- a/functional_tests/test_multiprocessing/test_concurrent_shared.py +++ b/functional_tests/test_multiprocessing/test_concurrent_shared.py @@ -1,32 +1,12 @@ import os -import unittest -from nose.plugins import PluginTester -from nose.plugins.skip import SkipTest -from nose.plugins.multiprocess import MultiProcess +from . import support, MPTestBase - -support = os.path.join(os.path.dirname(__file__), 'support') - - -def setup(): - try: - import multiprocessing - if 'active' in MultiProcess.status: - raise SkipTest("Multiprocess plugin is active. Skipping tests of " - "plugin itself.") - except ImportError: - raise SkipTest("multiprocessing module not available") - - -class TestConcurrentShared(PluginTester, unittest.TestCase): - activate = '--processes=2' - plugins = [MultiProcess()] +class TestConcurrentShared(MPTestBase): + processes = 2 suitepath = os.path.join(support, 'concurrent_shared') def runTest(self): assert 'Ran 2 tests in 1.' in self.output, "make sure two tests use 1.x seconds (no more than 2 seconsd)" assert str(self.output).strip().endswith('OK') - def tearDown(self): - MultiProcess.status.pop('active') diff --git a/functional_tests/test_multiprocessing/test_nameerror.py b/functional_tests/test_multiprocessing/test_nameerror.py index d3d7210..4837aca 100644 --- a/functional_tests/test_multiprocessing/test_nameerror.py +++ b/functional_tests/test_multiprocessing/test_nameerror.py @@ -1,33 +1,13 @@ import os -import unittest -from nose.plugins import PluginTester -from nose.plugins.skip import SkipTest -from nose.plugins.multiprocess import MultiProcess +from . import support, MPTestBase - -support = os.path.join(os.path.dirname(__file__), 'support') - - -def setup(): - try: - import multiprocessing - if 'active' in MultiProcess.status: - raise SkipTest("Multiprocess plugin is active. Skipping tests of " - "plugin itself.") - except ImportError: - raise SkipTest("multiprocessing module not available") - - -class TestMPNameError(PluginTester, unittest.TestCase): - activate = '--processes=2' - plugins = [MultiProcess()] +class TestMPNameError(MPTestBase): + processes = 2 suitepath = os.path.join(support, 'nameerror.py') def runTest(self): print str(self.output) assert 'NameError' in self.output assert "'undefined_variable' is not defined" in self.output - def tearDown(self): - MultiProcess.status.pop('active') diff --git a/functional_tests/test_multiprocessing/test_process_timeout.py b/functional_tests/test_multiprocessing/test_process_timeout.py index 27e0584..afef674 100644 --- a/functional_tests/test_multiprocessing/test_process_timeout.py +++ b/functional_tests/test_multiprocessing/test_process_timeout.py @@ -1,40 +1,21 @@ import os -import unittest -from nose.plugins import PluginTester -from nose.plugins.skip import SkipTest -from nose.plugins.multiprocess import MultiProcess +from . import support, MPTestBase -support = os.path.join(os.path.dirname(__file__), 'support') - - -def setup(): - try: - import multiprocessing - if 'active' in MultiProcess.status: - raise SkipTest("Multiprocess plugin is active. Skipping tests of " - "plugin itself.") - except ImportError: - raise SkipTest("multiprocessing module not available") - - - -class TestMPTimeout(PluginTester, unittest.TestCase): - activate = '--processes=2' +class TestMPTimeout(MPTestBase): args = ['--process-timeout=1'] - plugins = [MultiProcess()] suitepath = os.path.join(support, 'timeout.py') def runTest(self): assert "TimedOutException: 'timeout.test_timeout'" in self.output - + assert "Ran 2 tests in" in self.output + assert "FAILED (errors=1)" in self.output class TestMPTimeoutPass(TestMPTimeout): args = ['--process-timeout=3'] def runTest(self): assert "TimedOutException: 'timeout.test_timeout'" not in self.output + assert "Ran 2 tests in" in self.output assert str(self.output).strip().endswith('OK') - def tearDown(self): - MultiProcess.status.pop('active') -- cgit v1.2.1 From 104de87d8886c75c0a97b6e5470279d732575e23 Mon Sep 17 00:00:00 2001 From: Heng Liu Date: Thu, 3 Nov 2011 15:57:46 -0700 Subject: remove teardown in multiprocessing plugin tests class: not useful any more after the availability checking of multiprocessing module is moved to the __init__ script --- functional_tests/test_multiprocessing/__init__.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/functional_tests/test_multiprocessing/__init__.py b/functional_tests/test_multiprocessing/__init__.py index 856b0ad..2a52efd 100644 --- a/functional_tests/test_multiprocessing/__init__.py +++ b/functional_tests/test_multiprocessing/__init__.py @@ -26,6 +26,3 @@ class MPTestBase(PluginTester, TestCase): self.activate = '--processes=%d' % self.processes PluginTester.__init__(self) TestCase.__init__(self, *args, **kwargs) - - def tearDown(self): - MultiProcess.status.pop('active') -- cgit v1.2.1 From c0afc80c65e59b381e6544df3fdaf82068959b23 Mon Sep 17 00:00:00 2001 From: Heng Liu Date: Thu, 3 Nov 2011 16:04:42 -0700 Subject: properly handle ctrl+c (keyboard interrupt): first ctrl+c should interrupt all tests currently being executed by multiprocessing plugin, and no further tests should be run in these subprocesses after first keyboardinterrupt, multiprocessing plugin should still try to execute any scheduled teardowns. second keyboardinterrupt should interrupt teardown execution and return immediately. this also fixes problem where subprocesses keeps on running even after parent nose is killed by keyboardinterrupt. added functional tests for all the mentioned cases --- .../test_multiprocessing/support/fake_nosetest.py | 13 + .../support/keyboardinterrupt.py | 27 ++ .../support/keyboardinterrupt_twice.py | 27 ++ .../test_multiprocessing/test_keyboardinterrupt.py | 70 +++++ nose/plugins/multiprocess.py | 348 +++++++++++---------- 5 files changed, 319 insertions(+), 166 deletions(-) create mode 100644 functional_tests/test_multiprocessing/support/fake_nosetest.py create mode 100644 functional_tests/test_multiprocessing/support/keyboardinterrupt.py create mode 100644 functional_tests/test_multiprocessing/support/keyboardinterrupt_twice.py create mode 100644 functional_tests/test_multiprocessing/test_keyboardinterrupt.py diff --git a/functional_tests/test_multiprocessing/support/fake_nosetest.py b/functional_tests/test_multiprocessing/support/fake_nosetest.py new file mode 100644 index 0000000..886c569 --- /dev/null +++ b/functional_tests/test_multiprocessing/support/fake_nosetest.py @@ -0,0 +1,13 @@ +import sys + +import nose + +from nose.plugins.multiprocess import MultiProcess +from nose.config import Config +from nose.plugins.manager import PluginManager + +if __name__ == '__main__': + if len(sys.argv) < 2: + print "USAGE: %s TEST_FILE" % sys.argv[0] + sys.exit(1) + nose.main(defaultTest=sys.argv[1], argv=[sys.argv[0],'--processes=1','-v'], config=Config(plugins=PluginManager(plugins=[MultiProcess()]))) diff --git a/functional_tests/test_multiprocessing/support/keyboardinterrupt.py b/functional_tests/test_multiprocessing/support/keyboardinterrupt.py new file mode 100644 index 0000000..eeffd4a --- /dev/null +++ b/functional_tests/test_multiprocessing/support/keyboardinterrupt.py @@ -0,0 +1,27 @@ +from tempfile import mktemp +from time import sleep + +def log(w): + f = open(logfile, 'a') + f.write(w+"\n") + f.close() +#make sure all tests in this file are dispatched to the same subprocess +def setup(): + global logfile + logfile = mktemp() + print "tempfile is:",logfile + + log('setup') + pass + +def test_timeout(): + log('test_timeout') + sleep(2) + log('test_timeout_finished') + +# check timeout will not prevent remaining tests dispatched to the same subprocess to continue to run +def test_pass(): + log('test_pass') + +def teardown(): + log('teardown') diff --git a/functional_tests/test_multiprocessing/support/keyboardinterrupt_twice.py b/functional_tests/test_multiprocessing/support/keyboardinterrupt_twice.py new file mode 100644 index 0000000..b7d845a --- /dev/null +++ b/functional_tests/test_multiprocessing/support/keyboardinterrupt_twice.py @@ -0,0 +1,27 @@ +from tempfile import mktemp +from time import sleep + +logfile = mktemp() +print "tempfile is:",logfile + +def log(w): + f = open(logfile, 'a') + f.write(w+"\n") + f.close() +#make sure all tests in this file are dispatched to the same subprocess +def setup(): + log('setup') + +def test_timeout(): + log('test_timeout') + sleep(2) + log('test_timeout_finished') + +# check timeout will not prevent remaining tests dispatched to the same subprocess to continue to run +def test_pass(): + log('test_pass') + +def teardown(): + log('teardown') + sleep(10) + log('teardown_finished') diff --git a/functional_tests/test_multiprocessing/test_keyboardinterrupt.py b/functional_tests/test_multiprocessing/test_keyboardinterrupt.py new file mode 100644 index 0000000..d0cc508 --- /dev/null +++ b/functional_tests/test_multiprocessing/test_keyboardinterrupt.py @@ -0,0 +1,70 @@ +from subprocess import Popen,PIPE +import os +import sys +from time import sleep +import signal + +import nose + +from . import support + +PYTHONPATH = os.environ['PYTHONPATH'] if 'PYTHONPATH' in os.environ else '' +def setup(): + nose_parent_dir = os.path.normpath(os.path.join(os.path.abspath(os.path.dirname(nose.__file__)),'..')) + paths = [nose_parent_dir] + if PYTHONPATH: + paths.append(PYTHONPATH) + os.environ['PYTHONPATH'] = os.pathsep.join(paths) +def teardown(): + if PYTHONPATH: + os.environ['PYTHONPATH'] = PYTHONPATH + else: + del os.environ['PYTHONPATH'] + +runner = os.path.join(support, 'fake_nosetest.py') +def keyboardinterrupt(case): + #os.setsid would create a process group so signals sent to the + #parent process will propogates to all children processes + process = Popen([sys.executable,runner,os.path.join(support,case)], preexec_fn=os.setsid, stdout=PIPE, stderr=PIPE, bufsize=-1) + + sleep(0.5) + + os.killpg(process.pid, signal.SIGINT) + return process + +def get_log_content(stdout): + prefix = 'tempfile is: ' + if not stdout.startswith(prefix): + raise Exception('stdout does not contain tmp file name: '+stdout) + logfile = stdout[len(prefix):].strip() #remove trailing new line char + f = open(logfile) + content = f.read() + f.close() + return content + +def test_keyboardinterrupt(): + process = keyboardinterrupt('keyboardinterrupt.py') + stdout, stderr = process.communicate(None) + log = get_log_content(stdout) + assert 'setup' in log + assert 'test_timeout' in log + assert 'test_timeout_finished' not in log + assert 'test_pass' not in log + assert 'teardown' in log + assert 'Ran 0 tests' in stderr + assert 'KeyboardInterrupt' in stderr + +def test_keyboardinterrupt_twice(): + process = keyboardinterrupt('keyboardinterrupt_twice.py') + sleep(0.5) + os.killpg(process.pid, signal.SIGINT) + stdout, stderr = process.communicate(None) + log = get_log_content(stdout) + assert 'setup' in log + assert 'test_timeout' in log + assert 'test_timeout_finished' not in log + assert 'test_pass' not in log + assert 'teardown' in log + assert 'teardown_finished' not in log + assert 'Ran 0 tests' in stderr + assert 'KeyboardInterrupt' in stderr diff --git a/nose/plugins/multiprocess.py b/nose/plugins/multiprocess.py index fc4235b..abebf77 100644 --- a/nose/plugins/multiprocess.py +++ b/nose/plugins/multiprocess.py @@ -130,7 +130,8 @@ log = logging.getLogger(__name__) Process = Queue = Pool = Event = Value = Array = None -class TimedOutException(Exception): +# have to inherit KeyboardInterrupt to it will interrupt process properly +class TimedOutException(KeyboardInterrupt): def __init__(self, value = "Timed Out"): self.value = value def __str__(self): @@ -247,9 +248,12 @@ class MultiProcess(Plugin): config=self.config, loaderClass=self.loaderClass) +def signalhandler(sig, frame): + raise TimedOutException() + class MultiProcessTestRunner(TextTestRunner): waitkilltime = 5.0 # max time to wait to terminate a process that does not - # respond to SIGINT + # respond to SIGILL def __init__(self, **kw): self.loaderClass = kw.pop('loaderClass', loader.defaultTestLoader) super(MultiProcessTestRunner, self).__init__(**kw) @@ -294,6 +298,28 @@ class MultiProcessTestRunner(TextTestRunner): log.debug("Queued test %s (%s) to %s", len(tasks), test_addr, testQueue) + def startProcess(self, iworker, testQueue, resultQueue, shouldStop, result): + currentaddr = Value('c',bytes_('')) + currentstart = Value('d',time.time()) + keyboardCaught = Event() + p = Process(target=runner, + args=(iworker, testQueue, + resultQueue, + currentaddr, + currentstart, + keyboardCaught, + shouldStop, + self.loaderClass, + result.__class__, + pickle.dumps(self.config))) + p.currentaddr = currentaddr + p.currentstart = currentstart + p.keyboardCaught = keyboardCaught + old = signal.signal(signal.SIGILL, signalhandler) + p.start() + signal.signal(signal.SIGILL, old) + return p + def run(self, test): """ Execute the test (which may be a test suite). If the test is a suite, @@ -327,20 +353,7 @@ class MultiProcessTestRunner(TextTestRunner): log.debug("Starting %s workers", self.config.multiprocess_workers) for i in range(self.config.multiprocess_workers): - currentaddr = Value('c',bytes_('')) - currentstart = Value('d',0.0) - keyboardCaught = Event() - p = Process(target=runner, args=(i, testQueue, resultQueue, - currentaddr, currentstart, - keyboardCaught, shouldStop, - self.loaderClass, - result.__class__, - pickle.dumps(self.config))) - p.currentaddr = currentaddr - p.currentstart = currentstart - p.keyboardCaught = keyboardCaught - # p.setDaemon(True) - p.start() + p = self.startProcess(i, testQueue, resultQueue, shouldStop, result) workers.append(p) log.debug("Started worker process %s", i+1) @@ -348,162 +361,143 @@ class MultiProcessTestRunner(TextTestRunner): # need to keep track of the next time to check for timeouts in case # more than one process times out at the same time. nexttimeout=self.config.multiprocess_timeout - while tasks: - log.debug("Waiting for results (%s/%s tasks), next timeout=%.3fs", - len(completed), total_tasks,nexttimeout) - try: - iworker, addr, newtask_addrs, batch_result = resultQueue.get( - timeout=nexttimeout) - log.debug('Results received for worker %d, %s, new tasks: %d', - iworker,addr,len(newtask_addrs)) + thrownError = None + + try: + while tasks: + log.debug("Waiting for results (%s/%s tasks), next timeout=%.3fs", + len(completed), total_tasks,nexttimeout) try: + iworker, addr, newtask_addrs, batch_result = resultQueue.get( + timeout=nexttimeout) + log.debug('Results received for worker %d, %s, new tasks: %d', + iworker,addr,len(newtask_addrs)) try: - tasks.remove(addr) - except ValueError: - log.warn('worker %s failed to remove from tasks: %s', - iworker,addr) - total_tasks += len(newtask_addrs) - for newaddr in newtask_addrs: - tasks.append(newaddr) - except KeyError: - log.debug("Got result for unknown task? %s", addr) - log.debug("current: %s",str(list(tasks)[0])) - else: - completed.append([addr,batch_result]) - self.consolidate(result, batch_result) - if (self.config.stopOnError - and not result.wasSuccessful()): - # set the stop condition - shouldStop.set() - break - if self.config.multiprocess_restartworker: - log.debug('joining worker %s',iworker) - # wait for working, but not that important if worker - # cannot be joined in fact, for workers that add to - # testQueue, they will not terminate until all their - # items are read - workers[iworker].join(timeout=1) - if not shouldStop.is_set() and not testQueue.empty(): - log.debug('starting new process on worker %s',iworker) - currentaddr = Value('c',bytes_('')) - currentstart = Value('d',time.time()) - keyboardCaught = Event() - workers[iworker] = Process(target=runner, - args=(iworker, testQueue, - resultQueue, - currentaddr, - currentstart, - keyboardCaught, - shouldStop, - self.loaderClass, - result.__class__, - pickle.dumps(self.config))) - workers[iworker].currentaddr = currentaddr - workers[iworker].currentstart = currentstart - workers[iworker].keyboardCaught = keyboardCaught - workers[iworker].start() - except Empty: - log.debug("Timed out with %s tasks pending " - "(empty testQueue=%d): %s", - len(tasks),testQueue.empty(),str(tasks)) - any_alive = False - for iworker, w in enumerate(workers): - if w.is_alive(): - worker_addr = bytes_(w.currentaddr.value,'ascii') - timeprocessing = time.time() - w.currentstart.value - if ( len(worker_addr) == 0 + try: + tasks.remove(addr) + except ValueError: + log.warn('worker %s failed to remove from tasks: %s', + iworker,addr) + total_tasks += len(newtask_addrs) + tasks.extend(newtask_addrs) + except KeyError: + log.debug("Got result for unknown task? %s", addr) + log.debug("current: %s",str(list(tasks)[0])) + else: + completed.append([addr,batch_result]) + self.consolidate(result, batch_result) + if (self.config.stopOnError + and not result.wasSuccessful()): + # set the stop condition + shouldStop.set() + break + if self.config.multiprocess_restartworker: + log.debug('joining worker %s',iworker) + # wait for working, but not that important if worker + # cannot be joined in fact, for workers that add to + # testQueue, they will not terminate until all their + # items are read + workers[iworker].join(timeout=1) + if not shouldStop.is_set() and not testQueue.empty(): + log.debug('starting new process on worker %s',iworker) + workers[iworker] = self.startProcess(iworker, testQueue, resultQueue, shouldStop, result) + except Empty: + log.debug("Timed out with %s tasks pending " + "(empty testQueue=%r): %s", + len(tasks),testQueue.empty(),str(tasks)) + any_alive = False + for iworker, w in enumerate(workers): + if w.is_alive(): + worker_addr = bytes_(w.currentaddr.value,'ascii') + timeprocessing = time.time() - w.currentstart.value + if ( len(worker_addr) == 0 + and timeprocessing > self.config.multiprocess_timeout-0.1): + log.debug('worker %d has finished its work item, ' + 'but is not exiting? do we wait for it?', + iworker) + else: + any_alive = True + if (len(worker_addr) > 0 and timeprocessing > self.config.multiprocess_timeout-0.1): - log.debug('worker %d has finished its work item, ' - 'but is not exiting? do we wait for it?', - iworker) - else: - any_alive = True - if (len(worker_addr) > 0 - and timeprocessing > self.config.multiprocess_timeout-0.1): - log.debug('timed out worker %s: %s', - iworker,worker_addr) - w.currentaddr.value = bytes_('') - # If the process is in C++ code, sending a SIGINT - # might not send a python KeybordInterrupt exception - # therefore, send multiple signals until an - # exception is caught. If this takes too long, then - # terminate the process - w.keyboardCaught.clear() - startkilltime = time.time() - while not w.keyboardCaught.is_set() and w.is_alive(): - if time.time()-startkilltime > self.waitkilltime: - # have to terminate... - log.error("terminating worker %s",iworker) - w.terminate() - currentaddr = Value('c',bytes_('')) - currentstart = Value('d',time.time()) - keyboardCaught = Event() - workers[iworker] = Process(target=runner, - args=(iworker, testQueue, resultQueue, - currentaddr, currentstart, - keyboardCaught, shouldStop, - self.loaderClass, - result.__class__, - pickle.dumps(self.config))) - workers[iworker].currentaddr = currentaddr - workers[iworker].currentstart = currentstart - workers[iworker].keyboardCaught = keyboardCaught - workers[iworker].start() - # there is a small probability that the - # terminated process might send a result, - # which has to be specially handled or - # else processes might get orphaned. - w = workers[iworker] - break - os.kill(w.pid, signal.SIGINT) - time.sleep(0.1) - if not any_alive and testQueue.empty(): - log.debug("All workers dead") - break - nexttimeout=self.config.multiprocess_timeout - for w in workers: - if w.is_alive() and len(w.currentaddr.value) > 0: - timeprocessing = time.time()-w.currentstart.value - if timeprocessing <= self.config.multiprocess_timeout: - nexttimeout = min(nexttimeout, - self.config.multiprocess_timeout-timeprocessing) - - log.debug("Completed %s tasks (%s remain)", len(completed), len(tasks)) - - for case in to_teardown: - log.debug("Tearing down shared fixtures for %s", case) - try: - case.tearDown() - except (KeyboardInterrupt, SystemExit): - raise - except: - result.addError(case, sys.exc_info()) + log.debug('timed out worker %s: %s', + iworker,worker_addr) + w.currentaddr.value = bytes_('') + # If the process is in C++ code, sending a SIGILL + # might not send a python KeybordInterrupt exception + # therefore, send multiple signals until an + # exception is caught. If this takes too long, then + # terminate the process + w.keyboardCaught.clear() + startkilltime = time.time() + while not w.keyboardCaught.is_set() and w.is_alive(): + if time.time()-startkilltime > self.waitkilltime: + # have to terminate... + log.error("terminating worker %s",iworker) + w.terminate() + # there is a small probability that the + # terminated process might send a result, + # which has to be specially handled or + # else processes might get orphaned. + workers[iworker] = w = self.startProcess(iworker, testQueue, resultQueue, shouldStop, result) + break + os.kill(w.pid, signal.SIGILL) + time.sleep(0.1) + if not any_alive and testQueue.empty(): + log.debug("All workers dead") + break + nexttimeout=self.config.multiprocess_timeout + for w in workers: + if w.is_alive() and len(w.currentaddr.value) > 0: + timeprocessing = time.time()-w.currentstart.value + if timeprocessing <= self.config.multiprocess_timeout: + nexttimeout = min(nexttimeout, + self.config.multiprocess_timeout-timeprocessing) + log.debug("Completed %s tasks (%s remain)", len(completed), len(tasks)) + + except (KeyboardInterrupt, SystemExit), e: + log.info('parent received ctrl-c when waiting for test results') + thrownError = e + #resultQueue.get(False) + + result.addError(test, sys.exc_info()) + + try: + for case in to_teardown: + log.debug("Tearing down shared fixtures for %s", case) + try: + case.tearDown() + except (KeyboardInterrupt, SystemExit): + raise + except: + result.addError(case, sys.exc_info()) - stop = time.time() + stop = time.time() - # first write since can freeze on shutting down processes - result.printErrors() - result.printSummary(start, stop) - self.config.plugins.finalize(result) + # first write since can freeze on shutting down processes + result.printErrors() + result.printSummary(start, stop) + self.config.plugins.finalize(result) - log.debug("Tell all workers to stop") - for w in workers: - if w.is_alive(): - testQueue.put('STOP', block=False) + if thrownError is None: + log.debug("Tell all workers to stop") + for w in workers: + if w.is_alive(): + testQueue.put('STOP', block=False) - # wait for the workers to end - try: + # wait for the workers to end for iworker,worker in enumerate(workers): if worker.is_alive(): log.debug('joining worker %s',iworker) - worker.join()#10) + worker.join() if worker.is_alive(): log.debug('failed to join worker %s',iworker) - except KeyboardInterrupt: - log.info('parent received ctrl-c') + except (KeyboardInterrupt, SystemExit): + log.info('parent received ctrl-c when shutting down: stop all processes') for worker in workers: - worker.terminate() - worker.join() + if worker.is_alive(): + worker.terminate() + if thrownError: raise thrownError + else: raise return result @@ -696,17 +690,28 @@ def __runner(ix, testQueue, resultQueue, currentaddr, currentstart, test(result) currentaddr.value = bytes_('') resultQueue.put((ix, test_addr, test.tasks, batch(result))) - except KeyboardInterrupt: - keyboardCaught.set() - if len(currentaddr.value) > 0: - log.exception('Worker %s keyboard interrupt, failing ' - 'current test %s',ix,test_addr) + except KeyboardInterrupt, e: #TimedOutException: + timeout = isinstance(e, TimedOutException) + if timeout: + keyboardCaught.set() + if len(currentaddr.value): + if timeout: + msg = 'Worker %s timed out, failing current test %s' + else: + msg = 'Worker %s keyboard interrupt, failing current test %s' + log.exception(msg,ix,test_addr) currentaddr.value = bytes_('') failure.Failure(*sys.exc_info())(result) resultQueue.put((ix, test_addr, test.tasks, batch(result))) else: - log.debug('Worker %s test %s timed out',ix,test_addr) + if timeout: + msg = 'Worker %s test %s timed out' + else: + msg = 'Worker %s test %s keyboard interrupt' + log.debug(msg,ix,test_addr) resultQueue.put((ix, test_addr, test.tasks, batch(result))) + if not timeout: + raise except SystemExit: currentaddr.value = bytes_('') log.exception('Worker %s system exit',ix) @@ -754,6 +759,7 @@ class NoSharedFixtureContextSuite(ContextSuite): else: result, orig = result, result try: + #log.debug('setUp for %s', id(self)); self.setUp() except KeyboardInterrupt: raise @@ -776,16 +782,26 @@ class NoSharedFixtureContextSuite(ContextSuite): # each nose.case.Test will create its own result proxy # so the cases need the original result, to avoid proxy # chains + #log.debug('running test %s in suite %s', test, self); try: test(orig) - except KeyboardInterrupt,e: + except KeyboardInterrupt, e: + timeout = isinstance(e, TimedOutException) + if timeout: + msg = 'Timeout when running test %s in suite %s' + else: + msg = 'KeyboardInterrupt when running test %s in suite %s' + log.debug(msg, test, self) err = (TimedOutException,TimedOutException(str(test)), sys.exc_info()[2]) test.config.plugins.addError(test,err) orig.addError(test,err) + if not timeout: + raise finally: self.has_run = True try: + #log.debug('tearDown for %s', id(self)); self.tearDown() except KeyboardInterrupt: raise -- cgit v1.2.1 From fddd8ad03324f1b8cb48d3a5052b803235e5a627 Mon Sep 17 00:00:00 2001 From: Heng Liu Date: Thu, 3 Nov 2011 16:31:55 -0700 Subject: properly handle ctrl+c (keyboard interrupt): first ctrl+c should interrupt all tests currently being executed by multiprocessing plugin, and no further tests should be run in these subprocesses after first keyboardinterrupt, multiprocessing plugin should still try to execute any scheduled teardowns. second keyboardinterrupt should interrupt teardown execution and return immediately. this also fixes problem where subprocesses keeps on running even after parent nose is killed by keyboardinterrupt. added functional tests for all the mentioned cases --- functional_tests/test_multiprocessing/test_keyboardinterrupt.py | 4 ++-- nosetests.1 | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/functional_tests/test_multiprocessing/test_keyboardinterrupt.py b/functional_tests/test_multiprocessing/test_keyboardinterrupt.py index d0cc508..e29ffef 100644 --- a/functional_tests/test_multiprocessing/test_keyboardinterrupt.py +++ b/functional_tests/test_multiprocessing/test_keyboardinterrupt.py @@ -44,7 +44,7 @@ def get_log_content(stdout): def test_keyboardinterrupt(): process = keyboardinterrupt('keyboardinterrupt.py') - stdout, stderr = process.communicate(None) + stdout, stderr = [s.decode('utf-8') for s in process.communicate(None)] log = get_log_content(stdout) assert 'setup' in log assert 'test_timeout' in log @@ -58,7 +58,7 @@ def test_keyboardinterrupt_twice(): process = keyboardinterrupt('keyboardinterrupt_twice.py') sleep(0.5) os.killpg(process.pid, signal.SIGINT) - stdout, stderr = process.communicate(None) + stdout, stderr = [s.decode('utf-8') for s in process.communicate(None)] log = get_log_content(stdout) assert 'setup' in log assert 'test_timeout' in log diff --git a/nosetests.1 b/nosetests.1 index 36982b7..1ccc461 100644 --- a/nosetests.1 +++ b/nosetests.1 @@ -491,5 +491,5 @@ jpellerin+nose@gmail.com .SH COPYRIGHT LGPL -.\" Generated by docutils manpage writer on 2011-10-10 09:32. +.\" Generated by docutils manpage writer on 2011-11-03 16:30. .\" -- cgit v1.2.1 From 8318349981691a7d01eba9431d0926377cb89ed6 Mon Sep 17 00:00:00 2001 From: Heng Liu Date: Thu, 3 Nov 2011 17:24:57 -0700 Subject: make the multiprocessing server process ignore ctrl+c to keep communication channels open between subprocesses and main process modified test --- functional_tests/test_multiprocessing/test_keyboardinterrupt.py | 4 ++++ nose/plugins/multiprocess.py | 9 +++++++++ 2 files changed, 13 insertions(+) diff --git a/functional_tests/test_multiprocessing/test_keyboardinterrupt.py b/functional_tests/test_multiprocessing/test_keyboardinterrupt.py index e29ffef..7d26d10 100644 --- a/functional_tests/test_multiprocessing/test_keyboardinterrupt.py +++ b/functional_tests/test_multiprocessing/test_keyboardinterrupt.py @@ -53,6 +53,9 @@ def test_keyboardinterrupt(): assert 'teardown' in log assert 'Ran 0 tests' in stderr assert 'KeyboardInterrupt' in stderr + assert 'FAILED (errors=1)' in stderr + assert 'ERROR: Worker 0 keyboard interrupt, failing current test '+os.path.join(support,'keyboardinterrupt.py') in stderr + def test_keyboardinterrupt_twice(): process = keyboardinterrupt('keyboardinterrupt_twice.py') @@ -68,3 +71,4 @@ def test_keyboardinterrupt_twice(): assert 'teardown_finished' not in log assert 'Ran 0 tests' in stderr assert 'KeyboardInterrupt' in stderr + assert 'FAILED (errors=1)' in stderr diff --git a/nose/plugins/multiprocess.py b/nose/plugins/multiprocess.py index abebf77..48ba54d 100644 --- a/nose/plugins/multiprocess.py +++ b/nose/plugins/multiprocess.py @@ -141,7 +141,16 @@ def _import_mp(): global Process, Queue, Pool, Event, Value, Array try: from multiprocessing import Manager, Process + #prevent the server process created in the manager which holds Python + #objects and allows other processes to manipulate them using proxies + #to interrupt on SIGINT (keyboardinterrupt) so that the communication + #channel between subprocesses and main process is still usable after + #ctrl+C is received in the main process. + old=signal.signal(signal.SIGINT, signal.SIG_IGN) m = Manager() + #reset it back so main process will receive a KeyboardInterrupt + #exception on ctrl+c + signal.signal(signal.SIGINT, old) Queue, Pool, Event, Value, Array = ( m.Queue, m.Pool, m.Event, m.Value, m.Array ) -- cgit v1.2.1 From 047aaa3ac7b05f12c4c6b81dbab153a6cfbc918f Mon Sep 17 00:00:00 2001 From: Heng Liu Date: Thu, 3 Nov 2011 17:32:24 -0700 Subject: fix jenkins test failure --- .../test_multiprocessing/support/keyboardinterrupt.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/functional_tests/test_multiprocessing/support/keyboardinterrupt.py b/functional_tests/test_multiprocessing/support/keyboardinterrupt.py index eeffd4a..988d1eb 100644 --- a/functional_tests/test_multiprocessing/support/keyboardinterrupt.py +++ b/functional_tests/test_multiprocessing/support/keyboardinterrupt.py @@ -1,18 +1,16 @@ from tempfile import mktemp from time import sleep +logfile = mktemp() +print "tempfile is:",logfile + def log(w): f = open(logfile, 'a') f.write(w+"\n") f.close() #make sure all tests in this file are dispatched to the same subprocess def setup(): - global logfile - logfile = mktemp() - print "tempfile is:",logfile - log('setup') - pass def test_timeout(): log('test_timeout') -- cgit v1.2.1 From ee86f762d68d59b0e846fddd0a783a90843415f6 Mon Sep 17 00:00:00 2001 From: Heng Liu Date: Fri, 4 Nov 2011 10:02:38 -0700 Subject: rearrange keyboard interrupt tests --- .../test_multiprocessing/support/fake_nosetest.py | 7 ++++--- .../support/keyboardinterrupt.py | 8 ++++++-- .../support/keyboardinterrupt_twice.py | 11 +++++++++-- .../test_multiprocessing/test_keyboardinterrupt.py | 22 +++++++++++++--------- 4 files changed, 32 insertions(+), 16 deletions(-) diff --git a/functional_tests/test_multiprocessing/support/fake_nosetest.py b/functional_tests/test_multiprocessing/support/fake_nosetest.py index 886c569..f5a6289 100644 --- a/functional_tests/test_multiprocessing/support/fake_nosetest.py +++ b/functional_tests/test_multiprocessing/support/fake_nosetest.py @@ -1,13 +1,14 @@ +import os import sys import nose - from nose.plugins.multiprocess import MultiProcess from nose.config import Config from nose.plugins.manager import PluginManager if __name__ == '__main__': - if len(sys.argv) < 2: - print "USAGE: %s TEST_FILE" % sys.argv[0] + if len(sys.argv) < 3: + print "USAGE: %s TEST_FILE LOG_FILE" % sys.argv[0] sys.exit(1) + os.environ['NOSE_MP_LOG']=sys.argv[2] nose.main(defaultTest=sys.argv[1], argv=[sys.argv[0],'--processes=1','-v'], config=Config(plugins=PluginManager(plugins=[MultiProcess()]))) diff --git a/functional_tests/test_multiprocessing/support/keyboardinterrupt.py b/functional_tests/test_multiprocessing/support/keyboardinterrupt.py index 988d1eb..2c36d95 100644 --- a/functional_tests/test_multiprocessing/support/keyboardinterrupt.py +++ b/functional_tests/test_multiprocessing/support/keyboardinterrupt.py @@ -1,8 +1,12 @@ +import os + from tempfile import mktemp from time import sleep -logfile = mktemp() -print "tempfile is:",logfile +if 'NOSE_MP_LOG' not in os.environ: + raise Exception('Environment variable NOSE_MP_LOG is not set') + +logfile = os.environ['NOSE_MP_LOG'] def log(w): f = open(logfile, 'a') diff --git a/functional_tests/test_multiprocessing/support/keyboardinterrupt_twice.py b/functional_tests/test_multiprocessing/support/keyboardinterrupt_twice.py index b7d845a..3932bbd 100644 --- a/functional_tests/test_multiprocessing/support/keyboardinterrupt_twice.py +++ b/functional_tests/test_multiprocessing/support/keyboardinterrupt_twice.py @@ -1,8 +1,12 @@ +import os + from tempfile import mktemp from time import sleep -logfile = mktemp() -print "tempfile is:",logfile +if 'NOSE_MP_LOG' not in os.environ: + raise Exception('Environment variable NOSE_MP_LOG is not set') + +logfile = os.environ['NOSE_MP_LOG'] def log(w): f = open(logfile, 'a') @@ -10,6 +14,9 @@ def log(w): f.close() #make sure all tests in this file are dispatched to the same subprocess def setup(): + '''global logfile + logfile = mktemp() + print "tempfile is:",logfile''' log('setup') def test_timeout(): diff --git a/functional_tests/test_multiprocessing/test_keyboardinterrupt.py b/functional_tests/test_multiprocessing/test_keyboardinterrupt.py index 7d26d10..03c0890 100644 --- a/functional_tests/test_multiprocessing/test_keyboardinterrupt.py +++ b/functional_tests/test_multiprocessing/test_keyboardinterrupt.py @@ -25,27 +25,31 @@ runner = os.path.join(support, 'fake_nosetest.py') def keyboardinterrupt(case): #os.setsid would create a process group so signals sent to the #parent process will propogates to all children processes - process = Popen([sys.executable,runner,os.path.join(support,case)], preexec_fn=os.setsid, stdout=PIPE, stderr=PIPE, bufsize=-1) + from tempfile import mktemp + logfile = mktemp() + process = Popen([sys.executable,runner,os.path.join(support,case),logfile], preexec_fn=os.setsid, stdout=PIPE, stderr=PIPE, bufsize=-1) sleep(0.5) os.killpg(process.pid, signal.SIGINT) - return process + return process, logfile -def get_log_content(stdout): - prefix = 'tempfile is: ' +def get_log_content(logfile): + '''prefix = 'tempfile is: ' if not stdout.startswith(prefix): raise Exception('stdout does not contain tmp file name: '+stdout) - logfile = stdout[len(prefix):].strip() #remove trailing new line char + logfile = stdout[len(prefix):].strip() #remove trailing new line char''' f = open(logfile) content = f.read() f.close() + os.remove(logfile) return content def test_keyboardinterrupt(): - process = keyboardinterrupt('keyboardinterrupt.py') + process, logfile = keyboardinterrupt('keyboardinterrupt.py') stdout, stderr = [s.decode('utf-8') for s in process.communicate(None)] - log = get_log_content(stdout) + print stderr + log = get_log_content(logfile) assert 'setup' in log assert 'test_timeout' in log assert 'test_timeout_finished' not in log @@ -58,11 +62,11 @@ def test_keyboardinterrupt(): def test_keyboardinterrupt_twice(): - process = keyboardinterrupt('keyboardinterrupt_twice.py') + process, logfile = keyboardinterrupt('keyboardinterrupt_twice.py') sleep(0.5) os.killpg(process.pid, signal.SIGINT) stdout, stderr = [s.decode('utf-8') for s in process.communicate(None)] - log = get_log_content(stdout) + log = get_log_content(logfile) assert 'setup' in log assert 'test_timeout' in log assert 'test_timeout_finished' not in log -- cgit v1.2.1 From 378f8dc82fea04ff2a5a7e6f45a0238981187daa Mon Sep 17 00:00:00 2001 From: Heng Liu Date: Fri, 4 Nov 2011 10:32:53 -0700 Subject: it seems the failure in the jenkins is related to speed of the test machine wait longer before sending in SIGINT signal --- functional_tests/test_multiprocessing/test_keyboardinterrupt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/functional_tests/test_multiprocessing/test_keyboardinterrupt.py b/functional_tests/test_multiprocessing/test_keyboardinterrupt.py index 03c0890..c70d601 100644 --- a/functional_tests/test_multiprocessing/test_keyboardinterrupt.py +++ b/functional_tests/test_multiprocessing/test_keyboardinterrupt.py @@ -29,7 +29,7 @@ def keyboardinterrupt(case): logfile = mktemp() process = Popen([sys.executable,runner,os.path.join(support,case),logfile], preexec_fn=os.setsid, stdout=PIPE, stderr=PIPE, bufsize=-1) - sleep(0.5) + sleep(1) os.killpg(process.pid, signal.SIGINT) return process, logfile -- cgit v1.2.1 From bf47a3e232c74ebee2b99bdbc44ea77e43ba7c3a Mon Sep 17 00:00:00 2001 From: kumar Date: Fri, 4 Nov 2011 14:23:19 -0500 Subject: Fixes relative imports and path to support dir --- functional_tests/test_multiprocessing/test_class.py | 4 ++-- functional_tests/test_multiprocessing/test_concurrent_shared.py | 5 +++-- functional_tests/test_multiprocessing/test_nameerror.py | 4 ++-- functional_tests/test_multiprocessing/test_process_timeout.py | 4 ++-- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/functional_tests/test_multiprocessing/test_class.py b/functional_tests/test_multiprocessing/test_class.py index 6f42ac3..d92710d 100644 --- a/functional_tests/test_multiprocessing/test_class.py +++ b/functional_tests/test_multiprocessing/test_class.py @@ -1,11 +1,11 @@ import os -from . import support, MPTestBase +from test_multiprocessing import MPTestBase #test case for #462 class TestClassFixture(MPTestBase): - suitepath = os.path.join(support, 'class.py') + suitepath = os.path.join(os.path.dirname(__file__), 'support', 'class.py') def runTest(self): assert str(self.output).strip().endswith('OK') diff --git a/functional_tests/test_multiprocessing/test_concurrent_shared.py b/functional_tests/test_multiprocessing/test_concurrent_shared.py index 9974bfd..2552c2b 100644 --- a/functional_tests/test_multiprocessing/test_concurrent_shared.py +++ b/functional_tests/test_multiprocessing/test_concurrent_shared.py @@ -1,10 +1,11 @@ import os -from . import support, MPTestBase +from test_multiprocessing import MPTestBase class TestConcurrentShared(MPTestBase): processes = 2 - suitepath = os.path.join(support, 'concurrent_shared') + suitepath = os.path.join(os.path.dirname(__file__), 'support', + 'concurrent_shared') def runTest(self): assert 'Ran 2 tests in 1.' in self.output, "make sure two tests use 1.x seconds (no more than 2 seconsd)" diff --git a/functional_tests/test_multiprocessing/test_nameerror.py b/functional_tests/test_multiprocessing/test_nameerror.py index 4837aca..5e58226 100644 --- a/functional_tests/test_multiprocessing/test_nameerror.py +++ b/functional_tests/test_multiprocessing/test_nameerror.py @@ -1,10 +1,10 @@ import os -from . import support, MPTestBase +from test_multiprocessing import support, MPTestBase class TestMPNameError(MPTestBase): processes = 2 - suitepath = os.path.join(support, 'nameerror.py') + suitepath = os.path.join(os.path.dirname(__file__), 'support', 'nameerror.py') def runTest(self): print str(self.output) diff --git a/functional_tests/test_multiprocessing/test_process_timeout.py b/functional_tests/test_multiprocessing/test_process_timeout.py index afef674..6b858f8 100644 --- a/functional_tests/test_multiprocessing/test_process_timeout.py +++ b/functional_tests/test_multiprocessing/test_process_timeout.py @@ -1,10 +1,10 @@ import os -from . import support, MPTestBase +from test_multiprocessing import MPTestBase class TestMPTimeout(MPTestBase): args = ['--process-timeout=1'] - suitepath = os.path.join(support, 'timeout.py') + suitepath = os.path.join(os.path.dirname(__file__), 'support', 'timeout.py') def runTest(self): assert "TimedOutException: 'timeout.test_timeout'" in self.output -- cgit v1.2.1 From 670eb171cc1aa89fdff87e1dbe8a5bcc79632c90 Mon Sep 17 00:00:00 2001 From: kumar Date: Fri, 4 Nov 2011 14:30:51 -0500 Subject: Fixes relative import for one more test --- functional_tests/test_multiprocessing/test_keyboardinterrupt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/functional_tests/test_multiprocessing/test_keyboardinterrupt.py b/functional_tests/test_multiprocessing/test_keyboardinterrupt.py index c70d601..296bd19 100644 --- a/functional_tests/test_multiprocessing/test_keyboardinterrupt.py +++ b/functional_tests/test_multiprocessing/test_keyboardinterrupt.py @@ -6,7 +6,7 @@ import signal import nose -from . import support +support = os.path.join(os.path.dirname(__file__), 'support') PYTHONPATH = os.environ['PYTHONPATH'] if 'PYTHONPATH' in os.environ else '' def setup(): -- cgit v1.2.1 From 8618a9498479c7949aef427d9a5a03b3b60abd85 Mon Sep 17 00:00:00 2001 From: Heng Liu Date: Fri, 4 Nov 2011 12:45:26 -0700 Subject: fix typo in coverage plugin --- nose/plugins/cover.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nose/plugins/cover.py b/nose/plugins/cover.py index 21545bb..a055c92 100644 --- a/nose/plugins/cover.py +++ b/nose/plugins/cover.py @@ -161,7 +161,7 @@ class Coverage(Plugin): self.coverXmlFile = None if options.cover_xml: self.coverXmlFile = options.cover_xml_file - log.debug('Will put XML coverage report in %s', self.coverHtmlFile) + log.debug('Will put XML coverage report in %s', self.coverXmlFile) if self.enabled: self.status['active'] = True -- cgit v1.2.1 From 0f3ff413655950a3de8507f41575aec025e4b234 Mon Sep 17 00:00:00 2001 From: Heng Liu Date: Fri, 4 Nov 2011 13:45:33 -0700 Subject: make test_keyboardinterrupt multiprocess plugin test more robust on slow machines (like the CI server) --- functional_tests/test_multiprocessing/test_keyboardinterrupt.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/functional_tests/test_multiprocessing/test_keyboardinterrupt.py b/functional_tests/test_multiprocessing/test_keyboardinterrupt.py index 296bd19..8f07e54 100644 --- a/functional_tests/test_multiprocessing/test_keyboardinterrupt.py +++ b/functional_tests/test_multiprocessing/test_keyboardinterrupt.py @@ -29,7 +29,13 @@ def keyboardinterrupt(case): logfile = mktemp() process = Popen([sys.executable,runner,os.path.join(support,case),logfile], preexec_fn=os.setsid, stdout=PIPE, stderr=PIPE, bufsize=-1) - sleep(1) + #wait until logfile is created: + retry=100 + while not os.path.exists(logfile): + sleep(0.1) + retry -= 1 + if not retry: + raise Exception('Timeout while waiting for log file to be created by fake_nosetest.py') os.killpg(process.pid, signal.SIGINT) return process, logfile -- cgit v1.2.1