launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #25366
[Merge] ~cjwatson/launchpad-buildd:runSubProcess-bytes into launchpad-buildd:master
Colin Watson has proposed merging ~cjwatson/launchpad-buildd:runSubProcess-bytes into launchpad-buildd:master.
Commit message:
Fix handling of bytes arguments passed to BuildManager.runSubProcess
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~cjwatson/launchpad-buildd/+git/launchpad-buildd/+merge/391295
This requires some ridiculous contortions to avoid problems with Twisted logging, especially on Python 2.
--
Your team Launchpad code reviewers is requested to review the proposed merge of ~cjwatson/launchpad-buildd:runSubProcess-bytes into launchpad-buildd:master.
diff --git a/debian/changelog b/debian/changelog
index 1d3540b..e46111d 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,9 @@
+launchpad-buildd (193) UNRELEASED; urgency=medium
+
+ * Fix handling of bytes arguments passed to BuildManager.runSubProcess.
+
+ -- Colin Watson <cjwatson@xxxxxxxxxx> Thu, 24 Sep 2020 16:45:27 +0100
+
launchpad-buildd (192) bionic; urgency=medium
* Update Maintainer to launchpad-dev.
diff --git a/lpbuildd/builder.py b/lpbuildd/builder.py
index 4c4135a..5e459be 100644
--- a/lpbuildd/builder.py
+++ b/lpbuildd/builder.py
@@ -16,6 +16,7 @@ import shutil
import tempfile
import apt
+import six
from six.moves.urllib.request import (
build_opener,
HTTPBasicAuthHandler,
@@ -145,8 +146,11 @@ class BuildManager(object):
if iterate is None:
iterate = self.iterate
self._subprocess = RunCapture(self._builder, iterate, stdin=stdin)
+ text_args = [
+ arg.decode("UTF-8", "replace") if isinstance(arg, bytes) else arg
+ for arg in args[1:]]
self._builder.log("RUN: %s %s\n" % (
- command, " ".join(shell_escape(arg) for arg in args[1:])))
+ command, " ".join(shell_escape(arg) for arg in text_args)))
childfds = {
0: devnull.fileno() if stdin is None else "w",
1: "r",
@@ -516,11 +520,24 @@ class Builder(object):
data if isinstance(data, bytes) else data.encode("UTF-8"))
self._log.write(data_bytes)
self._log.flush()
- if not isinstance(data, str):
- data = data.decode("UTF-8", "replace")
- if data.endswith("\n"):
- data = data[:-1]
- log.msg("Build log: " + data)
+ data_text = (
+ data if isinstance(data, six.text_type)
+ else data.decode("UTF-8", "replace"))
+ if six.PY3:
+ data_str = data_text
+ else:
+ # Twisted's logger doesn't handle non-ASCII text very reliably
+ # on Python 2. This is just for debugging, so replace non-ASCII
+ # characters with the corresponding \u escapes. We need to go
+ # to ridiculous lengths here to avoid (e.g.) replacing newlines
+ # with "\n".
+ data_str = re.sub(
+ r"([^\x00-\x7f])",
+ lambda match: "\\u%04x" % ord(match.group(0)),
+ data_text).encode("UTF-8")
+ if data_str.endswith("\n"):
+ data_str = data_str[:-1]
+ log.msg("Build log: " + data_str)
def getLogTail(self):
"""Return the tail of the log.
diff --git a/lpbuildd/tests/test_builder.py b/lpbuildd/tests/test_builder.py
new file mode 100644
index 0000000..2048bc6
--- /dev/null
+++ b/lpbuildd/tests/test_builder.py
@@ -0,0 +1,83 @@
+# Copyright 2020 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Test BuildManager directly.
+
+Most tests are done on subclasses instead.
+"""
+
+import io
+import re
+
+from fixtures import (
+ FakeLogger,
+ TempDir,
+ )
+import six
+from testtools import TestCase
+from testtools.deferredruntest import AsynchronousDeferredRunTest
+from twisted.internet import defer
+from twisted.python import log
+
+from lpbuildd.builder import (
+ Builder,
+ BuildManager,
+ )
+from lpbuildd.tests.fakebuilder import FakeConfig
+
+
+class TestBuildManager(TestCase):
+
+ run_tests_with = AsynchronousDeferredRunTest.make_factory(timeout=5)
+
+ def setUp(self):
+ super(TestBuildManager, self).setUp()
+ observer = log.PythonLoggingObserver()
+ observer.start()
+ self.addCleanup(observer.stop)
+
+ @defer.inlineCallbacks
+ def test_runSubProcess(self):
+ logger = self.useFixture(FakeLogger())
+ config = FakeConfig()
+ config.set("builder", "filecache", self.useFixture(TempDir()).path)
+ builder = Builder(config)
+ builder._log = io.BytesIO()
+ manager = BuildManager(builder, "123")
+ d = defer.Deferred()
+ manager.iterate = d.callback
+ manager.runSubProcess("echo", ["echo", "hello world"])
+ code = yield d
+ self.assertEqual(0, code)
+ self.assertEqual(
+ b"RUN: echo 'hello world'\n"
+ b"hello world\n",
+ builder._log.getvalue())
+ self.assertEqual(
+ "Build log: RUN: echo 'hello world'\n"
+ "Build log: hello world\n",
+ logger.output)
+
+ @defer.inlineCallbacks
+ def test_runSubProcess_bytes(self):
+ logger = self.useFixture(FakeLogger())
+ config = FakeConfig()
+ config.set("builder", "filecache", self.useFixture(TempDir()).path)
+ builder = Builder(config)
+ builder._log = io.BytesIO()
+ manager = BuildManager(builder, "123")
+ d = defer.Deferred()
+ manager.iterate = d.callback
+ manager.runSubProcess("echo", ["echo", u"\N{SNOWMAN}".encode("UTF-8")])
+ code = yield d
+ self.assertEqual(0, code)
+ self.assertEqual(
+ u"RUN: echo '\N{SNOWMAN}'\n"
+ u"\N{SNOWMAN}\n".encode("UTF-8"),
+ builder._log.getvalue())
+ logged_snowman = '\N{SNOWMAN}' if six.PY3 else '\\u2603'
+ self.assertEqual(
+ ["Build log: RUN: echo '%s'" % logged_snowman,
+ "Build log: %s" % logged_snowman],
+ [re.sub(r".*? \[-\] (.*)", r"\1", line)
+ for line in logger.output.splitlines()])