> self.logger = ListLogger()
>
> old_errorlog = errorlog.globalErrorUtility
>
> === modified file 'lib/lp/testing/__init__.py'
> --- lib/lp/testing/__init__.py 2010-03-01 03:06:02 +0000
> +++ lib/lp/testing/__init__.py 2010-03-15 13:26:40 +0000
> @@ -29,6 +29,7 @@
> 'validate_mock_class',
> 'WindmillTestCase',
> 'with_anonymous_login',
> + 'ZopeTestInSubProcess',
> ]
>
> import copy
> @@ -38,6 +39,8 @@
> from pprint import pformat
> import shutil
> import subprocess
> +import subunit
> +import sys
> import tempfile
> import time
>
> @@ -62,6 +65,7 @@
> from zope.interface.verify import verifyClass, verifyObject
> from zope.security.proxy import (
> isinstance as zope_isinstance, removeSecurityProxy)
> +from zope.testing.testrunner.runner import TestResult as ZopeTestResult
>
> from canonical.launchpad.webapp import errorlog
> from canonical.config import config
> @@ -586,6 +590,57 @@
> self.client.open(url=u'http://launchpad.dev:8085')
>
>
> +class ZopeTestInSubProcess:
> + """Run tests in a sub-process, respecting Zope idiosyncrasies.
> +
> + Use this as a mixin with an interesting `TestCase` to isolate
> + tests with side-effects. Each and every test *method* in the test
> + case is run in a new, forked, sub-process. This will slow down
> + your tests, so use it sparingly. However, when you need to, for
> + example, start the Twisted reactor (which cannot currently be
> + safely stopped and restarted in process) it is invaluable.
> +
For the record, vast swathes of code have been written for Twisted that don't
require testing starting the reactor. Generally, starting and stopping the
reactor in application code is a bad pattern.
If we're going to go down that path, then I think having a mixin like this is
a good idea.
> + This is basically a reimplementation of subunit's
> + `IsolatedTestCase` or `IsolatedTestSuite`, but adjusted to work
> + with Zope. In particular, Zope's TestResult object is responsible
> + for calling testSetUp() and testTearDown() on the selected layer.
> + """
> +
> + def run(self, result):
> + assert isinstance(result, ZopeTestResult), (
> + "result must be a Zope result object, not %r." % (result,))
Why? Please add a comment explaining the case.
> + pread, pwrite = os.pipe()
> + pid = os.fork()
> + if pid == 0:
> + # Child.
> + os.close(pread)
> + fdwrite = os.fdopen(pwrite, 'w', 1)
> + # Send results to both the Zope result object (so that
> + # layer setup and teardown are done properly, etc.) and to
> + # the subunit stream client so that the parent process can
> + # obtain the result.
> + result = testtools.MultiTestResult(
> + result, subunit.TestProtocolClient(fdwrite))
> + super(ZopeTestInSubProcess, self).run(result)
> + fdwrite.flush()
> + sys.stdout.flush()
> + sys.stderr.flush()
> + # Exit hard.
> + os._exit(0)
Personally, I don't need a comment to explain what os._exit(0) does. It's more
interesting to know why we want to exit hard.
> + else:
> + # Parent.
> + os.close(pwrite)
> + fdread = os.fdopen(pread, 'rU')
> + # Accept the result from the child process. Skip all the
> + # Zope-specific result stuff by passing a super() of the
> + # result.
This comment would be more helpful if it explained why we want to skip the
zope-specific result stuff.
On Mon, Mar 15, 2010 at 1:30 PM, Gavin Panella services/ job/runner. py' services/ job/runner. py 2010-02-25 12:07:15 +0000 services/ job/runner. py 2010-03-15 13:26:40 +0000 LaunchpadCronSc ript): class=JobRunner ): class=JobRunner , test_args=None): name).dbuser ript, self)._ _init__ (self.config_ name, self.dbuser) ript, self).__init__(
<email address hidden> wrote:
> Latest incremental diff, r10440..
>
> === modified file 'lib/lp/
> --- lib/lp/
> +++ lib/lp/
> @@ -401,9 +401,10 @@
> class JobCronScript(
> """Base class for scripts that run jobs."""
>
> - def __init__(self, runner_
> + def __init__(self, runner_
> self.dbuser = getattr(config, self.config_
> - super(JobCronSc
> + super(JobCronSc
> + self.config_name, self.dbuser, test_args)
> self.runner_class = runner_class
>
> def main(self):
>
What's the motivation for this change?
> === modified file 'lib/lp/ services/ job/tests/ test_runner. py' services/ job/tests/ test_runner. py 2010-02-24 19:52:01 +0000 services/ job/tests/ test_runner. py 2010-03-15 13:26:40 +0000 ssLayer interfaces import IErrorReporting Utility launchpad. webapp import errorlog launchpad. webapp. interfaces import ( ssLayer interfaces. branchmergeprop osal import ( iffJobSource, ) mail_helpers import pop_notifications job.runner import ( iffJobSource) job.interfaces. job import JobStatus, IRunnableJob job.model. job import Job launchpad. webapp import errorlog launchpad. webapp. interfaces import ( job.runner import ( tory, ZopeTestInSubPr ocess mail_helpers import pop_notifications BaseRunnableJob ): append( input) unner(TestCaseW ithFactory) : unner(ZopeTestI nSubProcess, TestCaseWithFac tory): ssLayer pt(TestCaseWith Factory) : pt(ZopeTestInSu bProcess, TestCaseWithFac tory): ssLayer iffJobSource riptSubclass, self)._ _init__ (DummyRunner) riptSubclass, self).__init__(
> --- lib/lp/
> +++ lib/lp/
> @@ -11,23 +11,24 @@
> from unittest import TestLoader
>
> import transaction
> -from canonical.testing import LaunchpadZopele
> +
> from zope.component import getUtility
> from zope.error.
> from zope.interface import implements
>
> +from canonical.
> +from canonical.
> + DEFAULT_FLAVOR, IStoreSelector, MAIN_STORE)
> +from canonical.testing import LaunchpadZopele
> +
> from lp.code.
> - IUpdatePreviewD
> -from lp.testing.
> -from lp.services.
> - JobCronScript, JobRunner, BaseRunnableJob, TwistedJobRunner
> -)
> + IUpdatePreviewD
> from lp.services.
> from lp.services.
> -from lp.testing import TestCaseWithFactory
> -from canonical.
> -from canonical.
> - IStoreSelector, MAIN_STORE, DEFAULT_FLAVOR)
> +from lp.services.
> + BaseRunnableJob, JobCronScript, JobRunner, TwistedJobRunner)
> +from lp.testing import TestCaseWithFac
> +from lp.testing.
>
>
> class NullJob(
> @@ -300,7 +301,7 @@
> self.entries.
>
>
> -class TestTwistedJobR
> +class TestTwistedJobR
>
> layer = LaunchpadZopele
>
> @@ -325,7 +326,7 @@
> self.assertIn('Job ran too long.', oops.value)
>
>
> -class TestJobCronScri
> +class TestJobCronScri
>
> layer = LaunchpadZopele
>
> @@ -351,7 +352,8 @@
> source_interface = IUpdatePreviewD
>
> def __init__(self):
> - super(JobCronSc
> + super(JobCronSc
> + DummyRunner, test_args=[])
How's this related to the Zope isolation change?
> self.logger = ListLogger() globalErrorUtil ity testing/ __init_ _.py' testing/ __init_ _.py 2010-03-01 03:06:02 +0000 testing/ __init_ _.py 2010-03-15 13:26:40 +0000 mock_class' , _login' , rocess' , verify import verifyClass, verifyObject roxy) testrunner. runner import TestResult as ZopeTestResult launchpad. webapp import errorlog open(url= u'http:// launchpad. dev:8085') ocess:
>
> old_errorlog = errorlog.
>
> === modified file 'lib/lp/
> --- lib/lp/
> +++ lib/lp/
> @@ -29,6 +29,7 @@
> 'validate_
> 'WindmillTestCase',
> 'with_anonymous
> + 'ZopeTestInSubP
> ]
>
> import copy
> @@ -38,6 +39,8 @@
> from pprint import pformat
> import shutil
> import subprocess
> +import subunit
> +import sys
> import tempfile
> import time
>
> @@ -62,6 +65,7 @@
> from zope.interface.
> from zope.security.proxy import (
> isinstance as zope_isinstance, removeSecurityP
> +from zope.testing.
>
> from canonical.
> from canonical.config import config
> @@ -586,6 +590,57 @@
> self.client.
>
>
> +class ZopeTestInSubPr
> + """Run tests in a sub-process, respecting Zope idiosyncrasies.
> +
> + Use this as a mixin with an interesting `TestCase` to isolate
> + tests with side-effects. Each and every test *method* in the test
> + case is run in a new, forked, sub-process. This will slow down
> + your tests, so use it sparingly. However, when you need to, for
> + example, start the Twisted reactor (which cannot currently be
> + safely stopped and restarted in process) it is invaluable.
> +
For the record, vast swathes of code have been written for Twisted that don't
require testing starting the reactor. Generally, starting and stopping the
reactor in application code is a bad pattern.
If we're going to go down that path, then I think having a mixin like this is
a good idea.
> + This is basically a reimplementation of subunit's ite`, but adjusted to work
> + `IsolatedTestCase` or `IsolatedTestSu
> + with Zope. In particular, Zope's TestResult object is responsible
> + for calling testSetUp() and testTearDown() on the selected layer.
> + """
> +
> + def run(self, result):
> + assert isinstance(result, ZopeTestResult), (
> + "result must be a Zope result object, not %r." % (result,))
Why? Please add a comment explaining the case.
> + pread, pwrite = os.pipe() MultiTestResult ( TestProtocolCli ent(fdwrite) ) nSubProcess, self).run(result)
> + pid = os.fork()
> + if pid == 0:
> + # Child.
> + os.close(pread)
> + fdwrite = os.fdopen(pwrite, 'w', 1)
> + # Send results to both the Zope result object (so that
> + # layer setup and teardown are done properly, etc.) and to
> + # the subunit stream client so that the parent process can
> + # obtain the result.
> + result = testtools.
> + result, subunit.
> + super(ZopeTestI
> + fdwrite.flush()
> + sys.stdout.flush()
> + sys.stderr.flush()
> + # Exit hard.
> + os._exit(0)
Personally, I don't need a comment to explain what os._exit(0) does. It's more
interesting to know why we want to exit hard.
> + else:
> + # Parent.
> + os.close(pwrite)
> + fdread = os.fdopen(pread, 'rU')
> + # Accept the result from the child process. Skip all the
> + # Zope-specific result stuff by passing a super() of the
> + # result.
This comment would be more helpful if it explained why we want to skip the
zope-specific result stuff.
> + result = super(ZopeTestR esult, result) TestProtocolSer ver(result) readFrom( fdread)
> + protocol = subunit.
> + protocol.
> + fdread.close()
> + os.waitpid(pid, 0)
Thanks for persevering. Not long to go now :)
jml