Tom Wardill has proposed merging ~twom/launchpad-buildd:add-charm-build into launchpad-buildd:master.
Commit message: Add charm building Requested reviews: Launchpad code reviewers (launchpad-reviewers) For more details, see: https://code.launchpad.net/~twom/launchpad-buildd/+git/launchpad-buildd/+merge/403811 Add building for charms using charmcraft with local network support. -- Your team Launchpad code reviewers is requested to review the proposed merge of ~twom/launchpad-buildd:add-charm-build into launchpad-buildd:master.
diff --git a/debian/changelog b/debian/changelog index 26c21ed..6543629 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +launchpad-buildd (197) UNRELEASED; urgency=medium + + * Add charm building support + + -- Tom Wardill <[email protected]> Mon, 07 Jun 2021 10:22:50 +0100 + launchpad-buildd (196) bionic; urgency=medium * Handle symlinks in OCI image files diff --git a/lpbuildd/buildd-slave.tac b/lpbuildd/buildd-slave.tac index 3299b0d..7c1947e 100644 --- a/lpbuildd/buildd-slave.tac +++ b/lpbuildd/buildd-slave.tac @@ -23,6 +23,7 @@ from twisted.web import ( from lpbuildd.binarypackage import BinaryPackageBuildManager from lpbuildd.builder import XMLRPCBuilder +from lpbuildd.charm import CharmBuildManager from lpbuildd.oci import OCIBuildManager from lpbuildd.livefs import LiveFilesystemBuildManager from lpbuildd.log import RotatableFileLogObserver @@ -47,6 +48,7 @@ builder.registerManager( builder.registerManager(LiveFilesystemBuildManager, "livefs") builder.registerManager(SnapBuildManager, "snap") builder.registerManager(OCIBuildManager, "oci") +builder.registerManager(CharmBuildManager, "charm") application = service.Application('Builder') application.addComponent( diff --git a/lpbuildd/charm.py b/lpbuildd/charm.py new file mode 100644 index 0000000..a38c26f --- /dev/null +++ b/lpbuildd/charm.py @@ -0,0 +1,91 @@ +# Copyright 2021 Canonical Ltd. This software is licensed under the +# GNU Affero General Public License version 3 (see the file LICENSE). + +from __future__ import print_function + +__metaclass__ = type + +import os + +from lpbuildd.debian import ( + DebianBuildState, + DebianBuildManager, + ) + + +RETCODE_SUCCESS = 0 +RETCODE_FAILURE_INSTALL = 200 +RETCODE_FAILURE_BUILD = 201 + + +class CharmBuildState(DebianBuildState): + BUILD_CHARM = "BUILD_CHARM" + + +class CharmBuildManager(DebianBuildManager): + """Build a charm.""" + + backend_name = "lxd" + initial_build_state = CharmBuildState.BUILD_CHARM + + @property + def needs_sanitized_logs(self): + return True + + def initiate(self, files, chroot, extra_args): + """Initiate a build with a given set of files and chroot.""" + self.name = extra_args["name"] + self.branch = extra_args.get("branch") + self.git_repository = extra_args.get("git_repository") + self.git_path = extra_args.get("git_path") + self.build_path = extra_args.get("build_path") + + super(CharmBuildManager, self).initiate(files, chroot, extra_args) + + def doRunBuild(self): + """Run the process to build the charm.""" + args = [] + if self.branch is not None: + args.extend(["--branch", self.branch]) + if self.git_repository is not None: + args.extend(["--git-repository", self.git_repository]) + if self.git_path is not None: + args.extend(["--git-path", self.git_path]) + if self.build_path is not None: + args.extend(["--build-path", self.build_path]) + args.append(self.name) + self.runTargetSubProcess("build-charm", *args) + + def iterate_BUILD_CHARM(self, retcode): + """Finished building the charm.""" + if retcode == RETCODE_SUCCESS: + print("Returning build status: OK") + return self.deferGatherResults() + elif (retcode >= RETCODE_FAILURE_INSTALL and + retcode <= RETCODE_FAILURE_BUILD): + if not self.alreadyfailed: + self._builder.buildFail() + print("Returning build status: Build failed.") + self.alreadyfailed = True + else: + if not self.alreadyfailed: + self._builder.builderFail() + print("Returning build status: Builder failed.") + self.alreadyfailed = True + self.doReapProcesses(self._state) + + def iterateReap_BUILD_CHARM(self, retcode): + """Finished reaping after building the charm.""" + self._state = DebianBuildState.UMOUNT + self.doUnmounting() + + def gatherResults(self): + """Gather the results of the build and add them to the file cache.""" + output_path = os.path.join("/home/buildd", self.name) + if self.backend.path_exists(output_path): + for entry in sorted(self.backend.listdir(output_path)): + path = os.path.join(output_path, entry) + if self.backend.islink(path): + continue + if entry.endswith(".charm") or entry.endswith(".manifest"): + self.addWaitingFileFromBackend(path) diff --git a/lpbuildd/oci.py b/lpbuildd/oci.py index 7a17939..53198c8 100644 --- a/lpbuildd/oci.py +++ b/lpbuildd/oci.py @@ -59,7 +59,7 @@ class OCIBuildManager(SnapBuildProxyMixin, DebianBuildManager): super(OCIBuildManager, self).initiate(files, chroot, extra_args) def doRunBuild(self): - """Run the process to build the snap.""" + """Run the process to build the OCI image.""" args = [] args.extend(self.startProxy()) if self.revocation_endpoint: diff --git a/lpbuildd/target/backend.py b/lpbuildd/target/backend.py index 8356542..eb2cb2e 100644 --- a/lpbuildd/target/backend.py +++ b/lpbuildd/target/backend.py @@ -13,6 +13,10 @@ class BackendException(Exception): pass +class InvalidBuildFilePath(Exception): + pass + + class Backend: """A backend implementation for the environment where we run builds.""" diff --git a/lpbuildd/target/build_charm.py b/lpbuildd/target/build_charm.py new file mode 100644 index 0000000..f5eec28 --- /dev/null +++ b/lpbuildd/target/build_charm.py @@ -0,0 +1,116 @@ +# Copyright 2021 Canonical Ltd. This software is licensed under the +# GNU Affero General Public License version 3 (see the file LICENSE). + +from __future__ import print_function +import functools + +__metaclass__ = type + +from collections import OrderedDict +import logging +import os +import sys + +from lpbuildd.target.backend import InvalidBuildFilePath +from lpbuildd.target.operation import Operation +from lpbuildd.target.vcs import VCSOperationMixin + + +RETCODE_FAILURE_INSTALL = 200 +RETCODE_FAILURE_BUILD = 201 + + +logger = logging.getLogger(__name__) + + +class BuildCharm(VCSOperationMixin, Operation): + + description = "Build a charm." + + # charmcraft is a snap, so we'll need these + core_snap_names = ["core", "core20"] + + @classmethod + def add_arguments(cls, parser): + super(BuildCharm, cls).add_arguments(parser) + parser.add_argument( + "--build-path", default=".", + help="location of charm to build.") + parser.add_argument("name", help="name of charm to build") + + def __init__(self, args, parser): + super(BuildCharm, self).__init__(args, parser) + self.bin = os.path.dirname(sys.argv[0]) + self.buildd_path = os.path.join("/home/buildd", self.args.name) + + def _check_path_escape(self, path_to_check): + """Check the build file path doesn't escape the build directory.""" + build_file_path = os.path.realpath( + os.path.join(self.buildd_path, path_to_check)) + common_path = os.path.commonprefix((build_file_path, self.buildd_path)) + if common_path != self.buildd_path: + raise InvalidBuildFilePath("Invalid build file path.") + + def run_build_command(self, args, env=None, build_path=None, **kwargs): + """Run a build command in the target. + + :param args: the command and arguments to run. + :param env: dictionary of additional environment variables to set. + :param kwargs: any other keyword arguments to pass to Backend.run. + """ + full_env = OrderedDict() + full_env["LANG"] = "C.UTF-8" + full_env["SHELL"] = "/bin/sh" + if env: + full_env.update(env) + return self.backend.run( + args, cwd=self.buildd_path, env=full_env, **kwargs) + + def install(self): + logger.info("Running install phase") + deps = [] + if self.args.backend == "lxd": + # udev is installed explicitly to work around + # https://bugs.launchpad.net/snapd/+bug/1731519. + for dep in "snapd", "fuse", "squashfuse", "udev": + if self.backend.is_package_available(dep): + deps.append(dep) + deps.extend(self.vcs_deps) + self.backend.run(["apt-get", "-y", "install"] + deps) + for snap_name in self.core_snap_names: + self.backend.run(["snap", "install", snap_name]) + self.backend.run( + ["snap", "install", "charmcraft"]) + # The charmcraft snap can't see /build, so we have to do our work under + # /home/buildd instead. Make sure it exists. + self.backend.run(["mkdir", "-p", "/home/buildd"]) + + def repo(self): + """Collect git or bzr branch.""" + logger.info("Running repo phase...") + self.vcs_fetch(self.args.name, cwd="/home/buildd") + + def build(self): + logger.info("Running build phase...") + build_context_path = os.path.join( + "/home/buildd", + self.args.name, + self.args.build_path) + self._check_path_escape(build_context_path) + args = ["charmcraft", "build", "-f", build_context_path] + self.run_build_command(args) + + def run(self): + try: + self.install() + except Exception: + logger.exception('Install failed') + return RETCODE_FAILURE_INSTALL + try: + self.repo() + self.build() + except Exception: + logger.exception('Build failed') + return RETCODE_FAILURE_BUILD + return 0 + diff --git a/lpbuildd/target/build_oci.py b/lpbuildd/target/build_oci.py index af56671..275afea 100644 --- a/lpbuildd/target/build_oci.py +++ b/lpbuildd/target/build_oci.py @@ -12,6 +12,7 @@ import sys import tempfile from textwrap import dedent +from lpbuildd.target.backend import InvalidBuildFilePath from lpbuildd.target.operation import Operation from lpbuildd.target.snapbuildproxy import SnapBuildProxyOperationMixin from lpbuildd.target.snapstore import SnapStoreOperationMixin @@ -25,10 +26,6 @@ RETCODE_FAILURE_BUILD = 201 logger = logging.getLogger(__name__) -class InvalidBuildFilePath(Exception): - pass - - class BuildOCI(SnapBuildProxyOperationMixin, VCSOperationMixin, SnapStoreOperationMixin, Operation): @@ -47,7 +44,7 @@ class BuildOCI(SnapBuildProxyOperationMixin, VCSOperationMixin, help="A docker build ARG in the format of key=value. " "This option can be repeated many times. For example: " "--build-arg VAR1=A --build-arg VAR2=B") - parser.add_argument("name", help="name of snap to build") + parser.add_argument("name", help="name of image to build") def __init__(self, args, parser): super(BuildOCI, self).__init__(args, parser) diff --git a/lpbuildd/target/cli.py b/lpbuildd/target/cli.py index 94b291b..85e5b26 100644 --- a/lpbuildd/target/cli.py +++ b/lpbuildd/target/cli.py @@ -14,6 +14,7 @@ from lpbuildd.target.apt import ( OverrideSourcesList, Update, ) +from lpbuildd.target.build_charm import BuildCharm from lpbuildd.target.build_oci import BuildOCI from lpbuildd.target.build_livefs import BuildLiveFS from lpbuildd.target.build_snap import BuildSnap @@ -51,6 +52,7 @@ def configure_logging(): operations = { "add-trusted-keys": AddTrustedKeys, "build-oci": BuildOCI, + "build-charm": BuildCharm, "buildlivefs": BuildLiveFS, "buildsnap": BuildSnap, "generate-translation-templates": GenerateTranslationTemplates, diff --git a/lpbuildd/target/tests/test_build_charm.py b/lpbuildd/target/tests/test_build_charm.py new file mode 100644 index 0000000..b3a2a99 --- /dev/null +++ b/lpbuildd/target/tests/test_build_charm.py @@ -0,0 +1,342 @@ +# Copyright 2019 Canonical Ltd. This software is licensed under the +# GNU Affero General Public License version 3 (see the file LICENSE). + +__metaclass__ = type + +import os +import subprocess + +from fixtures import ( + FakeLogger, + TempDir, + ) +from testtools.matchers import ( + AnyMatch, + Equals, + Is, + MatchesAll, + MatchesDict, + MatchesListwise, + ) +from testtools.testcase import TestCase + +from lpbuildd.target.backend import InvalidBuildFilePath +from lpbuildd.target.build_charm import ( + RETCODE_FAILURE_BUILD, + RETCODE_FAILURE_INSTALL, + ) +from lpbuildd.tests.fakebuilder import FakeMethod +from lpbuildd.target.cli import parse_args + + +class RanCommand(MatchesListwise): + + def __init__(self, args, echo=None, cwd=None, input_text=None, + get_output=None, **env): + kwargs_matcher = {} + if echo is not None: + kwargs_matcher["echo"] = Is(echo) + if cwd: + kwargs_matcher["cwd"] = Equals(cwd) + if input_text: + kwargs_matcher["input_text"] = Equals(input_text) + if get_output is not None: + kwargs_matcher["get_output"] = Is(get_output) + if env: + kwargs_matcher["env"] = MatchesDict( + {key: Equals(value) for key, value in env.items()}) + 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, args, **kwargs): + kwargs.setdefault("LANG", "C.UTF-8") + kwargs.setdefault("SHELL", "/bin/sh") + super(RanBuildCommand, self).__init__(args, **kwargs) + + +class TestBuildCharm(TestCase): + + def test_run_build_command_no_env(self): + args = [ + "build-charm", + "--backend=fake", "--series=xenial", "--arch=amd64", "1", + "--branch", "lp:foo", "test-image", + ] + build_charm = parse_args(args=args).operation + build_charm.run_build_command(["echo", "hello world"]) + self.assertThat(build_charm.backend.run.calls, MatchesListwise([ + RanBuildCommand( + ["echo", "hello world"], + cwd="/home/buildd/test-image"), + ])) + + def test_run_build_command_env(self): + args = [ + "build-charm", + "--backend=fake", "--series=xenial", "--arch=amd64", "1", + "--branch", "lp:foo", "test-image", + ] + build_charm = parse_args(args=args).operation + build_charm.run_build_command( + ["echo", "hello world"], env={"FOO": "bar baz"}) + self.assertThat(build_charm.backend.run.calls, MatchesListwise([ + RanBuildCommand( + ["echo", "hello world"], + FOO="bar baz", + cwd="/home/buildd/test-image") + ])) + + def test_install_bzr(self): + args = [ + "build-charm", + "--backend=fake", "--series=xenial", "--arch=amd64", "1", + "--branch", "lp:foo", "test-image" + ] + build_charm = parse_args(args=args).operation + build_charm.install() + self.assertThat(build_charm.backend.run.calls, MatchesListwise([ + RanAptGet("install", "bzr"), + RanCommand(["snap", "install", "core"]), + RanCommand(["snap", "install", "core20"]), + RanCommand(["snap", "install", "charmcraft"]), + RanCommand(["mkdir", "-p", "/home/buildd"]), + ])) + + def test_install_git(self): + args = [ + "build-charm", + "--backend=fake", "--series=xenial", "--arch=amd64", "1", + "--git-repository", "lp:foo", "test-image" + ] + build_charm = parse_args(args=args).operation + build_charm.install() + self.assertThat(build_charm.backend.run.calls, MatchesListwise([ + RanAptGet("install", "git"), + RanCommand(["snap", "install", "core"]), + RanCommand(["snap", "install", "core20"]), + RanCommand(["snap", "install", "charmcraft"]), + RanCommand(["mkdir", "-p", "/home/buildd"]), + ])) + + def test_repo_bzr(self): + args = [ + "build-charm", + "--backend=fake", "--series=xenial", "--arch=amd64", "1", + "--branch", "lp:foo", "test-image", + ] + build_charm = parse_args(args=args).operation + build_charm.backend.build_path = self.useFixture(TempDir()).path + build_charm.backend.run = FakeMethod() + build_charm.repo() + self.assertThat(build_charm.backend.run.calls, MatchesListwise([ + RanBuildCommand( + ["bzr", "branch", "lp:foo", "test-image"], cwd="/home/buildd"), + ])) + + def test_repo_git(self): + args = [ + "build-charm", + "--backend=fake", "--series=xenial", "--arch=amd64", "1", + "--git-repository", "lp:foo", "test-image", + ] + build_charm = parse_args(args=args).operation + build_charm.backend.build_path = self.useFixture(TempDir()).path + build_charm.backend.run = FakeMethod() + build_charm.repo() + self.assertThat(build_charm.backend.run.calls, MatchesListwise([ + RanBuildCommand( + ["git", "clone", "lp:foo", "test-image"], cwd="/home/buildd"), + RanBuildCommand( + ["git", "submodule", "update", "--init", "--recursive"], + cwd="/home/buildd/test-image"), + ])) + + def test_repo_git_with_path(self): + args = [ + "build-charm", + "--backend=fake", "--series=xenial", "--arch=amd64", "1", + "--git-repository", "lp:foo", "--git-path", "next", "test-image", + ] + build_charm = parse_args(args=args).operation + build_charm.backend.build_path = self.useFixture(TempDir()).path + build_charm.backend.run = FakeMethod() + build_charm.repo() + self.assertThat(build_charm.backend.run.calls, MatchesListwise([ + RanBuildCommand( + ["git", "clone", "-b", "next", "lp:foo", "test-image"], + cwd="/home/buildd"), + RanBuildCommand( + ["git", "submodule", "update", "--init", "--recursive"], + cwd="/home/buildd/test-image"), + ])) + + def test_repo_git_with_tag_path(self): + args = [ + "build-charm", + "--backend=fake", "--series=xenial", "--arch=amd64", "1", + "--git-repository", "lp:foo", "--git-path", "refs/tags/1.0", + "test-image", + ] + build_charm = parse_args(args=args).operation + build_charm.backend.build_path = self.useFixture(TempDir()).path + build_charm.backend.run = FakeMethod() + build_charm.repo() + self.assertThat(build_charm.backend.run.calls, MatchesListwise([ + RanBuildCommand( + ["git", "clone", "-b", "1.0", "lp:foo", "test-image"], + cwd="/home/buildd"), + RanBuildCommand( + ["git", "submodule", "update", "--init", "--recursive"], + cwd="/home/buildd/test-image"), + ])) + + def test_build(self): + args = [ + "build-charm", + "--backend=fake", "--series=xenial", "--arch=amd64", "1", + "--branch", "lp:foo", "test-image", + ] + build_charm = parse_args(args=args).operation + build_charm.backend.add_dir('/build/test-directory') + build_charm.build() + self.assertThat(build_charm.backend.run.calls, MatchesListwise([ + RanBuildCommand( + ["charmcraft", "build", "-f", + "/home/buildd/test-image/."], + cwd="/home/buildd/test-image"), + ])) + + def test_build_with_path(self): + args = [ + "build-charm", + "--backend=fake", "--series=xenial", "--arch=amd64", "1", + "--branch", "lp:foo", "--build-path", "build-aux/", + "test-image", + ] + build_charm = parse_args(args=args).operation + build_charm.backend.add_dir('/build/test-directory') + build_charm.build() + self.assertThat(build_charm.backend.run.calls, MatchesListwise([ + RanBuildCommand( + ["charmcraft", "build", "-f", + "/home/buildd/test-image/build-aux/"], + cwd="/home/buildd/test-image"), + ])) + + def test_run_succeeds(self): + args = [ + "build-charm", + "--backend=fake", "--series=xenial", "--arch=amd64", "1", + "--branch", "lp:foo", "test-image", + ] + build_charm = parse_args(args=args).operation + build_charm.backend.build_path = self.useFixture(TempDir()).path + build_charm.backend.run = FakeMethod() + self.assertEqual(0, build_charm.run()) + self.assertThat(build_charm.backend.run.calls, MatchesAll( + AnyMatch(RanAptGet("install", "bzr"),), + AnyMatch(RanBuildCommand( + ["bzr", "branch", "lp:foo", "test-image"], + cwd="/home/buildd")), + AnyMatch(RanBuildCommand( + ["charmcraft", "build", "-f", + "/home/buildd/test-image/."], + cwd="/home/buildd/test-image")), + )) + + 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 = [ + "build-charm", + "--backend=fake", "--series=xenial", "--arch=amd64", "1", + "--branch", "lp:foo", "test-image", + ] + build_charm = parse_args(args=args).operation + build_charm.backend.run = FailInstall() + self.assertEqual(RETCODE_FAILURE_INSTALL, build_charm.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[:2] == ["bzr", "branch"]: + raise subprocess.CalledProcessError(1, run_args) + + self.useFixture(FakeLogger()) + args = [ + "build-charm", + "--backend=fake", "--series=xenial", "--arch=amd64", "1", + "--branch", "lp:foo", "test-image", + ] + build_charm = parse_args(args=args).operation + build_charm.backend.run = FailRepo() + self.assertEqual(RETCODE_FAILURE_BUILD, build_charm.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] == "charmcraft": + raise subprocess.CalledProcessError(1, run_args) + + self.useFixture(FakeLogger()) + args = [ + "build-charm", + "--backend=fake", "--series=xenial", "--arch=amd64", "1", + "--branch", "lp:foo", "test-image", + ] + build_charm = parse_args(args=args).operation + build_charm.backend.build_path = self.useFixture(TempDir()).path + build_charm.backend.run = FailBuild() + self.assertEqual(RETCODE_FAILURE_BUILD, build_charm.run()) + + def test_build_with_invalid_build_path_parent(self): + args = [ + "build-charm", + "--backend=fake", "--series=xenial", "--arch=amd64", "1", + "--branch", "lp:foo", "--build-path", "../", + "test-image", + ] + build_charm = parse_args(args=args).operation + build_charm.backend.add_dir('/build/test-directory') + self.assertRaises(InvalidBuildFilePath, build_charm.build) + + def test_build_with_invalid_build_path_absolute(self): + args = [ + "build-charm", + "--backend=fake", "--series=xenial", "--arch=amd64", "1", + "--branch", "lp:foo", "--build-path", "/etc", + "test-image", + ] + build_charm = parse_args(args=args).operation + build_charm.backend.add_dir('/build/test-directory') + self.assertRaises(InvalidBuildFilePath, build_charm.build) + + def test_build_with_invalid_build_path_symlink(self): + args = [ + "build-charm", + "--backend=fake", "--series=xenial", "--arch=amd64", "1", + "--branch", "lp:foo", "--build-path", "build/", + "test-image", + ] + build_charm = parse_args(args=args).operation + build_charm.buildd_path = self.useFixture(TempDir()).path + os.symlink( + '/etc/hosts', + os.path.join(build_charm.buildd_path, 'build')) + self.assertRaises(InvalidBuildFilePath, build_charm.build) diff --git a/lpbuildd/target/tests/test_build_oci.py b/lpbuildd/target/tests/test_build_oci.py index e58344f..3b9966a 100644 --- a/lpbuildd/target/tests/test_build_oci.py +++ b/lpbuildd/target/tests/test_build_oci.py @@ -24,8 +24,8 @@ from testtools.matchers import ( MatchesListwise, ) +from lpbuildd.target.backend import InvalidBuildFilePath from lpbuildd.target.build_oci import ( - InvalidBuildFilePath, RETCODE_FAILURE_BUILD, RETCODE_FAILURE_INSTALL, ) diff --git a/lpbuildd/tests/test_charm.py b/lpbuildd/tests/test_charm.py new file mode 100644 index 0000000..bdb2724 --- /dev/null +++ b/lpbuildd/tests/test_charm.py @@ -0,0 +1,139 @@ +# Copyright 2021 Canonical Ltd. This software is licensed under the +# GNU Affero General Public License version 3 (see the file LICENSE). + +__metaclass__ = type + +import os + +from fixtures import ( + EnvironmentVariable, + TempDir, + ) +from testtools import TestCase +from testtools.deferredruntest import AsynchronousDeferredRunTest +from twisted.internet import defer + +from lpbuildd.charm import CharmBuildManager, CharmBuildState +from lpbuildd.tests.fakebuilder import FakeBuilder +from lpbuildd.tests.matchers import HasWaitingFiles + + +class MockBuildManager(CharmBuildManager): + def __init__(self, *args, **kwargs): + super(MockBuildManager, self).__init__(*args, **kwargs) + self.commands = [] + self.iterators = [] + + def runSubProcess(self, path, command, iterate=None, env=None): + self.commands.append([path] + command) + if iterate is None: + iterate = self.iterate + self.iterators.append(iterate) + return 0 + + +class TestCharmBuildManagerIteration(TestCase): + """Run CharmBuildManager through its iteration steps.""" + + run_tests_with = AsynchronousDeferredRunTest.make_factory(timeout=5) + + def setUp(self): + super(TestCharmBuildManagerIteration, self).setUp() + self.working_dir = self.useFixture(TempDir()).path + builder_dir = os.path.join(self.working_dir, "builder") + home_dir = os.path.join(self.working_dir, "home") + for dir in (builder_dir, home_dir): + os.mkdir(dir) + self.useFixture(EnvironmentVariable("HOME", home_dir)) + self.builder = FakeBuilder(builder_dir) + self.buildid = "123" + self.buildmanager = MockBuildManager(self.builder, self.buildid) + self.buildmanager._cachepath = self.builder._cachepath + + def getState(self): + """Retrieve build manager's state.""" + return self.buildmanager._state + + @defer.inlineCallbacks + def startBuild(self, args=None, options=None): + # The build manager's iterate() kicks off the consecutive states + # after INIT. + extra_args = { + "series": "xenial", + "arch_tag": "i386", + "name": "test-charm", + } + if args is not None: + extra_args.update(args) + original_backend_name = self.buildmanager.backend_name + self.buildmanager.backend_name = "fake" + self.buildmanager.initiate({}, "chroot.tar.gz", extra_args) + self.buildmanager.backend_name = original_backend_name + + # Skip states that are done in DebianBuildManager to the state + # directly before BUILD_CHARM. + self.buildmanager._state = CharmBuildState.UPDATE + + # BUILD_OCI: Run the builder's payload to build the charm. + yield self.buildmanager.iterate(0) + self.assertEqual(CharmBuildState.BUILD_CHARM, self.getState()) + expected_command = [ + "sharepath/bin/in-target", "in-target", "build-charm", + "--backend=lxd", "--series=xenial", "--arch=i386", self.buildid, + ] + if options is not None: + expected_command.extend(options) + expected_command.append("test-charm") + self.assertEqual(expected_command, self.buildmanager.commands[-1]) + self.assertEqual( + self.buildmanager.iterate, self.buildmanager.iterators[-1]) + self.assertFalse(self.builder.wasCalled("chrootFail")) + + @defer.inlineCallbacks + def test_iterate(self): + # The build manager iterates a normal build from start to finish. + args = { + "git_repository": "https://git.launchpad.dev/~example/+git/charm", + "git_path": "master", + } + expected_options = [ + "--git-repository", "https://git.launchpad.dev/~example/+git/charm", + "--git-path", "master", + ] + yield self.startBuild(args, expected_options) + + log_path = os.path.join(self.buildmanager._cachepath, "buildlog") + with open(log_path, "w") as log: + log.write("I am a build log.") + + self.buildmanager.backend.add_file( + "/home/buildd/test-charm/test-charm_0_all.charm", + b"I am charming.") + + # After building the package, reap processes. + yield self.buildmanager.iterate(0) + expected_command = [ + "sharepath/bin/in-target", "in-target", "scan-for-processes", + "--backend=lxd", "--series=xenial", "--arch=i386", self.buildid, + ] + + self.assertEqual(CharmBuildState.BUILD_CHARM, self.getState()) + self.assertEqual(expected_command, self.buildmanager.commands[-1]) + self.assertNotEqual( + self.buildmanager.iterate, self.buildmanager.iterators[-1]) + self.assertFalse(self.builder.wasCalled("buildFail")) + self.assertThat(self.builder, HasWaitingFiles.byEquality({ + "test-charm_0_all.charm": b"I am charming.", + })) + + # Control returns to the DebianBuildManager in the UMOUNT state. + self.buildmanager.iterateReap(self.getState(), 0) + expected_command = [ + "sharepath/bin/in-target", "in-target", "umount-chroot", + "--backend=lxd", "--series=xenial", "--arch=i386", self.buildid, + ] + self.assertEqual(CharmBuildState.UMOUNT, self.getState()) + self.assertEqual(expected_command, self.buildmanager.commands[-1]) + self.assertEqual( + self.buildmanager.iterate, self.buildmanager.iterators[-1]) + self.assertFalse(self.builder.wasCalled("buildFail")) diff --git a/lpbuildd/tests/test_oci.py b/lpbuildd/tests/test_oci.py index 78b0dbb..7fc0b3a 100644 --- a/lpbuildd/tests/test_oci.py +++ b/lpbuildd/tests/test_oci.py @@ -89,7 +89,7 @@ class TestOCIBuildManagerIteration(TestCase): # directly before BUILD_OCI. self.buildmanager._state = OCIBuildState.UPDATE - # BUILD_OCI: Run the builder's payload to build the snap package. + # BUILD_OCI: Run the builder's payload to build the OCI image. yield self.buildmanager.iterate(0) self.assertEqual(OCIBuildState.BUILD_OCI, self.getState()) expected_command = [
_______________________________________________ Mailing list: https://launchpad.net/~launchpad-reviewers Post to : [email protected] Unsubscribe : https://launchpad.net/~launchpad-reviewers More help : https://help.launchpad.net/ListHelp

