← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~lifeless/launchpad/librarian into lp:launchpad/devel

 

Robert Collins has proposed merging lp:~lifeless/launchpad/librarian into lp:launchpad/devel with lp:~lifeless/launchpad/databasefixture as a prerequisite.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)
Related bugs:
  #661009 rebooting buildbot slave environment doesn't remove test librarian pid
  https://bugs.launchpad.net/bugs/661009
  #663165 librarian logs should be attached to tests
  https://bugs.launchpad.net/bugs/663165


Prepare the librarian for paralleltests, or at least approximate that.

Highlights:
 - newer fixtures, to get the useFixture helper there
 - allocate ports in the librarian, and use a passed-via-environment root, if LP_TEST_INSTANCE is set
 - use the exposed LibrarianServerFixture.service_config to push a config (and its pushed onto disk, so that other helpers will pick it up).
 - snarf the log file and put it in tests (a bit crude, we don't get 'log for the test', we get 'current log')
-- 
https://code.launchpad.net/~lifeless/launchpad/librarian/+merge/39013
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~lifeless/launchpad/librarian into lp:launchpad/devel.
=== modified file 'Makefile'
--- Makefile	2010-10-21 03:22:06 +0000
+++ Makefile	2010-10-21 04:51:25 +0000
@@ -355,7 +355,7 @@
 			  /var/tmp/bzrsync \
 			  /var/tmp/codehosting.test \
 			  /var/tmp/codeimport \
-			  /var/tmp/fatsam.appserver \
+			  /var/tmp/fatsam.test \
 			  /var/tmp/lperr \
 			  /var/tmp/lperr.test \
 			  /var/tmp/mailman \

=== modified file 'daemons/librarian.tac'
--- daemons/librarian.tac	2010-10-20 18:43:29 +0000
+++ daemons/librarian.tac	2010-10-21 04:51:25 +0000
@@ -4,6 +4,7 @@
 # Twisted Application Configuration file.
 # Use with "twistd2.4 -y <file.tac>", e.g. "twistd -noy server.tac"
 
+import os
 import signal
 
 from meliae import scanner
@@ -25,7 +26,12 @@
 dbconfig.setConfigSection('librarian')
 execute_zcml_for_scripts()
 
-path = config.librarian_server.root
+if os.environ.get('LP_TEST_INSTANCE'):
+    # Running in ephemeral mode: get the root dir from the environment and
+    # dynamically allocate ports.
+    path = os.environ['LP_LIBRARIAN_ROOT']
+else:
+    path = config.librarian_server.root
 if config.librarian_server.upstream_host:
     upstreamHost = config.librarian_server.upstream_host
     upstreamPort = config.librarian_server.upstream_port
@@ -61,15 +67,19 @@
     site.displayTracebacks = False
     strports.service(str(webPort), site).setServiceParent(librarianService)
 
-# Set up the public librarian.
-uploadPort = config.librarian.upload_port
-webPort = config.librarian.download_port
-setUpListener(uploadPort, webPort, restricted=False)
-
-# Set up the restricted librarian.
-webPort = config.librarian.restricted_download_port
-uploadPort = config.librarian.restricted_upload_port
-setUpListener(uploadPort, webPort, restricted=True)
+if os.environ.get('LP_TEST_INSTANCE'):
+    # Running in ephemeral mode: allocate ports on demand.
+    setUpListener(0, 0, restricted=False)
+    setUpListener(0, 0, restricted=True)
+else:
+    # Set up the public librarian.
+    uploadPort = config.librarian.upload_port
+    webPort = config.librarian.download_port
+    setUpListener(uploadPort, webPort, restricted=False)
+    # Set up the restricted librarian.
+    webPort = config.librarian.restricted_download_port
+    uploadPort = config.librarian.restricted_upload_port
+    setUpListener(uploadPort, webPort, restricted=True)
 
 # Log OOPS reports
 set_up_oops_reporting('librarian', 'librarian')

=== modified file 'lib/canonical/launchpad/doc/old-testing.txt'
--- lib/canonical/launchpad/doc/old-testing.txt	2010-10-17 18:31:43 +0000
+++ lib/canonical/launchpad/doc/old-testing.txt	2010-10-21 04:51:25 +0000
@@ -138,22 +138,22 @@
 >>> lpsetup.tearDown()
 
 
-LibrarianTestSetup
-------------------
+LibrarianServerFixture
+----------------------
 
 Code that needs to access the Librarian can do so easily. Note that
-LibrarianTestSetup requires the Launchpad database to be available, and
+LibrarianServerFixture requires the Launchpad database to be available, and
 thus requires LaunchpadTestSetup or similar to be used in tandam.
 You probably really want LaunchpadFunctionalLayer so you can access
 the Librarian as a Utility.
 
->>> from canonical.librarian.testing.server import LibrarianTestSetup
+>>> from canonical.librarian.testing.server import LibrarianServerFixture
 >>> from canonical.launchpad.ftests import login, ANONYMOUS
 >>> from zope.app import zapi
 >>> from canonical.librarian.interfaces import ILibrarianClient
 >>> from StringIO import StringIO
 
->>> librarian = LibrarianTestSetup()
+>>> librarian = LibrarianServerFixture()
 >>> librarian.setUp()
 >>> login(ANONYMOUS)
 

=== modified file 'lib/canonical/librarian/testing/server.py'
--- lib/canonical/librarian/testing/server.py	2010-09-27 02:08:32 +0000
+++ lib/canonical/librarian/testing/server.py	2010-10-21 04:51:25 +0000
@@ -5,18 +5,20 @@
 
 __metaclass__ = type
 __all__ = [
-    'cleanupLibrarianFiles',
     'fillLibrarianFile',
     'LibrarianServerFixture',
-    'LibrarianTestSetup',
     ]
 
 import os
 import shutil
 import tempfile
+from textwrap import dedent
 import warnings
 
-from fixtures import Fixture
+from fixtures import (
+    Fixture,
+    FunctionFixture,
+    )
 
 import canonical
 from canonical.config import config
@@ -32,74 +34,14 @@
 class LibrarianServerFixture(TacTestSetup):
     """Librarian server fixture.
 
-    >>> from urllib import urlopen
-    >>> from canonical.config import config
-
-    >>> librarian_url = "http://%s:%d"; % (
-    ...     config.librarian.download_host,
-    ...     config.librarian.download_port)
-    >>> restricted_librarian_url = "http://%s:%d"; % (
-    ...     config.librarian.restricted_download_host,
-    ...     config.librarian.restricted_download_port)
-
-    >>> fixture = LibrarianServerFixture()
-    >>> fixture.setUp()
-
-    Set a socket timeout, so that this test cannot hang indefinitely.
-
-    >>> import socket
-    >>> print socket.getdefaulttimeout()
-    None
-    >>> socket.setdefaulttimeout(1)
-
-    After setUp() is called, two librarian ports are available:
-    The regular one:
-
-    >>> 'Copyright' in urlopen(librarian_url).read()
-    True
-
-    And the restricted one:
-
-    >>> 'Copyright' in urlopen(restricted_librarian_url).read()
-    True
-
-    The librarian root is also available.
-
-    >>> import os
-    >>> os.path.isdir(config.librarian_server.root)
-    True
-
-    After tearDown() is called, both ports are closed:
-
-    >>> fixture.tearDown()
-
-    >>> urlopen(librarian_url).read()
-    Traceback (most recent call last):
-    ...
-    IOError: ...
-
-    >>> urlopen(restricted_librarian_url).read()
-    Traceback (most recent call last):
-    ...
-    IOError: ...
-
-    The root directory was removed:
-
-    >>> os.path.exists(config.librarian_server.root)
-    False
-
-    That fixture can be started and stopped multiple time in succession:
-
-    >>> fixture.setUp()
-    >>> 'Copyright' in urlopen(librarian_url).read()
-    True
-
-    Tidy up.
-
-    >>> fixture.tearDown()
-    >>> socket.setdefaulttimeout(None)
-
-    :ivar: pid pid of the external process.
+    :ivar service_config: A config fragment with the variables for this
+           service.
+    :ivar root: the root of the server storage area.
+    :ivar upload_port: the port to upload on.
+    :ivar download_port: the port to download from.
+    :ivar restricted_upload_port: the port to upload restricted files on.
+    :ivar restricted_download_port: the port to upload restricted files from.
+    :ivar pid: pid of the external process.
     """
 
     def __init__(self):
@@ -140,9 +82,8 @@
     def clear(self):
         """Clear all files from the Librarian"""
         # Make this smarter if our tests create huge numbers of files
-        root = config.librarian_server.root
-        if os.path.isdir(os.path.join(root, '00')):
-            shutil.rmtree(os.path.join(root, '00'))
+        if os.path.isdir(os.path.join(self.root, '00')):
+            shutil.rmtree(os.path.join(self.root, '00'))
 
     @property
     def pid(self):
@@ -155,22 +96,87 @@
     def _read_pid(self):
         return get_pid_from_file(self.pidfile)
 
+    def _dynamic_config(self):
+        """Is a dynamic config to be used?
+
+        True if LP_TEST_INSTANCE is set in the environment.
+        """
+        return 'LP_TEST_INSTANCE' in os.environ
+
     def _persistent_servers(self):
         return os.environ.get('LP_PERSISTENT_TEST_SERVICES') is not None
 
     @property
     def root(self):
         """The root directory for the librarian file repository."""
-        return config.librarian_server.root
+        if self._dynamic_config():
+            return self._root
+        else:
+            return config.librarian_server.root
 
     def setUpRoot(self):
         """Create the librarian root archive."""
-        # This should not happen in normal usage, but might if someone
-        # interrupts the test suite.
-        if os.path.exists(self.root):
-            self.tearDownRoot()
-        os.makedirs(self.root, 0700)
-        self.addCleanup(self.tearDownRoot)
+        if self._dynamic_config():
+            root_fixture = FunctionFixture(tempfile.mkdtemp, shutil.rmtree)
+            self.useFixture(root_fixture)
+            self._root = root_fixture.fn_result
+            os.chmod(self.root, 0700)
+            # Give the root to the new librarian.
+            os.environ['LP_LIBRARIAN_ROOT'] = self._root
+        else:
+            # This should not happen in normal usage, but might if someone
+            # interrupts the test suite.
+            if os.path.exists(self.root):
+                self.tearDownRoot()
+            self.addCleanup(self.tearDownRoot)
+            os.makedirs(self.root, 0700)
+
+    def _waitForDaemonStartup(self):
+        super(LibrarianServerFixture, self)._waitForDaemonStartup()
+        # Expose the dynamically allocated ports, if we used them.
+        if not self._dynamic_config():
+            self.download_port = config.librarian.download_port
+            self.upload_port = config.librarian.upload_port
+            self.restricted_download_port = \
+                config.librarian.restricted_download_port
+            self.restricted_upload_port = \
+                config.librarian.restricted_upload_port
+            return
+        chunks = self.logChunks()
+        # A typical startup: upload, download, restricted up, restricted down.
+        #2010-10-20 14:28:21+0530 [-] Log opened.
+        #2010-10-20 14:28:21+0530 [-] twistd 10.1.0 (/usr/bin/python 2.6.5) starting up.
+        #2010-10-20 14:28:21+0530 [-] reactor class: twisted.internet.selectreactor.SelectReactor.
+        #2010-10-20 14:28:21+0530 [-] canonical.librarian.libraryprotocol.FileUploadFactory starting on 59090
+        #2010-10-20 14:28:21+0530 [-] Starting factory <canonical.librarian.libraryprotocol.FileUploadFactory instance at 0x6f8ff38>
+        #2010-10-20 14:28:21+0530 [-] twisted.web.server.Site starting on 58000
+        #2010-10-20 14:28:21+0530 [-] Starting factory <twisted.web.server.Site instance at 0x6fb2638>
+        #2010-10-20 14:28:21+0530 [-] canonical.librarian.libraryprotocol.FileUploadFactory starting on 59095
+        #2010-10-20 14:28:21+0530 [-] Starting factory <canonical.librarian.libraryprotocol.FileUploadFactory instance at 0x6fb25f0>
+        #2010-10-20 14:28:21+0530 [-] twisted.web.server.Site starting on 58005
+        self.upload_port = int(chunks[3].split()[-1])
+        self.download_port = int(chunks[5].split()[-1])
+        self.restricted_upload_port = int(chunks[7].split()[-1])
+        self.restricted_download_port = int(chunks[9].split()[-1])
+        self.service_config = dedent("""\
+            [librarian_server]
+            root: %s
+            [librarian]
+            download_port: %s
+            upload_port: %s
+            download_url: http://launchpad.dev:%s/
+            restricted_download_port: %s
+            restricted_upload_port: %s
+            restricted_download_url: http://launchpad_dev:%s/
+            """) % (
+                self.root,
+                self.download_port,
+                self.upload_port,
+                self.download_port,
+                self.restricted_download_port,
+                self.restricted_upload_port,
+                self.restricted_download_port,
+                )
 
     def tearDownRoot(self):
         """Remove the librarian root archive."""
@@ -186,21 +192,26 @@
 
     @property
     def pidfile(self):
-        return os.path.join(self.root, 'librarian.pid')
+        try:
+            return os.path.join(self.root, 'librarian.pid')
+        except AttributeError:
+            # Not setup and using dynamic config: tachandler reads this early.
+            return '/tmp/unused/'
 
     @property
     def logfile(self):
         # Store the log in the server root; if its wanted after a test, that
         # test can use addDetail to grab the log and include it in its 
         # error.
-        return os.path.join(self.root, 'librarian.log')
-
-
-_global_fixture = LibrarianServerFixture()
-
-def LibrarianTestSetup():
-    """Support the stateless lie."""
-    return _global_fixture
+        try:
+            return os.path.join(self.root, 'librarian.log')
+        except AttributeError:
+            # Not setup and using dynamic config: tachandler reads this early.
+            return '/tmp/unused/'
+
+    def logChunks(self):
+        """Get a list with the contents of the librarian log in it."""
+        return open(self.logfile, 'rb').readlines()
 
 
 def fillLibrarianFile(fileid, content='Fake Content'):
@@ -214,8 +225,3 @@
     libfile = open(filepath, 'wb')
     libfile.write(content)
     libfile.close()
-
-
-def cleanupLibrarianFiles():
-    """Remove all librarian files present in disk."""
-    _global_fixture.clear()

=== modified file 'lib/canonical/librarian/testing/tests/test_server_fixture.py'
--- lib/canonical/librarian/testing/tests/test_server_fixture.py	2010-09-26 21:22:04 +0000
+++ lib/canonical/librarian/testing/tests/test_server_fixture.py	2010-10-21 04:51:25 +0000
@@ -1,29 +1,123 @@
 # Copyright 2010 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
+from __future__ import with_statement
+
 """Test the LibrarianServerFixture."""
 
 __metaclass__ = type
 
-import doctest
-import unittest
-
+import os
+from urllib import urlopen
+import socket
+from textwrap import dedent
+
+from testtools.content import Content
+from testtools.content_type import UTF8_TEXT
+
+from canonical.config import config
 from canonical.librarian.testing.server import LibrarianServerFixture
+from canonical.testing.ftests.test_layers import (
+    BaseLayerIsolator,
+    LayerFixture,
+    )
+from canonical.testing.layers import BaseLayer, DatabaseLayer
 from lp.testing import TestCase
 
-def test_suite():
-    result = unittest.TestLoader().loadTestsFromName(__name__)
-    result.addTest(doctest.DocTestSuite(
-            'canonical.librarian.testing.server',
-            optionflags=doctest.NORMALIZE_WHITESPACE | doctest.ELLIPSIS
-            ))
-    return result
-
 
 class TestLibrarianServerFixture(TestCase):
 
-    def test_on_init_no_pid(self):
-        fixture = LibrarianServerFixture()
+    layer = DatabaseLayer
+
+    def skip_if_persistent(self, fixture):
         if fixture._persistent_servers():
             self.skip('persistent server running.')
+
+    def test_on_init_no_pid(self):
+        fixture = LibrarianServerFixture()
+        self.skip_if_persistent(fixture)
         self.assertEqual(None, fixture.pid)
+
+    def test_setUp_allocates_resources(self):
+        fixture = LibrarianServerFixture()
+        self.skip_if_persistent(fixture)
+        with fixture:
+            try:
+                self.assertNotEqual(config.librarian_server.root, fixture.root)
+                self.assertNotEqual(
+                    config.librarian.download_port,
+                    fixture.download_port)
+                self.assertNotEqual(
+                    config.librarian.upload_port,
+                    fixture.upload_port)
+                self.assertNotEqual(
+                    config.librarian.restricted_download_port,
+                    fixture.restricted_download_port)
+                self.assertNotEqual(
+                    config.librarian.restricted_upload_port,
+                    fixture.restricted_upload_port)
+                # And it exposes a config fragment
+                # (Which is not activated by it.
+                expected_config = dedent("""\
+                    [librarian_server]
+                    root: %s
+                    [librarian]
+                    download_port: %s
+                    upload_port: %s
+                    download_url: http://launchpad.dev:%s/
+                    restricted_download_port: %s
+                    restricted_upload_port: %s
+                    restricted_download_url: http://launchpad_dev:%s/
+                    """) % (
+                        fixture.root,
+                        fixture.download_port,
+                        fixture.upload_port,
+                        fixture.download_port,
+                        fixture.restricted_download_port,
+                        fixture.restricted_upload_port,
+                        fixture.restricted_download_port,
+                        )
+                self.assertEqual(expected_config, fixture.service_config)
+            except:
+                self.attachLibrarianLog(fixture)
+                raise
+
+    def test_logChunks(self):
+        fixture = LibrarianServerFixture()
+        with fixture:
+            chunks = fixture.logChunks()
+            self.assertIsInstance(chunks, list)
+        found_started = False
+        for chunk in chunks:
+            if 'daemon ready' in chunk:
+                found_started = True
+        self.assertTrue(found_started)
+
+    def test_smoke_test(self):
+        # Avoid indefinite hangs:
+        import socket
+        self.addCleanup(socket.setdefaulttimeout, socket.getdefaulttimeout())
+        socket.setdefaulttimeout(1)
+        fixture = LibrarianServerFixture()
+        with fixture:
+            librarian_url = "http://%s:%d"; % (
+                config.librarian.download_host,
+                fixture.download_port)
+            restricted_librarian_url = "http://%s:%d"; % (
+                config.librarian.restricted_download_host,
+                fixture.restricted_download_port)
+            # Both download ports work:
+            self.assertTrue('Copyright' in urlopen(librarian_url).read())
+            self.assertTrue(
+                'Copyright' in urlopen(restricted_librarian_url).read())
+            os.path.isdir(fixture.root)
+        # Ports are closed on cleanUp.
+        self.assertRaises(IOError, urlopen, librarian_url)
+        self.assertRaises(IOError, urlopen, restricted_librarian_url)
+        self.assertFalse(os.path.exists(fixture.root))
+        # We can use the fixture again (gets a new URL):
+        with fixture:
+            librarian_url = "http://%s:%d"; % (
+                config.librarian.download_host,
+                fixture.download_port)
+            self.assertTrue('Copyright' in urlopen(librarian_url).read())

=== modified file 'lib/canonical/librarian/tests/test_sigdumpmem.py'
--- lib/canonical/librarian/tests/test_sigdumpmem.py	2010-09-27 01:03:03 +0000
+++ lib/canonical/librarian/tests/test_sigdumpmem.py	2010-10-21 04:51:25 +0000
@@ -9,12 +9,12 @@
 import time
 
 from canonical.librarian.interfaces import DUMP_FILE, SIGDUMPMEM
-from canonical.librarian.testing.server import LibrarianTestSetup
 from canonical.testing.layers import LibrarianLayer
 from lp.testing import TestCase
 
 
 class SIGDUMPMEMTestCase(TestCase):
+
     layer = LibrarianLayer
 
     def test_sigdumpmem(self):
@@ -23,10 +23,8 @@
             os.unlink(DUMP_FILE)
         self.assertFalse(os.path.exists(DUMP_FILE))
 
-        # Use the global instance used by the Layer machinery; it would
-        # be nice to be able to access those without globals / magical
-        # 'constructors'.
-        pid = LibrarianTestSetup().pid
+        # Use the global instance used by the Layer machinery
+        pid = LibrarianLayer.librarian_fixture.pid
 
         # Send the signal and ensure the dump file is created.
         os.kill(pid, SIGDUMPMEM)

=== modified file 'lib/canonical/testing/ftests/test_layers.py'
--- lib/canonical/testing/ftests/test_layers.py	2010-10-21 04:51:19 +0000
+++ lib/canonical/testing/ftests/test_layers.py	2010-10-21 04:51:25 +0000
@@ -1,13 +1,17 @@
 # Copyright 2009 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
+from __future__ import with_statement
+
 """ Test layers
 
 Note that many tests are performed at run time in the layers themselves
 to confirm that the environment hasn't been corrupted by tests
 """
+
 __metaclass__ = type
 
+from contextlib import nested
 from cStringIO import StringIO
 import os
 import signal
@@ -16,6 +20,7 @@
 from urllib import urlopen
 
 from fixtures import (
+    Fixture,
     EnvironmentVariableFixture,
     TestWithFixtures,
     )
@@ -48,30 +53,70 @@
 from lp.services.memcache.client import memcache_client_factory
 
 
+class BaseLayerIsolator(Fixture):
+    """A fixture for isolating BaseLayer.
+    
+    This is useful to test interactions with LP_PERSISTENT_TEST_SERVICES 
+    which makes tests within layers unable to test that easily.
+    """
+
+    def __init__(self, with_persistent=False):
+        """Create a BaseLayerIsolator.
+
+        :param with_persistent: If True LP_PERSISTENT_TEST_SERVICES will
+            be enabled during setUp.
+        """
+        super(BaseLayerIsolator, self).__init__()
+        self.with_persistent = with_persistent
+
+    def setUp(self):
+        super(BaseLayerIsolator, self).setUp()
+        if self.with_persistent:
+            env_value = ''
+        else:
+            env_value = None
+        self.useFixture(
+            EnvironmentVariableFixture('LP_PERSISTENT_TEST_SERVICES',
+            env_value))
+        self.useFixture(EnvironmentVariableFixture('LP_TEST_INSTANCE'))
+
+
+class LayerFixture(Fixture):
+    """Adapt a layer to a fixture.
+
+    Note that the layer setup/teardown are called, not the base class ones.
+
+    :ivar layer: The adapted layer.
+    """
+
+    def __init__(self, layer):
+        """Create a LayerFixture.
+
+        :param layer: The layer to use.
+        """
+        super(LayerFixture, self).__init__()
+        self.layer = layer
+
+    def setUp(self):
+        super(LayerFixture, self).setUp()
+        self.layer.setUp()
+        self.addCleanup(self.layer.tearDown)
+
+
 class TestBaseLayer(testtools.TestCase, TestWithFixtures):
 
     def test_allocates_LP_TEST_INSTANCE(self):
-        self.useFixture(
-            EnvironmentVariableFixture('LP_PERSISTENT_TEST_SERVICES'))
-        self.useFixture(EnvironmentVariableFixture('LP_TEST_INSTANCE'))
-        layer = BaseLayer
-        layer.setUp()
-        try:
-            self.assertEqual(str(os.getpid()), os.environ.get('LP_TEST_INSTANCE'))
-        finally:
-            layer.tearDown()
+        self.useFixture(BaseLayerIsolator())
+        with LayerFixture(BaseLayer):
+            self.assertEqual(
+                str(os.getpid()),
+                os.environ.get('LP_TEST_INSTANCE'))
         self.assertEqual(None, os.environ.get('LP_TEST_INSTANCE'))
 
     def test_persist_test_services_disables_LP_TEST_INSTANCE(self):
-        self.useFixture(
-            EnvironmentVariableFixture('LP_PERSISTENT_TEST_SERVICES', ''))
-        self.useFixture(EnvironmentVariableFixture('LP_TEST_INSTANCE'))
-        layer = BaseLayer
-        layer.setUp()
-        try:
+        self.useFixture(BaseLayerIsolator())
+        with LayerFixture(BaseLayer):
             self.assertEqual(None, os.environ.get('LP_TEST_INSTANCE'))
-        finally:
-            layer.tearDown()
         self.assertEqual(None, os.environ.get('LP_TEST_INSTANCE'))
 
     def test_generates_unique_config(self):
@@ -81,14 +126,10 @@
             EnvironmentVariableFixture('LP_PERSISTENT_TEST_SERVICES'))
         self.useFixture(EnvironmentVariableFixture('LP_TEST_INSTANCE'))
         self.useFixture(EnvironmentVariableFixture('LPCONFIG'))
-        layer = BaseLayer
-        layer.setUp()
-        try:
+        with LayerFixture(BaseLayer):
             self.assertEqual(
                 'testrunner_%s' % os.environ['LP_TEST_INSTANCE'],
                 config.instance_name)
-        finally:
-            layer.tearDown()
         self.assertEqual(orig_instance, config.instance_name)
 
     def test_generates_unique_config_dirs(self):
@@ -96,9 +137,7 @@
             EnvironmentVariableFixture('LP_PERSISTENT_TEST_SERVICES'))
         self.useFixture(EnvironmentVariableFixture('LP_TEST_INSTANCE'))
         self.useFixture(EnvironmentVariableFixture('LPCONFIG'))
-        layer = BaseLayer
-        layer.setUp()
-        try:
+        with LayerFixture(BaseLayer):
             runner_root = 'configs/%s' % config.instance_name
             runner_appserver_root = 'configs/testrunner-appserver_%s' % \
                 os.environ['LP_TEST_INSTANCE']
@@ -106,8 +145,6 @@
                 runner_root + '/launchpad-lazr.conf'))
             self.assertTrue(os.path.isfile(
                 runner_appserver_root + '/launchpad-lazr.conf'))
-        finally:
-            layer.tearDown()
         self.assertFalse(os.path.exists(runner_root))
         self.assertFalse(os.path.exists(runner_appserver_root))
 
@@ -246,6 +283,35 @@
             'foo.txt', len(data), StringIO(data), 'text/plain')
 
 
+class LibrarianLayerTest(testtools.TestCase, TestWithFixtures):
+
+    def test_makes_unique_instance(self):
+        # Capture the original settings
+        default_root = config.librarian_server.root
+        download_port = config.librarian.download_port
+        restricted_download_port = config.librarian.restricted_download_port
+        self.useFixture(BaseLayerIsolator())
+        with nested(
+            LayerFixture(BaseLayer),
+            LayerFixture(DatabaseLayer),
+            ):
+            with LayerFixture(LibrarianLayer):
+                active_root = config.librarian_server.root
+                # The config settings have changed:
+                self.assertNotEqual(default_root, active_root)
+                self.assertNotEqual(
+                    download_port, config.librarian.download_port)
+                self.assertNotEqual(
+                    restricted_download_port,
+                    config.librarian.restricted_download_port)
+                self.assertTrue(os.path.exists(active_root))
+            # This needs more sophistication in the config system (it needs to
+            # affect configs on disk and other perhaps other processes
+            # self.assertEqual(default_root, config.librarian_server.root)
+            # The working dir has to have been deleted.
+            self.assertFalse(os.path.exists(active_root))
+
+
 class LibrarianNoResetTestCase(testtools.TestCase):
     """Our page tests need to run multple tests without destroying
     the librarian database in between.

=== modified file 'lib/canonical/testing/layers.py'
--- lib/canonical/testing/layers.py	2010-10-21 04:51:19 +0000
+++ lib/canonical/testing/layers.py	2010-10-21 04:51:25 +0000
@@ -124,7 +124,7 @@
 from canonical.lazr.timeout import (
     get_default_timeout_function, set_default_timeout_function)
 from canonical.lp import initZopeless
-from canonical.librarian.testing.server import LibrarianTestSetup
+from canonical.librarian.testing.server import LibrarianServerFixture
 from canonical.testing import reset_logging
 from canonical.testing.profiled import profiled
 from canonical.testing.smtpd import SMTPController
@@ -789,35 +789,43 @@
     """
     _reset_between_tests = True
 
-    _is_setup = False
+    librarian_fixture = None
 
     @classmethod
     @profiled
     def setUp(cls):
-        cls._is_setup = True
-        if not LibrarianLayer._reset_between_tests:
+        if not cls._reset_between_tests:
             raise LayerInvariantError(
                     "_reset_between_tests changed before LibrarianLayer "
                     "was actually used."
                     )
-        the_librarian = LibrarianTestSetup()
-        the_librarian.setUp()
-        LibrarianLayer._check_and_reset()
-        atexit.register(the_librarian.tearDown)
+        cls.librarian_fixture = LibrarianServerFixture()
+        cls.librarian_fixture.setUp()
+        BaseLayer.config.add_section(cls.librarian_fixture.service_config)
+        config.reloadConfig()
+        cls._check_and_reset()
+        atexit.register(cls.librarian_fixture.tearDown)
 
     @classmethod
     @profiled
     def tearDown(cls):
-        if not cls._is_setup:
+        # Permit multiple teardowns while we sort out the layering
+        # responsibilities : not desirable though.
+        if cls.librarian_fixture is None:
             return
-        cls._is_setup = False
-        if not LibrarianLayer._reset_between_tests:
-            raise LayerInvariantError(
-                    "_reset_between_tests not reset before LibrarianLayer "
-                    "shutdown"
-                    )
-        LibrarianLayer._check_and_reset()
-        LibrarianTestSetup().tearDown()
+        try:
+            cls._check_and_reset()
+        finally:
+            librarian = cls.librarian_fixture
+            cls.librarian_fixture = None
+            try:
+                if not cls._reset_between_tests:
+                    raise LayerInvariantError(
+                            "_reset_between_tests not reset before LibrarianLayer "
+                            "shutdown"
+                            )
+            finally:
+                librarian.cleanUp()
 
     @classmethod
     @profiled
@@ -836,20 +844,20 @@
                     "the Librarian is restarted if it absolutely must be "
                     "shutdown: " + str(e)
                     )
-        if LibrarianLayer._reset_between_tests:
-            LibrarianTestSetup().clear()
+        if cls._reset_between_tests:
+            cls.librarian_fixture.clear()
 
     @classmethod
     @profiled
     def testSetUp(cls):
-        LibrarianLayer._check_and_reset()
+        cls._check_and_reset()
 
     @classmethod
     @profiled
     def testTearDown(cls):
-        if LibrarianLayer._hidden:
-            LibrarianLayer.reveal()
-        LibrarianLayer._check_and_reset()
+        if cls._hidden:
+            cls.reveal()
+        cls._check_and_reset()
 
     # Flag maintaining state of hide()/reveal() calls
     _hidden = False
@@ -866,17 +874,17 @@
         We do this by altering the configuration so the Librarian client
         looks for the Librarian server on the wrong port.
         """
-        LibrarianLayer._hidden = True
-        if LibrarianLayer._fake_upload_socket is None:
+        cls._hidden = True
+        if cls._fake_upload_socket is None:
             # Bind to a socket, but don't listen to it.  This way we
             # guarantee that connections to the given port will fail.
-            LibrarianLayer._fake_upload_socket = socket.socket(
+            cls._fake_upload_socket = socket.socket(
                 socket.AF_INET, socket.SOCK_STREAM)
             assert config.librarian.upload_host == 'localhost', (
                 'Can only hide librarian if it is running locally')
-            LibrarianLayer._fake_upload_socket.bind(('127.0.0.1', 0))
+            cls._fake_upload_socket.bind(('127.0.0.1', 0))
 
-        host, port = LibrarianLayer._fake_upload_socket.getsockname()
+        host, port = cls._fake_upload_socket.getsockname()
         librarian_data = dedent("""
             [librarian]
             upload_port: %s

=== modified file 'lib/lp/soyuz/browser/tests/distroseriesqueue-views.txt'
--- lib/lp/soyuz/browser/tests/distroseriesqueue-views.txt	2010-10-18 22:24:59 +0000
+++ lib/lp/soyuz/browser/tests/distroseriesqueue-views.txt	2010-10-21 04:51:25 +0000
@@ -294,5 +294,5 @@
 
 We created librarian files that need cleaning up before leaving the test.
 
-  >>> from canonical.librarian.testing.server import cleanupLibrarianFiles
-  >>> cleanupLibrarianFiles()
+  >>> from canonical.testing.layers import LibrarianLayer
+  >>> LibrarianLayer.librarian_fixture.clear()

=== modified file 'lib/lp/soyuz/doc/distroseriesqueue-notify.txt'
--- lib/lp/soyuz/doc/distroseriesqueue-notify.txt	2010-10-18 22:24:59 +0000
+++ lib/lp/soyuz/doc/distroseriesqueue-notify.txt	2010-10-21 04:51:25 +0000
@@ -259,5 +259,5 @@
 
 Clean up, otherwise stuff is left lying around in /var/tmp.
 
-  >>> from canonical.librarian.testing.server import cleanupLibrarianFiles
-  >>> cleanupLibrarianFiles()
+  >>> from canonical.testing.layers import LibrarianLayer
+  >>> LibrarianLayer.librarian_fixture.clear()

=== modified file 'lib/lp/soyuz/doc/distroseriesqueue.txt'
--- lib/lp/soyuz/doc/distroseriesqueue.txt	2010-10-18 22:24:59 +0000
+++ lib/lp/soyuz/doc/distroseriesqueue.txt	2010-10-21 04:51:25 +0000
@@ -1025,8 +1025,8 @@
 
 Clean up the librarian files:
 
-   >>> from canonical.librarian.testing.server import cleanupLibrarianFiles
-   >>> cleanupLibrarianFiles()
+  >>> from canonical.testing.layers import LibrarianLayer
+  >>> LibrarianLayer.librarian_fixture.clear()
 
 
 == Delayed copies ==

=== modified file 'lib/lp/soyuz/scripts/tests/test_queue.py'
--- lib/lp/soyuz/scripts/tests/test_queue.py	2010-10-04 20:46:55 +0000
+++ lib/lp/soyuz/scripts/tests/test_queue.py	2010-10-21 04:51:25 +0000
@@ -33,13 +33,13 @@
     )
 from canonical.launchpad.interfaces.librarian import ILibraryFileAliasSet
 from canonical.librarian.testing.server import (
-    cleanupLibrarianFiles,
     fillLibrarianFile,
     )
 from canonical.librarian.utils import filechunks
 from canonical.testing.layers import (
     DatabaseFunctionalLayer, 
     LaunchpadZopelessLayer,
+    LibrarianLayer,
     )
 from lp.archiveuploader.nascentupload import NascentUpload
 from lp.archiveuploader.tests import (
@@ -152,7 +152,7 @@
 
     def tearDown(self):
         """Remove test contents from disk."""
-        cleanupLibrarianFiles()
+        LibrarianLayer.librarian_fixture.clear()
 
     def uploadPackage(self,
             changesfile="suite/bar_1.0-1/bar_1.0-1_source.changes"):
@@ -998,7 +998,7 @@
         directory used as jail.
         """
         os.chdir(self._home)
-        cleanupLibrarianFiles()
+        LibrarianLayer.librarian_fixture.clear()
         shutil.rmtree(self._jail)
 
     def _listfiles(self):

=== modified file 'lib/lp/soyuz/scripts/tests/test_sync_source.py'
--- lib/lp/soyuz/scripts/tests/test_sync_source.py	2010-10-06 11:46:51 +0000
+++ lib/lp/soyuz/scripts/tests/test_sync_source.py	2010-10-21 04:51:25 +0000
@@ -25,10 +25,12 @@
 from canonical.config import config
 from canonical.launchpad.scripts import BufferLogger
 from canonical.librarian.testing.server import (
-    cleanupLibrarianFiles,
     fillLibrarianFile,
     )
-from canonical.testing.layers import LaunchpadZopelessLayer
+from canonical.testing.layers import (
+    LaunchpadZopelessLayer,
+    LibrarianLayer,
+    )
 from lp.archiveuploader.tagfiles import parse_tagfile
 from lp.registry.interfaces.distribution import IDistributionSet
 from lp.soyuz.scripts.ftpmaster import (
@@ -69,7 +71,7 @@
         """
         super(TestSyncSource, self).tearDown()
         os.chdir(self._home)
-        cleanupLibrarianFiles()
+        LibrarianLayer.librarian_fixture.clear()
         shutil.rmtree(self._jail)
 
     def _listFiles(self):

=== modified file 'lib/lp/soyuz/stories/soyuz/xx-queue-pages.txt'
--- lib/lp/soyuz/stories/soyuz/xx-queue-pages.txt	2010-10-18 22:24:59 +0000
+++ lib/lp/soyuz/stories/soyuz/xx-queue-pages.txt	2010-10-21 04:51:25 +0000
@@ -574,6 +574,6 @@
 
 == Clean up ==
 
-  >>> from canonical.librarian.testing.server import cleanupLibrarianFiles
-  >>> cleanupLibrarianFiles()
+  >>> from canonical.testing.layers import LibrarianLayer
+  >>> LibrarianLayer.librarian_fixture.clear()
 

=== modified file 'lib/lp/testing/__init__.py'
--- lib/lp/testing/__init__.py	2010-10-19 23:04:18 +0000
+++ lib/lp/testing/__init__.py	2010-10-21 04:51:25 +0000
@@ -490,14 +490,27 @@
                 content = Content(UTF8_TEXT, oops.get_chunks)
                 self.addDetail("oops-%d" % i, content)
 
+    def attachLibrarianLog(self, fixture):
+        """Include the logChunks from fixture in the test details."""
+        # Evaluate the log when called, not later, to permit the librarian to
+        # be shutdown before the detail is rendered.
+        chunks = fixture.logChunks()
+        content = Content(UTF8_TEXT, lambda:chunks)
+        self.addDetail('librarian-log', content)
+
     def setUp(self):
         testtools.TestCase.setUp(self)
         from lp.testing.factory import ObjectFactory
+        from canonical.testing.layers import LibrarianLayer
         self.factory = ObjectFactory()
         # Record the oopses generated during the test run.
         self.oopses = []
         self.useFixture(ZopeEventHandlerFixture(self._recordOops))
         self.addCleanup(self.attachOopses)
+        if LibrarianLayer.librarian_fixture is not None:
+            self.addCleanup(
+                self.attachLibrarianLog,
+                LibrarianLayer.librarian_fixture)
 
     @adapter(ErrorReportEvent)
     def _recordOops(self, event):

=== modified file 'versions.cfg'
--- versions.cfg	2010-10-21 04:51:19 +0000
+++ versions.cfg	2010-10-21 04:51:25 +0000
@@ -19,7 +19,7 @@
 epydoc = 3.0.1
 FeedParser = 4.1
 feedvalidator = 0.0.0DEV-r1049
-fixtures = 0.3.2
+fixtures = 0.3.3
 functest = 0.8.7
 funkload = 1.10.0
 grokcore.component = 1.6