Commits

Kumar McMillan  committed ccae89c Merge

Merged stable based branch into tip after pulling from un-stable

  • Participants
  • Parent commits 5aaf47b, 7156749

Comments (0)

Files changed (15)

File functional_tests/doc_tests/test_multiprocess/multiprocess.rst

 First we have to reset all of the test modules.
 
     >>> import sys
-    >>> sys.modules['test_shared'].called[:] = []
     >>> sys.modules['test_not_shared'].called[:] = []
     >>> sys.modules['test_can_split'].called[:] = []
 
 We have to reset all of the test modules again.
 
     >>> import sys
-    >>> sys.modules['test_shared'].called[:] = []
     >>> sys.modules['test_not_shared'].called[:] = []
     >>> sys.modules['test_can_split'].called[:] = []
 
   use subprocess to launch another copy of nose that also uses the
   multiprocess plugin. This is why this test is skipped under python 2.6 when
   run with the ``--processes`` switch.
-

File functional_tests/doc_tests/test_multiprocess/support/test_shared.py

+import os
 import sys
-called = []
+
+here = os.path.dirname(__file__)
+flag = os.path.join(here, 'shared_flag')
 
 _multiprocess_shared_ = 1
 
+def _log(val):
+    ff = open(flag, 'a+')
+    ff.write(val)
+    ff.write("\n")
+    ff.close()
+
+
+def _clear():
+    if os.path.isfile(flag):
+        os.unlink(flag)
+
+        
+def logged():
+    return [line for line in open(flag, 'r')]
+
+
 def setup():
     print >> sys.stderr, "setup called"
-    called.append('setup')
+    _log('setup')
 
 
 def teardown():
     print >> sys.stderr, "teardown called"
-    called.append('teardown')
+    _clear()
 
-
+    
 def test_a():
-    assert len(called) == 1, "len(%s) !=1" % called
+    assert len(logged()) == 1, "len(%s) !=1" % called
 
 
 def test_b():
-    assert len(called) == 1, "len(%s) !=1" % called
+    assert len(logged()) == 1, "len(%s) !=1" % called
 
 
 class TestMe:

File nose/__init__.py

 from nose.tools import with_setup
 
 __author__ = 'Jason Pellerin'
-__versioninfo__ = (0, 11, 1)
+__versioninfo__ = (0, 11, 2)
 __version__ = '.'.join(map(str, __versioninfo__))
 
 __all__ = [

File nose/config.py

         self.traverseNamespace = False
         self.firstPackageWins = False
         self.parserClass = OptionParser
+        self.worker = False
         
         self._default = self.__dict__.copy()
         self.update(kw)
         self._orig = self.__dict__.copy()
 
+    def __getstate__(self):
+        state = self.__dict__.copy()
+        del state['stream']
+        del state['_orig']
+        del state['_default']
+        del state['env']
+        del state['logStream']
+        # FIXME remove plugins, have only plugin manager class
+        state['plugins'] = self.plugins.__class__
+        return state
+
+    def __setstate__(self, state):
+        plugincls = state.pop('plugins')
+        self.update(state)
+        self.worker = True
+        # FIXME won't work for static plugin lists
+        self.plugins = plugincls()
+        self.plugins.loadPlugins()
+        # needed so .can_configure gets set appropriately
+        dummy_parser = self.parserClass()
+        self.plugins.addOptions(dummy_parser, {})
+        self.plugins.configure(self.options, self)
+    
     def __repr__(self):
         d = self.__dict__.copy()
         # don't expose env, could include sensitive info
-        d['env'] = {} 
+        d['env'] = {}
         keys = [ k for k in d.keys()
                  if not k.startswith('_') ]
         keys.sort()
 class NoOptions(object):
     """Options container that returns None for all options.
     """
+    def __getstate__(self):
+        return {}
+    
     def __getattr__(self, attr):
         return None
 

File nose/plugins/base.py

 
     def options(self, parser, env):
         """Register commandline options.
-        
+
         Implement this method for normal options behavior with protection from
         OptionConflictErrors. If you override this method and want the default
         --with-$name option to be registered, be sure to call super().

File nose/plugins/cover.py

         except KeyError:
             pass
         Plugin.configure(self, options, config)
+        if config.worker:
+            return
         if self.enabled:
             try:
                 import coverage

File nose/plugins/doctests.py

         self.extension = tolist(options.doctestExtension)
         self.fixtures = options.doctestFixtures
         self.finder = doctest.DocTestFinder()
-
+        
     def prepareTestLoader(self, loader):
         """Capture loader's suiteClass.
 
                 log.debug("Fixture module %s resolved to %s",
                           fixt_mod, fixture_context)
                 if hasattr(fixture_context, 'globs'):
-                    globs = fixture_context.globs(globs)
+                    globs = fixture_context.globs(globs)                    
             parser = doctest.DocTestParser()
             test = parser.get_doctest(
                 doc, globs=globs, name=name,

File nose/plugins/logcapture.py

 
 import logging
 from logging.handlers import BufferingHandler
+import threading
 
 from nose.plugins.base import Plugin
 from nose.util import ln, safe_str
             if rname == name or rname.startswith(name+'.'):
                 matched = True
         return matched
-
-
+    def __getstate__(self):
+        state = self.__dict__.copy()
+        del state['lock']
+        return state
+    def __setstate__(self, state):
+        self.__dict__.update(state)
+        self.lock = threading.RLock()
+        
 class LogCapture(Plugin):
     """
     Log capture plugin. Enabled by default. Disable with --nologcapture.

File nose/plugins/manager.py

 import os
 import sys
 from warnings import warn
+import nose.config
 from nose.failure import Failure
 from nose.plugins.base import IPluginInterface
 
+try:
+    import cPickle as pickle
+except:
+    import pickle
+try:
+    from cStringIO import StringIO
+except:
+    from StringIO import StringIO
+
+    
 __all__ = ['DefaultPluginManager', 'PluginManager', 'EntryPointPluginManager',
            'BuiltinPluginManager', 'RestrictedPluginManager']
 
         self.plugins = []
         for p in plugins:
             self.addPlugin(p, call)
-    
+
     def __call__(self, *arg, **kw):
         return self.call(*arg, **kw)
-    
+
     def addPlugin(self, plugin, call):
         """Add plugin to my list of plugins to call, if it has the attribute
         I'm bound to.
             return self.chain
         else:
             # return a value from the first plugin that returns non-None
-            return self.simple        
-            
+            return self.simple
+
     def chain(self, *arg, **kw):
         """Call plugins in a chain, where the result of each plugin call is
         sent to the next plugin as input. The final output result is returned.
     call.
     """
     proxyClass = PluginProxy
-    
+
     def __init__(self, plugins=(), proxyClass=None):
         self._plugins = []
         self._proxies = {}
             self.addPlugins(plugins)
         if proxyClass is not None:
             self.proxyClass = proxyClass
-        
+
     def __getattr__(self, call):
         try:
             return self._proxies[call]
 
     def options(self, parser, env=os.environ):
         self.plugin.add_options(parser, env)
-    
+
     def addError(self, test, err):
         if not hasattr(self.plugin, 'addError'):
             return
         elif issubclass(ec, DeprecatedTest):
             if not hasattr(self.plugin, 'addDeprecated'):
                 return
-            return self.plugin.addDeprecated(test.test)           
+            return self.plugin.addDeprecated(test.test)
         # add capt
         capt = test.capturedOutput
         return self.plugin.addError(test.test, err, capt)

File nose/plugins/multiprocess.py

 import time
 import traceback
 import unittest
+import pickle
 import nose.case
 from nose.core import TextTestRunner
 from nose import failure
 
 def _import_mp():
     global Process, Queue, Pool, Event
-    if sys.platform == 'win32':
-        warn("multiprocess plugin is not available on windows",
-             RuntimeWarning)
-        return
     try:
         from multiprocessing import Process as Process_, \
             Queue as Queue_, Pool as Pool_, Event as Event_
         warn("multiprocessing module is not available, multiprocess plugin "
              "cannot be used", RuntimeWarning)
 
-        
+
 class TestLet:
     def __init__(self, case):
         try:
     """
     score = 1000
     status = {}
-    
+
     def options(self, parser, env):
         """
         Register command-line options.
         """
-        if sys.platform == 'win32':
-            return
         parser.add_option("--processes", action="store",
                           default=env.get('NOSE_PROCESSES', 0),
                           dest="multiprocess_workers",
         """
         Configure plugin.
         """
-        if sys.platform == 'win32':
-            return
         try:
             self.status.pop('active')
         except KeyError:
         if not hasattr(options, 'multiprocess_workers'):
             self.enabled = False
             return
+        # don't start inside of a worker process
+        if config.worker:
+            return
         self.config = config
         try:
             workers = int(options.multiprocess_workers)
             self.config.multiprocess_workers = workers
             self.config.multiprocess_timeout = int(options.multiprocess_timeout)
             self.status['active'] = True
-            
+
     def prepareTestLoader(self, loader):
         """Remember loader class so MultiProcessTestRunner can instantiate
         the right loader.
                                              shouldStop,
                                              self.loaderClass,
                                              result.__class__,
-                                             self.config))
+                                             pickle.dumps(self.config)))
             # p.setDaemon(True)
             p.start()
             workers.append(p)
             or not getattr(test, 'can_split', True)
             or not isinstance(test, unittest.TestSuite)):
             # regular test case, or a suite with context fixtures
-            
+
             # special case: when run like nosetests path/to/module.py
             # the top-level suite has only one item, and it shares
             # the same context as that item. In that case, we want the
 
 def runner(ix, testQueue, resultQueue, shouldStop,
            loaderClass, resultClass, config):
+    config = pickle.loads(config)
+    config.plugins.begin()
     log.debug("Worker %s executing", ix)
+    log.debug("Active plugins worker %s: %s", ix, config.plugins._plugins)
     loader = loaderClass(config=config)
     loader.suiteClass.suiteClass = NoSharedFixtureContextSuite
-
+    
     def get():
         case = testQueue.get(timeout=config.multiprocess_timeout)
         return case

File nose/plugins/prof.py

     def available(cls):
         return hotshot is not None
     available = classmethod(available)
-        
+
     def begin(self):
         """Create profile stats file and load profiler.
         """
         self.fileno = None
         self.sort = options.profile_sort
         self.restrict = tolist(options.profile_restrict)
-            
+
     def prepareTest(self, test):
         """Wrap entire test run in :func:`prof.runcall`.
         """
             self._create_pfile()
             prof.runcall(test, result)
         return run_and_profile
-        
+
     def report(self, stream):
         """Output profiler report.
         """

File nose/plugins/xunit.py

 
 """This plugin provides test results in the standard XUnit XML format.
 
-It was designed for the `Hudson`_ continuous build system but will 
+It was designed for the `Hudson`_ continuous build system but will
 probably work for anything else that understands an XUnit-formatted XML
 representation of test results.
 
 Add this shell command to your builder ::
-    
+
     nosetests --with-xunit
 
-And by default a file named nosetests.xml will be written to the 
-working directory.  
+And by default a file named nosetests.xml will be written to the
+working directory.
 
 In a Hudson builder, tick the box named "Publish JUnit test result report"
 under the Post-build Actions and enter this value for Test report XMLs::
-    
+
     **/nosetests.xml
 
-If you need to change the name or location of the file, you can set the 
+If you need to change the name or location of the file, you can set the
 ``--xunit-file`` option.
 
 Here is an abbreviated version of what an XML test report might look like::
-    
+
     <?xml version="1.0" encoding="UTF-8"?>
     <testsuite name="nosetests" tests="1" errors="1" failures="0" skip="0">
-        <testcase classname="path_to_test_suite.TestSomething" 
+        <testcase classname="path_to_test_suite.TestSomething"
                   name="path_to_test_suite.TestSomething.test_it" time="0">
             <error type="exceptions.TypeError">
             Traceback (most recent call last):
-            ...            
+            ...
             TypeError: oops, wrong type
             </error>
         </testcase>
 
 def nice_classname(obj):
     """Returns a nice name for class object or class instance.
-    
+
         >>> nice_classname(Exception()) # doctest: +ELLIPSIS
         '...Exception'
         >>> nice_classname(Exception)
         'exceptions.Exception'
-    
+
     """
     if inspect.isclass(obj):
         cls_name = obj.__name__
     name = 'xunit'
     score = 2000
     encoding = 'UTF-8'
-    
+    error_report_file = None
+
     def _timeTaken(self):
         if hasattr(self, '_timer'):
             taken = time() - self._timer
             # due to custom TestResult munging
             taken = 0.0
         return taken
-    
+
     def _xmlsafe(self, s):
         return xmlsafe(s, encoding=self.encoding)
-    
+
     def options(self, parser, env):
         """Sets additional command line options."""
         Plugin.options(self, parser, env)
         """Add error output to Xunit report.
         """
         taken = self._timeTaken()
-            
+
         if issubclass(err[0], SkipTest):
             self.stats['skipped'] +=1
             return
              'tb': self._xmlsafe(tb),
              'taken': taken,
              })
-        
+
     def addSuccess(self, test, capt=None):
         """Add success output to Xunit report.
         """
         taken = self._timeTaken()
-            
         self.stats['passes'] += 1
         id = test.id()
         self.errorlist.append(
              'name': self._xmlsafe(id),
              'taken': taken,
              })
-
-    
         },
         test_suite = 'nose.collector',
         )
+
+    # This is required by multiprocess plugin; on Windows, if
+    # the launch script is not import-safe, spawned processes
+    # will re-run it, resulting in an infinite loop.
+    if sys.platform == 'win32':
+        import re
+        from setuptools.command.easy_install import easy_install
+
+        def wrap_write_script(self, script_name, contents, *arg, **kwarg):
+            bad_text = re.compile(
+                "\n"
+                "sys.exit\(\n"
+                "   load_entry_point\(([^\)]+)\)\(\)\n"
+                "\)\n")
+            good_text = (
+                "\n"
+                "if __name__ == '__main__':\n"
+                "    sys.exit(\n"
+                r"        load_entry_point(\1)()\n"
+                "    )\n"
+                )
+            contents = bad_text.sub(good_text, contents)
+            return self._write_script(script_name, contents, *arg, **kwarg)
+        easy_install._write_script = easy_install.write_script
+        easy_install.write_script = wrap_write_script
+    
 except ImportError:
     from distutils.core import setup
     addl_args = dict(
         ],
     **addl_args
     )
+
+            

File unit_tests/test_config.py

 import os
 import tempfile
 import unittest
+import warnings
+import pickle
+
 import nose.config
-import warnings
-
+from nose.plugins.manager import DefaultPluginManager
 
 
 class TestNoseConfig(unittest.TestCase):
         c.configure(['-v', 'mytests'])
         self.assertEqual(c.verbosity, 1)
 
+    def test_pickle_empty(self):
+        c = nose.config.Config()
+        cp = pickle.dumps(c)
+        cc = pickle.loads(cp)
+
+    def test_pickle_configured(self):
+        c = nose.config.Config(plugins=DefaultPluginManager())
+        c.configure(['--with-doctest', '--with-coverage', '--with-profile',
+                     '--with-id', '--attr=A', '--collect', '--all',
+                     '--with-isolation', '-d', '--with-xunit', '--processes=2',
+                     '--pdb'])
+        cp = pickle.dumps(c)
+        cc = pickle.loads(cp)
+        assert cc.plugins._plugins
+
+
 if __name__ == '__main__':
     unittest.main()

File unit_tests/test_multiprocess.py

+import pickle
+import sys
+import unittest
+
+from nose import case
+from nose.plugins import multiprocess
+from nose.plugins.skip import SkipTest
+from nose.config import Config
+from nose.loader import TestLoader
+
+
+class ArgChecker:
+    def __init__(self, target, args):
+        self.target = target
+        self.args = args
+        # skip the id and queues
+        pargs = args[4:]
+        self.pickled = pickle.dumps(pargs)
+    def start(self):
+        pass
+    def is_alive(self):
+        return False
+
+        
+def setup(mod):
+    multiprocess._import_mp()
+    if not multiprocess.Process:
+        raise SkipTest("multiprocessing not available")
+    mod.Process = multiprocess.Process
+    multiprocess.Process = ArgChecker
+        
+
+class T(unittest.TestCase):
+    __test__ = False
+    def runTest(self):
+        pass
+
+    
+def test_mp_process_args_pickleable():
+    test = case.Test(T('runTest'))
+    config = Config()
+    config.multiprocess_workers = 2
+    config.multiprocess_timeout = 0.1
+    runner = multiprocess.MultiProcessTestRunner(
+        stream=unittest._WritelnDecorator(sys.stdout),
+        verbosity=2,
+        loaderClass=TestLoader,
+        config=config)
+    runner.run(test)
+