← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~cjwatson/launchpad-buildd/fix-recipe-depfail into lp:launchpad-buildd

 

Colin Watson has proposed merging lp:~cjwatson/launchpad-buildd/fix-recipe-depfail into lp:launchpad-buildd.

Commit message:
Fix dep-wait detection when recipes fail to install build-dependencies.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)
Related bugs:
  Bug #1234621 in launchpad-buildd: "lp-buildd crashes on recipe failures: too many values to unpack"
  https://bugs.launchpad.net/launchpad-buildd/+bug/1234621

For more details, see:
https://code.launchpad.net/~cjwatson/launchpad-buildd/fix-recipe-depfail/+merge/189041

Fix a crash in the recipe build manager, introduced in launchpad-buildd 116.  This slipped through because the recipe build manager was untested, so I wrote a load of test code first.

QA: Try a recipe build with a deliberately unsatisfiable dependency, and make sure it lands in dep-wait.
-- 
https://code.launchpad.net/~cjwatson/launchpad-buildd/fix-recipe-depfail/+merge/189041
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~cjwatson/launchpad-buildd/fix-recipe-depfail into lp:launchpad-buildd.
=== modified file 'Makefile'
--- Makefile	2013-07-25 17:26:10 +0000
+++ Makefile	2013-10-03 10:41:22 +0000
@@ -31,4 +31,5 @@
 		   lpbuildd.tests.test_buildd_slave \
 		   lpbuildd.tests.test_check_implicit_pointer_functions \
 		   lpbuildd.tests.test_harness \
-		   lpbuildd.tests.test_translationtemplatesbuildmanager
+		   lpbuildd.tests.test_translationtemplatesbuildmanager \
+		   lpbuildd.tests.test_sourcepackagerecipe

=== modified file 'debian/changelog'
--- debian/changelog	2013-09-27 12:12:29 +0000
+++ debian/changelog	2013-10-03 10:41:22 +0000
@@ -1,3 +1,10 @@
+launchpad-buildd (117) UNRELEASED; urgency=low
+
+  * Fix dep-wait detection when recipes fail to install build-dependencies
+    (LP: #1234621).
+
+ -- Colin Watson <cjwatson@xxxxxxxxxx>  Thu, 03 Oct 2013 11:38:46 +0100
+
 launchpad-buildd (116) hardy; urgency=low
 
   [ Colin Watson ]

=== modified file 'lpbuildd/sourcepackagerecipe.py'
--- lpbuildd/sourcepackagerecipe.py	2013-09-19 11:01:10 +0000
+++ lpbuildd/sourcepackagerecipe.py	2013-10-03 10:41:22 +0000
@@ -102,7 +102,7 @@
                 rx = (
                     'The following packages have unmet dependencies:\n'
                     '.*: Depends: ([^ ]*( \([^)]*\))?)')
-                _, mo = self.searchLogContents([rx, re.M])
+                _, mo = self.searchLogContents([[rx, re.M]])
                 if mo:
                     self._slave.depFail(mo.group(1))
                     print("Returning build status: DEPFAIL")

=== added file 'lpbuildd/tests/test_sourcepackagerecipe.py'
--- lpbuildd/tests/test_sourcepackagerecipe.py	1970-01-01 00:00:00 +0000
+++ lpbuildd/tests/test_sourcepackagerecipe.py	2013-10-03 10:41:22 +0000
@@ -0,0 +1,212 @@
+# Copyright 2013 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+__metaclass__ = type
+
+import os
+import shutil
+import tempfile
+from textwrap import dedent
+
+from testtools import TestCase
+
+from lpbuildd.tests.fakeslave import FakeSlave
+from lpbuildd.sourcepackagerecipe import (
+    RETCODE_FAILURE_INSTALL_BUILD_DEPS,
+    SourcePackageRecipeBuildManager,
+    SourcePackageRecipeBuildState,
+    )
+
+
+class MockBuildManager(SourcePackageRecipeBuildManager):
+    def __init__(self, *args, **kwargs):
+        super(MockBuildManager, self).__init__(*args, **kwargs)
+        self.commands = []
+        self.iterators = []
+
+    def runSubProcess(self, path, command, iterate=None):
+        self.commands.append([path]+command)
+        if iterate is None:
+            iterate = self.iterate
+        self.iterators.append(iterate)
+        return 0
+
+
+class TestSourcePackageRecipeBuildManagerIteration(TestCase):
+    """Run SourcePackageRecipeBuildManager through its iteration steps."""
+    def setUp(self):
+        super(TestSourcePackageRecipeBuildManagerIteration, self).setUp()
+        self.working_dir = tempfile.mkdtemp()
+        self.addCleanup(lambda: shutil.rmtree(self.working_dir))
+        slave_dir = os.path.join(self.working_dir, 'slave')
+        home_dir = os.path.join(self.working_dir, 'home')
+        for dir in (slave_dir, home_dir):
+            os.mkdir(dir)
+        self.slave = FakeSlave(slave_dir)
+        self.buildid = '123'
+        self.buildmanager = MockBuildManager(self.slave, self.buildid)
+        self.buildmanager.home = home_dir
+        self.buildmanager._cachepath = self.slave._cachepath
+        self.chrootdir = os.path.join(
+            home_dir, 'build-%s' % self.buildid, 'chroot-autobuild')
+
+    def getState(self):
+        """Retrieve build manager's state."""
+        return self.buildmanager._state
+
+    def startBuild(self):
+        # The build manager's iterate() kicks off the consecutive states
+        # after INIT.
+        extra_args = {
+            'recipe_text': dedent("""\
+                # bzr-builder format 0.2 deb-version {debupstream}-0~{revno}
+                http://bazaar.launchpad.dev/~ppa-user/+junk/wakeonlan""";),
+            'suite': 'maverick',
+            'ogrecomponent': 'universe',
+            'author_name': 'Steve\u1234',
+            'author_email': 'stevea@xxxxxxxxxxx',
+            'archive_purpose': 'puppies',
+            'distroseries_name': 'maverick',
+            'archives': [
+                'deb http://archive.ubuntu.com/ubuntu maverick main universe',
+                'deb http://ppa.launchpad.net/launchpad/bzr-builder-dev/'
+                    'ubuntu main',
+                ],
+            }
+        self.buildmanager.initiate({}, 'chroot.tar.gz', extra_args)
+
+        # Skip states that are done in DebianBuildManager to the state
+        # directly before BUILD_RECIPE.
+        self.buildmanager._state = SourcePackageRecipeBuildState.UPDATE
+
+        # BUILD_RECIPE: Run the slave's payload to build the source package.
+        self.buildmanager.iterate(0)
+        self.assertEqual(
+            SourcePackageRecipeBuildState.BUILD_RECIPE, self.getState())
+        expected_command = [
+            'buildrecipepath', 'buildrecipe', self.buildid,
+            'Steve\u1234'.encode('utf-8'), 'stevea@xxxxxxxxxxx',
+            'maverick', 'maverick', 'universe', 'puppies',
+            ]
+        self.assertEqual(expected_command, self.buildmanager.commands[-1])
+        self.assertEqual(
+            self.buildmanager.iterate, self.buildmanager.iterators[-1])
+        self.assertFalse(self.slave.wasCalled('chrootFail'))
+
+    def test_iterate(self):
+        # The build manager iterates a normal build from start to finish.
+        self.startBuild()
+
+        log_path = os.path.join(self.buildmanager._cachepath, 'buildlog')
+        log = open(log_path, 'w')
+        log.write("I am a build log.")
+        log.close()
+
+        changes_path = os.path.join(
+            self.buildmanager.home, 'build-%s' % self.buildid,
+            'foo_1_source.changes')
+        changes = open(changes_path, 'w')
+        changes.write("I am a changes file.")
+        changes.close()
+
+        manifest_path = os.path.join(
+            self.buildmanager.home, 'build-%s' % self.buildid, 'manifest')
+        manifest = open(manifest_path, 'w')
+        manifest.write("I am a manifest file.")
+        manifest.close()
+
+        # After building the package, reap processes.
+        self.buildmanager.iterate(0)
+        expected_command = [
+            'processscanpath', 'scan-for-processes', self.buildid,
+            ]
+        self.assertEqual(
+            SourcePackageRecipeBuildState.BUILD_RECIPE, self.getState())
+        self.assertEqual(expected_command, self.buildmanager.commands[-1])
+        self.assertNotEqual(
+            self.buildmanager.iterate, self.buildmanager.iterators[-1])
+        self.assertFalse(self.slave.wasCalled('buildFail'))
+        self.assertEqual(
+            [((changes_path,), {}), ((manifest_path,), {})],
+            self.slave.addWaitingFile.calls)
+
+        # Control returns to the DebianBuildManager in the UMOUNT state.
+        self.buildmanager.iterateReap(self.getState(), 0)
+        expected_command = [
+            'umountpath', 'umount-chroot', self.buildid
+            ]
+        self.assertEqual(SourcePackageRecipeBuildState.UMOUNT, self.getState())
+        self.assertEqual(expected_command, self.buildmanager.commands[-1])
+        self.assertEqual(
+            self.buildmanager.iterate, self.buildmanager.iterators[-1])
+        self.assertFalse(self.slave.wasCalled('buildFail'))
+
+    def test_iterate_BUILD_RECIPE_install_build_deps_depfail(self):
+        # The build manager can detect dependency wait states.
+        self.startBuild()
+
+        log_path = os.path.join(self.buildmanager._cachepath, 'buildlog')
+        log = open(log_path, 'w')
+        log.write(
+            "The following packages have unmet dependencies:\n"
+            " pbuilder-satisfydepends-dummy : Depends: base-files (>= 1000)"
+            " but it is not going to be installed.\n")
+        log.close()
+
+        # The buildmanager calls depFail correctly and reaps processes.
+        self.buildmanager.iterate(RETCODE_FAILURE_INSTALL_BUILD_DEPS)
+        expected_command = [
+            'processscanpath', 'scan-for-processes', self.buildid
+            ]
+        self.assertEqual(
+            SourcePackageRecipeBuildState.BUILD_RECIPE, self.getState())
+        self.assertEqual(expected_command, self.buildmanager.commands[-1])
+        self.assertNotEqual(
+            self.buildmanager.iterate, self.buildmanager.iterators[-1])
+        self.assertFalse(self.slave.wasCalled('buildFail'))
+        self.assertEqual(
+            [(("base-files (>= 1000)",), {})], self.slave.depFail.calls)
+
+        # Control returns to the DebianBuildManager in the UMOUNT state.
+        self.buildmanager.iterateReap(self.getState(), 0)
+        expected_command = [
+            'umountpath', 'umount-chroot', self.buildid
+            ]
+        self.assertEqual(SourcePackageRecipeBuildState.UMOUNT, self.getState())
+        self.assertEqual(expected_command, self.buildmanager.commands[-1])
+        self.assertEqual(
+            self.buildmanager.iterate, self.buildmanager.iterators[-1])
+        self.assertFalse(self.slave.wasCalled('buildFail'))
+
+    def test_iterate_BUILD_RECIPE_install_build_deps_buildfail(self):
+        # If the build manager cannot detect a dependency wait from a
+        # build-dependency installation failure, it fails the build.
+        self.startBuild()
+
+        log_path = os.path.join(self.buildmanager._cachepath, 'buildlog')
+        log = open(log_path, 'w')
+        log.write("I am a failing build log.")
+        log.close()
+
+        # The buildmanager calls buildFail correctly and reaps processes.
+        self.buildmanager.iterate(RETCODE_FAILURE_INSTALL_BUILD_DEPS)
+        expected_command = [
+            'processscanpath', 'scan-for-processes', self.buildid
+            ]
+        self.assertEqual(
+            SourcePackageRecipeBuildState.BUILD_RECIPE, self.getState())
+        self.assertEqual(expected_command, self.buildmanager.commands[-1])
+        self.assertNotEqual(
+            self.buildmanager.iterate, self.buildmanager.iterators[-1])
+        self.assertTrue(self.slave.wasCalled('buildFail'))
+        self.assertFalse(self.slave.wasCalled('depFail'))
+
+        # Control returns to the DebianBuildManager in the UMOUNT state.
+        self.buildmanager.iterateReap(self.getState(), 0)
+        expected_command = [
+            'umountpath', 'umount-chroot', self.buildid
+            ]
+        self.assertEqual(SourcePackageRecipeBuildState.UMOUNT, self.getState())
+        self.assertEqual(expected_command, self.buildmanager.commands[-1])
+        self.assertEqual(
+            self.buildmanager.iterate, self.buildmanager.iterators[-1])


References