← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~cjwatson/launchpad-buildd/build-snap-operation into lp:launchpad-buildd

 

Colin Watson has proposed merging lp:~cjwatson/launchpad-buildd/build-snap-operation into lp:launchpad-buildd with lp:~cjwatson/launchpad-buildd/build-livefs-operation as a prerequisite.

Commit message:
Convert buildsnap to the new Operation framework and add unit tests.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~cjwatson/launchpad-buildd/build-snap-operation/+merge/328659
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~cjwatson/launchpad-buildd/build-snap-operation into lp:launchpad-buildd.
=== added file 'bin/buildsnap'
--- bin/buildsnap	1970-01-01 00:00:00 +0000
+++ bin/buildsnap	2017-08-07 11:46:50 +0000
@@ -0,0 +1,25 @@
+#! /usr/bin/python -u
+#
+# Copyright 2015-2017 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Build a snap."""
+
+from __future__ import print_function
+
+__metaclass__ = type
+
+import os.path
+import sys
+
+from lpbuildd.target.build_snap import BuildSnap
+from lpbuildd.target.operation import configure_logging
+
+
+def main():
+    configure_logging()
+    return BuildSnap(os.path.dirname(__file__)).run()
+
+
+if __name__ == "__main__":
+    sys.exit(main())

=== modified file 'debian/changelog'
--- debian/changelog	2017-08-07 11:46:50 +0000
+++ debian/changelog	2017-08-07 11:46:50 +0000
@@ -21,6 +21,7 @@
     unit tests.
   * Rewrite scan-for-processes in Python, allowing it to have unit tests.
   * Convert buildlivefs to the new Operation framework and add unit tests.
+  * Convert buildsnap to the new Operation framework and add unit tests.
 
  -- Colin Watson <cjwatson@xxxxxxxxxx>  Tue, 25 Jul 2017 23:07:58 +0100
 

=== modified file 'lpbuildd/snap.py'
--- lpbuildd/snap.py	2017-08-07 11:46:50 +0000
+++ lpbuildd/snap.py	2017-08-07 11:46:50 +0000
@@ -64,11 +64,7 @@
 
     def doRunBuild(self):
         """Run the process to build the snap."""
-        args = [
-            "buildsnap",
-            "--build-id", self._buildid,
-            "--arch", self.arch_tag,
-            ]
+        args = ["buildsnap"]
         if self.proxy_url:
             args.extend(["--proxy-url", self.proxy_url])
         if self.revocation_endpoint:
@@ -80,7 +76,7 @@
         if self.git_path is not None:
             args.extend(["--git-path", self.git_path])
         args.append(self.name)
-        self.runSubProcess(self.build_snap_path, args)
+        self.runTargetSubProcess(self.build_snap_path, args)
 
     def iterate_BUILD_SNAP(self, retcode):
         """Finished building the snap."""

=== renamed file 'bin/buildsnap' => 'lpbuildd/target/build_snap.py' (properties changed: +x to -x)
--- bin/buildsnap	2017-07-28 11:15:51 +0000
+++ lpbuildd/target/build_snap.py	2017-08-07 11:46:50 +0000
@@ -1,103 +1,94 @@
-#! /usr/bin/python -u
 # Copyright 2015-2017 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
-"""A script that builds a snap."""
-
 from __future__ import print_function
 
 __metaclass__ = type
 
 import base64
+from collections import OrderedDict
 import json
-from optparse import OptionParser
-import os
+import logging
+import os.path
 import subprocess
-import sys
-import traceback
 import urllib2
 from urlparse import urlparse
 
-from lpbuildd.util import (
-    set_personality,
-    shell_escape,
-    )
-
-
-RETCODE_SUCCESS = 0
+from lpbuildd.target.operation import Operation
+from lpbuildd.util import shell_escape
+
+
 RETCODE_FAILURE_INSTALL = 200
 RETCODE_FAILURE_BUILD = 201
 
 
-def get_build_path(build_id, *extra):
-    """Generate a path within the build directory.
-
-    :param build_id: the build id to use.
-    :param extra: the extra path segments within the build directory.
-    :return: the generated path.
-    """
-    return os.path.join(os.environ["HOME"], "build-" + build_id, *extra)
-
-
-class SnapBuilder:
-    """Builds a snap."""
-
-    def __init__(self, options, name):
-        self.options = options
-        self.name = name
-        self.chroot_path = get_build_path(
-            self.options.build_id, 'chroot-autobuild')
-        # Set to False for local testing if your chroot doesn't have an
+logger = logging.getLogger(__name__)
+
+
+class BuildSnap(Operation):
+
+    description = "Build a snap."
+
+    def __init__(self, slavebin, args=None):
+        super(BuildSnap, self).__init__(args=args)
+        self.slavebin = slavebin
+        # Set to False for local testing if your target doesn't have an
         # appropriate certificate for your codehosting system.
         self.ssl_verify = True
 
-    def chroot(self, args, echo=False, get_output=False):
-        """Run a command in the chroot.
-
-        :param args: the command and arguments to run.
-        :param echo: if True, print the command before executing it.
-        :param get_output: if True, return the output from the command.
-        """
-        args = set_personality(args, self.options.arch)
-        if echo:
-            print(
-                "Running in chroot: %s" % ' '.join(
-                "'%s'" % arg for arg in args))
-            sys.stdout.flush()
-        cmd = ["/usr/bin/sudo", "/usr/sbin/chroot", self.chroot_path] + args
-        if get_output:
-            return subprocess.check_output(cmd, universal_newlines=True)
-        else:
-            subprocess.check_call(cmd)
-
-    def run_build_command(self, args, path="/build", env=None, echo=False,
-                          get_output=False):
-        """Run a build command in the chroot.
-
-        This is unpleasant because we need to run it in /build under sudo
-        chroot, and there's no way to do this without either a helper
-        program in the chroot or unpleasant quoting.  We go for the
+    def make_parser(self):
+        parser = super(BuildSnap, self).make_parser()
+        build_from_group = parser.add_mutually_exclusive_group(required=True)
+        build_from_group.add_argument(
+            "--branch", metavar="BRANCH", help="build from this Bazaar branch")
+        build_from_group.add_argument(
+            "--git-repository", metavar="REPOSITORY",
+            help="build from this Git repository")
+        parser.add_argument(
+            "--git-path", metavar="REF-PATH",
+            help="build from this ref path in REPOSITORY")
+        parser.add_argument("--proxy-url", help="builder proxy url")
+        parser.add_argument(
+            "--revocation-endpoint",
+            help="builder proxy token revocation endpoint")
+        parser.add_argument("name", help="name of snap to build")
+        return parser
+
+    def parse_args(self, args=None):
+        parser = self.make_parser()
+        parsed_args = parser.parse_args(args=args)
+        if (parsed_args.git_repository is None and
+                parsed_args.git_path is not None):
+            parser.error("--git-path requires --git-repository")
+        self.args = parsed_args
+
+    def run_build_command(self, args, path="/build", env=None,
+                          get_output=False, echo=False):
+        """Run a build command in the target.
+
+        This is unpleasant because we need to run it with /build as the
+        working directory, and there's no way to do this without either a
+        helper program in the target or unpleasant quoting.  We go for the
         unpleasant quoting.
 
         :param args: the command and arguments to run.
-        :param path: the working directory to use in the chroot.
+        :param path: the working directory to use in the target.
         :param env: dictionary of additional environment variables to set.
+        :param get_output: if True, return the output from the command.
         :param echo: if True, print the command before executing it.
-        :param get_output: if True, return the output from the command.
         """
         args = [shell_escape(arg) for arg in args]
         path = shell_escape(path)
-        full_env = {
-            "LANG": "C.UTF-8",
-            }
+        full_env = OrderedDict()
+        full_env["LANG"] = "C.UTF-8"
         if env:
             full_env.update(env)
         args = ["env"] + [
             "%s=%s" % (key, shell_escape(value))
             for key, value in full_env.items()] + args
         command = "cd %s && %s" % (path, " ".join(args))
-        return self.chroot(
-            ["/bin/sh", "-c", command], echo=echo, get_output=get_output)
+        return self.backend.run(
+            ["/bin/sh", "-c", command], get_output=get_output, echo=echo)
 
     def save_status(self, status):
         """Save a dictionary of status information about this build.
@@ -105,161 +96,129 @@
         This will be picked up by the build manager and included in XML-RPC
         status responses.
         """
-        status_path = get_build_path(self.options.build_id, "status")
+        status_path = os.path.join(self.backend.build_path, "status")
         with open("%s.tmp" % status_path, "w") as status_file:
             json.dump(status, status_file)
         os.rename("%s.tmp" % status_path, status_path)
 
     def install(self):
-        print("Running install phase...")
+        logger.info("Running install phase...")
         deps = ["snapcraft"]
-        if self.options.branch is not None:
+        if self.args.branch is not None:
             deps.append("bzr")
         else:
             deps.append("git")
-        if self.options.proxy_url:
+        if self.args.proxy_url:
             deps.extend(["python3", "socat"])
-        self.chroot(["apt-get", "-y", "install"] + deps)
-        if self.options.proxy_url:
-            subprocess.check_call([
-                "sudo", "cp", "-a",
-                os.path.join(os.path.dirname(__file__), "snap-git-proxy"),
-                os.path.join(
-                    self.chroot_path, "usr", "local", "bin", "snap-git-proxy"),
-                ])
+        self.backend.run(["apt-get", "-y", "install"] + deps)
+        if self.args.proxy_url:
+            self.backend.copy_in(
+                os.path.join(self.slavebin, "snap-git-proxy"),
+                "/usr/local/bin/snap-git-proxy")
 
     def repo(self):
         """Collect git or bzr branch."""
-        print("Running repo phase...")
-        env = {}
-        if self.options.proxy_url:
-            env["http_proxy"] = self.options.proxy_url
-            env["https_proxy"] = self.options.proxy_url
+        logger.info("Running repo phase...")
+        env = OrderedDict()
+        if self.args.proxy_url:
+            env["http_proxy"] = self.args.proxy_url
+            env["https_proxy"] = self.args.proxy_url
             env["GIT_PROXY_COMMAND"] = "/usr/local/bin/snap-git-proxy"
-        if self.options.branch is not None:
+        if self.args.branch is not None:
             self.run_build_command(['ls', '/build'])
-            cmd = ["bzr", "branch", self.options.branch, self.name]
+            cmd = ["bzr", "branch", self.args.branch, self.args.name]
             if not self.ssl_verify:
                 cmd.insert(1, "-Ossl.cert_reqs=none")
         else:
-            assert self.options.git_repository is not None
+            assert self.args.git_repository is not None
             cmd = ["git", "clone"]
-            if self.options.git_path is not None:
-                cmd.extend(["-b", self.options.git_path])
-            cmd.extend([self.options.git_repository, self.name])
+            if self.args.git_path is not None:
+                cmd.extend(["-b", self.args.git_path])
+            cmd.extend([self.args.git_repository, self.args.name])
             if not self.ssl_verify:
                 env["GIT_SSL_NO_VERIFY"] = "1"
         self.run_build_command(cmd, env=env)
-        if self.options.git_repository is not None:
+        if self.args.git_repository is not None:
             try:
                 self.run_build_command(
-                    ["git", "-C", self.name,
+                    ["git", "-C", self.args.name,
                      "submodule", "update", "--init", "--recursive"],
                     env=env)
             except subprocess.CalledProcessError as e:
-                print(
+                logger.error(
                     "'git submodule update --init --recursive failed with "
                     "exit code %s (build may fail later)" % e.returncode)
         status = {}
-        if self.options.branch is not None:
+        if self.args.branch is not None:
             status["revision_id"] = self.run_build_command(
-                ["bzr", "revno", self.name], get_output=True).rstrip("\n")
+                ["bzr", "revno", self.args.name],
+                get_output=True).rstrip("\n")
         else:
             rev = (
-                self.options.git_path
-                if self.options.git_path is not None else "HEAD")
+                self.args.git_path
+                if self.args.git_path is not None else "HEAD")
             status["revision_id"] = self.run_build_command(
-                ["git", "-C", self.name, "rev-parse", rev],
+                ["git", "-C", self.args.name, "rev-parse", rev],
                 get_output=True).rstrip("\n")
         self.save_status(status)
 
     def pull(self):
         """Run pull phase."""
-        print("Running pull phase...")
-        env = {
-            "SNAPCRAFT_LOCAL_SOURCES": "1",
-            "SNAPCRAFT_SETUP_CORE": "1",
-            }
-        if self.options.proxy_url:
-            env["http_proxy"] = self.options.proxy_url
-            env["https_proxy"] = self.options.proxy_url
+        logger.info("Running pull phase...")
+        env = OrderedDict()
+        env["SNAPCRAFT_LOCAL_SOURCES"] = "1"
+        env["SNAPCRAFT_SETUP_CORE"] = "1"
+        if self.args.proxy_url:
+            env["http_proxy"] = self.args.proxy_url
+            env["https_proxy"] = self.args.proxy_url
             env["GIT_PROXY_COMMAND"] = "/usr/local/bin/snap-git-proxy"
         self.run_build_command(
             ["snapcraft", "pull"],
-            path=os.path.join("/build", self.name),
+            path=os.path.join("/build", self.args.name),
             env=env)
 
     def build(self):
         """Run all build, stage and snap phases."""
-        print("Running build phase...")
-        env = {}
-        if self.options.proxy_url:
-            env["http_proxy"] = self.options.proxy_url
-            env["https_proxy"] = self.options.proxy_url
+        logger.info("Running build phase...")
+        env = OrderedDict()
+        if self.args.proxy_url:
+            env["http_proxy"] = self.args.proxy_url
+            env["https_proxy"] = self.args.proxy_url
             env["GIT_PROXY_COMMAND"] = "/usr/local/bin/snap-git-proxy"
         self.run_build_command(
-            ["snapcraft"], path=os.path.join("/build", self.name), env=env)
+            ["snapcraft"],
+            path=os.path.join("/build", self.args.name),
+            env=env)
 
     def revoke_token(self):
         """Revoke builder proxy token."""
-        print("Revoking proxy token...")
-        url = urlparse(self.options.proxy_url)
+        logger.info("Revoking proxy token...")
+        url = urlparse(self.args.proxy_url)
         auth = '{}:{}'.format(url.username, url.password)
         headers = {
             'Authorization': 'Basic {}'.format(base64.b64encode(auth))
             }
-        req = urllib2.Request(self.options.revocation_endpoint, None, headers)
+        req = urllib2.Request(self.args.revocation_endpoint, None, headers)
         req.get_method = lambda: 'DELETE'
         try:
             urllib2.urlopen(req)
-        except (urllib2.HTTPError, urllib2.URLError) as e:
-            print('Unable to revoke token for %s: %s' % (url.username, e))
-
-
-def main():
-    parser = OptionParser("%prog [options] NAME")
-    parser.add_option("--build-id", help="build identifier")
-    parser.add_option(
-        "--arch", metavar="ARCH", help="build for architecture ARCH")
-    parser.add_option(
-        "--branch", metavar="BRANCH", help="build from this Bazaar branch")
-    parser.add_option(
-        "--git-repository", metavar="REPOSITORY",
-        help="build from this Git repository")
-    parser.add_option(
-        "--git-path", metavar="REF-PATH",
-        help="build from this ref path in REPOSITORY")
-    parser.add_option("--proxy-url", help="builder proxy url")
-    parser.add_option("--revocation-endpoint",
-                      help="builder proxy token revocation endpoint")
-    options, args = parser.parse_args()
-    if options.git_repository is None and options.git_path is not None:
-        parser.error("--git-path requires --git-repository")
-    if (options.branch is None) == (options.git_repository is None):
-        parser.error(
-            "must provide exactly one of --branch and --git-repository")
-    if len(args) != 1:
-        parser.error(
-            "must provide a package name and no other positional arguments")
-    [name] = args
-    builder = SnapBuilder(options, name)
-    try:
-        builder.install()
-    except Exception:
-        traceback.print_exc()
-        return RETCODE_FAILURE_INSTALL
-    try:
-        builder.repo()
-        builder.pull()
-        builder.build()
-    except Exception:
-        traceback.print_exc()
-        return RETCODE_FAILURE_BUILD
-    finally:
-        if options.revocation_endpoint is not None:
-            builder.revoke_token()
-    return RETCODE_SUCCESS
-
-
-if __name__ == "__main__":
-    sys.exit(main())
+        except (urllib2.HTTPError, urllib2.URLError):
+            logger.exception('Unable to revoke token for %s', url.username)
+
+    def run(self):
+        try:
+            self.install()
+        except Exception:
+            logger.exception('Install failed')
+            return RETCODE_FAILURE_INSTALL
+        try:
+            self.repo()
+            self.pull()
+            self.build()
+        except Exception:
+            logger.exception('Build failed')
+            return RETCODE_FAILURE_BUILD
+        finally:
+            if self.args.revocation_endpoint is not None:
+                self.revoke_token()
+        return 0

=== modified file 'lpbuildd/target/tests/test_build_livefs.py'
--- lpbuildd/target/tests/test_build_livefs.py	2017-08-07 11:46:50 +0000
+++ lpbuildd/target/tests/test_build_livefs.py	2017-08-07 11:46:50 +0000
@@ -5,6 +5,7 @@
 
 import subprocess
 
+from fixtures import FakeLogger
 from testtools import TestCase
 from testtools.matchers import (
     AnyMatch,
@@ -151,6 +152,7 @@
                 if run_args[0] == "apt-get":
                     raise subprocess.CalledProcessError(1, run_args)
 
+        self.useFixture(FakeLogger())
         args = [
             "--backend=fake", "--series=xenial", "--arch=amd64", "1",
             "--project=ubuntu",
@@ -166,6 +168,7 @@
                 if run_args[0] == "/bin/sh":
                     raise subprocess.CalledProcessError(1, run_args)
 
+        self.useFixture(FakeLogger())
         args = [
             "--backend=fake", "--series=xenial", "--arch=amd64", "1",
             "--project=ubuntu",

=== added file 'lpbuildd/target/tests/test_build_snap.py'
--- lpbuildd/target/tests/test_build_snap.py	1970-01-01 00:00:00 +0000
+++ lpbuildd/target/tests/test_build_snap.py	2017-08-07 11:46:50 +0000
@@ -0,0 +1,392 @@
+# Copyright 2017 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+__metaclass__ = type
+
+import json
+import os.path
+import stat
+import subprocess
+
+from fixtures import (
+    FakeLogger,
+    TempDir,
+    )
+from systemfixtures import FakeFilesystem
+from testtools import TestCase
+from testtools.matchers import (
+    AnyMatch,
+    Equals,
+    Is,
+    MatchesAll,
+    MatchesDict,
+    MatchesListwise,
+    )
+
+from lpbuildd.target.build_snap import (
+    BuildSnap,
+    RETCODE_FAILURE_BUILD,
+    RETCODE_FAILURE_INSTALL,
+    )
+from lpbuildd.tests.fakeslave import FakeMethod
+
+
+class RanCommand(MatchesListwise):
+
+    def __init__(self, args, get_output=None, echo=None, **env):
+        kwargs_matcher = {}
+        if get_output is not None:
+            kwargs_matcher["get_output"] = Is(get_output)
+        if echo is not None:
+            kwargs_matcher["echo"] = Is(echo)
+        if env:
+            kwargs_matcher["env"] = MatchesDict(env)
+        super(RanCommand, self).__init__(
+            [Equals((args,)), MatchesDict(kwargs_matcher)])
+
+
+class RanAptGet(RanCommand):
+
+    def __init__(self, *args):
+        super(RanAptGet, self).__init__(["apt-get", "-y"] + list(args))
+
+
+class RanBuildCommand(RanCommand):
+
+    def __init__(self, command, path="/build", get_output=False):
+        super(RanBuildCommand, self).__init__(
+            ["/bin/sh", "-c", "cd %s && %s" % (path, command)],
+            get_output=get_output, echo=False)
+
+
+class FakeRevisionID(FakeMethod):
+
+    def __init__(self, revision_id):
+        super(FakeRevisionID, self).__init__()
+        self.revision_id = revision_id
+
+    def __call__(self, run_args, *args, **kwargs):
+        super(FakeRevisionID, self).__call__(run_args, *args, **kwargs)
+        if run_args[0] == "/bin/sh":
+            command = run_args[2]
+            if "bzr revno" in command or "rev-parse" in command:
+                return "%s\n" % self.revision_id
+
+
+class TestBuildSnap(TestCase):
+
+    def test_run_build_command_no_env(self):
+        args = [
+            "--backend=fake", "--series=xenial", "--arch=amd64", "1",
+            "--branch", "lp:foo", "test-snap",
+            ]
+        build_snap = BuildSnap("/slavebin", args=args)
+        build_snap.run_build_command(["echo", "hello world"])
+        self.assertThat(build_snap.backend.run.calls, MatchesListwise([
+            RanBuildCommand("env LANG=C.UTF-8 echo 'hello world'"),
+            ]))
+
+    def test_run_build_command_env(self):
+        args = [
+            "--backend=fake", "--series=xenial", "--arch=amd64", "1",
+            "--branch", "lp:foo", "test-snap",
+            ]
+        build_snap = BuildSnap("/slavebin", args=args)
+        build_snap.run_build_command(
+            ["echo", "hello world"], env={"FOO": "bar baz"})
+        self.assertThat(build_snap.backend.run.calls, MatchesListwise([
+            RanBuildCommand(
+                "env LANG=C.UTF-8 FOO='bar baz' echo 'hello world'"),
+            ]))
+
+    def test_install_bzr(self):
+        args = [
+            "--backend=fake", "--series=xenial", "--arch=amd64", "1",
+            "--branch", "lp:foo", "test-snap"
+            ]
+        build_snap = BuildSnap("/slavebin", args=args)
+        build_snap.install()
+        self.assertThat(build_snap.backend.run.calls, MatchesListwise([
+            RanAptGet("install", "snapcraft", "bzr"),
+            ]))
+
+    def test_install_git(self):
+        args = [
+            "--backend=fake", "--series=xenial", "--arch=amd64", "1",
+            "--git-repository", "lp:foo", "test-snap"
+            ]
+        build_snap = BuildSnap("/slavebin", args=args)
+        build_snap.install()
+        self.assertThat(build_snap.backend.run.calls, MatchesListwise([
+            RanAptGet("install", "snapcraft", "git"),
+            ]))
+
+    def test_install_proxy(self):
+        args = [
+            "--backend=fake", "--series=xenial", "--arch=amd64", "1",
+            "--git-repository", "lp:foo",
+            "--proxy-url", "http://proxy.example:3128/";,
+            "test-snap",
+            ]
+        build_snap = BuildSnap("/slavebin", args=args)
+        self.useFixture(FakeFilesystem()).add("/slavebin")
+        os.mkdir("/slavebin")
+        with open("/slavebin/snap-git-proxy", "w") as proxy_script:
+            proxy_script.write("proxy script\n")
+            os.fchmod(proxy_script.fileno(), 0o755)
+        build_snap.install()
+        self.assertThat(build_snap.backend.run.calls, MatchesListwise([
+            RanAptGet("install", "snapcraft", "git", "python3", "socat"),
+            ]))
+        self.assertEqual(
+            (b"proxy script\n", stat.S_IFREG | 0o755),
+            build_snap.backend.backend_fs["/usr/local/bin/snap-git-proxy"])
+
+    def test_repo_bzr(self):
+        args = [
+            "--backend=fake", "--series=xenial", "--arch=amd64", "1",
+            "--branch", "lp:foo", "test-snap",
+            ]
+        build_snap = BuildSnap("/slavebin", args=args)
+        build_snap.backend.build_path = self.useFixture(TempDir()).path
+        build_snap.backend.run = FakeRevisionID("42")
+        build_snap.repo()
+        env = "env LANG=C.UTF-8 "
+        self.assertThat(build_snap.backend.run.calls, MatchesListwise([
+            RanBuildCommand(env + "ls /build"),
+            RanBuildCommand(env + "bzr branch lp:foo test-snap"),
+            RanBuildCommand(env + "bzr revno test-snap", get_output=True),
+            ]))
+        status_path = os.path.join(build_snap.backend.build_path, "status")
+        with open(status_path) as status:
+            self.assertEqual({"revision_id": "42"}, json.load(status))
+
+    def test_repo_git(self):
+        args = [
+            "--backend=fake", "--series=xenial", "--arch=amd64", "1",
+            "--git-repository", "lp:foo", "test-snap",
+            ]
+        build_snap = BuildSnap("/slavebin", args=args)
+        build_snap.backend.build_path = self.useFixture(TempDir()).path
+        build_snap.backend.run = FakeRevisionID("0" * 40)
+        build_snap.repo()
+        env = "env LANG=C.UTF-8 "
+        self.assertThat(build_snap.backend.run.calls, MatchesListwise([
+            RanBuildCommand(env + "git clone lp:foo test-snap"),
+            RanBuildCommand(
+                env + "git -C test-snap submodule update --init --recursive"),
+            RanBuildCommand(
+                env + "git -C test-snap rev-parse HEAD", get_output=True),
+            ]))
+        status_path = os.path.join(build_snap.backend.build_path, "status")
+        with open(status_path) as status:
+            self.assertEqual({"revision_id": "0" * 40}, json.load(status))
+
+    def test_repo_git_with_path(self):
+        args = [
+            "--backend=fake", "--series=xenial", "--arch=amd64", "1",
+            "--git-repository", "lp:foo", "--git-path", "next", "test-snap",
+            ]
+        build_snap = BuildSnap("/slavebin", args=args)
+        build_snap.backend.build_path = self.useFixture(TempDir()).path
+        build_snap.backend.run = FakeRevisionID("0" * 40)
+        build_snap.repo()
+        env = "env LANG=C.UTF-8 "
+        self.assertThat(build_snap.backend.run.calls, MatchesListwise([
+            RanBuildCommand(env + "git clone -b next lp:foo test-snap"),
+            RanBuildCommand(
+                env + "git -C test-snap submodule update --init --recursive"),
+            RanBuildCommand(
+                env + "git -C test-snap rev-parse next", get_output=True),
+            ]))
+        status_path = os.path.join(build_snap.backend.build_path, "status")
+        with open(status_path) as status:
+            self.assertEqual({"revision_id": "0" * 40}, json.load(status))
+
+    def test_repo_proxy(self):
+        args = [
+            "--backend=fake", "--series=xenial", "--arch=amd64", "1",
+            "--git-repository", "lp:foo",
+            "--proxy-url", "http://proxy.example:3128/";,
+            "test-snap",
+            ]
+        build_snap = BuildSnap("/slavebin", args=args)
+        build_snap.backend.build_path = self.useFixture(TempDir()).path
+        build_snap.backend.run = FakeRevisionID("0" * 40)
+        build_snap.repo()
+        env = (
+            "env LANG=C.UTF-8 http_proxy=http://proxy.example:3128/ "
+            "https_proxy=http://proxy.example:3128/ "
+            "GIT_PROXY_COMMAND=/usr/local/bin/snap-git-proxy ")
+        self.assertThat(build_snap.backend.run.calls, MatchesListwise([
+            RanBuildCommand(env + "git clone lp:foo test-snap"),
+            RanBuildCommand(
+                env + "git -C test-snap submodule update --init --recursive"),
+            RanBuildCommand(
+                "env LANG=C.UTF-8 git -C test-snap rev-parse HEAD",
+                get_output=True),
+            ]))
+        status_path = os.path.join(build_snap.backend.build_path, "status")
+        with open(status_path) as status:
+            self.assertEqual({"revision_id": "0" * 40}, json.load(status))
+
+    def test_pull(self):
+        args = [
+            "--backend=fake", "--series=xenial", "--arch=amd64", "1",
+            "--branch", "lp:foo", "test-snap",
+            ]
+        build_snap = BuildSnap("/slavebin", args=args)
+        build_snap.pull()
+        env = (
+            "env LANG=C.UTF-8 "
+            "SNAPCRAFT_LOCAL_SOURCES=1 SNAPCRAFT_SETUP_CORE=1 ")
+        self.assertThat(build_snap.backend.run.calls, MatchesListwise([
+            RanBuildCommand(env + "snapcraft pull", path="/build/test-snap"),
+            ]))
+
+    def test_pull_proxy(self):
+        args = [
+            "--backend=fake", "--series=xenial", "--arch=amd64", "1",
+            "--branch", "lp:foo", "--proxy-url", "http://proxy.example:3128/";,
+            "test-snap",
+            ]
+        build_snap = BuildSnap("/slavebin", args=args)
+        build_snap.pull()
+        env = (
+            "env LANG=C.UTF-8 "
+            "SNAPCRAFT_LOCAL_SOURCES=1 SNAPCRAFT_SETUP_CORE=1 "
+            "http_proxy=http://proxy.example:3128/ "
+            "https_proxy=http://proxy.example:3128/ "
+            "GIT_PROXY_COMMAND=/usr/local/bin/snap-git-proxy ")
+        self.assertThat(build_snap.backend.run.calls, MatchesListwise([
+            RanBuildCommand(env + "snapcraft pull", path="/build/test-snap"),
+            ]))
+
+    def test_build(self):
+        args = [
+            "--backend=fake", "--series=xenial", "--arch=amd64", "1",
+            "--branch", "lp:foo", "test-snap",
+            ]
+        build_snap = BuildSnap("/slavebin", args=args)
+        build_snap.build()
+        env = "env LANG=C.UTF-8 "
+        self.assertThat(build_snap.backend.run.calls, MatchesListwise([
+            RanBuildCommand(env + "snapcraft", path="/build/test-snap"),
+            ]))
+
+    def test_build_proxy(self):
+        args = [
+            "--backend=fake", "--series=xenial", "--arch=amd64", "1",
+            "--branch", "lp:foo", "--proxy-url", "http://proxy.example:3128/";,
+            "test-snap",
+            ]
+        build_snap = BuildSnap("/slavebin", args=args)
+        build_snap.build()
+        env = (
+            "env LANG=C.UTF-8 "
+            "http_proxy=http://proxy.example:3128/ "
+            "https_proxy=http://proxy.example:3128/ "
+            "GIT_PROXY_COMMAND=/usr/local/bin/snap-git-proxy ")
+        self.assertThat(build_snap.backend.run.calls, MatchesListwise([
+            RanBuildCommand(env + "snapcraft", path="/build/test-snap"),
+            ]))
+
+    # XXX cjwatson 2017-08-07: Test revoke_token.  It may be easiest to
+    # convert it to requests first.
+
+    def test_run_succeeds(self):
+        args = [
+            "--backend=fake", "--series=xenial", "--arch=amd64", "1",
+            "--branch", "lp:foo", "test-snap",
+            ]
+        build_snap = BuildSnap("/slavebin", args=args)
+        build_snap.backend.build_path = self.useFixture(TempDir()).path
+        build_snap.backend.run = FakeRevisionID("42")
+        self.assertEqual(0, build_snap.run())
+        self.assertThat(build_snap.backend.run.calls, MatchesAll(
+            AnyMatch(RanAptGet("install", "snapcraft", "bzr")),
+            AnyMatch(RanBuildCommand(
+                "env LANG=C.UTF-8 bzr branch lp:foo test-snap")),
+            AnyMatch(RanBuildCommand(
+                "env LANG=C.UTF-8 "
+                "SNAPCRAFT_LOCAL_SOURCES=1 SNAPCRAFT_SETUP_CORE=1 "
+                "snapcraft pull", path="/build/test-snap")),
+            AnyMatch(RanBuildCommand(
+                "env LANG=C.UTF-8 snapcraft", path="/build/test-snap")),
+            ))
+
+    def test_run_install_fails(self):
+        class FailInstall(FakeMethod):
+            def __call__(self, run_args, *args, **kwargs):
+                super(FailInstall, self).__call__(run_args, *args, **kwargs)
+                if run_args[0] == "apt-get":
+                    raise subprocess.CalledProcessError(1, run_args)
+
+        self.useFixture(FakeLogger())
+        args = [
+            "--backend=fake", "--series=xenial", "--arch=amd64", "1",
+            "--branch", "lp:foo", "test-snap",
+            ]
+        build_snap = BuildSnap("/slavebin", args=args)
+        build_snap.backend.run = FailInstall()
+        self.assertEqual(RETCODE_FAILURE_INSTALL, build_snap.run())
+
+    def test_run_repo_fails(self):
+        class FailRepo(FakeMethod):
+            def __call__(self, run_args, *args, **kwargs):
+                super(FailRepo, self).__call__(run_args, *args, **kwargs)
+                if run_args[0] == "/bin/sh":
+                    command = run_args[2]
+                    if "bzr branch" in command:
+                        raise subprocess.CalledProcessError(1, run_args)
+
+        self.useFixture(FakeLogger())
+        args = [
+            "--backend=fake", "--series=xenial", "--arch=amd64", "1",
+            "--branch", "lp:foo", "test-snap",
+            ]
+        build_snap = BuildSnap("/slavebin", args=args)
+        build_snap.backend.run = FailRepo()
+        self.assertEqual(RETCODE_FAILURE_BUILD, build_snap.run())
+
+    def test_run_pull_fails(self):
+        class FailPull(FakeMethod):
+            def __call__(self, run_args, *args, **kwargs):
+                super(FailPull, self).__call__(run_args, *args, **kwargs)
+                if run_args[0] == "/bin/sh":
+                    command = run_args[2]
+                    if "bzr revno" in command:
+                        return "42\n"
+                    elif "snapcraft pull" in command:
+                        raise subprocess.CalledProcessError(1, run_args)
+
+        self.useFixture(FakeLogger())
+        args = [
+            "--backend=fake", "--series=xenial", "--arch=amd64", "1",
+            "--branch", "lp:foo", "test-snap",
+            ]
+        build_snap = BuildSnap("/slavebin", args=args)
+        build_snap.backend.build_path = self.useFixture(TempDir()).path
+        build_snap.backend.run = FailPull()
+        self.assertEqual(RETCODE_FAILURE_BUILD, build_snap.run())
+
+    def test_run_build_fails(self):
+        class FailBuild(FakeMethod):
+            def __call__(self, run_args, *args, **kwargs):
+                super(FailBuild, self).__call__(run_args, *args, **kwargs)
+                if run_args[0] == "/bin/sh":
+                    command = run_args[2]
+                    if "bzr revno" in command:
+                        return "42\n"
+                    elif command.endswith(" snapcraft"):
+                        raise subprocess.CalledProcessError(1, run_args)
+
+        self.useFixture(FakeLogger())
+        args = [
+            "--backend=fake", "--series=xenial", "--arch=amd64", "1",
+            "--branch", "lp:foo", "test-snap",
+            ]
+        build_snap = BuildSnap("/slavebin", args=args)
+        build_snap.backend.build_path = self.useFixture(TempDir()).path
+        build_snap.backend.run = FailBuild()
+        self.assertEqual(RETCODE_FAILURE_BUILD, build_snap.run())

=== modified file 'lpbuildd/tests/test_snap.py'
--- lpbuildd/tests/test_snap.py	2017-08-07 11:46:50 +0000
+++ lpbuildd/tests/test_snap.py	2017-08-07 11:46:50 +0000
@@ -76,7 +76,7 @@
         self.assertEqual(SnapBuildState.BUILD_SNAP, self.getState())
         expected_command = [
             "sharepath/slavebin/buildsnap", "buildsnap",
-            "--build-id", self.buildid, "--arch", "i386",
+            "--backend=chroot", "--series=xenial", "--arch=i386", self.buildid,
             "--git-repository", "https://git.launchpad.dev/~example/+git/snap";,
             "--git-path", "master",
             "test-snap",