because sys.argv contains stuff that LaunchpadScript tries to digest. I
have to override that, and that's what test_args does.
>
> > 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.
In the case of the job runner, it's because Twisted is being shoe-horned
into an API that was designed before Twisted was introduced. In
checkwatches, it's like that because it's a transitionary API, almost a
learning point. Also, our scripts are expected to inherit from
LaunchpadScript (or, at least, that's the legacy we're working with),
not something suitable for twistd.
Is it such a bad pattern to start and stop the reactor? Other than it
doesn't work reliably. If it did work perfectly, what would be the
problem?
>
> If we're going to go down that path, then I think having a mixin like this is
> a good idea.
Okay, cool. I don't think we necessarily need to go down that path for
everything, or that we can't back up again if it's wrong. It's just
useful to be able to shoot something off into a sub-process when it's
badly behaved, or needs a lot of tear-down which a sub-process can do
for us more cheaply.
>
> > + 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.
It's because it does super(ZopeTestResult, result) later on. I could
change it to not care, then later do:
if isinstance(result, ZopeTestResult):
# Skip all the Zope-specific result stuff
# by passing a super() of the result.
result = super(ZopeTestResult, result)
But, for non-Zope test results, this could cause confusion (duplicated
stdout/stderr reporting, for example).
For example, the Zope test result object is responsible for calling
testSetUp() and testTearDown(), and for handling post-mortem
debugging. If another test result object did something similar, it would
make no sense to do it in the parent process.
>
> > + 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.
I copied this from subunit. I guess it's to avoid running onexit
handlers, but perhaps there are other reasons. Ah, maybe SystemExit is
caught and just logged as a test failure.
>
> > + 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.
Done.
>
> > + result = super(ZopeTestResult, result)
> > + protocol = subunit.TestProtocolServer(result)
> > + protocol.readFrom(fdread)
> > + fdread.close()
> > + os.waitpid(pid, 0)
>
> Thanks for persevering. Not long to go now :)
> 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?
It makes testing easier. I'll explain more below.
> 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__(
> > === modified file 'lib/lp/
> > --- 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?
It made this code easier to test. Without it, I can only test it (afaik)
like so:
bin/test test_runner
I cannot, for example, do:
bin/test -t 'test_( runner| checkwatches) |externalbugtra cker.txt'
because sys.argv contains stuff that LaunchpadScript tries to digest. I
have to override that, and that's what test_args does.
> 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:
> > self.logger = ListLogger()
> >
> > 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.
In the case of the job runner, it's because Twisted is being shoe-horned
into an API that was designed before Twisted was introduced. In
checkwatches, it's like that because it's a transitionary API, almost a
learning point. Also, our scripts are expected to inherit from
LaunchpadScript (or, at least, that's the legacy we're working with),
not something suitable for twistd.
Is it such a bad pattern to start and stop the reactor? Other than it
doesn't work reliably. If it did work perfectly, what would be the
problem?
>
> If we're going to go down that path, then I think having a mixin like this is
> a good idea.
Okay, cool. I don't think we necessarily need to go down that path for
everything, or that we can't back up again if it's wrong. It's just
useful to be able to shoot something off into a sub-process when it's
badly behaved, or needs a lot of tear-down which a sub-process can do
for us more cheaply.
> ite`, but adjusted to work
> > + This is basically a reimplementation of subunit's
> > + `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.
It's because it does super(ZopeTestR esult, result) later on. I could
change it to not care, then later do:
if isinstance(result, ZopeTestResult): esult, result)
# Skip all the Zope-specific result stuff
# by passing a super() of the result.
result = super(ZopeTestR
But, for non-Zope test results, this could cause confusion (duplicated
stdout/stderr reporting, for example).
For example, the Zope test result object is responsible for calling
testSetUp() and testTearDown(), and for handling post-mortem
debugging. If another test result object did something similar, it would
make no sense to do it in the parent process.
> MultiTestResult ( TestProtocolCli ent(fdwrite) ) nSubProcess, self).run(result)
> > + 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.
> > + 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.
I copied this from subunit. I guess it's to avoid running onexit
handlers, but perhaps there are other reasons. Ah, maybe SystemExit is
caught and just logged as a test failure.
>
> > + 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.
Done.
> esult, result) TestProtocolSer ver(result) readFrom( fdread)
> > + result = super(ZopeTestR
> > + protocol = subunit.
> > + protocol.
> > + fdread.close()
> > + os.waitpid(pid, 0)
>
> Thanks for persevering. Not long to go now :)
Thanks for the review :)