← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~bac/lpsetup/lxc-integration into lp:lpsetup

 

Brad Crittenden has proposed merging lp:~bac/lpsetup/lxc-integration into lp:lpsetup.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~bac/lpsetup/lxc-integration/+merge/115804

Add lxc-based integration test.

Discovered a problem with the handling of some command-line options
(working_dir and code_dir) which required some rejiggering.  They were
deleted in favor of specifying the repository and checkout-name
separately.

Refactored the test non-lxc.py in order to share code and structure
with lxc.py.

Fun with bzrlib.  In retrospect the win over just shelling out to
'bzr' was miniscule.
-- 
https://code.launchpad.net/~bac/lpsetup/lxc-integration/+merge/115804
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~bac/lpsetup/lxc-integration into lp:lpsetup.
=== modified file 'README.rst'
--- README.rst	2012-07-11 16:33:16 +0000
+++ README.rst	2012-07-19 18:30:30 +0000
@@ -59,11 +59,15 @@
 Integration tests can be found in lpsetup/tests/integration.  They
 require the "ubuntu" juju charm which, at the time of this writing, is
 not available in the charm store.  Check it out into the local juju
-charm repository from lp:~charmers/charms/precise/ubuntu/trunk.
+charm repository from lp:~charmers/charms/precise/ubuntu/trunk.  The
+test expects your Juju repository to be at ~/juju-charms
 
 The integration tests use a juju environment named "lpsetup-testing" so
 you must create an appropriately configured juju environment with that
-name before running the tests.
+name before running the tests in ~/.juju/environments.yaml.
+
+The tests bootstrap the environment for you and fail if it is already
+running.
 
 You may also want to add this to your SSH .config file to suppress the
 many yes/no prompts:

=== modified file 'commands.rst'
--- commands.rst	2012-07-10 19:06:12 +0000
+++ commands.rst	2012-07-19 18:30:30 +0000
@@ -51,13 +51,13 @@
 Completely sets up an LXC environment with Launchpad using the sandbox
 development model:
 
-~/launchpad
+~/launchpad/lp-branches
     bzr repository with --no-trees option.
-~/launchpad/devel
+~/launchpad/lp-branches/devel
     branch with no trees of trunk.
-~/launchpad/sandbox
+~/launchpad/lp-branches/sandbox
     lightweight checkout with a tree of ../devel.
-~/launchpad/bug-xyz
+~/launchpad/lp-branches/bug-xyz
     branch with no trees of trunk where bug work is done via switching
     inside sandbox.
 

=== modified file 'lpsetup/handlers.py'
--- lpsetup/handlers.py	2012-07-16 16:43:34 +0000
+++ lpsetup/handlers.py	2012-07-19 18:30:30 +0000
@@ -6,6 +6,7 @@
 
 __metaclass__ = type
 __all__ = [
+    'handle_code_dir',
     'handle_directories',
     'handle_lpuser_as_username',
     'handle_lpuser_from_lplogin',
@@ -221,13 +222,13 @@
 
 
 def handle_directories(namespace):
-    """Handle repository, code_dir, and dependencies directories.
+    """Handle repository and dependencies directories.
 
     - The ~ construction is automatically expanded.
     - The validation fails for directories not residing inside the home.
     - The validation fails if the directory contains spaces.
     """
-    for attr in ('repository', 'code_dir', 'dependencies_dir'):
+    for attr in ('repository', 'dependencies_dir'):
         directory = getattr(namespace, attr, None)
         if directory is None:
             continue
@@ -285,11 +286,6 @@
     return os.path.abspath(os.path.expanduser(path))
 
 
-def handle_working_dir(namespace):
-    """Handle path to the working directory."""
-    namespace.working_dir = normalize_path(namespace.working_dir)
-
-
 def handle_branch_and_checkout(namespace):
     """Handle branch and checkout names.
 
@@ -303,6 +299,7 @@
 
         - branch_name
         - checkout_name
+        - repository
 
     This handler does not modify the namespace.
     """
@@ -318,3 +315,12 @@
         raise ValidationError(
             'branch and checkout: can not use the same name ({0}).'.format(
                 checkout_name))
+
+
+def handle_code_dir(namespace):
+    """Handle the computed value for `code_dir`.
+
+    It must be invoked after handle_directories.
+    """
+    namespace.code_dir = os.path.join(
+        namespace.repository, namespace.checkout_name)

=== modified file 'lpsetup/settings.py'
--- lpsetup/settings.py	2012-07-17 17:55:38 +0000
+++ lpsetup/settings.py	2012-07-19 18:30:30 +0000
@@ -3,8 +3,6 @@
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Global settings and defaults for lpsetup."""
-import os.path
-
 
 APT_REPOSITORIES = (
     'deb http://archive.ubuntu.com/ubuntu {distro} multiverse',
@@ -56,7 +54,6 @@
 LP_BRANCH_NAME = 'devel'
 LP_CHECKOUT_NAME = 'sandbox'
 LP_REPOSITORY_DIR = '~/launchpad/lp-branches'
-LP_CODE_DIR = os.path.join(LP_REPOSITORY_DIR, LP_CHECKOUT_NAME)
 LP_PACKAGES = [
     # "launchpad-database-dependencies-9.1" can be removed once 8.x is
     # no longer an option in "launchpad-developer-dependencies.

=== modified file 'lpsetup/subcommands/finish_inithost.py'
--- lpsetup/subcommands/finish_inithost.py	2012-07-16 08:17:05 +0000
+++ lpsetup/subcommands/finish_inithost.py	2012-07-19 18:30:30 +0000
@@ -29,10 +29,15 @@
 
 from lpsetup import argparser
 from lpsetup.handlers import (
+    handle_code_dir,
     handle_directories,
     handle_user,
     )
-from lpsetup.settings import LP_CODE_DIR
+from lpsetup.settings import (
+    LP_CHECKOUT_NAME,
+    LP_REPOSITORY_DIR,
+    )
+
 from lpsetup.utils import call
 
 
@@ -75,22 +80,24 @@
     handlers = (
         handle_user,
         handle_directories,
+        handle_code_dir,
         )
 
-    @staticmethod
-    def add_common_arguments(parser):
-        parser.add_argument(
-            '-c', '--code-dir', default=LP_CODE_DIR,
-            help='The directory of the Launchpad code checkout.  '
-                 'The directory must reside under the home directory of the '
-                 'given user (see -u argument). '
-                 '[DEFAULT={0}]'.format(LP_CODE_DIR))
-
     def add_arguments(self, parser):
         super(SubCommand, self).add_arguments(parser)
-        self.add_common_arguments(parser)
         parser.add_argument(
             '-u', '--user',
             help='The name of the system user.  '
                  'The current user is used if this script is not run as '
                  'root and this argument is omitted.')
+        parser.add_argument(
+            '--checkout-name', default=LP_CHECKOUT_NAME,
+            help='Create a checkout with the given name. '
+                 'Ignored if --no-checkout is specified. '
+                 'Defaults to {0}.'.format(LP_CHECKOUT_NAME))
+        parser.add_argument(
+            '-r', '--repository', default=LP_REPOSITORY_DIR,
+            help='The directory of the Launchpad repository to be created. '
+                 'The directory must reside under the home directory of the '
+                 'given user (see -u argument). '
+                 '[DEFAULT={0}]'.format(LP_REPOSITORY_DIR))

=== modified file 'lpsetup/subcommands/initrepo.py'
--- lpsetup/subcommands/initrepo.py	2012-07-16 10:09:48 +0000
+++ lpsetup/subcommands/initrepo.py	2012-07-19 18:30:30 +0000
@@ -49,27 +49,39 @@
     is_lightweight = not no_checkout
     no_trees = '--no-trees' if is_lightweight else None
     # Initialize the repository.
-    try:
-        call('bzr', 'init-repo', '--quiet', repository, no_trees)
-    except subprocess.CalledProcessError as err:
-        msg = 'Error: unable to initialize the repository: '
-        raise exceptions.ExecutionError(msg + err.output)
+
+    # Skip creating the repository if it already exists.  There is the
+    # risk that it is not made with the same `no_trees` option as
+    # specified here.
+    if not os.path.exists(os.path.join(repository, '.bzr')):
+        try:
+            call('bzr', 'init-repo', '--quiet', repository, no_trees)
+        except subprocess.CalledProcessError as err:
+            msg = 'Error: unable to initialize the repository: '
+            raise exceptions.ExecutionError(msg + err.output)
     # Set up the codebase.
     branch_dir = os.path.join(repository, branch_name)
     # Retrieve the branch.
+    if os.path.exists(branch_dir):
+        # The branch already exists, so we pull to refresh it rather than
+        # creating it.
+        cmd = ['bzr', 'pull', '--overwrite', '-d', branch_dir, source]
+    else:
+        cmd = ['bzr', 'branch', source, branch_dir]
     try:
-        call('bzr', 'branch', source, branch_dir)
+        call(*cmd)
     except subprocess.CalledProcessError as err:
         msg = 'Error: unable to branch source: '
         raise exceptions.ExecutionError(msg + err.output)
     if is_lightweight:
         checkout_dir = os.path.join(repository, checkout_name)
-        # Create a lightweight checkout.
-        try:
-            call('bzr', 'co', '--lightweight', branch_dir, checkout_dir)
-        except subprocess.CalledProcessError as err:
-            msg = 'Error: unable to create the lightweight checkout: '
-            raise exceptions.ExecutionError(msg + err.output)
+        # Create a lightweight checkout if it doesn't exist.
+        if not os.path.exists(checkout_dir):
+            try:
+                call('bzr', 'co', '--lightweight', branch_dir, checkout_dir)
+            except subprocess.CalledProcessError as err:
+                msg = 'Error: unable to create the lightweight checkout: '
+                raise exceptions.ExecutionError(msg + err.output)
 
 
 def setup_bzr_locations(

=== modified file 'lpsetup/subcommands/install_lxc.py'
--- lpsetup/subcommands/install_lxc.py	2012-07-13 14:51:08 +0000
+++ lpsetup/subcommands/install_lxc.py	2012-07-19 18:30:30 +0000
@@ -13,6 +13,7 @@
 import os
 
 from lpsetup.handlers import (
+    handle_code_dir,
     handle_directories,
     handle_source,
     )
@@ -21,7 +22,6 @@
     SCRIPTS,
     )
 from lpsetup.subcommands import (
-    finish_inithost,
     initlxc,
     initrepo,
     update,
@@ -69,11 +69,9 @@
 
 
 def cmd_in_lxc(lxc_name, ssh_key_path, home_dir, args, as_user=None):
-    print "args = ", args
     cmd = this_command(home_dir, args)
     if as_user is not None:
         cmd = "su {} -c '{}'".format(as_user, cmd)
-    print "CMD = ", cmd
     ssh(lxc_name, cmd, key=ssh_key_path)
 
 
@@ -91,18 +89,20 @@
 
 def update_in_lxc(lxc_name, ssh_key_path, home_dir, user, external_path,
                   use_http, checkout_name, repository):
-    working_dir = os.path.join(repository, checkout_name)
     args = [
-        'update', '--external-path', external_path, '-W', working_dir,
+        'update', '--external-path', external_path,
+        '-r', repository, '--checkout-name', checkout_name,
         ]
     if use_http:
         args.append('--use-http')
     cmd_in_lxc(lxc_name, ssh_key_path, home_dir, args, as_user=user)
 
 
-def finish_inithost_in_lxc(lxc_name, ssh_key_path, home_dir, user, code_dir):
+def finish_inithost_in_lxc(lxc_name, ssh_key_path, home_dir, user,
+                           repository, checkout_name):
     args = [
-        'finish-init-host', '--user', user, '--code-dir', code_dir,
+        'finish-init-host', '--user', user, '--repository', repository,
+        '--checkout-name', checkout_name,
         ]
     cmd_in_lxc(lxc_name, ssh_key_path, home_dir, args)
 
@@ -121,14 +121,18 @@
         (update_in_lxc, 'lxc_name', 'ssh_key_path', 'home_dir', 'user',
          'external_path', 'use_http', 'checkout_name', 'repository'),
         (finish_inithost_in_lxc, 'lxc_name', 'ssh_key_path', 'home_dir',
-         'user', 'code_dir'),
+         'user', 'repository', 'checkout_name'),
         )
 
     help = __doc__
 
     def get_handlers(self, namespace):
         handlers = super(SubCommand, self).get_handlers(namespace)
-        return handlers + (handle_directories, handle_source)
+        return handlers + (
+            handle_directories,
+            handle_code_dir,
+            handle_source,
+            )
 
     def call_create_scripts(self, namespace, step, args):
         """Run the `create_scripts` step only if the related flag is set."""
@@ -140,7 +144,6 @@
         # Inherit arguments from subcommands we depend upon.
         initrepo.SubCommand.add_common_arguments(parser)
         update.SubCommand.add_common_arguments(parser)
-        finish_inithost.SubCommand.add_common_arguments(parser)
         # Add parallel testing related arguments.
         parser.add_argument(
             '-C', '--create-scripts', action='store_true',

=== modified file 'lpsetup/subcommands/update.py'
--- lpsetup/subcommands/update.py	2012-07-13 15:55:40 +0000
+++ lpsetup/subcommands/update.py	2012-07-19 18:30:30 +0000
@@ -19,48 +19,49 @@
 from lpsetup import argparser
 from lpsetup import handlers
 from lpsetup.settings import (
-    LP_CODE_DIR,
+    LP_CHECKOUT_NAME,
+    LP_REPOSITORY_DIR,
     LP_SOURCE_DEPS,
     )
 
 
-def initialize_directories(working_dir, external_path):
+def initialize_directories(code_dir, external_path):
     """Initialize the eggs, yui, and sourcecode directories.
 
     Create them if necessary.
     """
     for dir_ in ['eggs', 'yui', 'sourcecode']:
-        mkdirs(os.path.join(working_dir, external_path, dir_))
-
-
-def update_dependencies(working_dir, external_path, use_http):
+        mkdirs(os.path.join(code_dir, external_path, dir_))
+
+
+def update_dependencies(code_dir, external_path, use_http):
     """Update the external dependencies."""
     use_http_param = '--use-http' if use_http else None
-    cmd = os.path.join(working_dir, 'utilities', 'update-sourcecode')
+    cmd = os.path.join(code_dir, 'utilities', 'update-sourcecode')
     abs_external_path = os.path.abspath(
-        os.path.join(working_dir, external_path))
+        os.path.join(code_dir, external_path))
     source_path = os.path.join(abs_external_path, 'sourcecode')
     run(cmd, use_http_param, source_path)
 
     # Update the download cache.
-    download_cache = os.path.join(working_dir, 'download-cache')
+    download_cache = os.path.join(code_dir, 'download-cache')
     if os.path.exists(download_cache):
         run('bzr', 'up', download_cache)
     else:
         run('bzr', 'co', '-v', '--lightweight', LP_SOURCE_DEPS, download_cache)
 
     # Link to the external sourcecode.
-    if abs_external_path != working_dir:
+    if abs_external_path != code_dir:
         cmd = os.path.join(
-            working_dir, 'utilities', 'link-external-sourcecode')
+            code_dir, 'utilities', 'link-external-sourcecode')
         run(cmd,
-            '--target', working_dir,
+            '--target', code_dir,
             '--parent', external_path)
 
 
-def update_tree(working_dir):
-    """Update the tree at working_dir with the latest LP code."""
-    with cd(working_dir):
+def update_tree(code_dir):
+    """Update the tree at code_dir with the latest LP code."""
+    with cd(code_dir):
         run('bzr', 'pull')
 
 
@@ -71,14 +72,16 @@
     """
 
     steps = (
-        (initialize_directories, 'working_dir', 'external_path'),
-        (update_dependencies, 'working_dir', 'external_path', 'use_http'),
-        (update_tree, 'working_dir'),
+        (initialize_directories, 'code_dir', 'external_path'),
+        (update_dependencies, 'code_dir', 'external_path', 'use_http'),
+        (update_tree, 'code_dir'),
         )
     help = __doc__
     handlers = (
         # Normalize paths and default to cwd if none exists.
-        handlers.handle_working_dir,
+        handlers.handle_user,
+        handlers.handle_directories,
+        handlers.handle_code_dir,
         )
 
     @staticmethod
@@ -97,6 +100,13 @@
             help='Force bzr to use http to get the sourcecode '
                  'branches rather than using bzr+ssh.')
         parser.add_argument(
-            '-W', '--working-dir', default=LP_CODE_DIR,
-            help='Path to branch to update.  '
-                 '[DEFAULT={0}]'.format(LP_CODE_DIR))
+            '--checkout-name', default=LP_CHECKOUT_NAME,
+            help='Create a checkout with the given name. '
+                 'Ignored if --no-checkout is specified. '
+                 'Defaults to {0}.'.format(LP_CHECKOUT_NAME))
+        parser.add_argument(
+            '-r', '--repository', default=LP_REPOSITORY_DIR,
+            help='The directory of the Launchpad repository to be created. '
+                 'The directory must reside under the home directory of the '
+                 'given user (see -u argument). '
+                 '[DEFAULT={0}]'.format(LP_REPOSITORY_DIR))

=== added file 'lpsetup/tests/integration/common.py'
--- lpsetup/tests/integration/common.py	1970-01-01 00:00:00 +0000
+++ lpsetup/tests/integration/common.py	2012-07-19 18:30:30 +0000
@@ -0,0 +1,60 @@
+#!/usr/bin/env python
+# Copyright 2012 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Common resources for integration tests."""
+
+__metaclass__ = type
+__all__ = [
+    'IntegrationTestBase',
+    ]
+
+import argparse
+
+
+class IntegrationTestBase:
+
+    banner_width = 70
+    test_type = None
+
+    def __init__(self):
+        self.parser = argparse.ArgumentParser()
+        self.parser.add_argument('--no-tear-down', action='store_true',
+                                 default=False,
+                                 help='Do not run the tear down method for debugging.')
+        self.args = self.parser.parse_args()
+
+    def banner(self, msg, width=banner_width):
+        print '*' * width
+        print '* ' + msg.ljust(width - 4) + ' *'
+        print '*' * width
+
+    def set_up(self):
+        self.banner(
+            'Setting up the test environment for {}.'.format(self.test_type))
+
+    def tear_down(self):
+        self.banner('Cleaning up.')
+
+    def check_environment(self):
+        self.banner('Checking test environment.')
+
+    def do_test(self):
+        self.banner('Running integration tests for {}.'.format(self.test_type))
+
+    def run(self):
+        self.check_environment()
+        try:
+            self.set_up()
+            try:
+                self.do_test()
+            except Exception as err:
+                self.banner('Test failed.  Sorry.')
+                print err
+                return 1
+            else:
+                self.banner('Test succeeded.')
+                return 0
+        finally:
+            if not self.args.no_tear_down:
+                self.tear_down()

=== added file 'lpsetup/tests/integration/lxc.py'
--- lpsetup/tests/integration/lxc.py	1970-01-01 00:00:00 +0000
+++ lpsetup/tests/integration/lxc.py	2012-07-19 18:30:30 +0000
@@ -0,0 +1,110 @@
+#!/usr/bin/env python
+# Copyright 2012 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""A simple, end-to-end integration test."""
+
+from bzrlib.branch import Branch
+from bzrlib.errors import NotBranchError
+from bzrlib.missing import find_unmerged
+from bzrlib.status import show_tree_status
+from bzrlib.workingtree import WorkingTree
+from StringIO import StringIO
+import sys
+
+from shelltoolbox import (
+    run,
+    )
+from subprocess import (
+    check_call,
+    )
+
+from common import IntegrationTestBase
+
+
+LXC_NAME = 'lpsetup-testing'
+
+
+class LXCIntegrationTest(IntegrationTestBase):
+
+    test_type = 'LXC container'
+    repo = '~/launchpad-testing/lp-branches'
+
+    def get_branch(self, dir='.'):
+        branch, _ = Branch.open_containing(dir)
+        return branch
+
+    def get_push_location(self, branch=None):
+        if branch is None:
+            branch = self.get_branch()
+        return branch.get_push_location()
+
+    def check_environment(self):
+        """Be sure the test environment doesn't exist."""
+        # We want to be really sure we do not clobber an existing LXC
+        # container, therefore we make sure one doesn't exist before
+        # proceeding.
+        super(LXCIntegrationTest, self).check_environment()
+        cmd = 'sudo lxc-info -n {}'.format(LXC_NAME)
+        results = run(*cmd.split())
+        if results.split()[1] == 'RUNNING':
+            # The container should not be active.
+            raise RuntimeError(
+                "An LXC container named '{}' is unexpectedly "
+                "running.".format(LXC_NAME))
+        # Ensure the local branch has been pushed.
+        # For some reason 'bzr missing' cannot use a 'lp:' URL.  http or
+        # bzr+ssh URLs work.
+        self.branch = self.get_branch()
+        self.push_location = self.get_push_location(self.branch)
+        if self.push_location.startswith('lp:'):
+            raise RuntimeError(
+                "Push location must use http or bzr+ssh "
+                "protocol: {}".format(self.push_location))
+        # Ensure the branch has been pushed and has no missing revisions.
+        try:
+            push_branch = self.get_branch(self.push_location)
+        except NotBranchError:
+            raise RuntimeError(
+                'The branch has never been pushed to Launchpad.')
+        tree, _ = WorkingTree.open_containing_paths(None)
+        stream = StringIO()
+        show_tree_status(tree, to_file=stream)
+        if stream.getvalue():
+            raise RuntimeError(
+                'The tree has uncommitted changes.  Check them in '
+                'and push to Launchpad.')
+        near, far = find_unmerged(self.branch, push_branch)
+        if near or far:
+            raise RuntimeError('The local branch and the pushed version '
+                               'have diverged.')
+
+    def set_up(self):
+        """Set up the LXC container environment."""
+        super(LXCIntegrationTest, self).set_up()
+        cmd = 'sudo python setup.py install'
+        check_call(cmd.split())
+
+    def do_test(self):
+        """Run an end-to-end integration tests of the non-LXC lpsetup story."""
+        super(LXCIntegrationTest, self).do_test()
+        cmd = 'lp-setup install-lxc -B {} -r {} {}'.format(
+            self.push_location, self.repo, LXC_NAME)
+        check_call(cmd.split())
+
+    def tear_down(self):
+        super(LXCIntegrationTest, self).tear_down()
+
+        def lxc_cmd(cmd_name):
+            cmd = 'sudo {} -n {}'.format(cmd_name, LXC_NAME)
+            check_call(cmd.split())
+
+        try:
+            lxc_cmd('lxc-stop')
+            lxc_cmd('lxc-destroy')
+        except:
+            pass
+
+
+if __name__ == '__main__':
+    sys.exit(LXCIntegrationTest().run())

=== modified file 'lpsetup/tests/integration/non-lxc.py'
--- lpsetup/tests/integration/non-lxc.py	2012-07-10 19:58:56 +0000
+++ lpsetup/tests/integration/non-lxc.py	2012-07-19 18:30:30 +0000
@@ -14,96 +14,79 @@
     PIPE,
     )
 
-
-def banner(s):
-    width = 70
-    print '*' * width
-    print '* ' + s.ljust(width - 4) + ' *'
-    print '*' * width
-
-
-def on_remote(args):
-    if type(args) == str:
-        args = args.split()
-
-    check_call('juju ssh 1 -o StrictHostKeyChecking=no'
-        ' -o UserKnownHostsFile=/dev/null -e lpsetup-testing'.split()
-        + list(args))
-
-
-def check_environment():
-    """Be sure the test environment doesn't exist."""
-    banner('Checking test environment.')
-    # We want to be really sure we do not clobber an already-existing juju
-    # environment.  Therefore we make sure one doesn't exist before
-    # bootstrapping.
-    code = os.system('juju status -e lpsetup-testing')
-    if code == 0:
-        # The "juju status" should have failed.
-        raise RuntimeError('A juju environment unexpectedly exists.')
-
-
-def set_up():
-    """Set up a juju-managed instance to run the tests on."""
-    banner('Setting up the test environment.')
-    check_call('juju bootstrap -e lpsetup-testing'.split())
-    # XXX The "ubuntu" charm is broken, so it has to be loaded from the local
-    # repository.  Get it from lp:~charmers/charms/precise/ubuntu/trunk.
-    check_call('juju deploy local:ubuntu --repository ~/juju-charms'
-        ' -e lpsetup-testing --constraints instance-type=m1.large'.split())
-
-    start_time = time.time()
-    while time.time() - start_time < 600:
-        process = Popen('juju status -e lpsetup-testing'.split(), stdout=PIPE)
-        stdout, stderr = process.communicate()
-        if 'instance-state: running' in stdout:
-            break
-    else:
-        raise RuntimeError('starting the instance took too long')
-
-    # Even though the instance-state is "running", it's still not ready, so
-    # wait a little while before continuing.
-    time.sleep(30)
-
-    on_remote('sudo apt-add-repository --yes ppa:yellow/ppa')
-    on_remote('sudo apt-get update')
-    on_remote('sudo apt-get install python-shelltoolbox')
-    check_call('juju scp -o StrictHostKeyChecking=no'
-        ' -o UserKnownHostsFile=/dev/null -e lpsetup-testing'
-        ' -r . 1:lpsetup'.split())
-
-
-def do_test():
-    """Run an end-to-end integration tests of the non-LXC lpsetup story."""
-    banner('Running (non-LXC) integration test.')
-    # Since the most common scenario is to have bzr whoami setup, we do that
-    # instead of providing the arguments directly to lpsetup.
-    on_remote(('bzr', 'whoami', '"Not A Real Person <no@xxxxxxxxxxx>"'))
-    on_remote('cd lpsetup; ./lp-setup init-host'.split())
-
-
-def tear_down():
-    banner('Cleaning up.')
-    code = os.system('echo y | juju destroy-environment -e lpsetup-testing')
-    if code != 0:
-        raise RuntimeError('Destroying the test juju environment failed.')
-
-
-def main():
-    check_environment()
-    try:
-        set_up()
-        try:
-            do_test()
-        except:
-            banner('Test failed.  Sorry.')
-            return 1
+from common import IntegrationTestBase
+
+
+class NonLXCIntegrationTest(IntegrationTestBase):
+
+    test_type = 'non-LXC target'
+
+    def on_remote(self, args):
+        if type(args) == str:
+            args = args.split()
+
+        check_call('juju ssh 1 -o StrictHostKeyChecking=no'
+            ' -o UserKnownHostsFile=/dev/null -e lpsetup-testing'.split()
+            + list(args))
+
+    def check_environment(self):
+        """Be sure the test environment doesn't exist."""
+        # We want to be really sure we do not clobber an already-existing juju
+        # environment.  Therefore we make sure one doesn't exist before
+        # bootstrapping.
+        super(NonLXCIntegrationTest, self).check_environment()
+        code = os.system('juju status -e lpsetup-testing')
+        if code == 0:
+            # The "juju status" should have failed.
+            raise RuntimeError('A juju environment unexpectedly exists.')
+
+    def set_up(self):
+        """Set up a juju-managed instance to run the tests on."""
+        super(NonLXCIntegrationTest, self).set_up()
+        check_call('juju bootstrap -e lpsetup-testing'.split())
+        # XXX The "ubuntu" charm is broken, so it has to be loaded from the
+        # local repository.  Get it from
+        # lp:~charmers/charms/precise/ubuntu/trunk.
+        check_call('juju deploy local:ubuntu --repository ~/juju-charms'
+            ' -e lpsetup-testing --constraints instance-type=m1.large'.split())
+
+        start_time = time.time()
+        while time.time() - start_time < 600:
+            process = Popen(
+                'juju status -e lpsetup-testing'.split(), stdout=PIPE)
+            stdout, stderr = process.communicate()
+            if 'instance-state: running' in stdout:
+                break
         else:
-            banner('Test succeeded.')
-            return 0
-    finally:
-        tear_down()
+            raise RuntimeError('starting the instance took too long')
+
+        # Even though the instance-state is "running", it's still not ready, so
+        # wait a little while before continuing.
+        time.sleep(30)
+
+        self.on_remote('sudo apt-add-repository --yes ppa:yellow/ppa')
+        self.on_remote('sudo apt-get update')
+        self.on_remote('sudo apt-get install python-shelltoolbox')
+        check_call('juju scp -o StrictHostKeyChecking=no'
+            ' -o UserKnownHostsFile=/dev/null -e lpsetup-testing'
+            ' -r . 1:lpsetup'.split())
+
+    def do_test(self):
+        """Run an end-to-end integration tests of the non-LXC lpsetup story."""
+        # Since the most common scenario is to have bzr whoami setup, we do
+        # that instead of providing the arguments directly to lpsetup.
+        super(NonLXCIntegrationTest, self).do_test()
+        self.on_remote(
+            ('bzr', 'whoami', '"Not A Real Person <no@xxxxxxxxxxx>"'))
+        self.on_remote('cd lpsetup; ./lp-setup init-host'.split())
+
+    def tear_down(self):
+        super(NonLXCIntegrationTest, self).tear_down()
+        code = os.system(
+            'echo y | juju destroy-environment -e lpsetup-testing')
+        if code != 0:
+            raise RuntimeError('Destroying the test juju environment failed.')
 
 
 if __name__ == '__main__':
-    sys.exit(main())
+    sys.exit(NonLXCIntegrationTest().run())

=== modified file 'lpsetup/tests/subcommands/test_finish_inithost.py'
--- lpsetup/tests/subcommands/test_finish_inithost.py	2012-07-12 18:23:37 +0000
+++ lpsetup/tests/subcommands/test_finish_inithost.py	2012-07-19 18:30:30 +0000
@@ -19,9 +19,10 @@
 
 
 def get_arguments():
-    code_dir = '~/' + get_random_string()
+    repo = '~/' + get_random_string()
+    checkout = get_random_string()
     user = get_random_string()
-    return ('-c', code_dir, '-u', user)
+    return ('-r', repo, '--checkout-name', checkout, '-u', user)
 
 
 class FinishInitHostTest(StepsBasedSubCommandTestMixin, unittest.TestCase):
@@ -31,6 +32,7 @@
     expected_handlers = (
         handlers.handle_user,
         handlers.handle_directories,
+        handlers.handle_code_dir,
         )
 
     @property

=== modified file 'lpsetup/tests/subcommands/test_install_lxc.py'
--- lpsetup/tests/subcommands/test_install_lxc.py	2012-07-12 22:12:09 +0000
+++ lpsetup/tests/subcommands/test_install_lxc.py	2012-07-19 18:30:30 +0000
@@ -34,7 +34,8 @@
 
 finish_inithost_in_lxc_step = (
     install_lxc.finish_inithost_in_lxc, [
-        'lxc_name', 'ssh_key_path', 'home_dir', 'user', 'code_dir',
+        'lxc_name', 'ssh_key_path', 'home_dir', 'user', 'repository',
+        'checkout_name',
         ])
 
 
@@ -52,6 +53,7 @@
     expected_arguments = get_arguments()
     expected_handlers = test_initlxc.InitLxcTest.expected_handlers + (
         handlers.handle_directories,
+        handlers.handle_code_dir,
         handlers.handle_source,
         )
     expected_steps = test_initlxc.InitLxcTest.expected_steps + (

=== modified file 'lpsetup/tests/subcommands/test_update.py'
--- lpsetup/tests/subcommands/test_update.py	2012-07-13 15:55:40 +0000
+++ lpsetup/tests/subcommands/test_update.py	2012-07-19 18:30:30 +0000
@@ -18,15 +18,15 @@
     return (
         '--external-path', get_random_string(),
         '--use-http',
-        '-e', '../devel',
-        '-W', '~/' + get_random_string(),
+        '--repository', get_random_string(),
+        '--checkout-name', get_random_string(),
         )
 
 init_dir_step = (
-    update.initialize_directories, ['working_dir', 'external_path'])
+    update.initialize_directories, ['code_dir', 'external_path'])
 update_dep_step = (
-    update.update_dependencies, ['working_dir', 'external_path', 'use_http'])
-update_tree_step = (update.update_tree, ['working_dir'])
+    update.update_dependencies, ['code_dir', 'external_path', 'use_http'])
+update_tree_step = (update.update_tree, ['code_dir'])
 
 
 class UpdateTest(StepsBasedSubCommandTestMixin, unittest.TestCase):
@@ -35,7 +35,9 @@
     sub_command_class = update.SubCommand
     expected_arguments = get_arguments()
     expected_handlers = (
-        handlers.handle_working_dir,
+        handlers.handle_user,
+        handlers.handle_directories,
+        handlers.handle_code_dir,
         )
     expected_steps = (
         init_dir_step,

=== modified file 'pre-commit.sh'
--- pre-commit.sh	2012-07-11 15:50:01 +0000
+++ pre-commit.sh	2012-07-19 18:30:30 +0000
@@ -1,4 +1,4 @@
 #!/bin/bash
 
-pyfiles=`find . -name "*.py" | grep -v distribute_setup.py`
-pocketlint $pyfiles && pep8 $pyfiles && nosetests
+pyfiles=`find . -name build -prune -o -name "*.py" | grep -v distribute_setup.py`
+pocketlint $pyfiles && pep8 --exclude=build $pyfiles && nosetests


Follow ups