← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] ~cjwatson/launchpad-buildd:refactor-run-build-command into launchpad-buildd:master

 

Colin Watson has proposed merging ~cjwatson/launchpad-buildd:refactor-run-build-command into launchpad-buildd:master.

Commit message:
Refactor multiple implementations of operation command helpers

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~cjwatson/launchpad-buildd/+git/launchpad-buildd/+merge/413166

We had several nearly-identical implementations of `run_build_command` and related testtools matchers floating around.  Much of launchpad-buildd is glue between other programs, so it makes sense for it to have very careful checks on which subprocesses it runs, but it doesn't need to have quite so much duplicated code in the process; consolidate all these into common locations.
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of ~cjwatson/launchpad-buildd:refactor-run-build-command into launchpad-buildd:master.
diff --git a/lpbuildd/target/build_charm.py b/lpbuildd/target/build_charm.py
index 5d0b3ab..3e667b9 100644
--- a/lpbuildd/target/build_charm.py
+++ b/lpbuildd/target/build_charm.py
@@ -5,7 +5,6 @@ from __future__ import print_function
 
 __metaclass__ = type
 
-from collections import OrderedDict
 import logging
 import os
 import sys
@@ -51,22 +50,6 @@ class BuildCharm(BuilderProxyOperationMixin, VCSOperationMixin,
         self.bin = os.path.dirname(sys.argv[0])
         self.buildd_path = os.path.join("/home/buildd", self.args.name)
 
-    def run_build_command(self, args, env=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)
-        cwd = kwargs.pop('cwd', self.buildd_path)
-        return self.backend.run(
-            args, cwd=cwd, env=full_env, **kwargs)
-
     def install(self):
         logger.info("Running install phase")
         deps = []
diff --git a/lpbuildd/target/build_livefs.py b/lpbuildd/target/build_livefs.py
index 57f61e1..00707ae 100644
--- a/lpbuildd/target/build_livefs.py
+++ b/lpbuildd/target/build_livefs.py
@@ -81,14 +81,6 @@ class BuildLiveFS(SnapStoreOperationMixin, Operation):
             "--debug", default=False, action="store_true",
             help="enable detailed live-build debugging")
 
-    def run_build_command(self, args, **kwargs):
-        """Run a build command in the chroot.
-
-        :param args: the command and arguments to run.
-        :param kwargs: any other keyword arguments to pass to Backend.run.
-        """
-        return self.backend.run(args, cwd="/build", **kwargs)
-
     def install(self):
         deps = ["livecd-rootfs"]
         if self.args.backend == "lxd":
diff --git a/lpbuildd/target/build_oci.py b/lpbuildd/target/build_oci.py
index 7f2a9ed..1142899 100644
--- a/lpbuildd/target/build_oci.py
+++ b/lpbuildd/target/build_oci.py
@@ -5,7 +5,6 @@ from __future__ import print_function
 
 __metaclass__ = type
 
-from collections import OrderedDict
 import logging
 import os.path
 import sys
@@ -69,21 +68,6 @@ class BuildOCI(BuilderProxyOperationMixin, VCSOperationMixin,
                 systemd_file.flush()
                 self.backend.copy_in(systemd_file.name, file_path)
 
-    def run_build_command(self, args, env=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 = []
diff --git a/lpbuildd/target/build_snap.py b/lpbuildd/target/build_snap.py
index 7ec133b..1b270ac 100644
--- a/lpbuildd/target/build_snap.py
+++ b/lpbuildd/target/build_snap.py
@@ -6,7 +6,6 @@ from __future__ import print_function
 __metaclass__ = type
 
 import argparse
-from collections import OrderedDict
 import json
 import logging
 import os.path
@@ -85,20 +84,6 @@ class BuildSnap(BuilderProxyOperationMixin, VCSOperationMixin,
         super(BuildSnap, self).__init__(args, parser)
         self.bin = os.path.dirname(sys.argv[0])
 
-    def run_build_command(self, args, env=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, env=full_env, **kwargs)
-
     def install_svn_servers(self):
         proxy = urlparse(self.args.proxy_url)
         svn_servers = dedent("""\
diff --git a/lpbuildd/target/operation.py b/lpbuildd/target/operation.py
index 2f9fe64..bbc17cc 100644
--- a/lpbuildd/target/operation.py
+++ b/lpbuildd/target/operation.py
@@ -1,10 +1,12 @@
-# Copyright 2017 Canonical Ltd.  This software is licensed under the
+# Copyright 2017-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
 
+from collections import OrderedDict
+
 from lpbuildd.target.backend import make_backend
 
 
@@ -12,6 +14,7 @@ class Operation:
     """An operation to perform on the target environment."""
 
     description = "An unidentified operation."
+    buildd_path = "/build"
 
     @classmethod
     def add_arguments(cls, parser):
@@ -31,5 +34,20 @@ class Operation:
             self.args.backend, self.args.build_id,
             series=self.args.series, arch=self.args.arch)
 
+    def run_build_command(self, args, env=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)
+        cwd = kwargs.pop("cwd", self.buildd_path)
+        return self.backend.run(args, cwd=cwd, env=full_env, **kwargs)
+
     def run(self):
         raise NotImplementedError
diff --git a/lpbuildd/target/tests/matchers.py b/lpbuildd/target/tests/matchers.py
new file mode 100644
index 0000000..fbecb05
--- /dev/null
+++ b/lpbuildd/target/tests/matchers.py
@@ -0,0 +1,59 @@
+# Copyright 2021 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+__metaclass__ = type
+
+from testtools.matchers import (
+    Equals,
+    Is,
+    MatchesDict,
+    MatchesListwise,
+    )
+
+
+class RanCommand(MatchesListwise):
+
+    def __init__(self, args, echo=None, cwd=None, input_text=None,
+                 stdout=None, stderr=None, get_output=None,
+                 universal_newlines=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 stdout is not None:
+            kwargs_matcher["stdout"] = Equals(stdout)
+        if stderr is not None:
+            kwargs_matcher["stderr"] = Equals(stderr)
+        if get_output is not None:
+            kwargs_matcher["get_output"] = Is(get_output)
+        if universal_newlines is not None:
+            kwargs_matcher["universal_newlines"] = Is(universal_newlines)
+        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 RanSnap(RanCommand):
+
+    def __init__(self, *args):
+        super(RanSnap, self).__init__(["snap"] + list(args))
+
+
+class RanBuildCommand(RanCommand):
+
+    def __init__(self, args, **kwargs):
+        kwargs.setdefault("cwd", "/build")
+        kwargs.setdefault("LANG", "C.UTF-8")
+        kwargs.setdefault("SHELL", "/bin/sh")
+        super(RanBuildCommand, self).__init__(args, **kwargs)
diff --git a/lpbuildd/target/tests/test_build_charm.py b/lpbuildd/target/tests/test_build_charm.py
index a095f8b..cd4ebca 100644
--- a/lpbuildd/target/tests/test_build_charm.py
+++ b/lpbuildd/target/tests/test_build_charm.py
@@ -17,10 +17,7 @@ import responses
 from systemfixtures import FakeFilesystem
 from testtools.matchers import (
     AnyMatch,
-    Equals,
-    Is,
     MatchesAll,
-    MatchesDict,
     MatchesListwise,
     )
 from testtools.testcase import TestCase
@@ -36,42 +33,11 @@ from lpbuildd.target.tests.test_build_snap import (
     RanSnap,
     )
 from lpbuildd.target.cli import parse_args
-
-
-class RanCommand(MatchesListwise):
-
-    def __init__(self, args, echo=None, cwd=None, input_text=None,
-                 get_output=None, universal_newlines=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 universal_newlines is not None:
-            kwargs_matcher["universal_newlines"] = Is(universal_newlines)
-        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)
+from lpbuildd.target.tests.matchers import (
+    RanAptGet,
+    RanBuildCommand,
+    RanCommand,
+    )
 
 
 class TestBuildCharm(TestCase):
diff --git a/lpbuildd/target/tests/test_build_livefs.py b/lpbuildd/target/tests/test_build_livefs.py
index 06ae149..2fb2b19 100644
--- a/lpbuildd/target/tests/test_build_livefs.py
+++ b/lpbuildd/target/tests/test_build_livefs.py
@@ -11,10 +11,7 @@ import responses
 from testtools import TestCase
 from testtools.matchers import (
     AnyMatch,
-    Equals,
-    Is,
     MatchesAll,
-    MatchesDict,
     MatchesListwise,
     )
 
@@ -23,66 +20,16 @@ from lpbuildd.target.build_livefs import (
     RETCODE_FAILURE_INSTALL,
     )
 from lpbuildd.target.cli import parse_args
+from lpbuildd.target.tests.matchers import (
+    RanAptGet,
+    RanBuildCommand,
+    RanCommand,
+    )
 from lpbuildd.tests.fakebuilder import FakeMethod
 
 
-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):
-        super(RanBuildCommand, self).__init__(args, cwd="/build", **kwargs)
-
-
 class TestBuildLiveFS(TestCase):
 
-    def test_run_build_command_no_env(self):
-        args = [
-            "buildlivefs",
-            "--backend=fake", "--series=xenial", "--arch=amd64", "1",
-            ]
-        build_livefs = parse_args(args=args).operation
-        build_livefs.run_build_command(["echo", "hello world"])
-        self.assertThat(build_livefs.backend.run.calls, MatchesListwise([
-            RanBuildCommand(["echo", "hello world"]),
-            ]))
-
-    def test_run_build_command_env(self):
-        args = [
-            "buildlivefs",
-            "--backend=fake", "--series=xenial", "--arch=amd64", "1",
-            ]
-        build_livefs = parse_args(args=args).operation
-        build_livefs.run_build_command(
-            ["echo", "hello world"], env={"FOO": "bar baz"})
-        self.assertThat(build_livefs.backend.run.calls, MatchesListwise([
-            RanBuildCommand(["echo", "hello world"], FOO="bar baz"),
-            ]))
-
     def test_install(self):
         args = [
             "buildlivefs",
diff --git a/lpbuildd/target/tests/test_build_oci.py b/lpbuildd/target/tests/test_build_oci.py
index 3a9a12e..199d3b3 100644
--- a/lpbuildd/target/tests/test_build_oci.py
+++ b/lpbuildd/target/tests/test_build_oci.py
@@ -17,10 +17,7 @@ from systemfixtures import FakeFilesystem
 from testtools import TestCase
 from testtools.matchers import (
     AnyMatch,
-    Equals,
-    Is,
     MatchesAll,
-    MatchesDict,
     MatchesListwise,
     )
 
@@ -30,49 +27,14 @@ from lpbuildd.target.build_oci import (
     RETCODE_FAILURE_INSTALL,
     )
 from lpbuildd.target.cli import parse_args
+from lpbuildd.target.tests.matchers import (
+    RanAptGet,
+    RanBuildCommand,
+    RanCommand,
+    )
 from lpbuildd.tests.fakebuilder import FakeMethod
 
 
-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 RanSnap(RanCommand):
-
-    def __init__(self, *args, **kwargs):
-        super(RanSnap, self).__init__(["snap"] + list(args), **kwargs)
-
-
-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 TestBuildOCI(TestCase):
 
     def test_run_build_command_no_env(self):
diff --git a/lpbuildd/target/tests/test_build_snap.py b/lpbuildd/target/tests/test_build_snap.py
index 52b1867..2bda8bb 100644
--- a/lpbuildd/target/tests/test_build_snap.py
+++ b/lpbuildd/target/tests/test_build_snap.py
@@ -18,10 +18,7 @@ from systemfixtures import FakeFilesystem
 from testtools import TestCase
 from testtools.matchers import (
     AnyMatch,
-    Equals,
-    Is,
     MatchesAll,
-    MatchesDict,
     MatchesListwise,
     )
 
@@ -30,51 +27,15 @@ from lpbuildd.target.build_snap import (
     RETCODE_FAILURE_INSTALL,
     )
 from lpbuildd.target.cli import parse_args
+from lpbuildd.target.tests.matchers import (
+    RanAptGet,
+    RanBuildCommand,
+    RanCommand,
+    RanSnap,
+    )
 from lpbuildd.tests.fakebuilder import FakeMethod
 
 
-class RanCommand(MatchesListwise):
-
-    def __init__(self, args, echo=None, cwd=None, input_text=None,
-                 get_output=None, universal_newlines=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 universal_newlines is not None:
-            kwargs_matcher["universal_newlines"] = Is(universal_newlines)
-        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 RanSnap(RanCommand):
-
-    def __init__(self, *args):
-        super(RanSnap, self).__init__(["snap"] + 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 FakeRevisionID(FakeMethod):
 
     def __init__(self, revision_id):
@@ -90,31 +51,6 @@ class FakeRevisionID(FakeMethod):
 
 class TestBuildSnap(TestCase):
 
-    def test_run_build_command_no_env(self):
-        args = [
-            "buildsnap",
-            "--backend=fake", "--series=xenial", "--arch=amd64", "1",
-            "--branch", "lp:foo", "test-snap",
-            ]
-        build_snap = parse_args(args=args).operation
-        build_snap.run_build_command(["echo", "hello world"])
-        self.assertThat(build_snap.backend.run.calls, MatchesListwise([
-            RanBuildCommand(["echo", "hello world"]),
-            ]))
-
-    def test_run_build_command_env(self):
-        args = [
-            "buildsnap",
-            "--backend=fake", "--series=xenial", "--arch=amd64", "1",
-            "--branch", "lp:foo", "test-snap",
-            ]
-        build_snap = parse_args(args=args).operation
-        build_snap.run_build_command(
-            ["echo", "hello world"], env={"FOO": "bar baz"})
-        self.assertThat(build_snap.backend.run.calls, MatchesListwise([
-            RanBuildCommand(["echo", "hello world"], FOO="bar baz"),
-            ]))
-
     def test_install_bzr(self):
         args = [
             "buildsnap",
diff --git a/lpbuildd/target/tests/test_generate_translation_templates.py b/lpbuildd/target/tests/test_generate_translation_templates.py
index 12ec2b3..d9dfe38 100644
--- a/lpbuildd/target/tests/test_generate_translation_templates.py
+++ b/lpbuildd/target/tests/test_generate_translation_templates.py
@@ -6,6 +6,10 @@ __metaclass__ = type
 import os
 import subprocess
 import tarfile
+try:
+    from unittest import mock
+except ImportError:
+    import mock
 
 from fixtures import (
     EnvironmentVariable,
@@ -14,38 +18,16 @@ from fixtures import (
     )
 from testtools import TestCase
 from testtools.matchers import (
-    ContainsDict,
     Equals,
-    Is,
-    MatchesDict,
     MatchesListwise,
     MatchesSetwise,
     )
 
 from lpbuildd.target.cli import parse_args
-
-
-class RanCommand(MatchesListwise):
-
-    def __init__(self, args, get_output=None, echo=None, cwd=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 cwd:
-            kwargs_matcher["cwd"] = Equals(cwd)
-        if env:
-            kwargs_matcher["env"] = MatchesDict(
-                {key: Equals(value) for key, value in env.items()})
-        super(RanCommand, self).__init__(
-            [Equals((args,)), ContainsDict(kwargs_matcher)])
-
-
-class RanAptGet(RanCommand):
-
-    def __init__(self, *args):
-        super(RanAptGet, self).__init__(["apt-get", "-y"] + list(args))
+from lpbuildd.target.tests.matchers import (
+    RanAptGet,
+    RanCommand,
+    )
 
 
 class TestGenerateTranslationTemplates(TestCase):
@@ -234,9 +216,12 @@ class TestGenerateTranslationTemplates(TestCase):
                 ["rm", "-f",
                  os.path.join(po_dir, "missing"),
                  os.path.join(po_dir, "notexist")]),
-            RanCommand(["/usr/bin/intltool-update", "-m"], cwd=po_dir),
             RanCommand(
-                ["/usr/bin/intltool-update", "-p", "-g", "test"], cwd=po_dir),
+                ["/usr/bin/intltool-update", "-m"],
+                stdout=mock.ANY, stderr=mock.ANY, cwd=po_dir),
+            RanCommand(
+                ["/usr/bin/intltool-update", "-p", "-g", "test"],
+                stdout=mock.ANY, stderr=mock.ANY, cwd=po_dir),
             RanCommand(
                 ["tar", "-C", branch_dir, "-czf", result_path, "po/test.pot"]),
             ]))
@@ -270,9 +255,12 @@ class TestGenerateTranslationTemplates(TestCase):
                 ["rm", "-f",
                  os.path.join(po_dir, "missing"),
                  os.path.join(po_dir, "notexist")]),
-            RanCommand(["/usr/bin/intltool-update", "-m"], cwd=po_dir),
             RanCommand(
-                ["/usr/bin/intltool-update", "-p", "-g", "test"], cwd=po_dir),
+                ["/usr/bin/intltool-update", "-m"],
+                stdout=mock.ANY, stderr=mock.ANY, cwd=po_dir),
+            RanCommand(
+                ["/usr/bin/intltool-update", "-p", "-g", "test"],
+                stdout=mock.ANY, stderr=mock.ANY, cwd=po_dir),
             RanCommand(
                 ["tar", "-C", branch_dir, "-czf", result_path, "po/test.pot"]),
             ]))
diff --git a/lpbuildd/target/tests/test_operation.py b/lpbuildd/target/tests/test_operation.py
new file mode 100644
index 0000000..00ee29a
--- /dev/null
+++ b/lpbuildd/target/tests/test_operation.py
@@ -0,0 +1,36 @@
+# Copyright 2017-2021 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+__metaclass__ = type
+
+from argparse import ArgumentParser
+
+from testtools import TestCase
+from testtools.matchers import MatchesListwise
+
+from lpbuildd.target.operation import Operation
+from lpbuildd.target.tests.matchers import RanBuildCommand
+
+
+class TestOperation(TestCase):
+
+    def test_run_build_command_no_env(self):
+        parser = ArgumentParser()
+        Operation.add_arguments(parser)
+        args = ["--backend=fake", "--series=xenial", "--arch=amd64", "1"]
+        operation = Operation(parser.parse_args(args=args), parser)
+        operation.run_build_command(["echo", "hello world"])
+        self.assertThat(operation.backend.run.calls, MatchesListwise([
+            RanBuildCommand(["echo", "hello world"]),
+            ]))
+
+    def test_run_build_command_env(self):
+        parser = ArgumentParser()
+        Operation.add_arguments(parser)
+        args = ["--backend=fake", "--series=xenial", "--arch=amd64", "1"]
+        operation = Operation(parser.parse_args(args=args), parser)
+        operation.run_build_command(
+            ["echo", "hello world"], env={"FOO": "bar baz"})
+        self.assertThat(operation.backend.run.calls, MatchesListwise([
+            RanBuildCommand(["echo", "hello world"], FOO="bar baz"),
+            ]))