Merge lp:~lifeless/launchpad/uniqueconfig into lp:launchpad

Proposed by Robert Collins
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
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.

To post a comment you must log in.
Revision history for this message
Henning Eggers (henninge) 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.

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

Revision history for this message
Robert Collins (lifeless) wrote :
Download full text (4.4 KiB)

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://pypi.python.org/pypi/fixtures has 0.3.2 and had that before
this branch was pushed. Its true it wasn't in trunk, that was
oversight. As for changing dependencies, we should either review [the
conten...

Read more...

Revision history for this message
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

Revision history for this message
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.

Revision history for this message
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

Revision history for this message
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://pypi.python.org/pypi/fixtures. I looked for fixtures on Launchpad and found https://launchpad.net/python-fixtures which now seems out of date to me. A link to python.org would be a real improvement here. I have never been confronted with assessing external dependencies and am surprised to hear we don't audit them currently. That sounds like a hole in our process.

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

review: Needs Fixing (code)
Revision history for this message
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

Revision history for this message
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.

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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