Merge lp:~allenap/launchpad/isolate-tests into lp:launchpad
- isolate-tests
- Merge into devel
Status: | Merged |
---|---|
Approved by: | Gavin Panella |
Approved revision: | no longer in the source branch. |
Merged at revision: | not available |
Proposed branch: | lp:~allenap/launchpad/isolate-tests |
Merge into: | lp:launchpad |
Diff against target: |
319 lines (+217/-15) 4 files modified
lib/lp/services/job/runner.py (+3/-2) lib/lp/services/job/tests/test_runner.py (+15/-13) lib/lp/testing/__init__.py (+68/-0) lib/lp/testing/tests/test_zope_test_in_subprocess.py (+131/-0) |
To merge this branch: | bzr merge lp:~allenap/launchpad/isolate-tests |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Jonathan Lange (community) | Approve | ||
Björn Tillenius (community) | Abstain | ||
Review via email: mp+20916@code.launchpad.net |
Commit message
New class, ZopeTestInSubPr
Description of the change
Implement a new class - ZopeIsolatedTes
This branch also uses ZopeIsolatedTes
Gavin Panella (allenap) wrote : | # |
> On Mon, Mar 08, 2010 at 06:14:32PM -0000, Gavin Panella wrote:
> > Gavin Panella has proposed merging lp:~allenap/launchpad/isolate-tests into
> lp:launchpad/devel.
> >
> > Requested reviews:
> > Canonical Launchpad Engineering (launchpad): code
> > Related bugs:
> > #491870 Use Twisted's thread support instead of the threading module in
> checkwatches
> > https:/
> >
> >
> > Implement a new class - ZopeIsolatedTes
> > mixin with more interesting TestCase subclasses. It runs each test
> > method in a forked sub-process, passing the results back to the parent
> > process with subunit. This is needed to safely run tests that need to,
> > for example, start the Twisted reactor, which cannot be safely restarted
> > in the same process once stopped.
> >
> > This branch also uses ZopeIsolatedTes
> > tests. Additionally, one of these tests, TestJobCronScript, has been
> > fixed to override the command line arguments passed to the script class
> > (otherwise sys.argv is used, which is stuffed with arguments passed to
> > bin/test).
>
> vote needsinfo
>
> If the job runner tests ran fine previously, why is this
> ZopeIsolatedTes
Jono once told me - and an IRC discussion between Aaron, Jono and
myself yesterday reconfirmed this - that reactor restarting is not
supported, and is considered by Twisted developers to not be a working
feature right now. They want it to work, but it's not high on the
list. Jono suggested subunit.
Apart from that, I discovered than one of my new tests for
checkwatches ran fine on its own, or when run alongside many other
tests, but hung indefinitely when run in the same process as the job
runner tests.
> > === modified file 'lib/lp/
> > --- lib/lp/
> > +++ lib/lp/
>
> > @@ -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
>
> Will we ever use ZopeIsolatedTes
> TestCaseWithFac
Yes, possibly. I've already made some changes to this branch since the
merge proposal went out, one of which was to not inherit from
anything. The reason is that it can be mixed in with TestSuite (and
its subclasses) too, without alteration, to provide process isolation
for a whole suite. I've updated the docs to explain the behaviour.
Full diff of changes since the proposal:
http://
> > === modified file 'lib/lp/
> > --- lib/lp/
> > +++ lib/lp/
> > @@ -586,6 ...
Jonathan Lange (jml) wrote : | # |
2010/3/9 Gavin Panella <email address hidden>:
>> On Mon, Mar 08, 2010 at 06:14:32PM -0000, Gavin Panella wrote:
>> > Gavin Panella has proposed merging lp:~allenap/launchpad/isolate-tests into
>> lp:launchpad/devel.
>> >
>> > Requested reviews:
>> > Canonical Launchpad Engineering (launchpad): code
>> > Related bugs:
>> > #491870 Use Twisted's thread support instead of the threading module in
>> checkwatches
>> > https:/
>> >
>> >
>> > Implement a new class - ZopeIsolatedTes
>> > mixin with more interesting TestCase subclasses. It runs each test
>> > method in a forked sub-process, passing the results back to the parent
>> > process with subunit. This is needed to safely run tests that need to,
>> > for example, start the Twisted reactor, which cannot be safely restarted
>> > in the same process once stopped.
>> >
>> > This branch also uses ZopeIsolatedTes
>> > tests. Additionally, one of these tests, TestJobCronScript, has been
>> > fixed to override the command line arguments passed to the script class
>> > (otherwise sys.argv is used, which is stuffed with arguments passed to
>> > bin/test).
>>
>> vote needsinfo
>>
>> If the job runner tests ran fine previously, why is this
>> ZopeIsolatedTes
>
> Jono once told me - and an IRC discussion between Aaron, Jono and
> myself yesterday reconfirmed this - that reactor restarting is not
> supported, and is considered by Twisted developers to not be a working
> feature right now. They want it to work, but it's not high on the
> list. Jono suggested subunit.
>
To be clear, this kind of solution (run tests in their own
subprocesses) is hardly ever used within the Twisted world, since it's
considered good practice there for asynchronous APIs to return
Deferreds, and to test things at that level. It's extremely rare to
have application code that needs to start and stop the reactor.
jml
Björn Tillenius (bjornt) wrote : | # |
On Tue, Mar 09, 2010 at 04:23:44PM -0000, Gavin Panella wrote:
> > > -class TestJobCronScri
> > > +class TestJobCronScri
> >
> > Will we ever use ZopeIsolatedTes
> > TestCaseWithFac
>
> Yes, possibly. I've already made some changes to this branch since the
> merge proposal went out, one of which was to not inherit from
> anything. The reason is that it can be mixed in with TestSuite (and
> its subclasses) too, without alteration, to provide process isolation
> for a whole suite. I've updated the docs to explain the behaviour.
>
> Full diff of changes since the proposal:
> http://
In the future, could you please attach the changes instead? Makes it
easier to review and comment.
>
> > > === modified file 'lib/lp/
> > > --- lib/lp/
> > > +++ lib/lp/
> > > @@ -586,6 +591,56 @@
> > > self.client.
> > >
> > >
> > > +class ZopeIsolatedTes
> > > + """Run tests in a sub-process, respecting Zope idiosyncrasies.
> > > +
> > > + Use this as a baseclass for test cases, or as a mixin with an
> > > + interesting `TestCase` subclass.
> > > +
> > > + This is basically a reimplementation of subunit's
> > > + `IsolatedTestCase`, but adjusted to work with Zope. In particular,
> > > + Zope's TestResult object is responsible for calling testSetUp()
> > > + and testTearDown() on the selected layer.
> > > + """
> >
> > I can see why you named it ZopeIsolatedTes
> > good name? I mean, don't TestCase classes in general run their tests in
> > an isolated environment?
>
> ZopeIsolatedTes
> name form came from subunit.
> similar job. I'm happy to rename it though.
I would probably have SubProcess, or Fork, in the name, to make it more
clear in what way it provides the isolation.
> > > + def run(self, result):
> > > + assert isinstance(result, ZopeTestResult), (
> > > + "result must be a Zope result object, not %r." % (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(ZopeIsola
> > > + fdwrite.flush()
> > > + sys.stdout.flush()
> > > + sys.stderr.flush()
> > > + # Exit hard.
> > > + os._exit(0)
> > > + else:
> > > +...
Gavin Panella (allenap) wrote : | # |
On Wed, 10 Mar 2010 14:15:31 -0000
Björn Tillenius <email address hidden> wrote:
...
> > ZopeIsolatedTes
> > name form came from subunit.
> > similar job. I'm happy to rename it though.
>
> I would probably have SubProcess, or Fork, in the name, to make it more
> clear in what way it provides the isolation.
It's now ZopeTestInSubPr
...
> > > I'm a bit interested in how this works with layers. Would you run away
> > > screaming if I asked you to add some tests demonstrating this? :)
Have a look at the new unit test I've added.
Basically, layer setUp() and tearDown() get called in the parent,
testSetUp() and testTearDown() in the child. For the test case,
__init__() is called in the parent, setUp(), tearDown() and test
methods are called in the child.
> > > It'd be interesting to see in which process the setUp(), testSetUp(),
> > > tearDown(), and testTearDown() methods of the layer are executed. I'm
> > > wondering if we can see subtle errors due to doing the wrong things in
> > > there.
Yes, there probably will be incompatibilities with some layers :-/
...
> > If it's mixed-in with a TestSuite I think it gets more complicated.
I haven't written tests for this yet. I may not do so (and restrict
ZopeTestInSubPr
...
> > > Also, how this this TestClass related to TwistedLayer? Oh right, that
> > > leads me to another question. You say that the reactor can't be
> > > restarted easily. But can it be shared between tests, just like the
> > > component architecture?
> >
> > That's an interesting idea. reactor.run() blocks, but I don't know of
> > any reason why it couldn't be started in a thread. I might get sent to
> > hell for it, but at least it's warmer than here.
>
> If it could, than maybe it would be better to use a layer instead, since
> that what we use elsewhere.
I haven't thought about this yet, but I will.
> Some other questions popped into my mind. What happens if you specify
> -D, and a test in the forked process fails?
This works as normal; you get a pdb prompt in the right place in the
child process.
> How well does this play with profiling? Do you still get all profiling
> information if you specify --profile=cProfile?
Not tried this yet. Not actually sure how to test this; it's a very
long time since I did any profiling.
So, I still have some work to do!
Gavin.
1 | === modified file 'lib/lp/services/job/tests/test_runner.py' |
2 | --- lib/lp/services/job/tests/test_runner.py 2010-03-09 14:28:11 +0000 |
3 | +++ lib/lp/services/job/tests/test_runner.py 2010-03-11 16:36:43 +0000 |
4 | @@ -27,7 +27,7 @@ |
5 | from lp.services.job.model.job import Job |
6 | from lp.services.job.runner import ( |
7 | BaseRunnableJob, JobCronScript, JobRunner, TwistedJobRunner) |
8 | -from lp.testing import TestCaseWithFactory, ZopeIsolatedTest |
9 | +from lp.testing import TestCaseWithFactory, ZopeTestInSubProcess |
10 | from lp.testing.mail_helpers import pop_notifications |
11 | |
12 | |
13 | @@ -301,7 +301,7 @@ |
14 | self.entries.append(input) |
15 | |
16 | |
17 | -class TestTwistedJobRunner(ZopeIsolatedTest, TestCaseWithFactory): |
18 | +class TestTwistedJobRunner(ZopeTestInSubProcess, TestCaseWithFactory): |
19 | |
20 | layer = LaunchpadZopelessLayer |
21 | |
22 | @@ -326,7 +326,7 @@ |
23 | self.assertIn('Job ran too long.', oops.value) |
24 | |
25 | |
26 | -class TestJobCronScript(ZopeIsolatedTest, TestCaseWithFactory): |
27 | +class TestJobCronScript(ZopeTestInSubProcess, TestCaseWithFactory): |
28 | |
29 | layer = LaunchpadZopelessLayer |
30 | |
31 | |
32 | === modified file 'lib/lp/testing/__init__.py' |
33 | --- lib/lp/testing/__init__.py 2010-03-09 14:28:11 +0000 |
34 | +++ lib/lp/testing/__init__.py 2010-03-11 16:36:43 +0000 |
35 | @@ -29,7 +29,7 @@ |
36 | 'validate_mock_class', |
37 | 'WindmillTestCase', |
38 | 'with_anonymous_login', |
39 | - 'ZopeIsolatedTest', |
40 | + 'ZopeTestInSubProcess', |
41 | ] |
42 | |
43 | import copy |
44 | @@ -590,7 +590,7 @@ |
45 | self.client.open(url=u'http://launchpad.dev:8085') |
46 | |
47 | |
48 | -class ZopeIsolatedTest: |
49 | +class ZopeTestInSubProcess: |
50 | """Run tests in a sub-process, respecting Zope idiosyncrasies. |
51 | |
52 | Use this as a mixin with an interesting `TestCase` or `TestSuite` |
53 | @@ -631,7 +631,7 @@ |
54 | # obtain the result. |
55 | result = testtools.MultiTestResult( |
56 | result, subunit.TestProtocolClient(fdwrite)) |
57 | - super(ZopeIsolatedTest, self).run(result) |
58 | + super(ZopeTestInSubProcess, self).run(result) |
59 | fdwrite.flush() |
60 | sys.stdout.flush() |
61 | sys.stderr.flush() |
62 | |
63 | === added file 'lib/lp/testing/tests/test_zope_test_in_subprocess.py' |
64 | --- lib/lp/testing/tests/test_zope_test_in_subprocess.py 1970-01-01 00:00:00 +0000 |
65 | +++ lib/lp/testing/tests/test_zope_test_in_subprocess.py 2010-03-11 21:36:45 +0000 |
66 | @@ -0,0 +1,118 @@ |
67 | +# Copyright 2010 Canonical Ltd. This software is licensed under the |
68 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
69 | + |
70 | +"""Test `lp.testing.ZopeTestInSubProcess`.""" |
71 | + |
72 | +__metaclass__ = type |
73 | + |
74 | +import functools |
75 | +import os |
76 | +import unittest |
77 | + |
78 | +from lp.testing import ZopeTestInSubProcess |
79 | + |
80 | + |
81 | +def record_pid(method): |
82 | + """Decorator that records the pid at method invocation. |
83 | + |
84 | + Will probably only DTRT with class methods or bound instance |
85 | + methods. |
86 | + """ |
87 | + @functools.wraps(method) |
88 | + def wrapper(self, *args, **kwargs): |
89 | + setattr(self, 'pid_in_%s' % method.__name__, os.getpid()) |
90 | + return method(self, *args, **kwargs) |
91 | + return wrapper |
92 | + |
93 | + |
94 | +class TestZopeTestInSubProcessLayer: |
95 | + """Helper to test `ZopeTestInSubProcess`. |
96 | + |
97 | + Asserts that layers are set up and torn down in the expected way, |
98 | + namely that setUp() and tearDown() are called in the parent |
99 | + process, and testSetUp() and testTearDown() are called in the |
100 | + child process. |
101 | + |
102 | + The assertions for tearDown() and testTearDown() must be done here |
103 | + because the test case runs before these methods are called. In the |
104 | + interests of symmetry and clarity, the assertions for setUp() and |
105 | + testSetUp() are done here too. |
106 | + """ |
107 | + |
108 | + @record_pid |
109 | + def __init__(self, test): |
110 | + self.test = test |
111 | + # These are needed to satisfy the requirements of the |
112 | + # byzantine Zope layer machinery. |
113 | + self.__name__ = self.__class__.__name__ |
114 | + self.__bases__ = self.__class__.__bases__ |
115 | + |
116 | + @record_pid |
117 | + def setUp(self): |
118 | + # Runs in the parent process. |
119 | + assert self.pid_in___init__ == self.pid_in_setUp, ( |
120 | + "layer.setUp() not called in parent process.") |
121 | + |
122 | + @record_pid |
123 | + def tearDown(self): |
124 | + # Runs in the parent process. |
125 | + assert self.pid_in___init__ == self.pid_in_tearDown, ( |
126 | + "layer.tearDown() not called in parent process.") |
127 | + |
128 | + @record_pid |
129 | + def testSetUp(self): |
130 | + # Runs in the child process. |
131 | + assert self.pid_in___init__ != self.pid_in_testSetUp, ( |
132 | + "layer.testSetUp() called in parent process.") |
133 | + |
134 | + @record_pid |
135 | + def testTearDown(self): |
136 | + # Runs in the child process. |
137 | + assert self.pid_in_testSetUp == self.pid_in_testTearDown, ( |
138 | + "layer.testTearDown() not called in same process as testSetUp().") |
139 | + |
140 | + |
141 | +class TestZopeTestInSubProcess(ZopeTestInSubProcess, unittest.TestCase): |
142 | + """Test `ZopeTestInSubProcess`. |
143 | + |
144 | + Assert that setUp(), test() and tearDown() are called in the child |
145 | + process. |
146 | + """ |
147 | + |
148 | + @record_pid |
149 | + def __init__(self, method_name='runTest'): |
150 | + # Runs in the parent process. |
151 | + super(TestZopeTestInSubProcess, self).__init__(method_name) |
152 | + self.layer = TestZopeTestInSubProcessLayer(self) |
153 | + |
154 | + @record_pid |
155 | + def setUp(self): |
156 | + # Runs in the child process. |
157 | + super(TestZopeTestInSubProcess, self).setUp() |
158 | + self.failUnlessEqual( |
159 | + self.layer.pid_in_testSetUp, self.pid_in_setUp, |
160 | + "setUp() not called in same process as layer.testSetUp().") |
161 | + |
162 | + @record_pid |
163 | + def test(self): |
164 | + # Runs in the child process. |
165 | + self.failUnlessEqual( |
166 | + self.pid_in_setUp, self.pid_in_test, |
167 | + "test method not run in same process as setUp().") |
168 | + |
169 | + @record_pid |
170 | + def tearDown(self): |
171 | + # Runs in the child process. |
172 | + super(TestZopeTestInSubProcess, self).tearDown() |
173 | + self.failUnlessEqual( |
174 | + self.pid_in_setUp, self.pid_in_tearDown, |
175 | + "tearDown() not run in same process as setUp().") |
176 | + |
177 | + |
178 | +def test_suite(): |
179 | + suite = unittest.TestSuite() |
180 | + loader = unittest.TestLoader() |
181 | + suite.addTests( |
182 | + loader.loadTestsFromTestCase( |
183 | + TestZopeTestInSubProcess)) |
184 | + return suite |
Gavin Panella (allenap) wrote : | # |
> On Wed, 10 Mar 2010 14:15:31 -0000
> Björn Tillenius <email address hidden> wrote:
>
> ...
> > > If it's mixed-in with a TestSuite I think it gets more complicated.
>
> I haven't written tests for this yet. I may not do so (and restrict
> ZopeTestInSubPr
I've removed the suggestion that it can be mixed-in with a TestSuite,
with the following commit message:
Don't advertise ZopeTestInSubPr
one suite is added to another with addTests() the *tests* are
copied, not the suite, so behaviour in a custom TestSuite is
lost. This is too fragile and likely to cause confusion.
> ...
> > > > Also, how this this TestClass related to TwistedLayer? Oh right, that
> > > > leads me to another question. You say that the reactor can't be
> > > > restarted easily. But can it be shared between tests, just like the
> > > > component architecture?
> > >
> > > That's an interesting idea. reactor.run() blocks, but I don't know of
> > > any reason why it couldn't be started in a thread. I might get sent to
> > > hell for it, but at least it's warmer than here.
> >
> > If it could, than maybe it would be better to use a layer instead, since
> > that what we use elsewhere.
>
> I haven't thought about this yet, but I will.
Now that this works, and seems to work well in the situations I'm
using it (I am also using it in a dependent branch for checkwatches),
I'm reluctant to spend more time on figuring out how to do it in a
layer.
Both the job runner code and my checkwatches code start and stop the
reactor as part of their operation, so trying to fudge them to use an
already-started reactor skews the test and means more moving parts.
Forking is cheap as chips so, although I warn in the docstring that it
will slow down tests, I doubt it will make much of a difference,
especially when testing network code, database code, etc. I haven't
measured it though.
...
> > How well does this play with profiling? Do you still get all profiling
> > information if you specify --profile=cProfile?
>
> Not tried this yet. Not actually sure how to test this; it's a very
> long time since I did any profiling.
It looks like no profiling information is collected from the
sub-process. I'm not sure how to fix this. How important is this to
you? Can this branch be landed without that support, with a bug filed
to record that it needs to be added?
Gavin.
Björn Tillenius (bjornt) wrote : | # |
On Fri, Mar 12, 2010 at 11:27:26AM -0000, Gavin Panella wrote:
> > On Wed, 10 Mar 2010 14:15:31 -0000
> > Björn Tillenius <email address hidden> wrote:
> >
> > ...
> > > > If it's mixed-in with a TestSuite I think it gets more complicated.
> >
> > I haven't written tests for this yet. I may not do so (and restrict
> > ZopeTestInSubPr
>
> I've removed the suggestion that it can be mixed-in with a TestSuite,
> with the following commit message:
>
> Don't advertise ZopeTestInSubPr
> one suite is added to another with addTests() the *tests* are
> copied, not the suite, so behaviour in a custom TestSuite is
> lost. This is too fragile and likely to cause confusion.
Ok.
> > > > > Also, how this this TestClass related to TwistedLayer? Oh right, that
> > > > > leads me to another question. You say that the reactor can't be
> > > > > restarted easily. But can it be shared between tests, just like the
> > > > > component architecture?
> > > >
> > > > That's an interesting idea. reactor.run() blocks, but I don't know of
> > > > any reason why it couldn't be started in a thread. I might get sent to
> > > > hell for it, but at least it's warmer than here.
> > >
> > > If it could, than maybe it would be better to use a layer instead, since
> > > that what we use elsewhere.
> >
> > I haven't thought about this yet, but I will.
>
> Now that this works, and seems to work well in the situations I'm
> using it (I am also using it in a dependent branch for checkwatches),
> I'm reluctant to spend more time on figuring out how to do it in a
> layer.
Ok.
> > > How well does this play with profiling? Do you still get all profiling
> > > information if you specify --profile=cProfile?
> >
> > Not tried this yet. Not actually sure how to test this; it's a very
> > long time since I did any profiling.
>
> It looks like no profiling information is collected from the
> sub-process. I'm not sure how to fix this. How important is this to
> you? Can this branch be landed without that support, with a bug filed
> to record that it needs to be added?
As long as it's not used too much, it's not crucial to have it.
However, I don't see any tests for this. Oh, now I found them, they just
don't look like tests. Hmm. Some more comments would help, to understand
why, and what's being tested. I would have expected a test that manually
calls the run() method. Would that be hard? It would mean that you would
get much better failure messages, in case of something went wrong.
Having tests in setUp and tearDown isn't ideal.
Sorry, but you'll have to ask someone else to carry on reviewing this
branch, since I'm unavailable next week.
vote abstain
--
Björn Tillenius | https:/
Gavin Panella (allenap) wrote : | # |
Björn Tillenius wrote:
> > > > How well does this play with profiling? Do you still get all profiling
> > > > information if you specify --profile=cProfile?
> > >
> > > Not tried this yet. Not actually sure how to test this; it's a very
> > > long time since I did any profiling.
> >
> > It looks like no profiling information is collected from the
> > sub-process. I'm not sure how to fix this. How important is this to
> > you? Can this branch be landed without that support, with a bug filed
> > to record that it needs to be added?
>
> As long as it's not used too much, it's not crucial to have it.
>
> However, I don't see any tests for this. Oh, now I found them, they just
> don't look like tests. Hmm. Some more comments would help, to understand
> why, and what's being tested. I would have expected a test that manually
> calls the run() method. Would that be hard? It would mean that you would
> get much better failure messages, in case of something went wrong.
I've looked into calling the run() method manually, but it means that we
would only be able to test layer.testSetUp(), test.setUp(), the test
method itself, test.tearDown() and layer.testTearD
not bad, but it misses out on layer.setUp() and layer.tearDown(), which
are quite important. The way it's structured at the moment means that
everything gets tested, and tested with the real test objects (i.e. the
runner and result) that zope.testing provides.
I have tried to replicate the zope.testing set up - by using Runner from
zope.testing.
to the tests and the output. And I haven't yet figured out how to get it
to work properly.
> Having tests in setUp and tearDown isn't ideal.
It's not ideal, but failures here get reported as test failures, rather
than causing a complete test run failure. Unfortunately, failures in
layer methods do cause the run to fail.
To summarise, I'd like to leave the tests as they are. They demonstrate
the operation of ZopeTestInSubPr
tear-down, which I think is valuable. I'll add some more comments to
make it clearer to future developers.
> Sorry, but you'll have to ask someone else to carry on reviewing this
> branch, since I'm unavailable next week.
Thank you for asking so many good questions :) Have a good break.
Gavin Panella (allenap) wrote : | # |
Latest incremental diff, r10440..
1 | === modified file 'lib/lp/testing/__init__.py' |
2 | --- lib/lp/testing/__init__.py 2010-03-11 16:37:22 +0000 |
3 | +++ lib/lp/testing/__init__.py 2010-03-12 10:58:17 +0000 |
4 | @@ -593,22 +593,12 @@ |
5 | class ZopeTestInSubProcess: |
6 | """Run tests in a sub-process, respecting Zope idiosyncrasies. |
7 | |
8 | - Use this as a mixin with an interesting `TestCase` or `TestSuite` |
9 | - subclass to quickly isolate tests with side-effects. |
10 | - |
11 | - When mixed-in with a `TestCase`: |
12 | - |
13 | - Each and every test *method* in the test case is run in a new, |
14 | - forked, sub-process. This will slow down your tests, so use it |
15 | - sparingly. However, when you need to, for example, start the |
16 | - Twisted reactor (which cannot currently be safely stopped and |
17 | - restarted in process) it is invaluable. |
18 | - |
19 | - When mixed-in with a `TestSuite`: |
20 | - |
21 | - All tests in this suite are run in a single, newly forked, |
22 | - sub-process. This isolates the parent process from side-effects, |
23 | - but the tests in this suite are not isolated from one another. |
24 | + Use this as a mixin with an interesting `TestCase` to isolate |
25 | + tests with side-effects. Each and every test *method* in the test |
26 | + case is run in a new, forked, sub-process. This will slow down |
27 | + your tests, so use it sparingly. However, when you need to, for |
28 | + example, start the Twisted reactor (which cannot currently be |
29 | + safely stopped and restarted in process) it is invaluable. |
30 | |
31 | This is basically a reimplementation of subunit's |
32 | `IsolatedTestCase` or `IsolatedTestSuite`, but adjusted to work |
33 | |
34 | === modified file 'lib/lp/testing/tests/test_zope_test_in_subprocess.py' |
35 | --- lib/lp/testing/tests/test_zope_test_in_subprocess.py 2010-03-11 21:37:41 +0000 |
36 | +++ lib/lp/testing/tests/test_zope_test_in_subprocess.py 2010-03-15 11:46:49 +0000 |
37 | @@ -1,7 +1,18 @@ |
38 | # Copyright 2010 Canonical Ltd. This software is licensed under the |
39 | # GNU Affero General Public License version 3 (see the file LICENSE). |
40 | |
41 | -"""Test `lp.testing.ZopeTestInSubProcess`.""" |
42 | +"""Test `lp.testing.ZopeTestInSubProcess`. |
43 | + |
44 | +How does it do this? |
45 | + |
46 | +A `TestCase`, mixed-in with `ZopeTestInSubProcess`, is run by the Zope |
47 | +test runner. This test case sets its own layer, to keep track of the |
48 | +PIDs when certain methods are called. It also records pids for its own |
49 | +methods. Assertions are made as these methods are called to ensure that |
50 | +they are running in the correct process - the parent or the child. |
51 | + |
52 | +Recording of the PIDs is handled using the `record_pid` decorator. |
53 | +""" |
54 | |
55 | __metaclass__ = type |
56 | |
57 | @@ -37,11 +48,13 @@ |
58 | because the test case runs before these methods are called. In the |
59 | interests of symmetry and clarity, the assertions for setUp() and |
60 | testSetUp() are done here too. |
61 | + |
62 | + This layer expects to be *instantiated*, which is not the norm for |
63 | + Zope layers. See `TestZopeTestInSubProcess` for its use. |
64 | """ |
65 | |
66 | @record_pid |
67 | - def __init__(self, test): |
68 | - self.test = test |
69 | + def __init__(self): |
70 | # These are needed to satisfy the requirements of the |
71 | # byzantine Zope layer machinery. |
72 | self.__name__ = self.__class__.__name__ |
73 | @@ -54,12 +67,6 @@ |
74 | "layer.setUp() not called in parent process.") |
75 | |
76 | @record_pid |
77 | - def tearDown(self): |
78 | - # Runs in the parent process. |
79 | - assert self.pid_in___init__ == self.pid_in_tearDown, ( |
80 | - "layer.tearDown() not called in parent process.") |
81 | - |
82 | - @record_pid |
83 | def testSetUp(self): |
84 | # Runs in the child process. |
85 | assert self.pid_in___init__ != self.pid_in_testSetUp, ( |
86 | @@ -71,19 +78,30 @@ |
87 | assert self.pid_in_testSetUp == self.pid_in_testTearDown, ( |
88 | "layer.testTearDown() not called in same process as testSetUp().") |
89 | |
90 | + @record_pid |
91 | + def tearDown(self): |
92 | + # Runs in the parent process. |
93 | + assert self.pid_in___init__ == self.pid_in_tearDown, ( |
94 | + "layer.tearDown() not called in parent process.") |
95 | + |
96 | |
97 | class TestZopeTestInSubProcess(ZopeTestInSubProcess, unittest.TestCase): |
98 | """Test `ZopeTestInSubProcess`. |
99 | |
100 | Assert that setUp(), test() and tearDown() are called in the child |
101 | process. |
102 | + |
103 | + Sets its own layer attribute. This layer is then responsible for |
104 | + recording the PID at interesting moments. Specifically, |
105 | + layer.testSetUp() must be called in the same process as |
106 | + test.setUp(). |
107 | """ |
108 | |
109 | @record_pid |
110 | def __init__(self, method_name='runTest'): |
111 | # Runs in the parent process. |
112 | super(TestZopeTestInSubProcess, self).__init__(method_name) |
113 | - self.layer = TestZopeTestInSubProcessLayer(self) |
114 | + self.layer = TestZopeTestInSubProcessLayer() |
115 | |
116 | @record_pid |
117 | def setUp(self): |
118 | @@ -110,9 +128,4 @@ |
119 | |
120 | |
121 | def test_suite(): |
122 | - suite = unittest.TestSuite() |
123 | - loader = unittest.TestLoader() |
124 | - suite.addTests( |
125 | - loader.loadTestsFromTestCase( |
126 | - TestZopeTestInSubProcess)) |
127 | - return suite |
128 | + return unittest.TestLoader().loadTestsFromName(__name__) |
Jonathan Lange (jml) wrote : | # |
On Mon, Mar 15, 2010 at 1:30 PM, Gavin Panella
<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/
> --- 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(Job...
Jonathan Lange (jml) wrote : | # |
Just a few changes required to the comments.
Gavin Panella (allenap) wrote : | # |
> On Mon, Mar 15, 2010 at 1:30 PM, Gavin Panella
> <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.
>
> > === 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
> >
> >...
Jonathan Lange (jml) wrote : | # |
On Tue, Mar 16, 2010 at 4:58 PM, Gavin Panella
<email address hidden> wrote:
>> On Mon, Mar 15, 2010 at 1:30 PM, Gavin Panella
>> <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.
>
>>
>> > === 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...
Jonathan Lange (jml) : | # |
Preview Diff
1 | === modified file 'lib/lp/services/job/runner.py' |
2 | --- lib/lp/services/job/runner.py 2010-02-25 12:07:15 +0000 |
3 | +++ lib/lp/services/job/runner.py 2010-03-16 18:02:40 +0000 |
4 | @@ -401,9 +401,10 @@ |
5 | class JobCronScript(LaunchpadCronScript): |
6 | """Base class for scripts that run jobs.""" |
7 | |
8 | - def __init__(self, runner_class=JobRunner): |
9 | + def __init__(self, runner_class=JobRunner, test_args=None): |
10 | self.dbuser = getattr(config, self.config_name).dbuser |
11 | - super(JobCronScript, self).__init__(self.config_name, self.dbuser) |
12 | + super(JobCronScript, self).__init__( |
13 | + self.config_name, self.dbuser, test_args) |
14 | self.runner_class = runner_class |
15 | |
16 | def main(self): |
17 | |
18 | === modified file 'lib/lp/services/job/tests/test_runner.py' |
19 | --- lib/lp/services/job/tests/test_runner.py 2010-02-24 19:52:01 +0000 |
20 | +++ lib/lp/services/job/tests/test_runner.py 2010-03-16 18:02:40 +0000 |
21 | @@ -11,23 +11,24 @@ |
22 | from unittest import TestLoader |
23 | |
24 | import transaction |
25 | -from canonical.testing import LaunchpadZopelessLayer |
26 | + |
27 | from zope.component import getUtility |
28 | from zope.error.interfaces import IErrorReportingUtility |
29 | from zope.interface import implements |
30 | |
31 | +from canonical.launchpad.webapp import errorlog |
32 | +from canonical.launchpad.webapp.interfaces import ( |
33 | + DEFAULT_FLAVOR, IStoreSelector, MAIN_STORE) |
34 | +from canonical.testing import LaunchpadZopelessLayer |
35 | + |
36 | from lp.code.interfaces.branchmergeproposal import ( |
37 | - IUpdatePreviewDiffJobSource,) |
38 | -from lp.testing.mail_helpers import pop_notifications |
39 | -from lp.services.job.runner import ( |
40 | - JobCronScript, JobRunner, BaseRunnableJob, TwistedJobRunner |
41 | -) |
42 | + IUpdatePreviewDiffJobSource) |
43 | from lp.services.job.interfaces.job import JobStatus, IRunnableJob |
44 | from lp.services.job.model.job import Job |
45 | -from lp.testing import TestCaseWithFactory |
46 | -from canonical.launchpad.webapp import errorlog |
47 | -from canonical.launchpad.webapp.interfaces import ( |
48 | - IStoreSelector, MAIN_STORE, DEFAULT_FLAVOR) |
49 | +from lp.services.job.runner import ( |
50 | + BaseRunnableJob, JobCronScript, JobRunner, TwistedJobRunner) |
51 | +from lp.testing import TestCaseWithFactory, ZopeTestInSubProcess |
52 | +from lp.testing.mail_helpers import pop_notifications |
53 | |
54 | |
55 | class NullJob(BaseRunnableJob): |
56 | @@ -300,7 +301,7 @@ |
57 | self.entries.append(input) |
58 | |
59 | |
60 | -class TestTwistedJobRunner(TestCaseWithFactory): |
61 | +class TestTwistedJobRunner(ZopeTestInSubProcess, TestCaseWithFactory): |
62 | |
63 | layer = LaunchpadZopelessLayer |
64 | |
65 | @@ -325,7 +326,7 @@ |
66 | self.assertIn('Job ran too long.', oops.value) |
67 | |
68 | |
69 | -class TestJobCronScript(TestCaseWithFactory): |
70 | +class TestJobCronScript(ZopeTestInSubProcess, TestCaseWithFactory): |
71 | |
72 | layer = LaunchpadZopelessLayer |
73 | |
74 | @@ -351,7 +352,8 @@ |
75 | source_interface = IUpdatePreviewDiffJobSource |
76 | |
77 | def __init__(self): |
78 | - super(JobCronScriptSubclass, self).__init__(DummyRunner) |
79 | + super(JobCronScriptSubclass, self).__init__( |
80 | + DummyRunner, test_args=[]) |
81 | self.logger = ListLogger() |
82 | |
83 | old_errorlog = errorlog.globalErrorUtility |
84 | |
85 | === modified file 'lib/lp/testing/__init__.py' |
86 | --- lib/lp/testing/__init__.py 2010-03-01 03:06:02 +0000 |
87 | +++ lib/lp/testing/__init__.py 2010-03-16 18:02:40 +0000 |
88 | @@ -29,6 +29,7 @@ |
89 | 'validate_mock_class', |
90 | 'WindmillTestCase', |
91 | 'with_anonymous_login', |
92 | + 'ZopeTestInSubProcess', |
93 | ] |
94 | |
95 | import copy |
96 | @@ -38,6 +39,8 @@ |
97 | from pprint import pformat |
98 | import shutil |
99 | import subprocess |
100 | +import subunit |
101 | +import sys |
102 | import tempfile |
103 | import time |
104 | |
105 | @@ -62,6 +65,7 @@ |
106 | from zope.interface.verify import verifyClass, verifyObject |
107 | from zope.security.proxy import ( |
108 | isinstance as zope_isinstance, removeSecurityProxy) |
109 | +from zope.testing.testrunner.runner import TestResult as ZopeTestResult |
110 | |
111 | from canonical.launchpad.webapp import errorlog |
112 | from canonical.config import config |
113 | @@ -586,6 +590,70 @@ |
114 | self.client.open(url=u'http://launchpad.dev:8085') |
115 | |
116 | |
117 | +class ZopeTestInSubProcess: |
118 | + """Run tests in a sub-process, respecting Zope idiosyncrasies. |
119 | + |
120 | + Use this as a mixin with an interesting `TestCase` to isolate |
121 | + tests with side-effects. Each and every test *method* in the test |
122 | + case is run in a new, forked, sub-process. This will slow down |
123 | + your tests, so use it sparingly. However, when you need to, for |
124 | + example, start the Twisted reactor (which cannot currently be |
125 | + safely stopped and restarted in process) it is invaluable. |
126 | + |
127 | + This is basically a reimplementation of subunit's |
128 | + `IsolatedTestCase` or `IsolatedTestSuite`, but adjusted to work |
129 | + with Zope. In particular, Zope's TestResult object is responsible |
130 | + for calling testSetUp() and testTearDown() on the selected layer. |
131 | + """ |
132 | + |
133 | + def run(self, result): |
134 | + # The result must be an instance of Zope's TestResult because |
135 | + # we construct a super() of it later on. Other result classes |
136 | + # could be supported with a more general approach, but it's |
137 | + # unlikely that any one approach is going to work for every |
138 | + # class. It's better to fail early and draw attention here. |
139 | + assert isinstance(result, ZopeTestResult), ( |
140 | + "result must be a Zope result object, not %r." % (result,)) |
141 | + pread, pwrite = os.pipe() |
142 | + pid = os.fork() |
143 | + if pid == 0: |
144 | + # Child. |
145 | + os.close(pread) |
146 | + fdwrite = os.fdopen(pwrite, 'w', 1) |
147 | + # Send results to both the Zope result object (so that |
148 | + # layer setup and teardown are done properly, etc.) and to |
149 | + # the subunit stream client so that the parent process can |
150 | + # obtain the result. |
151 | + result = testtools.MultiTestResult( |
152 | + result, subunit.TestProtocolClient(fdwrite)) |
153 | + super(ZopeTestInSubProcess, self).run(result) |
154 | + fdwrite.flush() |
155 | + sys.stdout.flush() |
156 | + sys.stderr.flush() |
157 | + # Exit hard to avoid running onexit handlers and to avoid |
158 | + # anything that could suppress SystemExit; this exit must |
159 | + # not be prevented. |
160 | + os._exit(0) |
161 | + else: |
162 | + # Parent. |
163 | + os.close(pwrite) |
164 | + fdread = os.fdopen(pread, 'rU') |
165 | + # Skip all the Zope-specific result stuff by using a |
166 | + # super() of the result. This is because the Zope result |
167 | + # object calls testSetUp() and testTearDown() on the |
168 | + # layer, and handles post-mortem debugging. These things |
169 | + # do not make sense in the parent process. More |
170 | + # immediately, it also means that the results are not |
171 | + # reported twice; they are reported on stdout by the child |
172 | + # process, so they need to be suppressed here. |
173 | + result = super(ZopeTestResult, result) |
174 | + # Accept the result from the child process. |
175 | + protocol = subunit.TestProtocolServer(result) |
176 | + protocol.readFrom(fdread) |
177 | + fdread.close() |
178 | + os.waitpid(pid, 0) |
179 | + |
180 | + |
181 | def capture_events(callable_obj, *args, **kwargs): |
182 | """Capture the events emitted by a callable. |
183 | |
184 | |
185 | === added file 'lib/lp/testing/tests/test_zope_test_in_subprocess.py' |
186 | --- lib/lp/testing/tests/test_zope_test_in_subprocess.py 1970-01-01 00:00:00 +0000 |
187 | +++ lib/lp/testing/tests/test_zope_test_in_subprocess.py 2010-03-16 18:02:40 +0000 |
188 | @@ -0,0 +1,131 @@ |
189 | +# Copyright 2010 Canonical Ltd. This software is licensed under the |
190 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
191 | + |
192 | +"""Test `lp.testing.ZopeTestInSubProcess`. |
193 | + |
194 | +How does it do this? |
195 | + |
196 | +A `TestCase`, mixed-in with `ZopeTestInSubProcess`, is run by the Zope |
197 | +test runner. This test case sets its own layer, to keep track of the |
198 | +PIDs when certain methods are called. It also records pids for its own |
199 | +methods. Assertions are made as these methods are called to ensure that |
200 | +they are running in the correct process - the parent or the child. |
201 | + |
202 | +Recording of the PIDs is handled using the `record_pid` decorator. |
203 | +""" |
204 | + |
205 | +__metaclass__ = type |
206 | + |
207 | +import functools |
208 | +import os |
209 | +import unittest |
210 | + |
211 | +from lp.testing import ZopeTestInSubProcess |
212 | + |
213 | + |
214 | +def record_pid(method): |
215 | + """Decorator that records the pid at method invocation. |
216 | + |
217 | + Will probably only DTRT with class methods or bound instance |
218 | + methods. |
219 | + """ |
220 | + @functools.wraps(method) |
221 | + def wrapper(self, *args, **kwargs): |
222 | + setattr(self, 'pid_in_%s' % method.__name__, os.getpid()) |
223 | + return method(self, *args, **kwargs) |
224 | + return wrapper |
225 | + |
226 | + |
227 | +class TestZopeTestInSubProcessLayer: |
228 | + """Helper to test `ZopeTestInSubProcess`. |
229 | + |
230 | + Asserts that layers are set up and torn down in the expected way, |
231 | + namely that setUp() and tearDown() are called in the parent |
232 | + process, and testSetUp() and testTearDown() are called in the |
233 | + child process. |
234 | + |
235 | + The assertions for tearDown() and testTearDown() must be done here |
236 | + because the test case runs before these methods are called. In the |
237 | + interests of symmetry and clarity, the assertions for setUp() and |
238 | + testSetUp() are done here too. |
239 | + |
240 | + This layer expects to be *instantiated*, which is not the norm for |
241 | + Zope layers. See `TestZopeTestInSubProcess` for its use. |
242 | + """ |
243 | + |
244 | + @record_pid |
245 | + def __init__(self): |
246 | + # These are needed to satisfy the requirements of the |
247 | + # byzantine Zope layer machinery. |
248 | + self.__name__ = self.__class__.__name__ |
249 | + self.__bases__ = self.__class__.__bases__ |
250 | + |
251 | + @record_pid |
252 | + def setUp(self): |
253 | + # Runs in the parent process. |
254 | + assert self.pid_in___init__ == self.pid_in_setUp, ( |
255 | + "layer.setUp() not called in parent process.") |
256 | + |
257 | + @record_pid |
258 | + def testSetUp(self): |
259 | + # Runs in the child process. |
260 | + assert self.pid_in___init__ != self.pid_in_testSetUp, ( |
261 | + "layer.testSetUp() called in parent process.") |
262 | + |
263 | + @record_pid |
264 | + def testTearDown(self): |
265 | + # Runs in the child process. |
266 | + assert self.pid_in_testSetUp == self.pid_in_testTearDown, ( |
267 | + "layer.testTearDown() not called in same process as testSetUp().") |
268 | + |
269 | + @record_pid |
270 | + def tearDown(self): |
271 | + # Runs in the parent process. |
272 | + assert self.pid_in___init__ == self.pid_in_tearDown, ( |
273 | + "layer.tearDown() not called in parent process.") |
274 | + |
275 | + |
276 | +class TestZopeTestInSubProcess(ZopeTestInSubProcess, unittest.TestCase): |
277 | + """Test `ZopeTestInSubProcess`. |
278 | + |
279 | + Assert that setUp(), test() and tearDown() are called in the child |
280 | + process. |
281 | + |
282 | + Sets its own layer attribute. This layer is then responsible for |
283 | + recording the PID at interesting moments. Specifically, |
284 | + layer.testSetUp() must be called in the same process as |
285 | + test.setUp(). |
286 | + """ |
287 | + |
288 | + @record_pid |
289 | + def __init__(self, method_name='runTest'): |
290 | + # Runs in the parent process. |
291 | + super(TestZopeTestInSubProcess, self).__init__(method_name) |
292 | + self.layer = TestZopeTestInSubProcessLayer() |
293 | + |
294 | + @record_pid |
295 | + def setUp(self): |
296 | + # Runs in the child process. |
297 | + super(TestZopeTestInSubProcess, self).setUp() |
298 | + self.failUnlessEqual( |
299 | + self.layer.pid_in_testSetUp, self.pid_in_setUp, |
300 | + "setUp() not called in same process as layer.testSetUp().") |
301 | + |
302 | + @record_pid |
303 | + def test(self): |
304 | + # Runs in the child process. |
305 | + self.failUnlessEqual( |
306 | + self.pid_in_setUp, self.pid_in_test, |
307 | + "test method not run in same process as setUp().") |
308 | + |
309 | + @record_pid |
310 | + def tearDown(self): |
311 | + # Runs in the child process. |
312 | + super(TestZopeTestInSubProcess, self).tearDown() |
313 | + self.failUnlessEqual( |
314 | + self.pid_in_setUp, self.pid_in_tearDown, |
315 | + "tearDown() not run in same process as setUp().") |
316 | + |
317 | + |
318 | +def test_suite(): |
319 | + return unittest.TestLoader().loadTestsFromName(__name__) |
On Mon, Mar 08, 2010 at 06:14:32PM -0000, Gavin Panella wrote: /bugs.launchpad .net/bugs/ 491870 tCase - that can be used as a tCase with a couple of job runner
> Gavin Panella has proposed merging lp:~allenap/launchpad/isolate-tests into lp:launchpad/devel.
>
> Requested reviews:
> Canonical Launchpad Engineering (launchpad): code
> Related bugs:
> #491870 Use Twisted's thread support instead of the threading module in checkwatches
> https:/
>
>
> Implement a new class - ZopeIsolatedTes
> mixin with more interesting TestCase subclasses. It runs each test
> method in a forked sub-process, passing the results back to the parent
> process with subunit. This is needed to safely run tests that need to,
> for example, start the Twisted reactor, which cannot be safely restarted
> in the same process once stopped.
>
> This branch also uses ZopeIsolatedTes
> tests. Additionally, one of these tests, TestJobCronScript, has been
> fixed to override the command line arguments passed to the script class
> (otherwise sys.argv is used, which is stuffed with arguments passed to
> bin/test).
vote needsinfo
If the job runner tests ran fine previously, why is this tCase needed?
ZopeIsolatedTes
> === 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-08 18:14:32 +0000
> --- lib/lp/
> +++ lib/lp/
> @@ -300,7 +301,7 @@ append( input) unner(TestCaseW ithFactory) : unner(ZopeIsola tedTestCase, TestCaseWithFac tory): ssLayer pt(TestCaseWith Factory) : pt(ZopeIsolated TestCase, TestCaseWithFac tory):
> self.entries.
>
>
> -class TestTwistedJobR
> +class TestTwistedJobR
>
> layer = LaunchpadZopele
>
> @@ -325,7 +326,7 @@
> self.assertIn('Job ran too long.', oops.value)
>
>
> -class TestJobCronScri
> +class TestJobCronScri
Will we ever use ZopeIsolatedTes tCase without mixing in tory?
TestCaseWithFac
> === modified file 'lib/lp/ testing/ __init_ _.py' testing/ __init_ _.py 2010-02-17 20:54:36 +0000 testing/ __init_ _.py 2010-03-08 18:14:32 +0000 open(url= u'http:// launchpad. dev:8085') tCase(unittest. TestCase) :
> --- lib/lp/
> +++ lib/lp/
> @@ -586,6 +591,56 @@
> self.client.
>
>
> +class ZopeIsolatedTes
> + """Run tests in a sub-process, respecting Zope idiosyncrasies.
> +
> + Use this as a baseclass for test cases, or as a mixin with an
> + interesting `TestCase` subclass.
> +
> + This is basically a reimplementation of subunit's
> + `IsolatedTestCase`, but adjusted to work with Zope. In particular,
> + Zope's TestResult object is responsible for calling testSetUp()
> + and testTearDown() on the selected layer.
> + """
I can see why you named it ZopeIsolatedTes tCase, but is that really a
good name? I mean, don't TestCase classes in general run their tests in
an isolated environment?
> +
> + def run(self, result):
> + assert isinstance(result, ZopeTestResult), (
> + "result must be a Zope result object, not %r." % (result,))
> + pread, pwrite = os.pipe()
> + pid = os.fork()
> + if pid == 0:
> + # Child.
> + os.close(pread)
> + ...