← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] ~cjwatson/launchpad-buildd:py3-fix-lxd-run into launchpad-buildd:master

 

Colin Watson has proposed merging ~cjwatson/launchpad-buildd:py3-fix-lxd-run into launchpad-buildd:master.

Commit message:
Fix LXD.run to not default to universal_newlines=True

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

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

This was confusingly different from subprocess.Popen's default, and it broke Backend.find in a way that wasn't detected by the test suite because FakeProcess doesn't check type-safety here.  I've arranged for some more stringent testing so that we don't run into this category of bug in future.
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of ~cjwatson/launchpad-buildd:py3-fix-lxd-run into launchpad-buildd:master.
diff --git a/debian/changelog b/debian/changelog
index 867b35e..fd35575 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -21,6 +21,7 @@ launchpad-buildd (190) UNRELEASED; urgency=medium
   * Rename [slave] configuration section to [builder].
   * Convert translation templates builds to the VCS mixin, thereby adding
     git support.
+  * Fix LXD.run to not default to universal_newlines=True.
 
  -- Colin Watson <cjwatson@xxxxxxxxxx>  Tue, 28 Apr 2020 10:19:27 +0100
 
diff --git a/lpbuildd/oci.py b/lpbuildd/oci.py
index 0b56623..27d34f1 100644
--- a/lpbuildd/oci.py
+++ b/lpbuildd/oci.py
@@ -152,7 +152,7 @@ class OCIBuildManager(SnapBuildProxyMixin, DebianBuildManager):
         extract_path = tempfile.mkdtemp(prefix=self.name)
         proc = self.backend.run(
             ['docker', 'save', self.name],
-            get_output=True, universal_newlines=False, return_process=True)
+            get_output=True, return_process=True)
         try:
             tar = tarfile.open(fileobj=proc.stdout, mode="r|")
         except Exception as e:
diff --git a/lpbuildd/target/build_snap.py b/lpbuildd/target/build_snap.py
index 9bc1dad..50fab7a 100644
--- a/lpbuildd/target/build_snap.py
+++ b/lpbuildd/target/build_snap.py
@@ -178,7 +178,7 @@ class BuildSnap(SnapBuildProxyOperationMixin, VCSOperationMixin,
             status["revision_id"] = self.run_build_command(
                 ["bzr", "revno"],
                 cwd=os.path.join("/build", self.args.name),
-                get_output=True).rstrip("\n")
+                get_output=True, universal_newlines=True).rstrip("\n")
         else:
             rev = (
                 self.args.git_path
@@ -188,7 +188,7 @@ class BuildSnap(SnapBuildProxyOperationMixin, VCSOperationMixin,
                 # recursively until we get an actual commit.
                 ["git", "rev-parse", rev + "^{}"],
                 cwd=os.path.join("/build", self.args.name),
-                get_output=True).rstrip("\n")
+                get_output=True, universal_newlines=True).rstrip("\n")
         self.save_status(status)
 
     @property
diff --git a/lpbuildd/target/chroot.py b/lpbuildd/target/chroot.py
index 54d48eb..203142c 100644
--- a/lpbuildd/target/chroot.py
+++ b/lpbuildd/target/chroot.py
@@ -96,7 +96,10 @@ class Chroot(Backend):
             if get_output:
                 if echo:
                     print("Output:")
-                    print(output)
+                    output_text = output
+                    if isinstance(output_text, bytes):
+                        output_text = output_text.decode("UTF-8", "replace")
+                    print(output_text)
                 return output
 
     def copy_in(self, source_path, target_path):
diff --git a/lpbuildd/target/lxd.py b/lpbuildd/target/lxd.py
index ed1002b..fcd6706 100644
--- a/lpbuildd/target/lxd.py
+++ b/lpbuildd/target/lxd.py
@@ -504,8 +504,7 @@ class LXD(Backend):
              "/etc/systemd/system/snapd.refresh.timer"])
 
     def run(self, args, cwd=None, env=None, input_text=None, get_output=False,
-            echo=False, return_process=False, universal_newlines=True,
-            **kwargs):
+            echo=False, return_process=False, **kwargs):
         """See `Backend`."""
         env_params = []
         if env:
@@ -538,9 +537,7 @@ class LXD(Backend):
         else:
             if get_output:
                 kwargs["stdout"] = subprocess.PIPE
-            proc = subprocess.Popen(
-                cmd, stdin=subprocess.PIPE,
-                universal_newlines=universal_newlines, **kwargs)
+            proc = subprocess.Popen(cmd, stdin=subprocess.PIPE, **kwargs)
             if return_process:
                 return proc
             output, _ = proc.communicate(input_text)
@@ -549,7 +546,10 @@ class LXD(Backend):
             if get_output:
                 if echo:
                     print("Output:")
-                    print(output)
+                    output_text = output
+                    if isinstance(output_text, bytes):
+                        output_text = output_text.decode("UTF-8", "replace")
+                    print(output_text)
                 return output
 
     def copy_in(self, source_path, target_path):
diff --git a/lpbuildd/target/tests/test_build_snap.py b/lpbuildd/target/tests/test_build_snap.py
index 5f8952f..3e9f320 100644
--- a/lpbuildd/target/tests/test_build_snap.py
+++ b/lpbuildd/target/tests/test_build_snap.py
@@ -36,7 +36,7 @@ from lpbuildd.tests.fakebuilder import FakeMethod
 class RanCommand(MatchesListwise):
 
     def __init__(self, args, echo=None, cwd=None, input_text=None,
-                 get_output=None, **env):
+                 get_output=None, universal_newlines=None, **env):
         kwargs_matcher = {}
         if echo is not None:
             kwargs_matcher["echo"] = Is(echo)
@@ -46,6 +46,8 @@ class RanCommand(MatchesListwise):
             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()})
@@ -230,7 +232,9 @@ class TestBuildSnap(TestCase):
             RanBuildCommand(
                 ["bzr", "branch", "lp:foo", "test-snap"], cwd="/build"),
             RanBuildCommand(
-                ["bzr", "revno"], cwd="/build/test-snap", get_output=True),
+                ["bzr", "revno"],
+                cwd="/build/test-snap", get_output=True,
+                universal_newlines=True),
             ]))
         status_path = os.path.join(build_snap.backend.build_path, "status")
         with open(status_path) as status:
@@ -254,7 +258,8 @@ class TestBuildSnap(TestCase):
                 cwd="/build/test-snap"),
             RanBuildCommand(
                 ["git", "rev-parse", "HEAD^{}"],
-                cwd="/build/test-snap", get_output=True),
+                cwd="/build/test-snap",
+                get_output=True, universal_newlines=True),
             ]))
         status_path = os.path.join(build_snap.backend.build_path, "status")
         with open(status_path) as status:
@@ -279,7 +284,8 @@ class TestBuildSnap(TestCase):
                 cwd="/build/test-snap"),
             RanBuildCommand(
                 ["git", "rev-parse", "next^{}"],
-                cwd="/build/test-snap", get_output=True),
+                cwd="/build/test-snap", get_output=True,
+                universal_newlines=True),
             ]))
         status_path = os.path.join(build_snap.backend.build_path, "status")
         with open(status_path) as status:
@@ -305,7 +311,8 @@ class TestBuildSnap(TestCase):
                 cwd="/build/test-snap"),
             RanBuildCommand(
                 ["git", "rev-parse", "refs/tags/1.0^{}"],
-                cwd="/build/test-snap", get_output=True),
+                cwd="/build/test-snap", get_output=True,
+                universal_newlines=True),
             ]))
         status_path = os.path.join(build_snap.backend.build_path, "status")
         with open(status_path) as status:
@@ -336,7 +343,8 @@ class TestBuildSnap(TestCase):
                 cwd="/build/test-snap", **env),
             RanBuildCommand(
                 ["git", "rev-parse", "HEAD^{}"],
-                cwd="/build/test-snap", get_output=True),
+                cwd="/build/test-snap", get_output=True,
+                universal_newlines=True),
             ]))
         status_path = os.path.join(build_snap.backend.build_path, "status")
         with open(status_path) as status:
diff --git a/lpbuildd/target/tests/test_chroot.py b/lpbuildd/target/tests/test_chroot.py
index 861a893..88e3137 100644
--- a/lpbuildd/target/tests/test_chroot.py
+++ b/lpbuildd/target/tests/test_chroot.py
@@ -24,6 +24,7 @@ from testtools.matchers import DirContains
 from lpbuildd.target.backend import BackendException
 from lpbuildd.target.chroot import Chroot
 from lpbuildd.target.tests.testfixtures import (
+    CarefulFakeProcessFixture,
     FakeFilesystem,
     KillFixture,
     SudoUmount,
@@ -32,6 +33,10 @@ from lpbuildd.target.tests.testfixtures import (
 
 class TestChroot(TestCase):
 
+    def setUp(self):
+        super(TestChroot, self).setUp()
+        self.useFixture(CarefulFakeProcessFixture())
+
     def test_create(self):
         self.useFixture(EnvironmentVariable("HOME", "/expected/home"))
         processes_fixture = self.useFixture(FakeProcesses())
diff --git a/lpbuildd/target/tests/test_lxd.py b/lpbuildd/target/tests/test_lxd.py
index 77e7f77..145630b 100644
--- a/lpbuildd/target/tests/test_lxd.py
+++ b/lpbuildd/target/tests/test_lxd.py
@@ -49,6 +49,7 @@ from lpbuildd.target.lxd import (
     fallback_hosts,
     policy_rc_d,
     )
+from lpbuildd.target.tests.testfixtures import CarefulFakeProcessFixture
 
 
 LXD_RUNNING = 103
@@ -118,6 +119,10 @@ class FakeFilesystem(_FakeFilesystem):
 
 class TestLXD(TestCase):
 
+    def setUp(self):
+        super(TestLXD, self).setUp()
+        self.useFixture(CarefulFakeProcessFixture())
+
     def make_chroot_tarball(self, output_path):
         source = self.useFixture(TempDir()).path
         hello = os.path.join(source, "bin", "hello")
diff --git a/lpbuildd/target/tests/testfixtures.py b/lpbuildd/target/tests/testfixtures.py
index 41116ff..02cd995 100644
--- a/lpbuildd/target/tests/testfixtures.py
+++ b/lpbuildd/target/tests/testfixtures.py
@@ -4,10 +4,13 @@
 __metaclass__ = type
 
 import argparse
+import io
 import os
 import shutil
 
 from fixtures import MonkeyPatch
+from fixtures._fixtures import popen
+import six
 from systemfixtures import FakeFilesystem as _FakeFilesystem
 
 
@@ -101,3 +104,37 @@ class FakeFilesystem(_FakeFilesystem):
             if path.startswith(prefix):
                 return False
         return super(FakeFilesystem, self)._is_fake_path(path, *args, **kwargs)
+
+
+class CarefulFakeProcess(popen.FakeProcess):
+    """A version of FakeProcess that is more careful about text mode."""
+
+    def __init__(self, *args, **kwargs):
+        super(CarefulFakeProcess, self).__init__(*args, **kwargs)
+        text_mode = bool(self._args.get("universal_newlines"))
+        if not self.stdout:
+            self.stdout = io.StringIO() if text_mode else io.BytesIO()
+        if not self.stderr:
+            self.stderr = io.StringIO() if text_mode else io.BytesIO()
+
+    def communicate(self, *args, **kwargs):
+        out, err = super(CarefulFakeProcess, self).communicate(*args, **kwargs)
+        if self._args.get("universal_newlines"):
+            if isinstance(out, bytes):
+                raise TypeError("Process stdout is bytes, expecting text")
+            if isinstance(err, bytes):
+                raise TypeError("Process stderr is bytes, expecting text")
+        else:
+            if isinstance(out, six.text_type):
+                raise TypeError("Process stdout is text, expecting bytes")
+            if isinstance(err, six.text_type):
+                raise TypeError("Process stderr is text, expecting bytes")
+        return out, err
+
+
+class CarefulFakeProcessFixture(MonkeyPatch):
+    """Patch the Popen fixture to be more careful about text mode."""
+
+    def __init__(self):
+        super(CarefulFakeProcessFixture, self).__init__(
+            "fixtures._fixtures.popen.FakeProcess", CarefulFakeProcess)