Merge lp:~lifeless/launchpad/uniqueconfig into lp:launchpad
- uniqueconfig
- Merge into devel
Status: | Merged |
---|---|
Approved by: | Robert Collins |
Approved revision: | no longer in the source branch. |
Merged at revision: | 11776 |
Proposed branch: | lp:~lifeless/launchpad/uniqueconfig |
Merge into: | lp:launchpad |
Diff against target: |
686 lines (+329/-37) 15 files modified
configs/README.txt (+4/-0) lib/canonical/config/__init__.py (+12/-0) lib/canonical/config/fixture.py (+70/-0) lib/canonical/config/tests/test_fixture.py (+60/-0) lib/canonical/ftests/pgsql.py (+8/-2) lib/canonical/ftests/test_pgsql.py (+20/-4) lib/canonical/launchpad/doc/canonical-config.txt (+8/-6) lib/canonical/launchpad/pagetests/webservice/xx-wadl.txt (+3/-2) lib/canonical/launchpad/scripts/__init__.py (+1/-1) lib/canonical/launchpad/scripts/logger.py (+1/-1) lib/canonical/testing/ftests/test_layers.py (+81/-12) lib/canonical/testing/layers.py (+58/-7) lib/lp/scripts/utilities/lpwindmill.py (+1/-0) lib/lp/services/mail/sendmail.py (+1/-1) versions.cfg (+1/-1) |
To merge this branch: | bzr merge lp:~lifeless/launchpad/uniqueconfig |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Henning Eggers (community) | code | Approve | |
Review via email: mp+38689@code.launchpad.net |
Commit message
Generate unique LP config directories during test time.
Description of the change
Generate unique LP config directories during test time. Another step towards parallel testing.
Henning Eggers (henninge) wrote : | # |
Robert Collins (lifeless) wrote : | # |
On Tue, Oct 19, 2010 at 5:42 AM, Henning Eggers
<email address hidden> wrote:
> Hi Robert,
> as mentioned on IRC, I don't feel good about adding new files to the canonical tree if they are Launchpad specific. The feeling has not gone away and I ask you to consider moving canonical.config to lp.config. This will most likely require an extra branch as there are many call sites for canonical.config but they are all mechanical changes.
Thanks for the review.
I think that it is a separate concern - I appreciate and can share it,
but the debt of having lp specific stuff in canonical module *in the
lp source tree* already exists, I don't see the benefit to us of
blocking *any* improvement due to that existing problem. Happy to work
on that separately : unhappy - extremely unhappy - at tying orthogonal
(related but different concerns) together.
Reviews *should* be catching functional and maintenance issues *with
changes presented*, not assessing the global state of the system.
Assessing the global state would be a time consuming and depressing
task - and every review would have a huge pile of tech debt land on
it, which is unreasonable to the submitter, to the reviewer, slows
cycle time down and creates a disincentive to touch areas of the code
base associated with tech debt (because the toucher will be asked to
do more as a condition of landing their code). This is unreasonable.
Its very reasonable to say 'hey, what you are doing contains a tech
debt interest payment. It would be wonderful if you could follow up
with removal of that tech debt before you consider yourself finished
with whatever arc of changes you're working on'. In this case I'm
adding a module to a package. The package is mislabelled (nowadays) as
being canonical specific when its lp specific. Moving the entire
module at once isn't going to be easer or harder due to having a new
submodule [well ok, a /tiny/ bit harder]. In particular, if the
original tech debt was paid off, we wouldn't be paying interest;
putting the new code elsewhere in the tree would split things that
shouldn't be; having the existing tech debt get bigger is paying
interest on it. So sure, I can move the config module to
lp.services.config if I get some time, and I (obviously) would do that
as a separate branch. But I see many reviews saying (paraphrased) 'you
touched something dirty, you need to clean your hands and the dirty
thing'. This really does create an incentive not to touch dirty
things.
Thats very different to doing something that adds brand new tech debt
(which actually, and slightly embarassingly I did very recently, but
I'm definitely going to get to that one :))
> Also, I am worried about your bumping the version number for fixtures to 0.3.2 when that version is not available from the project page or branch on Launchpad. Neither was 0.3.1 it seems. Intentionally or not, this sneeks unreviewed and unproven code into the Launchpad tree. I have a bad feeling about this practice, too.
http://
this branch was pushed. Its true it wasn't in trunk, that was
oversight. As for changing dependencies, we should either review [the
conten...
Robert Collins (lifeless) wrote : | # |
On Tue, Oct 19, 2010 at 8:15 AM, Robert Collins
<email address hidden> wrote:
> Thanks for the review.
....
Wow, that was a pretty intense reply I sent - out of proportion to
your particular review.
I want to be clear that I'm really addressing a generic systemic
issue I've been perceiving and thinking (and discussing around and
about) - your review was simply caught me first thing in the morning
and I guess the back of my head had finally come up with some clear
statements about the issues I see.
We probably want to take this to the list :)
-Rob
Brad Crittenden (bac) wrote : | # |
On Oct 19, 2010, at 02:15 , Robert Collins wrote:
> On Tue, Oct 19, 2010 at 5:42 AM, Henning Eggers
>
>
>> Please talk to Brad about both issues and see what he says.
>
> I'm not sure why Brad specifically - the review team was always a
> democracy ever since it was formulated : we really want broad input,
> still we can start with Brad... Brad, what do you think?
The review team is, of course, a democracy. I think Henning was probably just looking for a sanity check before involving the entire team.
As Rob indicated, the last few AsiaPac reviewers meetings have spent considerable time talking about lots of wide-ranging topics regarding reviews. Some of those ideas have been written up now in a subsequent email Rob sent proposing the optional review experiment.
A discussion of eliminating tech debt was brought up by Curtis recently in the AmEu meeting ("don't add to canonical.launchpad as we're trying to get rid of it") and was uncontroversial. We all agree tech debt is a burden for many reasons and in a perfect world we would make it go away. Curtis proposed one approach and most people thought it was reasonable. Robert has a different view, as expressed here, and in the AsiaPac meetings.
The topic comes up in the reviewers meeting because a) it is the only meeting where we all get together regularly, ignoring the fact we are forced to do it regionally with a single conduit (me), and b) the review is the last chance for a peer to catch the fact you may be contributing to tech debt or simply not cleaning it up.
The "you touch it, you fix it" mode of working is certainly not fair but it does cut across the board if we all agree to the idea. I have no qualms as a reviewer saying "I know you didn't create this problem but while you're there would you spend a few minutes to clean it up?" Most people are happy to oblige, not because I am wielding enormous reviewer power, but because we all agree we want to get rid of warts.
As to the fear that people will be unwilling to wander into the dark corners of the code base for fear they will be accountable for cleaning it all up, I'm not convinced that will be the way most of us will work. Our enforcement of cleaning up things you touch is not heavy handed. Henning asked that you have a follow-on branch but did not demand it as a condition of approval. It is simply our agreement that we'll all share, more or less, in the less interesting chore of cleaning up after ourselves.
Robert I think your views expressed here are bigger than the issue at hand and represent an idea that our development process, not just our review process, is fundamentally flawed. I think you need to find the appropriate forum to make suggestions and solicit input from everyone.
Robert Collins (lifeless) wrote : | # |
On Tue, Oct 19, 2010 at 11:32 PM, Brad Crittenden
<email address hidden> wrote:
> As to the fear that people will be unwilling to wander into the dark corners of the code base for fear they will be accountable for cleaning it all up, I'm not convinced that will be the way most of us will work. Our enforcement of cleaning up things you touch is not heavy handed. Henning asked that you have a follow-on branch but did not demand it as a condition of approval. It is simply our agreement that we'll all share, more or less, in the less interesting chore of cleaning up after ourselves.
This isn't a fear I'm expressing, it's direct and indirect
observation; just today I had an anecdote repeated to me of someone
being told (by their mentor in the job when they joined) 'dont touch
that thing, you'll be asked to clean it up'.
> Robert I think your views expressed here are bigger than the issue at hand and represent an idea that our development process, not just our review process, is fundamentally flawed. I think you need to find the appropriate forum to make suggestions and solicit input from everyone.
Oh, I'm sure they are, and those things are being discussed.
I didn't think I was being required to do a follow on branch; but
henninge expressed deep concern about an interest payment on tech
debt; thats evidence that something is wrong: we have the tech debt
already, its a chilling effect to be concerned about working on things
which have tech debt *because they have debt*.
I have two things that are important here:
- what to do with this patch
- moving the larger discussion forward
The second aspect is already underway.
-Rob
Henning Eggers (henninge) wrote : | # |
Hi Robert,
let me clarify in reverse order. ;-)
I picked Brad because he is kind of the head reviewer (by experience, otherwise we are democratic as mentioned) and because he currently works nearer to your time zone than I do. I was unavailable this (my) morning but it is always good to get a second opinion.
I guess I am too Launchpad-centric to notice http://
As Brad already mentioned, we do follow the strategy that whoever touches dirty code should fix it. I find that a good strategy because otherwise nobody will ever take the time to pay back tech debt. Instead we are just paying interest on top of it, as you correctly put it. Adding new files to the old structure is a lot of interest. I think the best approach in this case would be to create your new files at the new place (lp.services.config sounds good) and file a bug about moving the remaining canonical.config code there, too. Ideally you'd assign that bug to you and go about fixing it next.
Cheers,
Henning
Robert Collins (lifeless) wrote : | # |
Hi Henning, thanks for clarifying.
We can discuss the tech debt strategy separately; my views don't need
repeating right now. The reason I really don't want to put the new
file in a different place is that that *increases* the amount of added
tech debt: rather than an interest payment, its interest + a
withdrawal.
-Rob
Henning Eggers (henninge) wrote : | # |
Thanks for the XXX's and the bug. ;-) The other changes look good, too. Please remember to add the "config.is_testing" property soon to get rid of the multiline if statements.
Preview Diff
1 | === modified file 'configs/README.txt' |
2 | --- configs/README.txt 2009-04-29 19:10:17 +0000 |
3 | +++ configs/README.txt 2010-10-21 18:14:53 +0000 |
4 | @@ -281,9 +281,13 @@ |
5 | | | |
6 | | + authserver-lazr.conf |
7 | | | |
8 | + | + testrunner_\d+/launchpad-lazr.conf |
9 | + | | |
10 | | + testrunner-appserver/launchpad-lazr.conf |
11 | | | |
12 | | + authserver-lazr.conf |
13 | + | | |
14 | + | + testrunner-appserver_\d+/launchpad-lazr.conf |
15 | | |
16 | + staging-lazr.conf |
17 | | | |
18 | |
19 | === modified file 'lib/canonical/config/__init__.py' |
20 | --- lib/canonical/config/__init__.py 2010-05-26 10:42:10 +0000 |
21 | +++ lib/canonical/config/__init__.py 2010-10-21 18:14:53 +0000 |
22 | @@ -6,6 +6,8 @@ |
23 | |
24 | The configuration section used is specified using the LPCONFIG |
25 | environment variable, and defaults to 'development' |
26 | + |
27 | +XXX: Robert Collins 2010-10-20 bug=663454 this is in the wrong namespace. |
28 | ''' |
29 | |
30 | __metaclass__ = type |
31 | @@ -154,6 +156,16 @@ |
32 | self._invalidateConfig() |
33 | self._getConfig() |
34 | |
35 | + def isTestRunner(self): |
36 | + """Return true if the current config is a 'testrunner' config. |
37 | + |
38 | + That is, if it is the testrunner config, or a unique variation of it, |
39 | + but not if its the testrunner-appserver, development or production |
40 | + config. |
41 | + """ |
42 | + return (self.instance_name == 'testrunner' or |
43 | + self.instance_name.startswith('testrunner_')) |
44 | + |
45 | @property |
46 | def process_name(self): |
47 | """Return or set the current process's name to select a conf. |
48 | |
49 | === added file 'lib/canonical/config/fixture.py' |
50 | --- lib/canonical/config/fixture.py 1970-01-01 00:00:00 +0000 |
51 | +++ lib/canonical/config/fixture.py 2010-10-21 18:14:53 +0000 |
52 | @@ -0,0 +1,70 @@ |
53 | +# Copyright 2010 Canonical Ltd. This software is licensed under the |
54 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
55 | + |
56 | +from __future__ import with_statement |
57 | +"""Fixtures related to configs. |
58 | + |
59 | +XXX: Robert Collins 2010-10-20 bug=663454 this is in the wrong namespace. |
60 | +""" |
61 | + |
62 | +__metaclass__ = type |
63 | + |
64 | +__all__ = [ |
65 | + 'ConfigFixture', |
66 | + 'ConfigUseFixture', |
67 | + ] |
68 | + |
69 | +import os.path |
70 | +import shutil |
71 | +from textwrap import dedent |
72 | + |
73 | +from fixtures import Fixture |
74 | + |
75 | +from canonical.config import config |
76 | + |
77 | + |
78 | +class ConfigFixture(Fixture): |
79 | + """Create a unique launchpad config.""" |
80 | + |
81 | + _extend_str = dedent("""\ |
82 | + [meta] |
83 | + extends: ../%s/launchpad-lazr.conf |
84 | + |
85 | + """) |
86 | + |
87 | + def __init__(self, instance_name, copy_from_instance): |
88 | + """Create a ConfigFixture. |
89 | + |
90 | + :param instance_name: The name of the instance to create. |
91 | + :param copy_from_instance: An existing instance to clone. |
92 | + """ |
93 | + self.instance_name = instance_name |
94 | + self.copy_from_instance = copy_from_instance |
95 | + |
96 | + def setUp(self): |
97 | + super(ConfigFixture, self).setUp() |
98 | + root = 'configs/' + self.instance_name |
99 | + os.mkdir(root) |
100 | + absroot = os.path.abspath(root) |
101 | + self.addCleanup(shutil.rmtree, absroot) |
102 | + source = 'configs/' + self.copy_from_instance |
103 | + for basename in os.listdir(source): |
104 | + if basename == 'launchpad-lazr.conf': |
105 | + with open(root + '/launchpad-lazr.conf', 'wb') as out: |
106 | + out.write(self._extend_str % self.copy_from_instance) |
107 | + continue |
108 | + with open(source + '/' + basename, 'rb') as input: |
109 | + with open(root + '/' + basename, 'wb') as out: |
110 | + out.write(input.read()) |
111 | + |
112 | + |
113 | +class ConfigUseFixture(Fixture): |
114 | + """Use a config and restore the current config after.""" |
115 | + |
116 | + def __init__(self, instance_name): |
117 | + self.instance_name = instance_name |
118 | + |
119 | + def setUp(self): |
120 | + super(ConfigUseFixture, self).setUp() |
121 | + self.addCleanup(config.setInstance, config.instance_name) |
122 | + config.setInstance(self.instance_name) |
123 | |
124 | === added file 'lib/canonical/config/tests/test_fixture.py' |
125 | --- lib/canonical/config/tests/test_fixture.py 1970-01-01 00:00:00 +0000 |
126 | +++ lib/canonical/config/tests/test_fixture.py 2010-10-21 18:14:53 +0000 |
127 | @@ -0,0 +1,60 @@ |
128 | +# Copyright 2010 Canonical Ltd. This software is licensed under the |
129 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
130 | + |
131 | +"""Tests of the config fixtures.""" |
132 | + |
133 | +__metaclass__ = type |
134 | + |
135 | +import os |
136 | +from textwrap import dedent |
137 | + |
138 | +from testtools import TestCase |
139 | + |
140 | +from canonical.config import config |
141 | +from canonical.config.fixture import ( |
142 | + ConfigFixture, |
143 | + ConfigUseFixture, |
144 | + ) |
145 | + |
146 | + |
147 | +class TestConfigUseFixture(TestCase): |
148 | + |
149 | + def test_sets_restores_instance(self): |
150 | + fixture = ConfigUseFixture('foo') |
151 | + orig_instance = config.instance_name |
152 | + fixture.setUp() |
153 | + try: |
154 | + self.assertEqual('foo', config.instance_name) |
155 | + finally: |
156 | + fixture.cleanUp() |
157 | + self.assertEqual(orig_instance, config.instance_name) |
158 | + |
159 | + |
160 | +class TestConfigFixture(TestCase): |
161 | + |
162 | + def test_copies_and_derives(self): |
163 | + fixture = ConfigFixture('testtestconfig', 'testrunner') |
164 | + to_copy = [ |
165 | + 'apidoc-configure-normal.zcml', |
166 | + 'launchpad.conf', |
167 | + 'test-process-lazr.conf', |
168 | + ] |
169 | + fixture.setUp() |
170 | + try: |
171 | + for base in to_copy: |
172 | + path = 'configs/testtestconfig/' + base |
173 | + source = 'configs/testrunner/' + base |
174 | + old = open(source, 'rb').read() |
175 | + new = open(path, 'rb').read() |
176 | + self.assertEqual(old, new) |
177 | + confpath = 'configs/testtestconfig/launchpad-lazr.conf' |
178 | + lazr_config = open(confpath, 'rb').read() |
179 | + self.assertEqual( |
180 | + dedent("""\ |
181 | + [meta] |
182 | + extends: ../testrunner/launchpad-lazr.conf |
183 | + |
184 | + """), |
185 | + lazr_config) |
186 | + finally: |
187 | + fixture.cleanUp() |
188 | |
189 | === modified file 'lib/canonical/ftests/pgsql.py' |
190 | --- lib/canonical/ftests/pgsql.py 2010-10-17 05:30:43 +0000 |
191 | +++ lib/canonical/ftests/pgsql.py 2010-10-21 18:14:53 +0000 |
192 | @@ -169,7 +169,12 @@ |
193 | if template is not None: |
194 | self.template = template |
195 | if dbname is PgTestSetup.dynamic: |
196 | - self.dbname = self.__class__.dbname + "_" + str(os.getpid()) |
197 | + if os.environ.get('LP_TEST_INSTANCE'): |
198 | + self.dbname = "%s_%s" % ( |
199 | + self.__class__.dbname, os.environ.get('LP_TEST_INSTANCE')) |
200 | + else: |
201 | + # Fallback to the class name. |
202 | + self.dbname = self.__class__.dbname |
203 | elif dbname is not None: |
204 | self.dbname = dbname |
205 | else: |
206 | @@ -269,7 +274,6 @@ |
207 | ConnectionWrapper.dirty = False |
208 | if PgTestSetup._reset_db: |
209 | self.dropDb() |
210 | - PgTestSetup._reset_db = True |
211 | #uninstallFakeConnect() |
212 | |
213 | def connect(self): |
214 | @@ -329,6 +333,8 @@ |
215 | cur.execute('VACUUM pg_catalog.pg_shdepend') |
216 | finally: |
217 | con.close() |
218 | + # Any further setUp's must make a new DB. |
219 | + PgTestSetup._reset_db = True |
220 | |
221 | def force_dirty_database(self): |
222 | """flag the database as being dirty |
223 | |
224 | === modified file 'lib/canonical/ftests/test_pgsql.py' |
225 | --- lib/canonical/ftests/test_pgsql.py 2010-10-17 06:26:54 +0000 |
226 | +++ lib/canonical/ftests/test_pgsql.py 2010-10-21 18:14:53 +0000 |
227 | @@ -3,6 +3,10 @@ |
228 | |
229 | import os |
230 | |
231 | +from fixtures import ( |
232 | + EnvironmentVariableFixture, |
233 | + TestWithFixtures, |
234 | + ) |
235 | import testtools |
236 | |
237 | from canonical.ftests.pgsql import ( |
238 | @@ -11,11 +15,10 @@ |
239 | ) |
240 | |
241 | |
242 | -class TestPgTestSetup(testtools.TestCase): |
243 | +class TestPgTestSetup(testtools.TestCase, TestWithFixtures): |
244 | |
245 | - def test_db_naming(self): |
246 | - fixture = PgTestSetup(dbname=PgTestSetup.dynamic) |
247 | - expected_name = "%s_%s" % (PgTestSetup.dbname, os.getpid()) |
248 | + def assertDBName(self, expected_name, fixture): |
249 | + """Check that fixture uses expected_name as its dbname.""" |
250 | self.assertEqual(expected_name, fixture.dbname) |
251 | fixture.setUp() |
252 | self.addCleanup(fixture.dropDb) |
253 | @@ -25,6 +28,19 @@ |
254 | where = cur.fetchone()[0] |
255 | self.assertEqual(expected_name, where) |
256 | |
257 | + def test_db_naming_LP_TEST_INSTANCE_set(self): |
258 | + # when LP_TEST_INSTANCE is set, it is used for dynamic db naming. |
259 | + self.useFixture(EnvironmentVariableFixture('LP_TEST_INSTANCE', 'xx')) |
260 | + fixture = PgTestSetup(dbname=PgTestSetup.dynamic) |
261 | + expected_name = "%s_xx" % (PgTestSetup.dbname,) |
262 | + self.assertDBName(expected_name, fixture) |
263 | + |
264 | + def test_db_naming_without_LP_TEST_INSTANCE_is_static(self): |
265 | + self.useFixture(EnvironmentVariableFixture('LP_TEST_INSTANCE')) |
266 | + fixture = PgTestSetup(dbname=PgTestSetup.dynamic) |
267 | + expected_name = PgTestSetup.dbname |
268 | + self.assertDBName(expected_name, fixture) |
269 | + |
270 | def testOptimization(self): |
271 | # Test to ensure that the database is destroyed only when necessary |
272 | |
273 | |
274 | === modified file 'lib/canonical/launchpad/doc/canonical-config.txt' |
275 | --- lib/canonical/launchpad/doc/canonical-config.txt 2010-10-17 05:45:55 +0000 |
276 | +++ lib/canonical/launchpad/doc/canonical-config.txt 2010-10-21 18:14:53 +0000 |
277 | @@ -46,18 +46,20 @@ |
278 | will choose the conf file that matches its process_name if it exists, |
279 | otherwise it loads launchpad-lazr.conf. The general rule is |
280 | configs/<instance_name>/<process_name>.conf. The testrunner sets the |
281 | -instance_name to 'testrunner' to select testrunner/launchpad-lazr.conf, |
282 | -which extends default/launchpad-lazr.conf. |
283 | +instance_name to 'testrunner' and additionally uses unique temporary |
284 | +configs to permit changing the config during tests (but not if we |
285 | +are using persistent helpers - see canonical.testing.layers). |
286 | |
287 | - >>> config.instance_name |
288 | - 'testrunner' |
289 | + >>> (config.instance_name == 'testrunner' or |
290 | + ... config.instance_name == os.environ['LPCONFIG']) |
291 | + True |
292 | >>> config.process_name |
293 | 'test' |
294 | |
295 | >>> config.filename |
296 | - '.../configs/testrunner/launchpad-lazr.conf' |
297 | + '.../launchpad-lazr.conf' |
298 | >>> config.extends.filename |
299 | - '.../configs/development/launchpad-lazr.conf' |
300 | + '.../launchpad-lazr.conf' |
301 | |
302 | CanonicalConfig provides __contains__ and __getitem__ to check and |
303 | access lazr.config sections and keys. |
304 | |
305 | === modified file 'lib/canonical/launchpad/pagetests/webservice/xx-wadl.txt' |
306 | --- lib/canonical/launchpad/pagetests/webservice/xx-wadl.txt 2010-10-18 22:24:59 +0000 |
307 | +++ lib/canonical/launchpad/pagetests/webservice/xx-wadl.txt 2010-10-21 18:14:53 +0000 |
308 | @@ -14,12 +14,13 @@ |
309 | |
310 | If WADL is present in a certain file on disk--the filename depends on |
311 | the Launchpad configuration and the web service version--it will be |
312 | -loaded from disk and not generated from scratch. But the testrunner |
313 | +loaded from disk and not generated from scratch. But the test config |
314 | does not have any WADL files written to disk. |
315 | |
316 | >>> import os |
317 | + >>> from canonical.config import config |
318 | >>> wadl_filename = WebServiceApplication.cachedWADLPath( |
319 | - ... 'testrunner', 'devel') |
320 | + ... config.instance_name, 'devel') |
321 | >>> os.path.exists(wadl_filename) |
322 | False |
323 | |
324 | |
325 | === modified file 'lib/canonical/launchpad/scripts/__init__.py' |
326 | --- lib/canonical/launchpad/scripts/__init__.py 2010-08-20 20:31:18 +0000 |
327 | +++ lib/canonical/launchpad/scripts/__init__.py 2010-10-21 18:14:53 +0000 |
328 | @@ -71,7 +71,7 @@ |
329 | Instead, your test should use the Zopeless layer. |
330 | """ |
331 | |
332 | - if config.instance_name == 'testrunner': |
333 | + if config.isTestRunner(): |
334 | scriptzcmlfilename = 'script-testing.zcml' |
335 | else: |
336 | scriptzcmlfilename = 'script.zcml' |
337 | |
338 | === modified file 'lib/canonical/launchpad/scripts/logger.py' |
339 | --- lib/canonical/launchpad/scripts/logger.py 2010-09-27 08:46:26 +0000 |
340 | +++ lib/canonical/launchpad/scripts/logger.py 2010-10-21 18:14:53 +0000 |
341 | @@ -172,7 +172,7 @@ |
342 | |
343 | def __init__(self, fmt=None, datefmt=None): |
344 | if fmt is None: |
345 | - if config.instance_name == 'testrunner': |
346 | + if config.isTestRunner(): |
347 | # Don't output timestamps in the test environment |
348 | fmt = '%(levelname)-7s %(message)s' |
349 | else: |
350 | |
351 | === modified file 'lib/canonical/testing/ftests/test_layers.py' |
352 | --- lib/canonical/testing/ftests/test_layers.py 2010-10-17 19:08:32 +0000 |
353 | +++ lib/canonical/testing/ftests/test_layers.py 2010-10-21 18:14:53 +0000 |
354 | @@ -8,15 +8,19 @@ |
355 | """ |
356 | __metaclass__ = type |
357 | |
358 | +from cStringIO import StringIO |
359 | import os |
360 | import signal |
361 | import smtplib |
362 | -import unittest |
363 | - |
364 | from cStringIO import StringIO |
365 | from urllib import urlopen |
366 | + |
367 | +from fixtures import ( |
368 | + EnvironmentVariableFixture, |
369 | + TestWithFixtures, |
370 | + ) |
371 | import psycopg2 |
372 | - |
373 | +import testtools |
374 | from zope.component import getUtility, ComponentLookupError |
375 | |
376 | from canonical.config import config, dbconfig |
377 | @@ -43,7 +47,72 @@ |
378 | ) |
379 | from lp.services.memcache.client import memcache_client_factory |
380 | |
381 | -class BaseTestCase(unittest.TestCase): |
382 | + |
383 | +class TestBaseLayer(testtools.TestCase, TestWithFixtures): |
384 | + |
385 | + def test_allocates_LP_TEST_INSTANCE(self): |
386 | + self.useFixture( |
387 | + EnvironmentVariableFixture('LP_PERSISTENT_TEST_SERVICES')) |
388 | + self.useFixture(EnvironmentVariableFixture('LP_TEST_INSTANCE')) |
389 | + layer = BaseLayer |
390 | + layer.setUp() |
391 | + try: |
392 | + self.assertEqual(str(os.getpid()), os.environ.get('LP_TEST_INSTANCE')) |
393 | + finally: |
394 | + layer.tearDown() |
395 | + self.assertEqual(None, os.environ.get('LP_TEST_INSTANCE')) |
396 | + |
397 | + def test_persist_test_services_disables_LP_TEST_INSTANCE(self): |
398 | + self.useFixture( |
399 | + EnvironmentVariableFixture('LP_PERSISTENT_TEST_SERVICES', '')) |
400 | + self.useFixture(EnvironmentVariableFixture('LP_TEST_INSTANCE')) |
401 | + layer = BaseLayer |
402 | + layer.setUp() |
403 | + try: |
404 | + self.assertEqual(None, os.environ.get('LP_TEST_INSTANCE')) |
405 | + finally: |
406 | + layer.tearDown() |
407 | + self.assertEqual(None, os.environ.get('LP_TEST_INSTANCE')) |
408 | + |
409 | + def test_generates_unique_config(self): |
410 | + config.setInstance('testrunner') |
411 | + orig_instance = config.instance_name |
412 | + self.useFixture( |
413 | + EnvironmentVariableFixture('LP_PERSISTENT_TEST_SERVICES')) |
414 | + self.useFixture(EnvironmentVariableFixture('LP_TEST_INSTANCE')) |
415 | + self.useFixture(EnvironmentVariableFixture('LPCONFIG')) |
416 | + layer = BaseLayer |
417 | + layer.setUp() |
418 | + try: |
419 | + self.assertEqual( |
420 | + 'testrunner_%s' % os.environ['LP_TEST_INSTANCE'], |
421 | + config.instance_name) |
422 | + finally: |
423 | + layer.tearDown() |
424 | + self.assertEqual(orig_instance, config.instance_name) |
425 | + |
426 | + def test_generates_unique_config_dirs(self): |
427 | + self.useFixture( |
428 | + EnvironmentVariableFixture('LP_PERSISTENT_TEST_SERVICES')) |
429 | + self.useFixture(EnvironmentVariableFixture('LP_TEST_INSTANCE')) |
430 | + self.useFixture(EnvironmentVariableFixture('LPCONFIG')) |
431 | + layer = BaseLayer |
432 | + layer.setUp() |
433 | + try: |
434 | + runner_root = 'configs/%s' % config.instance_name |
435 | + runner_appserver_root = 'configs/testrunner-appserver_%s' % \ |
436 | + os.environ['LP_TEST_INSTANCE'] |
437 | + self.assertTrue(os.path.isfile( |
438 | + runner_root + '/launchpad-lazr.conf')) |
439 | + self.assertTrue(os.path.isfile( |
440 | + runner_appserver_root + '/launchpad-lazr.conf')) |
441 | + finally: |
442 | + layer.tearDown() |
443 | + self.assertFalse(os.path.exists(runner_root)) |
444 | + self.assertFalse(os.path.exists(runner_appserver_root)) |
445 | + |
446 | + |
447 | +class BaseTestCase(testtools.TestCase): |
448 | """Both the Base layer tests, as well as the base Test Case |
449 | for all the other Layer tests. |
450 | """ |
451 | @@ -178,7 +247,7 @@ |
452 | ) |
453 | |
454 | |
455 | -class LibrarianNoResetTestCase(unittest.TestCase): |
456 | +class LibrarianNoResetTestCase(testtools.TestCase): |
457 | """Our page tests need to run multple tests without destroying |
458 | the librarian database in between. |
459 | """ |
460 | @@ -221,7 +290,7 @@ |
461 | self.failIfEqual(data, self.sample_data) |
462 | |
463 | |
464 | -class LibrarianHideTestCase(unittest.TestCase): |
465 | +class LibrarianHideTestCase(testtools.TestCase): |
466 | layer = LaunchpadLayer |
467 | |
468 | def testHideLibrarian(self): |
469 | @@ -389,12 +458,13 @@ |
470 | LayerProcessController.startSMTPServer) |
471 | |
472 | |
473 | -class LayerProcessControllerTestCase(unittest.TestCase): |
474 | +class LayerProcessControllerTestCase(testtools.TestCase): |
475 | """Tests for the `LayerProcessController`.""" |
476 | # We need the database to be set up, no more. |
477 | layer = DatabaseLayer |
478 | |
479 | def tearDown(self): |
480 | + super(LayerProcessControllerTestCase, self).tearDown() |
481 | # Stop both servers. It's okay if they aren't running. |
482 | LayerProcessController.stopSMTPServer() |
483 | LayerProcessController.stopAppServer() |
484 | @@ -402,6 +472,7 @@ |
485 | def test_stopAppServer(self): |
486 | # Test that stopping the app server kills the process and remove the |
487 | # PID file. |
488 | + LayerProcessController._setConfig() |
489 | LayerProcessController.startAppServer() |
490 | pid = LayerProcessController.appserver.pid |
491 | pid_file = pidfile_path('launchpad', |
492 | @@ -415,6 +486,7 @@ |
493 | def test_postTestInvariants(self): |
494 | # A LayerIsolationError should be raised if the app server dies in the |
495 | # middle of a test. |
496 | + LayerProcessController._setConfig() |
497 | LayerProcessController.startAppServer() |
498 | pid = LayerProcessController.appserver.pid |
499 | os.kill(pid, signal.SIGTERM) |
500 | @@ -424,6 +496,7 @@ |
501 | |
502 | def test_postTestInvariants_dbIsReset(self): |
503 | # The database should be reset by the test invariants. |
504 | + LayerProcessController._setConfig() |
505 | LayerProcessController.startAppServer() |
506 | LayerProcessController.postTestInvariants() |
507 | # XXX: Robert Collins 2010-10-17 bug=661967 - this isn't a reset, its |
508 | @@ -432,14 +505,10 @@ |
509 | self.assertEquals(True, LaunchpadTestSetup()._reset_db) |
510 | |
511 | |
512 | -class TestNameTestCase(unittest.TestCase): |
513 | +class TestNameTestCase(testtools.TestCase): |
514 | layer = BaseLayer |
515 | def testTestName(self): |
516 | self.failUnlessEqual( |
517 | BaseLayer.test_name, |
518 | "testTestName " |
519 | "(canonical.testing.ftests.test_layers.TestNameTestCase)") |
520 | - |
521 | - |
522 | -def test_suite(): |
523 | - return unittest.TestLoader().loadTestsFromName(__name__) |
524 | |
525 | === modified file 'lib/canonical/testing/layers.py' |
526 | --- lib/canonical/testing/layers.py 2010-10-21 10:53:42 +0000 |
527 | +++ lib/canonical/testing/layers.py 2010-10-21 18:14:53 +0000 |
528 | @@ -57,6 +57,7 @@ |
529 | import gc |
530 | import logging |
531 | import os |
532 | +import shutil |
533 | import signal |
534 | import socket |
535 | import subprocess |
536 | @@ -64,12 +65,12 @@ |
537 | import tempfile |
538 | import threading |
539 | import time |
540 | - |
541 | from cProfile import Profile |
542 | from textwrap import dedent |
543 | from unittest import TestCase, TestResult |
544 | from urllib import urlopen |
545 | |
546 | +from fixtures import Fixture |
547 | import psycopg2 |
548 | from storm.zope.interfaces import IZStorm |
549 | import transaction |
550 | @@ -97,6 +98,10 @@ |
551 | from canonical.launchpad.webapp.vhosts import allvhosts |
552 | from canonical.lazr import pidfile |
553 | from canonical.config import CanonicalConfig, config, dbconfig |
554 | +from canonical.config.fixture import ( |
555 | + ConfigFixture, |
556 | + ConfigUseFixture, |
557 | + ) |
558 | from canonical.database.revision import ( |
559 | confirm_dbrevision, confirm_dbrevision_on_startup) |
560 | from canonical.database.sqlbase import ( |
561 | @@ -253,18 +258,54 @@ |
562 | # LP_PERSISTENT_TEST_SERVICES environment variable. |
563 | persist_test_services = False |
564 | |
565 | + # Things we need to cleanup. |
566 | + fixture = None |
567 | + |
568 | + # The config names that are generated for this layer |
569 | + config_name = None |
570 | + appserver_config_name = None |
571 | + |
572 | + @classmethod |
573 | + def make_config(cls, config_name, clone_from): |
574 | + """Create a temporary config and link it into the layer cleanup.""" |
575 | + cfg_fixture = ConfigFixture(config_name, clone_from) |
576 | + cls.fixture.addCleanup(cfg_fixture.cleanUp) |
577 | + cfg_fixture.setUp() |
578 | + |
579 | @classmethod |
580 | @profiled |
581 | def setUp(cls): |
582 | BaseLayer.isSetUp = True |
583 | + cls.fixture = Fixture() |
584 | + cls.fixture.setUp() |
585 | + cls.fixture.addCleanup(setattr, cls, 'fixture', None) |
586 | BaseLayer.persist_test_services = ( |
587 | os.environ.get('LP_PERSISTENT_TEST_SERVICES') is not None) |
588 | - # Kill any Memcached or Librarian left running from a previous |
589 | - # test run, or from the parent test process if the current |
590 | - # layer is being run in a subprocess. No need to be polite |
591 | - # about killing memcached - just do it quickly. |
592 | + # We can only do unique test allocation and parallelisation if |
593 | + # LP_PERSISTENT_TEST_SERVICES is off. |
594 | if not BaseLayer.persist_test_services: |
595 | + test_instance = str(os.getpid()) |
596 | + os.environ['LP_TEST_INSTANCE'] = test_instance |
597 | + cls.fixture.addCleanup(os.environ.pop, 'LP_TEST_INSTANCE', '') |
598 | + # Kill any Memcached or Librarian left running from a previous |
599 | + # test run, or from the parent test process if the current |
600 | + # layer is being run in a subprocess. No need to be polite |
601 | + # about killing memcached - just do it quickly. |
602 | kill_by_pidfile(MemcachedLayer.getPidFile(), num_polls=0) |
603 | + config_name = 'testrunner_%s' % test_instance |
604 | + cls.make_config(config_name, 'testrunner') |
605 | + app_config_name = 'testrunner-appserver_%s' % test_instance |
606 | + cls.make_config(app_config_name, 'testrunner-appserver') |
607 | + else: |
608 | + config_name = 'testrunner' |
609 | + app_config_name = 'testrunner-appserver' |
610 | + cls.config_name = config_name |
611 | + cls.fixture.addCleanup(setattr, cls, 'config_name', None) |
612 | + cls.appserver_config_name = app_config_name |
613 | + cls.fixture.addCleanup(setattr, cls, 'appserver_config_name', None) |
614 | + use_fixture = ConfigUseFixture(config_name) |
615 | + cls.fixture.addCleanup(use_fixture.cleanUp) |
616 | + use_fixture.setUp() |
617 | # Kill any database left lying around from a previous test run. |
618 | db_fixture = LaunchpadTestSetup() |
619 | try: |
620 | @@ -278,6 +319,7 @@ |
621 | @classmethod |
622 | @profiled |
623 | def tearDown(cls): |
624 | + cls.fixture.cleanUp() |
625 | BaseLayer.isSetUp = False |
626 | |
627 | @classmethod |
628 | @@ -1636,8 +1678,17 @@ |
629 | # configs/testrunner-appserver/mail-configure.zcml |
630 | smtp_controller = None |
631 | |
632 | - # The DB fixture in use |
633 | - _db_fixture = None |
634 | + @classmethod |
635 | + def _setConfig(cls): |
636 | + """Stash a config for use.""" |
637 | + cls.appserver_config = CanonicalConfig( |
638 | + BaseLayer.appserver_config_name, 'runlaunchpad') |
639 | + |
640 | + @classmethod |
641 | + def setUp(cls): |
642 | + cls._setConfig() |
643 | + cls.startSMTPServer() |
644 | + cls.startAppServer() |
645 | |
646 | @classmethod |
647 | @profiled |
648 | |
649 | === modified file 'lib/canonical/testing/tests/test_layers.py' |
650 | === modified file 'lib/lp/scripts/utilities/lpwindmill.py' |
651 | --- lib/lp/scripts/utilities/lpwindmill.py 2010-08-20 20:31:18 +0000 |
652 | +++ lib/lp/scripts/utilities/lpwindmill.py 2010-10-21 18:14:53 +0000 |
653 | @@ -41,6 +41,7 @@ |
654 | atexit.register(DatabaseLayer.tearDown) |
655 | LibrarianLayer.setUp() |
656 | GoogleServiceLayer.setUp() |
657 | + LayerProcessController._setConfig() |
658 | LayerProcessController.startSMTPServer() |
659 | LayerProcessController.startAppServer() |
660 | sys.stderr.write('done.\n') |
661 | |
662 | === modified file 'lib/lp/services/mail/sendmail.py' |
663 | --- lib/lp/services/mail/sendmail.py 2010-09-04 05:37:08 +0000 |
664 | +++ lib/lp/services/mail/sendmail.py 2010-10-21 18:14:53 +0000 |
665 | @@ -421,7 +421,7 @@ |
666 | # should be fine. |
667 | # TODO: Store a timeline action for zopeless mail. |
668 | |
669 | - if config.instance_name == 'testrunner': |
670 | + if config.isTestRunner(): |
671 | # when running in the testing environment, store emails |
672 | TestMailer().send( |
673 | config.canonical.bounce_address, to_addrs, raw_message) |
674 | |
675 | === modified file 'versions.cfg' |
676 | --- versions.cfg 2010-10-21 02:16:57 +0000 |
677 | +++ versions.cfg 2010-10-21 18:14:53 +0000 |
678 | @@ -19,7 +19,7 @@ |
679 | epydoc = 3.0.1 |
680 | FeedParser = 4.1 |
681 | feedvalidator = 0.0.0DEV-r1049 |
682 | -fixtures = 0.3.1 |
683 | +fixtures = 0.3.2 |
684 | functest = 0.8.7 |
685 | funkload = 1.10.0 |
686 | grokcore.component = 1.6 |
Hi Robert,
as mentioned on IRC, I don't feel good about adding new files to the canonical tree if they are Launchpad specific. The feeling has not gone away and I ask you to consider moving canonical.config to lp.config. This will most likely require an extra branch as there are many call sites for canonical.config but they are all mechanical changes.
Also, I am worried about your bumping the version number for fixtures to 0.3.2 when that version is not available from the project page or branch on Launchpad. Neither was 0.3.1 it seems. Intentionally or not, this sneeks unreviewed and unproven code into the Launchpad tree. I have a bad feeling about this practice, too.
Please talk to Brad about both issues and see what he says.
Henning