Merge lp:~cjwatson/launchpad-buildd/fix-abort into lp:launchpad-buildd

Proposed by Colin Watson
Status: Merged
Approved by: William Grant
Approved revision: 64
Merged at revision: 68
Proposed branch: lp:~cjwatson/launchpad-buildd/fix-abort
Merge into: lp:launchpad-buildd
Diff against target: 1160 lines (+562/-180)
14 files modified
Makefile (+1/-0)
debian/changelog (+7/-0)
debian/upgrade-config (+19/-1)
lpbuildd/binarypackage.py (+9/-6)
lpbuildd/debian.py (+34/-20)
lpbuildd/slave.py (+123/-48)
lpbuildd/sourcepackagerecipe.py (+6/-2)
lpbuildd/tests/buildd-slave-test.conf (+1/-2)
lpbuildd/tests/fakeslave.py (+104/-0)
lpbuildd/tests/harness.py (+1/-0)
lpbuildd/tests/test_binarypackage.py (+206/-0)
lpbuildd/tests/test_translationtemplatesbuildmanager.py (+44/-96)
lpbuildd/translationtemplates.py (+6/-4)
template-buildd-slave.conf (+1/-1)
To merge this branch: bzr merge lp:~cjwatson/launchpad-buildd/fix-abort
Reviewer Review Type Date Requested Status
William Grant code Approve
Adam Conrad (community) Approve
Review via email: mp+177003@code.launchpad.net

Commit message

Make abort work properly, calling scan-for-processes to kill all processes in the chroot.

Description of the change

Make the abort operation work, so that we have a hope of being able to cancel builds.

This turned out to be gratuitously complicated, involving moving scan-for-processes configuration up to the slave, rearranging how reaping is handled in the state machine so that it can reasonably easily happen at multiple stages, and introducing a new BuildStatus.ABORTED state (which should eventually supersede BuilderStatus.ABORTED) so that cancelled builds can be WAITING/ABORTED rather than just ABORTED. But I believe it all works now.

To post a comment you must log in.
60. By Colin Watson

Handle calls to abort while already ABORTING.

61. By Colin Watson

Avoid awkward Python conditional expression syntax.

62. By Colin Watson

Move processscanpath to the end of the allmanagers section on upgrade, not the start.

Revision history for this message
Adam Conrad (adconrad) wrote :

Looks good to me after several readings, and having seen it in action last week. Given that it was a longer change than expected, I'd like to see a review from William too (especially as he's better versed in nit-picking Python/LP bits), but it all logically makes sense to me.

review: Approve
63. By Colin Watson

Merge trunk.

64. By Colin Watson

Merge trunk.

Revision history for this message
William Grant (wgrant) :
review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'Makefile'
--- Makefile 2011-11-21 01:47:22 +0000
+++ Makefile 2013-07-30 11:30:43 +0000
@@ -27,6 +27,7 @@
27# a Launchpad checkout.27# a Launchpad checkout.
28check:28check:
29 PYTHONPATH=$(PWD):$(PYTHONPATH) $(PYTHON) -m testtools.run -v \29 PYTHONPATH=$(PWD):$(PYTHONPATH) $(PYTHON) -m testtools.run -v \
30 lpbuildd.tests.test_binarypackage \
30 lpbuildd.tests.test_buildd_slave \31 lpbuildd.tests.test_buildd_slave \
31 lpbuildd.tests.test_check_implicit_pointer_functions \32 lpbuildd.tests.test_check_implicit_pointer_functions \
32 lpbuildd.tests.test_harness \33 lpbuildd.tests.test_harness \
3334
=== modified file 'debian/changelog'
--- debian/changelog 2013-07-29 23:48:58 +0000
+++ debian/changelog 2013-07-30 11:30:43 +0000
@@ -1,5 +1,6 @@
1launchpad-buildd (115) UNRELEASED; urgency=low1launchpad-buildd (115) UNRELEASED; urgency=low
22
3 [ Adam Conrad ]
3 * Short the readlink call in scan-for-process with a true to avoid4 * Short the readlink call in scan-for-process with a true to avoid
4 prematurely exiting the process scan when tripping over zombies.5 prematurely exiting the process scan when tripping over zombies.
5 * Write to temporary cache files and then rename after validation6 * Write to temporary cache files and then rename after validation
@@ -12,6 +13,12 @@
12 * Strip trailing whitespace in buildd-genconfig because I'm anal.13 * Strip trailing whitespace in buildd-genconfig because I'm anal.
13 * Mangle recipe versions to match backports policy (LP: #1095103)14 * Mangle recipe versions to match backports policy (LP: #1095103)
1415
16 [ Colin Watson ]
17 * Move scan-for-processes up to the top-level slave code so that it is
18 available for more general use.
19 * Make abort work properly, calling scan-for-processes to kill all
20 processes in the chroot.
21
15 -- Adam Conrad <adconrad@ubuntu.com> Mon, 29 Apr 2013 15:47:20 -060022 -- Adam Conrad <adconrad@ubuntu.com> Mon, 29 Apr 2013 15:47:20 -0600
1623
17launchpad-buildd (114) hardy; urgency=low24launchpad-buildd (114) hardy; urgency=low
1825
=== modified file 'debian/upgrade-config'
--- debian/upgrade-config 2011-12-05 02:39:05 +0000
+++ debian/upgrade-config 2013-07-30 11:30:43 +0000
@@ -7,7 +7,6 @@
77
8import re8import re
9import sys9import sys
10import os
11import subprocess10import subprocess
1211
13(old_version, conf_file) = sys.argv[1:]12(old_version, conf_file) = sys.argv[1:]
@@ -114,6 +113,23 @@
114 in_file.close()113 in_file.close()
115 out_file.close()114 out_file.close()
116115
116def upgrade_to_115():
117 print "Upgrading %s to version 115" % conf_file
118 subprocess.call(["mv", conf_file, conf_file+"-prev115~"])
119 in_allmanagers = False
120 in_file = open(conf_file+"-prev115~", "r")
121 out_file = open(conf_file, "w")
122 for line in in_file:
123 if line.startswith("[allmanagers]"):
124 in_allmanagers = True
125 elif in_allmanagers and (line.startswith("[") or not line.strip()):
126 out_file.write("processscanpath = /usr/share/launchpad-buildd/slavebin/scan-for-processes\n")
127 in_allmanagers = False
128 if not line.startswith("processscanpath = "):
129 out_file.write(line)
130 in_file.close()
131 out_file.close()
132
117if __name__ == "__main__":133if __name__ == "__main__":
118 old_version = re.sub(r'[~-].*', '', old_version)134 old_version = re.sub(r'[~-].*', '', old_version)
119 if int(old_version) < 12:135 if int(old_version) < 12:
@@ -132,4 +148,6 @@
132 upgrade_to_63()148 upgrade_to_63()
133 if int(old_version) < 110:149 if int(old_version) < 110:
134 upgrade_to_110()150 upgrade_to_110()
151 if int(old_version) < 115:
152 upgrade_to_115()
135153
136154
=== modified file 'lpbuildd/binarypackage.py'
--- lpbuildd/binarypackage.py 2011-11-10 04:55:02 +0000
+++ lpbuildd/binarypackage.py 2013-07-30 11:30:43 +0000
@@ -39,8 +39,8 @@
3939
40 initial_build_state = BinaryPackageBuildState.SBUILD40 initial_build_state = BinaryPackageBuildState.SBUILD
4141
42 def __init__(self, slave, buildid):42 def __init__(self, slave, buildid, **kwargs):
43 DebianBuildManager.__init__(self, slave, buildid)43 DebianBuildManager.__init__(self, slave, buildid, **kwargs)
44 self._sbuildpath = slave._config.get("binarypackagemanager", "sbuildpath")44 self._sbuildpath = slave._config.get("binarypackagemanager", "sbuildpath")
45 self._sbuildargs = slave._config.get("binarypackagemanager",45 self._sbuildargs = slave._config.get("binarypackagemanager",
46 "sbuildargs").split(" ")46 "sbuildargs").split(" ")
@@ -124,10 +124,13 @@
124 print("Returning build status: BUILDERFAIL")124 print("Returning build status: BUILDERFAIL")
125 self._slave.builderFail()125 self._slave.builderFail()
126 self.alreadyfailed = True126 self.alreadyfailed = True
127 self._state = DebianBuildState.REAP127 self.doReapProcesses(self._state)
128 self.doReapProcesses()
129 else:128 else:
130 print("Returning build status: OK")129 print("Returning build status: OK")
131 self.gatherResults()130 self.gatherResults()
132 self._state = DebianBuildState.REAP131 self.doReapProcesses(self._state)
133 self.doReapProcesses()132
133 def iterateReap_SBUILD(self, success):
134 """Finished reaping after sbuild run."""
135 self._state = DebianBuildState.UMOUNT
136 self.doUnmounting()
134137
=== modified file 'lpbuildd/debian.py'
--- lpbuildd/debian.py 2011-11-18 04:17:27 +0000
+++ lpbuildd/debian.py 2013-07-30 11:30:43 +0000
@@ -22,7 +22,6 @@
22 MOUNT = "MOUNT"22 MOUNT = "MOUNT"
23 SOURCES = "SOURCES"23 SOURCES = "SOURCES"
24 UPDATE = "UPDATE"24 UPDATE = "UPDATE"
25 REAP = "REAP"
26 UMOUNT = "UMOUNT"25 UMOUNT = "UMOUNT"
27 CLEANUP = "CLEANUP"26 CLEANUP = "CLEANUP"
2827
@@ -30,10 +29,9 @@
30class DebianBuildManager(BuildManager):29class DebianBuildManager(BuildManager):
31 """Base behaviour for Debian chrooted builds."""30 """Base behaviour for Debian chrooted builds."""
3231
33 def __init__(self, slave, buildid):32 def __init__(self, slave, buildid, **kwargs):
34 BuildManager.__init__(self, slave, buildid)33 BuildManager.__init__(self, slave, buildid, **kwargs)
35 self._updatepath = slave._config.get("debianmanager", "updatepath")34 self._updatepath = slave._config.get("debianmanager", "updatepath")
36 self._scanpath = slave._config.get("debianmanager", "processscanpath")
37 self._sourcespath = slave._config.get("debianmanager", "sourcespath")35 self._sourcespath = slave._config.get("debianmanager", "sourcespath")
38 self._cachepath = slave._config.get("slave","filecache")36 self._cachepath = slave._config.get("slave","filecache")
39 self._state = DebianBuildState.INIT37 self._state = DebianBuildState.INIT
@@ -74,10 +72,6 @@
74 """72 """
75 raise NotImplementedError()73 raise NotImplementedError()
7674
77 def doReapProcesses(self):
78 """Reap any processes left lying around in the chroot."""
79 self.runSubProcess( self._scanpath, [self._scanpath, self._buildid] )
80
81 @staticmethod75 @staticmethod
82 def _parseChangesFile(linesIter):76 def _parseChangesFile(linesIter):
83 """A generator that iterates over files listed in a changes file.77 """A generator that iterates over files listed in a changes file.
@@ -98,7 +92,7 @@
9892
99 def getChangesFilename(self):93 def getChangesFilename(self):
100 changes = self._dscfile[:-4] + "_" + self.arch_tag + ".changes"94 changes = self._dscfile[:-4] + "_" + self.arch_tag + ".changes"
101 return get_build_path(self._buildid, changes)95 return get_build_path(self.home, self._buildid, changes)
10296
103 def gatherResults(self):97 def gatherResults(self):
104 """Gather the results of the build and add them to the file cache.98 """Gather the results of the build and add them to the file cache.
@@ -113,7 +107,8 @@
113 seenfiles = False107 seenfiles = False
114108
115 for fn in self._parseChangesFile(chfile):109 for fn in self._parseChangesFile(chfile):
116 self._slave.addWaitingFile(get_build_path(self._buildid, fn))110 self._slave.addWaitingFile(
111 get_build_path(self.home, self._buildid, fn))
117112
118 chfile.close()113 chfile.close()
119114
@@ -128,6 +123,15 @@
128 raise ValueError, "Unknown internal state " + self._state123 raise ValueError, "Unknown internal state " + self._state
129 func(success)124 func(success)
130125
126 def iterateReap(self, state, success):
127 print ("Iterating with success flag %s against stage %s after reaping "
128 "processes"
129 % (success, state))
130 func = getattr(self, "iterateReap_" + state, None)
131 if func is None:
132 raise ValueError, "Unknown internal post-reap state " + state
133 func(success)
134
131 def iterate_INIT(self, success):135 def iterate_INIT(self, success):
132 """Just finished initializing the build."""136 """Just finished initializing the build."""
133 if success != 0:137 if success != 0:
@@ -183,26 +187,29 @@
183 if not self.alreadyfailed:187 if not self.alreadyfailed:
184 self._slave.chrootFail()188 self._slave.chrootFail()
185 self.alreadyfailed = True189 self.alreadyfailed = True
186 self._state = DebianBuildState.REAP190 self.doReapProcesses(self._state)
187 self.doReapProcesses()
188 else:191 else:
189 self._state = DebianBuildState.UPDATE192 self._state = DebianBuildState.UPDATE
190 self.doUpdateChroot()193 self.doUpdateChroot()
191194
195 def iterateReap_SOURCES(self, success):
196 """Just finished reaping after failure to overwrite sources.list."""
197 self._state = DebianBuildState.UMOUNT
198 self.doUnmounting()
199
192 def iterate_UPDATE(self, success):200 def iterate_UPDATE(self, success):
193 """Just finished updating the chroot."""201 """Just finished updating the chroot."""
194 if success != 0:202 if success != 0:
195 if not self.alreadyfailed:203 if not self.alreadyfailed:
196 self._slave.chrootFail()204 self._slave.chrootFail()
197 self.alreadyfailed = True205 self.alreadyfailed = True
198 self._state = DebianBuildState.REAP206 self.doReapProcesses(self._state)
199 self.doReapProcesses()
200 else:207 else:
201 self._state = self.initial_build_state208 self._state = self.initial_build_state
202 self.doRunBuild()209 self.doRunBuild()
203210
204 def iterate_REAP(self, success):211 def iterateReap_UPDATE(self, success):
205 """Finished reaping processes; ignore error returns."""212 """Just finished reaping after failure to update the chroot."""
206 self._state = DebianBuildState.UMOUNT213 self._state = DebianBuildState.UMOUNT
207 self.doUnmounting()214 self.doUnmounting()
208215
@@ -227,13 +234,20 @@
227 self._slave.buildOK()234 self._slave.buildOK()
228 self._slave.buildComplete()235 self._slave.buildComplete()
229236
230237 def abortReap(self):
231def get_build_path(build_id, *extra):238 """Abort by killing all processes in the chroot, as hard as we can.
239
240 Overridden here to handle state management.
241 """
242 self.doReapProcesses(self._state, notify=False)
243
244
245def get_build_path(home, build_id, *extra):
232 """Generate a path within the build directory.246 """Generate a path within the build directory.
233247
248 :param home: the user's home directory.
234 :param build_id: the build id to use.249 :param build_id: the build id to use.
235 :param extra: the extra path segments within the build directory.250 :param extra: the extra path segments within the build directory.
236 :return: the generated path.251 :return: the generated path.
237 """252 """
238 return os.path.join(253 return os.path.join(home, "build-" + build_id, *extra)
239 os.environ["HOME"], "build-" + build_id, *extra)
240254
=== modified file 'lpbuildd/slave.py'
--- lpbuildd/slave.py 2013-07-30 11:27:43 +0000
+++ lpbuildd/slave.py 2013-07-30 11:30:43 +0000
@@ -8,6 +8,7 @@
88
9__metaclass__ = type9__metaclass__ = type
1010
11from functools import partial
11import hashlib12import hashlib
12import os13import os
13import re14import re
@@ -15,7 +16,7 @@
15import xmlrpclib16import xmlrpclib
1617
17from twisted.internet import protocol18from twisted.internet import protocol
18from twisted.internet import reactor19from twisted.internet import reactor as default_reactor
19from twisted.internet import process20from twisted.internet import process
20from twisted.web import xmlrpc21from twisted.web import xmlrpc
2122
@@ -52,7 +53,8 @@
52 def __init__(self, slave, callback):53 def __init__(self, slave, callback):
53 self.slave = slave54 self.slave = slave
54 self.notify = callback55 self.notify = callback
55 self.killCall = None56 self.builderFailCall = None
57 self.ignore = False
5658
57 def outReceived(self, data):59 def outReceived(self, data):
58 """Pass on stdout data to the log."""60 """Pass on stdout data to the log."""
@@ -67,30 +69,27 @@
67 def processEnded(self, statusobject):69 def processEnded(self, statusobject):
68 """This method is called when a child process got terminated.70 """This method is called when a child process got terminated.
6971
70 Three actions are required at this point: identify if we are within an72 Two actions are required at this point: eliminate pending calls to
71 "aborting" process, eliminate pending calls to "kill" and invoke the73 "builderFail", and invoke the programmed notification callback. The
72 programmed notification callback. We only really care about invoking74 notification callback must be invoked last.
73 the notification callback last thing in this method. The order
74 of the rest of the method is not critical.
75 """75 """
76 # finishing the ABORTING workflow76 if self.ignore:
77 if self.slave.builderstatus == BuilderStatus.ABORTING:77 # The build manager no longer cares about this process.
78 self.slave.builderstatus = BuilderStatus.ABORTED78 return
7979
80 # check if there is a pending request for kill the process,80 # Since the process terminated, we don't need to fail the builder.
81 # in afirmative case simply cancel this request since it81 if self.builderFailCall and self.builderFailCall.active():
82 # already died.82 self.builderFailCall.cancel()
83 if self.killCall and self.killCall.active():
84 self.killCall.cancel()
8583
86 # notify the slave, it'll perform the required actions84 # notify the slave, it'll perform the required actions
87 self.notify(statusobject.value.exitCode)85 if self.notify is not None:
86 self.notify(statusobject.value.exitCode)
8887
8988
90class BuildManager(object):89class BuildManager(object):
91 """Build Daemon slave build manager abstract parent"""90 """Build Daemon slave build manager abstract parent"""
9291
93 def __init__(self, slave, buildid):92 def __init__(self, slave, buildid, reactor=None):
94 """Create a BuildManager.93 """Create a BuildManager.
9594
96 :param slave: A `BuildDSlave`.95 :param slave: A `BuildDSlave`.
@@ -99,20 +98,29 @@
99 object.__init__(self)98 object.__init__(self)
100 self._buildid = buildid99 self._buildid = buildid
101 self._slave = slave100 self._slave = slave
101 if reactor is None:
102 reactor = default_reactor
103 self._reactor = reactor
102 self._preppath = slave._config.get("allmanagers", "preppath")104 self._preppath = slave._config.get("allmanagers", "preppath")
103 self._unpackpath = slave._config.get("allmanagers", "unpackpath")105 self._unpackpath = slave._config.get("allmanagers", "unpackpath")
104 self._cleanpath = slave._config.get("allmanagers", "cleanpath")106 self._cleanpath = slave._config.get("allmanagers", "cleanpath")
105 self._mountpath = slave._config.get("allmanagers", "mountpath")107 self._mountpath = slave._config.get("allmanagers", "mountpath")
106 self._umountpath = slave._config.get("allmanagers", "umountpath")108 self._umountpath = slave._config.get("allmanagers", "umountpath")
109 self._scanpath = slave._config.get("allmanagers", "processscanpath")
110 self._subprocess = None
111 self._reaped_states = set()
107 self.is_archive_private = False112 self.is_archive_private = False
108 self.home = os.environ['HOME']113 self.home = os.environ['HOME']
114 self.abort_timeout = 120
109115
110 def runSubProcess(self, command, args):116 def runSubProcess(self, command, args, iterate=None):
111 """Run a sub process capturing the results in the log."""117 """Run a sub process capturing the results in the log."""
112 self._subprocess = RunCapture(self._slave, self.iterate)118 if iterate is None:
119 iterate = self.iterate
120 self._subprocess = RunCapture(self._slave, iterate)
113 self._slave.log("RUN: %s %r\n" % (command, args))121 self._slave.log("RUN: %s %r\n" % (command, args))
114 childfds = {0: devnull.fileno(), 1: "r", 2: "r"}122 childfds = {0: devnull.fileno(), 1: "r", 2: "r"}
115 reactor.spawnProcess(123 self._reactor.spawnProcess(
116 self._subprocess, command, args, env=os.environ,124 self._subprocess, command, args, env=os.environ,
117 path=self.home, childFDs=childfds)125 path=self.home, childFDs=childfds)
118126
@@ -122,6 +130,25 @@
122 self._unpackpath,130 self._unpackpath,
123 ["unpack-chroot", self._buildid, self._chroottarfile])131 ["unpack-chroot", self._buildid, self._chroottarfile])
124132
133 def doReapProcesses(self, state, notify=True):
134 """Reap any processes left lying around in the chroot."""
135 if state is not None and state in self._reaped_states:
136 # We've already reaped this state. To avoid a loop, proceed
137 # immediately to the next iterator.
138 self._slave.log("Already reaped from state %s" % state)
139 if notify:
140 self.iterateReap(state, 0)
141 else:
142 if state is not None:
143 self._reaped_states.add(state)
144 if notify:
145 iterate = partial(self.iterateReap, state)
146 else:
147 iterate = lambda success: None
148 self.runSubProcess(
149 self._scanpath, [self._scanpath, self._buildid],
150 iterate=iterate)
151
125 def doCleanup(self):152 def doCleanup(self):
126 """Remove the build tree etc."""153 """Remove the build tree etc."""
127 self.runSubProcess(self._cleanpath, ["remove-build", self._buildid])154 self.runSubProcess(self._cleanpath, ["remove-build", self._buildid])
@@ -175,37 +202,66 @@
175 raise NotImplementedError("BuildManager should be subclassed to be "202 raise NotImplementedError("BuildManager should be subclassed to be "
176 "used")203 "used")
177204
205 def iterateReap(self, state, success):
206 """Perform an iteration of the slave following subprocess reaping.
207
208 Subprocess reaping is special, typically occurring at several
209 positions in a build manager's state machine. We therefore keep
210 track of the state being reaped so that we can select the
211 appropriate next state.
212 """
213 raise NotImplementedError("BuildManager should be subclassed to be "
214 "used")
215
216 def abortReap(self):
217 """Abort by killing all processes in the chroot, as hard as we can.
218
219 We expect this to result in the main build process exiting non-zero
220 and giving us some useful logs.
221
222 This may be overridden in subclasses so that they can perform their
223 own state machine management.
224 """
225 self.doReapProcesses(None, notify=False)
226
178 def abort(self):227 def abort(self):
179 """Abort the build by killing the subprocess."""228 """Abort the build by killing the subprocess."""
180 if self.alreadyfailed or self._subprocess is None:229 if self.alreadyfailed or self._subprocess is None:
181 return230 return
182 else:231 else:
183 self.alreadyfailed = True232 self.alreadyfailed = True
184 # Either SIGKILL and SIGTERM presents the same behavior, the233 primary_subprocess = self._subprocess
185 # process is just killed some time after the signal was sent 234 self.abortReap()
186 # 10 s ~ 40 s, and returns None as exit_code, instead of the normal235 # In extreme cases the build may be hung too badly for
187 # integer. See further info on DebianBuildermanager.iterate in236 # scan-for-processes to manage to kill it (blocked on I/O,
188 # debian.py237 # forkbombing test suite, etc.). In this case, fail the builder and
189 # XXX cprov 2005-09-02:238 # let an admin sort it out.
190 # we may want to follow the canonical.tachandler kill process239 self._subprocess.builderFailCall = self._reactor.callLater(
191 # style, which sends SIGTERM to the process wait a given timeout240 self.abort_timeout, self.builderFail,
192 # and if was not killed sends a SIGKILL. IMO it only would be worth241 "Failed to kill all processes.", primary_subprocess)
193 # if we found different behaviour than the previous described.242
194 self._subprocess.transport.signalProcess('TERM')243 def builderFail(self, reason, primary_subprocess):
195 # alternatively to simply send SIGTERM, we can pend a request to244 """Mark the builder as failed."""
196 # send SIGKILL to the process if nothing happened in 10 seconds245 self._slave.log("ABORTING: %s\n" % reason)
197 # see base class process246 self._subprocess.builderFailCall = None
198 self._subprocess.killCall = reactor.callLater(10, self.kill)247 self._slave.builderFail()
199248 self.alreadyfailed = True
200 def kill(self):249 # If we failed to kill all processes in the chroot, then the primary
201 """Send SIGKILL to child process250 # subprocess (i.e. the one running immediately before
202251 # doReapProcesses was called) may not have exited. Kill it so that
203 Mask exception generated when the child process has already exited.252 # we can proceed.
204 """
205 try:253 try:
206 self._subprocess.transport.signalProcess('KILL')254 primary_subprocess.transport.signalProcess('KILL')
207 except process.ProcessExitedAlready:255 except process.ProcessExitedAlready:
208 self._slave.log("ABORTING: Process Exited Already\n")256 self._slave.log("ABORTING: Process Exited Already\n")
257 primary_subprocess.transport.loseConnection()
258 # Leave the reaper running, but disconnect it from our state
259 # machine. Perhaps an admin can make something of it, and in any
260 # case scan-for-processes elevates itself to root so it's awkward to
261 # kill it.
262 self._subprocess.ignore = True
263 self._subprocess.transport.loseConnection()
264
209265
210class BuilderStatus:266class BuilderStatus:
211 """Status values for the builder."""267 """Status values for the builder."""
@@ -229,6 +285,7 @@
229 PACKAGEFAIL = "BuildStatus.PACKAGEFAIL"285 PACKAGEFAIL = "BuildStatus.PACKAGEFAIL"
230 CHROOTFAIL = "BuildStatus.CHROOTFAIL"286 CHROOTFAIL = "BuildStatus.CHROOTFAIL"
231 BUILDERFAIL = "BuildStatus.BUILDERFAIL"287 BUILDERFAIL = "BuildStatus.BUILDERFAIL"
288 ABORTED = "BuildStatus.ABORTED"
232289
233290
234class BuildDSlave(object):291class BuildDSlave(object):
@@ -355,6 +412,11 @@
355 # XXX: dsilvers: 2005-01-21: Current abort mechanism doesn't wait412 # XXX: dsilvers: 2005-01-21: Current abort mechanism doesn't wait
356 # for abort to complete. This is potentially an issue in a heavy413 # for abort to complete. This is potentially an issue in a heavy
357 # load situation.414 # load situation.
415 if self.builderstatus == BuilderStatus.ABORTING:
416 # This might happen if the master side restarts in the middle of
417 # an abort cycle.
418 self.log("Slave already ABORTING when asked to abort")
419 return
358 if self.builderstatus != BuilderStatus.BUILDING:420 if self.builderstatus != BuilderStatus.BUILDING:
359 # XXX: Should raise a known Fault so that the client can make421 # XXX: Should raise a known Fault so that the client can make
360 # useful decisions about the error!422 # useful decisions about the error!
@@ -458,8 +520,10 @@
458520
459 def builderFail(self):521 def builderFail(self):
460 """Cease building because the builder has a problem."""522 """Cease building because the builder has a problem."""
461 if self.builderstatus != BuilderStatus.BUILDING:523 if self.builderstatus not in (BuilderStatus.BUILDING,
462 raise ValueError("Slave is not BUILDING when set to BUILDERFAIL")524 BuilderStatus.ABORTING):
525 raise ValueError("Slave is not BUILDING|ABORTING when set to "
526 "BUILDERFAIL")
463 self.buildstatus = BuildStatus.BUILDERFAIL527 self.buildstatus = BuildStatus.BUILDERFAIL
464528
465 def chrootFail(self):529 def chrootFail(self):
@@ -497,14 +561,25 @@
497 raise ValueError("Slave is not BUILDING when set to GIVENBACK")561 raise ValueError("Slave is not BUILDING when set to GIVENBACK")
498 self.buildstatus = BuildStatus.GIVENBACK562 self.buildstatus = BuildStatus.GIVENBACK
499563
564 def buildAborted(self):
565 """Mark a build as aborted."""
566 if self.builderstatus != BuilderStatus.ABORTING:
567 raise ValueError("Slave is not ABORTING when set to ABORTED")
568 if self.buildstatus != BuildStatus.BUILDERFAIL:
569 self.buildstatus = BuildStatus.ABORTED
570
500 def buildComplete(self):571 def buildComplete(self):
501 """Mark the build as complete and waiting interaction from the build572 """Mark the build as complete and waiting interaction from the build
502 daemon master.573 daemon master.
503 """574 """
504 if self.builderstatus != BuilderStatus.BUILDING:575 if self.builderstatus == BuilderStatus.BUILDING:
505 raise ValueError("Slave is not BUILDING when told build is "576 self.builderstatus = BuilderStatus.WAITING
506 "complete")577 elif self.builderstatus == BuilderStatus.ABORTING:
507 self.builderstatus = BuilderStatus.WAITING578 self.buildAborted()
579 self.builderstatus = BuilderStatus.WAITING
580 else:
581 raise ValueError("Slave is not BUILDING|ABORTING when told build "
582 "is complete")
508583
509 def sanitizeBuildlog(self, log_path):584 def sanitizeBuildlog(self, log_path):
510 """Removes passwords from buildlog URLs.585 """Removes passwords from buildlog URLs.
511586
=== modified file 'lpbuildd/sourcepackagerecipe.py'
--- lpbuildd/sourcepackagerecipe.py 2011-11-10 04:55:02 +0000
+++ lpbuildd/sourcepackagerecipe.py 2013-07-30 11:30:43 +0000
@@ -124,8 +124,12 @@
124 self._slave.builderFail()124 self._slave.builderFail()
125 print("Returning build status: Builder failed.")125 print("Returning build status: Builder failed.")
126 self.alreadyfailed = True126 self.alreadyfailed = True
127 self._state = DebianBuildState.REAP127 self.doReapProcesses(self._state)
128 self.doReapProcesses()128
129 def iterateReap_BUILD_RECIPE(self, retcode):
130 """Finished reaping after recipe building."""
131 self._state = DebianBuildState.UMOUNT
132 self.doUnmounting()
129133
130 def getChangesFilename(self):134 def getChangesFilename(self):
131 """Return the path to the changes file."""135 """Return the path to the changes file."""
132136
=== modified file 'lpbuildd/tests/buildd-slave-test.conf'
--- lpbuildd/tests/buildd-slave-test.conf 2011-12-08 22:13:03 +0000
+++ lpbuildd/tests/buildd-slave-test.conf 2013-07-30 11:30:43 +0000
@@ -12,17 +12,16 @@
12mountpath = /var/tmp/buildd/slavebin/mount-chroot12mountpath = /var/tmp/buildd/slavebin/mount-chroot
13umountpath = /var/tmp/buildd/slavebin/umount-chroot13umountpath = /var/tmp/buildd/slavebin/umount-chroot
14preppath = /var/tmp/buildd/slavebin/slave-prep14preppath = /var/tmp/buildd/slavebin/slave-prep
15processscanpath = /var/tmp/buildd/slavebin/scan-for-processes
1516
16[debianmanager]17[debianmanager]
17sbuildpath = /var/tmp/buildd/slavebin/sbuild-package18sbuildpath = /var/tmp/buildd/slavebin/sbuild-package
18sbuildargs = -dautobuild --nolog --batch19sbuildargs = -dautobuild --nolog --batch
19updatepath = /var/tmp/buildd/slavebin/update-debian-chroot20updatepath = /var/tmp/buildd/slavebin/update-debian-chroot
20processscanpath = /var/tmp/buildd/slavebin/scan-for-processes
21sourcespath = /usr/share/launchpad-buildd/slavebin/override-sources-list21sourcespath = /usr/share/launchpad-buildd/slavebin/override-sources-list
2222
23[binarypackagemanager]23[binarypackagemanager]
24sbuildpath = /var/tmp/buildd/slavebin/sbuild-package24sbuildpath = /var/tmp/buildd/slavebin/sbuild-package
25sbuildargs = -dautobuild --nolog --batch25sbuildargs = -dautobuild --nolog --batch
26updatepath = /var/tmp/buildd/slavebin/update-debian-chroot26updatepath = /var/tmp/buildd/slavebin/update-debian-chroot
27processscanpath = /var/tmp/buildd/slavebin/scan-for-processes
28sourcespath = /usr/share/launchpad-buildd/slavebin/override-sources-list27sourcespath = /usr/share/launchpad-buildd/slavebin/override-sources-list
2928
=== added file 'lpbuildd/tests/fakeslave.py'
--- lpbuildd/tests/fakeslave.py 1970-01-01 00:00:00 +0000
+++ lpbuildd/tests/fakeslave.py 2013-07-30 11:30:43 +0000
@@ -0,0 +1,104 @@
1# Copyright 2013 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4__metaclass__ = type
5__all__ = [
6 'FakeMethod',
7 'FakeSlave',
8 ]
9
10import os
11
12
13class FakeMethod:
14 """Catch any function or method call, and record the fact.
15
16 Use this for easy stubbing. The call operator can return a fixed
17 value, or raise a fixed exception object.
18
19 This is useful when unit-testing code that does things you don't
20 want to integration-test, e.g. because it wants to talk to remote
21 systems.
22 """
23
24 def __init__(self, result=None, failure=None):
25 """Set up a fake function or method.
26
27 :param result: Value to return.
28 :param failure: Exception to raise.
29 """
30 self.result = result
31 self.failure = failure
32
33 # A log of arguments for each call to this method.
34 self.calls = []
35
36 def __call__(self, *args, **kwargs):
37 """Catch an invocation to the method.
38
39 Increment `call_count`, and adds the arguments to `calls`.
40
41 Accepts any and all parameters. Raises the failure passed to
42 the constructor, if any; otherwise, returns the result value
43 passed to the constructor.
44 """
45 self.calls.append((args, kwargs))
46
47 if self.failure is None:
48 return self.result
49 else:
50 # pylint thinks this raises None, which is clearly not
51 # possible. That's why this test disables pylint message
52 # E0702.
53 raise self.failure
54
55 @property
56 def call_count(self):
57 return len(self.calls)
58
59 def extract_args(self):
60 """Return just the calls' positional-arguments tuples."""
61 return [args for args, kwargs in self.calls]
62
63 def extract_kwargs(self):
64 """Return just the calls' keyword-arguments dicts."""
65 return [kwargs for args, kwargs in self.calls]
66
67
68class FakeConfig:
69 def get(self, section, key):
70 return key
71
72
73class FakeSlave:
74 def __init__(self, tempdir):
75 self._cachepath = tempdir
76 self._config = FakeConfig()
77 self._was_called = set()
78 self.waitingfiles = {}
79
80 def cachePath(self, file):
81 return os.path.join(self._cachepath, file)
82
83 def anyMethod(self, *args, **kwargs):
84 pass
85
86 fake_methods = [
87 'emptyLog', 'log', 'chrootFail', 'buildFail', 'builderFail',
88 ]
89 def __getattr__(self, name):
90 """Remember which fake methods were called."""
91 if name not in self.fake_methods:
92 raise AttributeError(
93 "'%s' object has no attribute '%s'" % (self.__class__, name))
94 self._was_called.add(name)
95 return self.anyMethod
96
97 def wasCalled(self, name):
98 return name in self._was_called
99
100 def getArch(self):
101 return 'i386'
102
103 storeFile = FakeMethod()
104 addWaitingFile = FakeMethod()
0105
=== modified file 'lpbuildd/tests/harness.py'
--- lpbuildd/tests/harness.py 2012-06-05 17:11:03 +0000
+++ lpbuildd/tests/harness.py 2013-07-30 11:30:43 +0000
@@ -4,6 +4,7 @@
4__metaclass__ = type4__metaclass__ = type
5__all__ = [5__all__ = [
6 'BuilddTestCase',6 'BuilddTestCase',
7 'FakeSlave',
7 ]8 ]
89
9import os10import os
1011
=== added file 'lpbuildd/tests/test_binarypackage.py'
--- lpbuildd/tests/test_binarypackage.py 1970-01-01 00:00:00 +0000
+++ lpbuildd/tests/test_binarypackage.py 2013-07-30 11:30:43 +0000
@@ -0,0 +1,206 @@
1# Copyright 2013 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4__metaclass__ = type
5
6import tempfile
7
8import os
9import shutil
10from testtools import TestCase
11
12from twisted.internet.task import Clock
13
14from lpbuildd.binarypackage import (
15 BinaryPackageBuildManager,
16 BinaryPackageBuildState,
17 )
18from lpbuildd.tests.fakeslave import (
19 FakeMethod,
20 FakeSlave,
21 )
22
23
24class MockTransport:
25 loseConnection = FakeMethod()
26 signalProcess = FakeMethod()
27
28
29class MockSubprocess:
30 def __init__(self, path):
31 self.path = path
32 self.transport = MockTransport()
33
34
35class MockBuildManager(BinaryPackageBuildManager):
36 def __init__(self, *args, **kwargs):
37 super(MockBuildManager, self).__init__(*args, **kwargs)
38 self.commands = []
39 self.iterators = []
40
41 def runSubProcess(self, path, command, iterate=None):
42 self.commands.append([path]+command)
43 if iterate is None:
44 iterate = self.iterate
45 self.iterators.append(iterate)
46 self._subprocess = MockSubprocess(path)
47 return 0
48
49
50class TestBinaryPackageBuildManagerIteration(TestCase):
51 """Run BinaryPackageBuildManager through its iteration steps."""
52 def setUp(self):
53 super(TestBinaryPackageBuildManagerIteration, self).setUp()
54 self.working_dir = tempfile.mkdtemp()
55 self.addCleanup(lambda: shutil.rmtree(self.working_dir))
56 slave_dir = os.path.join(self.working_dir, 'slave')
57 home_dir = os.path.join(self.working_dir, 'home')
58 for dir in (slave_dir, home_dir):
59 os.mkdir(dir)
60 self.slave = FakeSlave(slave_dir)
61 self.buildid = '123'
62 self.clock = Clock()
63 self.buildmanager = MockBuildManager(
64 self.slave, self.buildid, reactor=self.clock)
65 self.buildmanager.home = home_dir
66 self.buildmanager._cachepath = self.slave._cachepath
67 self.chrootdir = os.path.join(
68 home_dir, 'build-%s' % self.buildid, 'chroot-autobuild')
69
70 def getState(self):
71 """Retrieve build manager's state."""
72 return self.buildmanager._state
73
74 def startBuild(self):
75 # The build manager's iterate() kicks off the consecutive states
76 # after INIT.
77 self.buildmanager.initiate(
78 {'foo_1.dsc': ''}, 'chroot.tar.gz',
79 {'suite': 'warty', 'ogrecomponent': 'main'})
80
81 # Skip DebianBuildManager states to the state directly before
82 # SBUILD.
83 self.buildmanager._state = BinaryPackageBuildState.UPDATE
84
85 # SBUILD: Build the package.
86 self.buildmanager.iterate(0)
87 self.assertEqual(BinaryPackageBuildState.SBUILD, self.getState())
88 expected_command = [
89 'sbuildpath', 'sbuild-package', self.buildid, 'i386', 'warty',
90 'sbuildargs', '--dist=warty', '--architecture=i386', '--comp=main',
91 'foo_1.dsc',
92 ]
93 self.assertEqual(expected_command, self.buildmanager.commands[-1])
94 self.assertEqual(
95 self.buildmanager.iterate, self.buildmanager.iterators[-1])
96 self.assertFalse(self.slave.wasCalled('chrootFail'))
97
98 def test_iterate(self):
99 # The build manager iterates a normal build from start to finish.
100 self.startBuild()
101
102 log_path = os.path.join(self.buildmanager._cachepath, 'buildlog')
103 log = open(log_path, 'w')
104 log.write("I am a build log.")
105 log.close()
106
107 changes_path = os.path.join(
108 self.buildmanager.home, 'build-%s' % self.buildid,
109 'foo_1_i386.changes')
110 changes = open(changes_path, 'w')
111 changes.write("I am a changes file.")
112 changes.close()
113
114 # After building the package, reap processes.
115 self.buildmanager.iterate(0)
116 expected_command = [
117 'processscanpath', 'processscanpath', self.buildid,
118 ]
119 self.assertEqual(BinaryPackageBuildState.SBUILD, self.getState())
120 self.assertEqual(expected_command, self.buildmanager.commands[-1])
121 self.assertNotEqual(
122 self.buildmanager.iterate, self.buildmanager.iterators[-1])
123 self.assertFalse(self.slave.wasCalled('buildFail'))
124 self.assertEqual([], self.slave.addWaitingFile.calls)
125
126 # Control returns to the DebianBuildManager in the UMOUNT state.
127 self.buildmanager.iterateReap(self.getState(), 0)
128 expected_command = [
129 'umountpath', 'umount-chroot', self.buildid
130 ]
131 self.assertEqual(BinaryPackageBuildState.UMOUNT, self.getState())
132 self.assertEqual(expected_command, self.buildmanager.commands[-1])
133 self.assertEqual(
134 self.buildmanager.iterate, self.buildmanager.iterators[-1])
135 self.assertFalse(self.slave.wasCalled('buildFail'))
136
137 def test_abort_sbuild(self):
138 # Aborting sbuild kills processes in the chroot.
139 self.startBuild()
140
141 # Send an abort command. The build manager reaps processes.
142 self.buildmanager.abort()
143 expected_command = [
144 'processscanpath', 'processscanpath', self.buildid
145 ]
146 self.assertEqual(BinaryPackageBuildState.SBUILD, self.getState())
147 self.assertEqual(expected_command, self.buildmanager.commands[-1])
148 self.assertNotEqual(
149 self.buildmanager.iterate, self.buildmanager.iterators[-1])
150 self.assertFalse(self.slave.wasCalled('buildFail'))
151
152 # If reaping completes successfully, the build manager returns
153 # control to the DebianBuildManager in the UMOUNT state.
154 self.buildmanager.iterateReap(self.getState(), 0)
155 expected_command = [
156 'umountpath', 'umount-chroot', self.buildid
157 ]
158 self.assertEqual(BinaryPackageBuildState.UMOUNT, self.getState())
159 self.assertEqual(expected_command, self.buildmanager.commands[-1])
160 self.assertEqual(
161 self.buildmanager.iterate, self.buildmanager.iterators[-1])
162 self.assertFalse(self.slave.wasCalled('buildFail'))
163
164 def test_abort_sbuild_fail(self):
165 # If killing processes in the chroot hangs, the build manager does
166 # its best to clean up and fails the builder.
167 self.startBuild()
168 sbuild_subprocess = self.buildmanager._subprocess
169
170 # Send an abort command. The build manager reaps processes.
171 self.buildmanager.abort()
172 expected_command = [
173 'processscanpath', 'processscanpath', self.buildid
174 ]
175 self.assertEqual(BinaryPackageBuildState.SBUILD, self.getState())
176 self.assertEqual(expected_command, self.buildmanager.commands[-1])
177 self.assertNotEqual(
178 self.buildmanager.iterate, self.buildmanager.iterators[-1])
179 self.assertFalse(self.slave.wasCalled('builderFail'))
180 reap_subprocess = self.buildmanager._subprocess
181
182 # If reaping fails, the builder is failed, sbuild is killed, and the
183 # reaper is disconnected.
184 self.clock.advance(120)
185 self.assertTrue(self.slave.wasCalled('builderFail'))
186 self.assertEqual(
187 [(('KILL',), {})], sbuild_subprocess.transport.signalProcess.calls)
188 self.assertNotEqual(
189 [], sbuild_subprocess.transport.loseConnection.calls)
190 self.assertNotEqual([], reap_subprocess.transport.loseConnection.calls)
191
192 log_path = os.path.join(self.buildmanager._cachepath, 'buildlog')
193 log = open(log_path, 'w')
194 log.write("I am a build log.")
195 log.close()
196
197 # When sbuild exits, it does not reap processes again, but proceeds
198 # directly to UMOUNT.
199 self.buildmanager.iterate(128 + 9) # SIGKILL
200 expected_command = [
201 'umountpath', 'umount-chroot', self.buildid
202 ]
203 self.assertEqual(BinaryPackageBuildState.UMOUNT, self.getState())
204 self.assertEqual(expected_command, self.buildmanager.commands[-1])
205 self.assertEqual(
206 self.buildmanager.iterate, self.buildmanager.iterators[-1])
0207
=== modified file 'lpbuildd/tests/test_translationtemplatesbuildmanager.py'
--- lpbuildd/tests/test_translationtemplatesbuildmanager.py 2011-11-21 05:41:27 +0000
+++ lpbuildd/tests/test_translationtemplatesbuildmanager.py 2013-07-30 11:30:43 +0000
@@ -9,109 +9,24 @@
99
10from testtools import TestCase10from testtools import TestCase
1111
12from lpbuildd.tests.fakeslave import FakeSlave
12from lpbuildd.translationtemplates import (13from lpbuildd.translationtemplates import (
13 TranslationTemplatesBuildManager,14 TranslationTemplatesBuildManager,
14 TranslationTemplatesBuildState,15 TranslationTemplatesBuildState,
15 )16 )
1617
1718
18class FakeMethod:
19 """Catch any function or method call, and record the fact.
20
21 Use this for easy stubbing. The call operator can return a fixed
22 value, or raise a fixed exception object.
23
24 This is useful when unit-testing code that does things you don't
25 want to integration-test, e.g. because it wants to talk to remote
26 systems.
27 """
28
29 def __init__(self, result=None, failure=None):
30 """Set up a fake function or method.
31
32 :param result: Value to return.
33 :param failure: Exception to raise.
34 """
35 self.result = result
36 self.failure = failure
37
38 # A log of arguments for each call to this method.
39 self.calls = []
40
41 def __call__(self, *args, **kwargs):
42 """Catch an invocation to the method.
43
44 Increment `call_count`, and adds the arguments to `calls`.
45
46 Accepts any and all parameters. Raises the failure passed to
47 the constructor, if any; otherwise, returns the result value
48 passed to the constructor.
49 """
50 self.calls.append((args, kwargs))
51
52 if self.failure is None:
53 return self.result
54 else:
55 # pylint thinks this raises None, which is clearly not
56 # possible. That's why this test disables pylint message
57 # E0702.
58 raise self.failure
59
60 @property
61 def call_count(self):
62 return len(self.calls)
63
64 def extract_args(self):
65 """Return just the calls' positional-arguments tuples."""
66 return [args for args, kwargs in self.calls]
67
68 def extract_kwargs(self):
69 """Return just the calls' keyword-arguments dicts."""
70 return [kwargs for args, kwargs in self.calls]
71
72
73class FakeConfig:
74 def get(self, section, key):
75 return key
76
77
78class FakeSlave:
79 def __init__(self, tempdir):
80 self._cachepath = tempdir
81 self._config = FakeConfig()
82 self._was_called = set()
83
84 def cachePath(self, file):
85 return os.path.join(self._cachepath, file)
86
87 def anyMethod(self, *args, **kwargs):
88 pass
89
90 fake_methods = ['emptyLog', 'chrootFail', 'buildFail', 'builderFail',]
91 def __getattr__(self, name):
92 """Remember which fake methods were called."""
93 if name not in self.fake_methods:
94 raise AttributeError(
95 "'%s' object has no attribute '%s'" % (self.__class__, name))
96 self._was_called.add(name)
97 return self.anyMethod
98
99 def wasCalled(self, name):
100 return name in self._was_called
101
102 def getArch(self):
103 return 'i386'
104
105 addWaitingFile = FakeMethod()
106
107
108class MockBuildManager(TranslationTemplatesBuildManager):19class MockBuildManager(TranslationTemplatesBuildManager):
109 def __init__(self, *args, **kwargs):20 def __init__(self, *args, **kwargs):
110 super(MockBuildManager, self).__init__(*args, **kwargs)21 super(MockBuildManager, self).__init__(*args, **kwargs)
111 self.commands = []22 self.commands = []
23 self.iterators = []
11224
113 def runSubProcess(self, path, command):25 def runSubProcess(self, path, command, iterate=None):
114 self.commands.append([path]+command)26 self.commands.append([path]+command)
27 if iterate is None:
28 iterate = self.iterate
29 self.iterators.append(iterate)
115 return 030 return 0
11631
11732
@@ -143,7 +58,7 @@
143 # after INIT.58 # after INIT.
144 self.buildmanager.initiate({}, 'chroot.tar.gz', {'branch_url': url})59 self.buildmanager.initiate({}, 'chroot.tar.gz', {'branch_url': url})
14560
146 # Skip states that are done in DebianBuldManager to the state61 # Skip states that are done in DebianBuildManager to the state
147 # directly before INSTALL.62 # directly before INSTALL.
148 self.buildmanager._state = TranslationTemplatesBuildState.UPDATE63 self.buildmanager._state = TranslationTemplatesBuildState.UPDATE
14964
@@ -158,6 +73,8 @@
158 'apt-get',73 'apt-get',
159 ]74 ]
160 self.assertEqual(expected_command, self.buildmanager.commands[-1][:5])75 self.assertEqual(expected_command, self.buildmanager.commands[-1][:5])
76 self.assertEqual(
77 self.buildmanager.iterate, self.buildmanager.iterators[-1])
16178
162 # GENERATE: Run the slave's payload, the script that generates79 # GENERATE: Run the slave's payload, the script that generates
163 # templates.80 # templates.
@@ -168,6 +85,8 @@
168 'generatepath', 'generatepath', self.buildid, url, 'resultarchive'85 'generatepath', 'generatepath', self.buildid, url, 'resultarchive'
169 ]86 ]
170 self.assertEqual(expected_command, self.buildmanager.commands[-1])87 self.assertEqual(expected_command, self.buildmanager.commands[-1])
88 self.assertEqual(
89 self.buildmanager.iterate, self.buildmanager.iterators[-1])
171 self.assertFalse(self.slave.wasCalled('chrootFail'))90 self.assertFalse(self.slave.wasCalled('chrootFail'))
17291
173 outfile_path = os.path.join(92 outfile_path = os.path.join(
@@ -179,18 +98,32 @@
179 outfile.write("I am a template tarball. Seriously.")98 outfile.write("I am a template tarball. Seriously.")
180 outfile.close()99 outfile.close()
181100
182 # The control returns to the DebianBuildManager in the REAP state.101 # After generating templates, reap processes.
183 self.buildmanager.iterate(0)102 self.buildmanager.iterate(0)
184 expected_command = [103 expected_command = [
185 'processscanpath', 'processscanpath', self.buildid104 'processscanpath', 'processscanpath', self.buildid
186 ]105 ]
187 self.assertEqual(106 self.assertEqual(
188 TranslationTemplatesBuildState.REAP, self.getState())107 TranslationTemplatesBuildState.GENERATE, self.getState())
189 self.assertEqual(expected_command, self.buildmanager.commands[-1])108 self.assertEqual(expected_command, self.buildmanager.commands[-1])
109 self.assertNotEqual(
110 self.buildmanager.iterate, self.buildmanager.iterators[-1])
190 self.assertFalse(self.slave.wasCalled('buildFail'))111 self.assertFalse(self.slave.wasCalled('buildFail'))
191 self.assertEqual(112 self.assertEqual(
192 [((outfile_path,), {})], self.slave.addWaitingFile.calls)113 [((outfile_path,), {})], self.slave.addWaitingFile.calls)
193114
115 # The control returns to the DebianBuildManager in the UMOUNT state.
116 self.buildmanager.iterateReap(self.getState(), 0)
117 expected_command = [
118 'umountpath', 'umount-chroot', self.buildid
119 ]
120 self.assertEqual(
121 TranslationTemplatesBuildState.UMOUNT, self.getState())
122 self.assertEqual(expected_command, self.buildmanager.commands[-1])
123 self.assertEqual(
124 self.buildmanager.iterate, self.buildmanager.iterators[-1])
125 self.assertFalse(self.slave.wasCalled('buildFail'))
126
194 def test_iterate_fail_INSTALL(self):127 def test_iterate_fail_INSTALL(self):
195 # See that a failing INSTALL is handled properly.128 # See that a failing INSTALL is handled properly.
196 url = 'lp:~my/branch'129 url = 'lp:~my/branch'
@@ -209,6 +142,8 @@
209 'umountpath', 'umount-chroot', self.buildid142 'umountpath', 'umount-chroot', self.buildid
210 ]143 ]
211 self.assertEqual(expected_command, self.buildmanager.commands[-1])144 self.assertEqual(expected_command, self.buildmanager.commands[-1])
145 self.assertEqual(
146 self.buildmanager.iterate, self.buildmanager.iterators[-1])
212 self.assertTrue(self.slave.wasCalled('chrootFail'))147 self.assertTrue(self.slave.wasCalled('chrootFail'))
213148
214 def test_iterate_fail_GENERATE(self):149 def test_iterate_fail_GENERATE(self):
@@ -221,12 +156,25 @@
221 # Skip states to the INSTALL state.156 # Skip states to the INSTALL state.
222 self.buildmanager._state = TranslationTemplatesBuildState.GENERATE157 self.buildmanager._state = TranslationTemplatesBuildState.GENERATE
223158
224 # The buildmanager fails and iterates to the REAP state.159 # The buildmanager fails and reaps processes.
225 self.buildmanager.iterate(-1)160 self.buildmanager.iterate(-1)
226 expected_command = [161 expected_command = [
227 'processscanpath', 'processscanpath', self.buildid162 'processscanpath', 'processscanpath', self.buildid
228 ]163 ]
229 self.assertEqual(164 self.assertEqual(
230 TranslationTemplatesBuildState.REAP, self.getState())165 TranslationTemplatesBuildState.GENERATE, self.getState())
231 self.assertEqual(expected_command, self.buildmanager.commands[-1])166 self.assertEqual(expected_command, self.buildmanager.commands[-1])
167 self.assertNotEqual(
168 self.buildmanager.iterate, self.buildmanager.iterators[-1])
232 self.assertTrue(self.slave.wasCalled('buildFail'))169 self.assertTrue(self.slave.wasCalled('buildFail'))
170
171 # The buildmanager iterates to the UMOUNT state.
172 self.buildmanager.iterateReap(self.getState(), 0)
173 self.assertEqual(
174 TranslationTemplatesBuildState.UMOUNT, self.getState())
175 expected_command = [
176 'umountpath', 'umount-chroot', self.buildid
177 ]
178 self.assertEqual(expected_command, self.buildmanager.commands[-1])
179 self.assertEqual(
180 self.buildmanager.iterate, self.buildmanager.iterators[-1])
233181
=== modified file 'lpbuildd/translationtemplates.py'
--- lpbuildd/translationtemplates.py 2011-11-10 04:55:02 +0000
+++ lpbuildd/translationtemplates.py 2013-07-30 11:30:43 +0000
@@ -88,12 +88,14 @@
88 if success == 0:88 if success == 0:
89 # It worked! Now let's bring in the harvest.89 # It worked! Now let's bring in the harvest.
90 self.gatherResults()90 self.gatherResults()
91 self._state = TranslationTemplatesBuildState.REAP91 self.doReapProcesses(self._state)
92 self.doReapProcesses()
93 else:92 else:
94 if not self.alreadyfailed:93 if not self.alreadyfailed:
95 self._slave.buildFail()94 self._slave.buildFail()
96 self.alreadyfailed = True95 self.alreadyfailed = True
97 self._state = TranslationTemplatesBuildState.REAP96 self.doReapProcesses(self._state)
98 self.doReapProcesses()
9997
98 def iterateReap_GENERATE(self, success):
99 """Finished reaping after template generation."""
100 self._state = TranslationTemplatesBuildState.UMOUNT
101 self.doUnmounting()
100102
=== modified file 'template-buildd-slave.conf'
--- template-buildd-slave.conf 2011-12-05 02:39:05 +0000
+++ template-buildd-slave.conf 2013-07-30 11:30:43 +0000
@@ -15,10 +15,10 @@
15cleanpath = /usr/share/launchpad-buildd/slavebin/remove-build15cleanpath = /usr/share/launchpad-buildd/slavebin/remove-build
16mountpath = /usr/share/launchpad-buildd/slavebin/mount-chroot16mountpath = /usr/share/launchpad-buildd/slavebin/mount-chroot
17umountpath = /usr/share/launchpad-buildd/slavebin/umount-chroot17umountpath = /usr/share/launchpad-buildd/slavebin/umount-chroot
18processscanpath = /usr/share/launchpad-buildd/slavebin/scan-for-processes
1819
19[debianmanager]20[debianmanager]
20updatepath = /usr/share/launchpad-buildd/slavebin/update-debian-chroot21updatepath = /usr/share/launchpad-buildd/slavebin/update-debian-chroot
21processscanpath = /usr/share/launchpad-buildd/slavebin/scan-for-processes
22sourcespath = /usr/share/launchpad-buildd/slavebin/override-sources-list22sourcespath = /usr/share/launchpad-buildd/slavebin/override-sources-list
2323
24[binarypackagemanager]24[binarypackagemanager]

Subscribers

People subscribed via source and target branches

to all changes: