Merge lp:~cjwatson/launchpad-buildd/fix-abort into lp:launchpad-buildd
- fix-abort
- Merge into trunk
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 |
Related bugs: |
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.
- 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.
- 63. By Colin Watson
-
Merge trunk.
- 64. By Colin Watson
-
Merge trunk.
William Grant (wgrant) : | # |
Preview Diff
1 | === modified file 'Makefile' | |||
2 | --- Makefile 2011-11-21 01:47:22 +0000 | |||
3 | +++ Makefile 2013-07-30 11:30:43 +0000 | |||
4 | @@ -27,6 +27,7 @@ | |||
5 | 27 | # a Launchpad checkout. | 27 | # a Launchpad checkout. |
6 | 28 | check: | 28 | check: |
7 | 29 | PYTHONPATH=$(PWD):$(PYTHONPATH) $(PYTHON) -m testtools.run -v \ | 29 | PYTHONPATH=$(PWD):$(PYTHONPATH) $(PYTHON) -m testtools.run -v \ |
8 | 30 | lpbuildd.tests.test_binarypackage \ | ||
9 | 30 | lpbuildd.tests.test_buildd_slave \ | 31 | lpbuildd.tests.test_buildd_slave \ |
10 | 31 | lpbuildd.tests.test_check_implicit_pointer_functions \ | 32 | lpbuildd.tests.test_check_implicit_pointer_functions \ |
11 | 32 | lpbuildd.tests.test_harness \ | 33 | lpbuildd.tests.test_harness \ |
12 | 33 | 34 | ||
13 | === modified file 'debian/changelog' | |||
14 | --- debian/changelog 2013-07-29 23:48:58 +0000 | |||
15 | +++ debian/changelog 2013-07-30 11:30:43 +0000 | |||
16 | @@ -1,5 +1,6 @@ | |||
17 | 1 | launchpad-buildd (115) UNRELEASED; urgency=low | 1 | launchpad-buildd (115) UNRELEASED; urgency=low |
18 | 2 | 2 | ||
19 | 3 | [ Adam Conrad ] | ||
20 | 3 | * Short the readlink call in scan-for-process with a true to avoid | 4 | * Short the readlink call in scan-for-process with a true to avoid |
21 | 4 | prematurely exiting the process scan when tripping over zombies. | 5 | prematurely exiting the process scan when tripping over zombies. |
22 | 5 | * Write to temporary cache files and then rename after validation | 6 | * Write to temporary cache files and then rename after validation |
23 | @@ -12,6 +13,12 @@ | |||
24 | 12 | * Strip trailing whitespace in buildd-genconfig because I'm anal. | 13 | * Strip trailing whitespace in buildd-genconfig because I'm anal. |
25 | 13 | * Mangle recipe versions to match backports policy (LP: #1095103) | 14 | * Mangle recipe versions to match backports policy (LP: #1095103) |
26 | 14 | 15 | ||
27 | 16 | [ Colin Watson ] | ||
28 | 17 | * Move scan-for-processes up to the top-level slave code so that it is | ||
29 | 18 | available for more general use. | ||
30 | 19 | * Make abort work properly, calling scan-for-processes to kill all | ||
31 | 20 | processes in the chroot. | ||
32 | 21 | |||
33 | 15 | -- Adam Conrad <adconrad@ubuntu.com> Mon, 29 Apr 2013 15:47:20 -0600 | 22 | -- Adam Conrad <adconrad@ubuntu.com> Mon, 29 Apr 2013 15:47:20 -0600 |
34 | 16 | 23 | ||
35 | 17 | launchpad-buildd (114) hardy; urgency=low | 24 | launchpad-buildd (114) hardy; urgency=low |
36 | 18 | 25 | ||
37 | === modified file 'debian/upgrade-config' | |||
38 | --- debian/upgrade-config 2011-12-05 02:39:05 +0000 | |||
39 | +++ debian/upgrade-config 2013-07-30 11:30:43 +0000 | |||
40 | @@ -7,7 +7,6 @@ | |||
41 | 7 | 7 | ||
42 | 8 | import re | 8 | import re |
43 | 9 | import sys | 9 | import sys |
44 | 10 | import os | ||
45 | 11 | import subprocess | 10 | import subprocess |
46 | 12 | 11 | ||
47 | 13 | (old_version, conf_file) = sys.argv[1:] | 12 | (old_version, conf_file) = sys.argv[1:] |
48 | @@ -114,6 +113,23 @@ | |||
49 | 114 | in_file.close() | 113 | in_file.close() |
50 | 115 | out_file.close() | 114 | out_file.close() |
51 | 116 | 115 | ||
52 | 116 | def upgrade_to_115(): | ||
53 | 117 | print "Upgrading %s to version 115" % conf_file | ||
54 | 118 | subprocess.call(["mv", conf_file, conf_file+"-prev115~"]) | ||
55 | 119 | in_allmanagers = False | ||
56 | 120 | in_file = open(conf_file+"-prev115~", "r") | ||
57 | 121 | out_file = open(conf_file, "w") | ||
58 | 122 | for line in in_file: | ||
59 | 123 | if line.startswith("[allmanagers]"): | ||
60 | 124 | in_allmanagers = True | ||
61 | 125 | elif in_allmanagers and (line.startswith("[") or not line.strip()): | ||
62 | 126 | out_file.write("processscanpath = /usr/share/launchpad-buildd/slavebin/scan-for-processes\n") | ||
63 | 127 | in_allmanagers = False | ||
64 | 128 | if not line.startswith("processscanpath = "): | ||
65 | 129 | out_file.write(line) | ||
66 | 130 | in_file.close() | ||
67 | 131 | out_file.close() | ||
68 | 132 | |||
69 | 117 | if __name__ == "__main__": | 133 | if __name__ == "__main__": |
70 | 118 | old_version = re.sub(r'[~-].*', '', old_version) | 134 | old_version = re.sub(r'[~-].*', '', old_version) |
71 | 119 | if int(old_version) < 12: | 135 | if int(old_version) < 12: |
72 | @@ -132,4 +148,6 @@ | |||
73 | 132 | upgrade_to_63() | 148 | upgrade_to_63() |
74 | 133 | if int(old_version) < 110: | 149 | if int(old_version) < 110: |
75 | 134 | upgrade_to_110() | 150 | upgrade_to_110() |
76 | 151 | if int(old_version) < 115: | ||
77 | 152 | upgrade_to_115() | ||
78 | 135 | 153 | ||
79 | 136 | 154 | ||
80 | === modified file 'lpbuildd/binarypackage.py' | |||
81 | --- lpbuildd/binarypackage.py 2011-11-10 04:55:02 +0000 | |||
82 | +++ lpbuildd/binarypackage.py 2013-07-30 11:30:43 +0000 | |||
83 | @@ -39,8 +39,8 @@ | |||
84 | 39 | 39 | ||
85 | 40 | initial_build_state = BinaryPackageBuildState.SBUILD | 40 | initial_build_state = BinaryPackageBuildState.SBUILD |
86 | 41 | 41 | ||
89 | 42 | def __init__(self, slave, buildid): | 42 | def __init__(self, slave, buildid, **kwargs): |
90 | 43 | DebianBuildManager.__init__(self, slave, buildid) | 43 | DebianBuildManager.__init__(self, slave, buildid, **kwargs) |
91 | 44 | self._sbuildpath = slave._config.get("binarypackagemanager", "sbuildpath") | 44 | self._sbuildpath = slave._config.get("binarypackagemanager", "sbuildpath") |
92 | 45 | self._sbuildargs = slave._config.get("binarypackagemanager", | 45 | self._sbuildargs = slave._config.get("binarypackagemanager", |
93 | 46 | "sbuildargs").split(" ") | 46 | "sbuildargs").split(" ") |
94 | @@ -124,10 +124,13 @@ | |||
95 | 124 | print("Returning build status: BUILDERFAIL") | 124 | print("Returning build status: BUILDERFAIL") |
96 | 125 | self._slave.builderFail() | 125 | self._slave.builderFail() |
97 | 126 | self.alreadyfailed = True | 126 | self.alreadyfailed = True |
100 | 127 | self._state = DebianBuildState.REAP | 127 | self.doReapProcesses(self._state) |
99 | 128 | self.doReapProcesses() | ||
101 | 129 | else: | 128 | else: |
102 | 130 | print("Returning build status: OK") | 129 | print("Returning build status: OK") |
103 | 131 | self.gatherResults() | 130 | self.gatherResults() |
106 | 132 | self._state = DebianBuildState.REAP | 131 | self.doReapProcesses(self._state) |
107 | 133 | self.doReapProcesses() | 132 | |
108 | 133 | def iterateReap_SBUILD(self, success): | ||
109 | 134 | """Finished reaping after sbuild run.""" | ||
110 | 135 | self._state = DebianBuildState.UMOUNT | ||
111 | 136 | self.doUnmounting() | ||
112 | 134 | 137 | ||
113 | === modified file 'lpbuildd/debian.py' | |||
114 | --- lpbuildd/debian.py 2011-11-18 04:17:27 +0000 | |||
115 | +++ lpbuildd/debian.py 2013-07-30 11:30:43 +0000 | |||
116 | @@ -22,7 +22,6 @@ | |||
117 | 22 | MOUNT = "MOUNT" | 22 | MOUNT = "MOUNT" |
118 | 23 | SOURCES = "SOURCES" | 23 | SOURCES = "SOURCES" |
119 | 24 | UPDATE = "UPDATE" | 24 | UPDATE = "UPDATE" |
120 | 25 | REAP = "REAP" | ||
121 | 26 | UMOUNT = "UMOUNT" | 25 | UMOUNT = "UMOUNT" |
122 | 27 | CLEANUP = "CLEANUP" | 26 | CLEANUP = "CLEANUP" |
123 | 28 | 27 | ||
124 | @@ -30,10 +29,9 @@ | |||
125 | 30 | class DebianBuildManager(BuildManager): | 29 | class DebianBuildManager(BuildManager): |
126 | 31 | """Base behaviour for Debian chrooted builds.""" | 30 | """Base behaviour for Debian chrooted builds.""" |
127 | 32 | 31 | ||
130 | 33 | def __init__(self, slave, buildid): | 32 | def __init__(self, slave, buildid, **kwargs): |
131 | 34 | BuildManager.__init__(self, slave, buildid) | 33 | BuildManager.__init__(self, slave, buildid, **kwargs) |
132 | 35 | self._updatepath = slave._config.get("debianmanager", "updatepath") | 34 | self._updatepath = slave._config.get("debianmanager", "updatepath") |
133 | 36 | self._scanpath = slave._config.get("debianmanager", "processscanpath") | ||
134 | 37 | self._sourcespath = slave._config.get("debianmanager", "sourcespath") | 35 | self._sourcespath = slave._config.get("debianmanager", "sourcespath") |
135 | 38 | self._cachepath = slave._config.get("slave","filecache") | 36 | self._cachepath = slave._config.get("slave","filecache") |
136 | 39 | self._state = DebianBuildState.INIT | 37 | self._state = DebianBuildState.INIT |
137 | @@ -74,10 +72,6 @@ | |||
138 | 74 | """ | 72 | """ |
139 | 75 | raise NotImplementedError() | 73 | raise NotImplementedError() |
140 | 76 | 74 | ||
141 | 77 | def doReapProcesses(self): | ||
142 | 78 | """Reap any processes left lying around in the chroot.""" | ||
143 | 79 | self.runSubProcess( self._scanpath, [self._scanpath, self._buildid] ) | ||
144 | 80 | |||
145 | 81 | @staticmethod | 75 | @staticmethod |
146 | 82 | def _parseChangesFile(linesIter): | 76 | def _parseChangesFile(linesIter): |
147 | 83 | """A generator that iterates over files listed in a changes file. | 77 | """A generator that iterates over files listed in a changes file. |
148 | @@ -98,7 +92,7 @@ | |||
149 | 98 | 92 | ||
150 | 99 | def getChangesFilename(self): | 93 | def getChangesFilename(self): |
151 | 100 | changes = self._dscfile[:-4] + "_" + self.arch_tag + ".changes" | 94 | changes = self._dscfile[:-4] + "_" + self.arch_tag + ".changes" |
153 | 101 | return get_build_path(self._buildid, changes) | 95 | return get_build_path(self.home, self._buildid, changes) |
154 | 102 | 96 | ||
155 | 103 | def gatherResults(self): | 97 | def gatherResults(self): |
156 | 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. |
157 | @@ -113,7 +107,8 @@ | |||
158 | 113 | seenfiles = False | 107 | seenfiles = False |
159 | 114 | 108 | ||
160 | 115 | for fn in self._parseChangesFile(chfile): | 109 | for fn in self._parseChangesFile(chfile): |
162 | 116 | self._slave.addWaitingFile(get_build_path(self._buildid, fn)) | 110 | self._slave.addWaitingFile( |
163 | 111 | get_build_path(self.home, self._buildid, fn)) | ||
164 | 117 | 112 | ||
165 | 118 | chfile.close() | 113 | chfile.close() |
166 | 119 | 114 | ||
167 | @@ -128,6 +123,15 @@ | |||
168 | 128 | raise ValueError, "Unknown internal state " + self._state | 123 | raise ValueError, "Unknown internal state " + self._state |
169 | 129 | func(success) | 124 | func(success) |
170 | 130 | 125 | ||
171 | 126 | def iterateReap(self, state, success): | ||
172 | 127 | print ("Iterating with success flag %s against stage %s after reaping " | ||
173 | 128 | "processes" | ||
174 | 129 | % (success, state)) | ||
175 | 130 | func = getattr(self, "iterateReap_" + state, None) | ||
176 | 131 | if func is None: | ||
177 | 132 | raise ValueError, "Unknown internal post-reap state " + state | ||
178 | 133 | func(success) | ||
179 | 134 | |||
180 | 131 | def iterate_INIT(self, success): | 135 | def iterate_INIT(self, success): |
181 | 132 | """Just finished initializing the build.""" | 136 | """Just finished initializing the build.""" |
182 | 133 | if success != 0: | 137 | if success != 0: |
183 | @@ -183,26 +187,29 @@ | |||
184 | 183 | if not self.alreadyfailed: | 187 | if not self.alreadyfailed: |
185 | 184 | self._slave.chrootFail() | 188 | self._slave.chrootFail() |
186 | 185 | self.alreadyfailed = True | 189 | self.alreadyfailed = True |
189 | 186 | self._state = DebianBuildState.REAP | 190 | self.doReapProcesses(self._state) |
188 | 187 | self.doReapProcesses() | ||
190 | 188 | else: | 191 | else: |
191 | 189 | self._state = DebianBuildState.UPDATE | 192 | self._state = DebianBuildState.UPDATE |
192 | 190 | self.doUpdateChroot() | 193 | self.doUpdateChroot() |
193 | 191 | 194 | ||
194 | 195 | def iterateReap_SOURCES(self, success): | ||
195 | 196 | """Just finished reaping after failure to overwrite sources.list.""" | ||
196 | 197 | self._state = DebianBuildState.UMOUNT | ||
197 | 198 | self.doUnmounting() | ||
198 | 199 | |||
199 | 192 | def iterate_UPDATE(self, success): | 200 | def iterate_UPDATE(self, success): |
200 | 193 | """Just finished updating the chroot.""" | 201 | """Just finished updating the chroot.""" |
201 | 194 | if success != 0: | 202 | if success != 0: |
202 | 195 | if not self.alreadyfailed: | 203 | if not self.alreadyfailed: |
203 | 196 | self._slave.chrootFail() | 204 | self._slave.chrootFail() |
204 | 197 | self.alreadyfailed = True | 205 | self.alreadyfailed = True |
207 | 198 | self._state = DebianBuildState.REAP | 206 | self.doReapProcesses(self._state) |
206 | 199 | self.doReapProcesses() | ||
208 | 200 | else: | 207 | else: |
209 | 201 | self._state = self.initial_build_state | 208 | self._state = self.initial_build_state |
210 | 202 | self.doRunBuild() | 209 | self.doRunBuild() |
211 | 203 | 210 | ||
214 | 204 | def iterate_REAP(self, success): | 211 | def iterateReap_UPDATE(self, success): |
215 | 205 | """Finished reaping processes; ignore error returns.""" | 212 | """Just finished reaping after failure to update the chroot.""" |
216 | 206 | self._state = DebianBuildState.UMOUNT | 213 | self._state = DebianBuildState.UMOUNT |
217 | 207 | self.doUnmounting() | 214 | self.doUnmounting() |
218 | 208 | 215 | ||
219 | @@ -227,13 +234,20 @@ | |||
220 | 227 | self._slave.buildOK() | 234 | self._slave.buildOK() |
221 | 228 | self._slave.buildComplete() | 235 | self._slave.buildComplete() |
222 | 229 | 236 | ||
225 | 230 | 237 | def abortReap(self): | |
226 | 231 | def get_build_path(build_id, *extra): | 238 | """Abort by killing all processes in the chroot, as hard as we can. |
227 | 239 | |||
228 | 240 | Overridden here to handle state management. | ||
229 | 241 | """ | ||
230 | 242 | self.doReapProcesses(self._state, notify=False) | ||
231 | 243 | |||
232 | 244 | |||
233 | 245 | def get_build_path(home, build_id, *extra): | ||
234 | 232 | """Generate a path within the build directory. | 246 | """Generate a path within the build directory. |
235 | 233 | 247 | ||
236 | 248 | :param home: the user's home directory. | ||
237 | 234 | :param build_id: the build id to use. | 249 | :param build_id: the build id to use. |
238 | 235 | :param extra: the extra path segments within the build directory. | 250 | :param extra: the extra path segments within the build directory. |
239 | 236 | :return: the generated path. | 251 | :return: the generated path. |
240 | 237 | """ | 252 | """ |
243 | 238 | return os.path.join( | 253 | return os.path.join(home, "build-" + build_id, *extra) |
242 | 239 | os.environ["HOME"], "build-" + build_id, *extra) | ||
244 | 240 | 254 | ||
245 | === modified file 'lpbuildd/slave.py' | |||
246 | --- lpbuildd/slave.py 2013-07-30 11:27:43 +0000 | |||
247 | +++ lpbuildd/slave.py 2013-07-30 11:30:43 +0000 | |||
248 | @@ -8,6 +8,7 @@ | |||
249 | 8 | 8 | ||
250 | 9 | __metaclass__ = type | 9 | __metaclass__ = type |
251 | 10 | 10 | ||
252 | 11 | from functools import partial | ||
253 | 11 | import hashlib | 12 | import hashlib |
254 | 12 | import os | 13 | import os |
255 | 13 | import re | 14 | import re |
256 | @@ -15,7 +16,7 @@ | |||
257 | 15 | import xmlrpclib | 16 | import xmlrpclib |
258 | 16 | 17 | ||
259 | 17 | from twisted.internet import protocol | 18 | from twisted.internet import protocol |
261 | 18 | from twisted.internet import reactor | 19 | from twisted.internet import reactor as default_reactor |
262 | 19 | from twisted.internet import process | 20 | from twisted.internet import process |
263 | 20 | from twisted.web import xmlrpc | 21 | from twisted.web import xmlrpc |
264 | 21 | 22 | ||
265 | @@ -52,7 +53,8 @@ | |||
266 | 52 | def __init__(self, slave, callback): | 53 | def __init__(self, slave, callback): |
267 | 53 | self.slave = slave | 54 | self.slave = slave |
268 | 54 | self.notify = callback | 55 | self.notify = callback |
270 | 55 | self.killCall = None | 56 | self.builderFailCall = None |
271 | 57 | self.ignore = False | ||
272 | 56 | 58 | ||
273 | 57 | def outReceived(self, data): | 59 | def outReceived(self, data): |
274 | 58 | """Pass on stdout data to the log.""" | 60 | """Pass on stdout data to the log.""" |
275 | @@ -67,30 +69,27 @@ | |||
276 | 67 | def processEnded(self, statusobject): | 69 | def processEnded(self, statusobject): |
277 | 68 | """This method is called when a child process got terminated. | 70 | """This method is called when a child process got terminated. |
278 | 69 | 71 | ||
284 | 70 | Three actions are required at this point: identify if we are within an | 72 | Two actions are required at this point: eliminate pending calls to |
285 | 71 | "aborting" process, eliminate pending calls to "kill" and invoke the | 73 | "builderFail", and invoke the programmed notification callback. The |
286 | 72 | programmed notification callback. We only really care about invoking | 74 | notification callback must be invoked last. |
282 | 73 | the notification callback last thing in this method. The order | ||
283 | 74 | of the rest of the method is not critical. | ||
287 | 75 | """ | 75 | """ |
291 | 76 | # finishing the ABORTING workflow | 76 | if self.ignore: |
292 | 77 | if self.slave.builderstatus == BuilderStatus.ABORTING: | 77 | # The build manager no longer cares about this process. |
293 | 78 | self.slave.builderstatus = BuilderStatus.ABORTED | 78 | return |
294 | 79 | 79 | ||
300 | 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. |
301 | 81 | # in afirmative case simply cancel this request since it | 81 | if self.builderFailCall and self.builderFailCall.active(): |
302 | 82 | # already died. | 82 | self.builderFailCall.cancel() |
298 | 83 | if self.killCall and self.killCall.active(): | ||
299 | 84 | self.killCall.cancel() | ||
303 | 85 | 83 | ||
304 | 86 | # notify the slave, it'll perform the required actions | 84 | # notify the slave, it'll perform the required actions |
306 | 87 | self.notify(statusobject.value.exitCode) | 85 | if self.notify is not None: |
307 | 86 | self.notify(statusobject.value.exitCode) | ||
308 | 88 | 87 | ||
309 | 89 | 88 | ||
310 | 90 | class BuildManager(object): | 89 | class BuildManager(object): |
311 | 91 | """Build Daemon slave build manager abstract parent""" | 90 | """Build Daemon slave build manager abstract parent""" |
312 | 92 | 91 | ||
314 | 93 | def __init__(self, slave, buildid): | 92 | def __init__(self, slave, buildid, reactor=None): |
315 | 94 | """Create a BuildManager. | 93 | """Create a BuildManager. |
316 | 95 | 94 | ||
317 | 96 | :param slave: A `BuildDSlave`. | 95 | :param slave: A `BuildDSlave`. |
318 | @@ -99,20 +98,29 @@ | |||
319 | 99 | object.__init__(self) | 98 | object.__init__(self) |
320 | 100 | self._buildid = buildid | 99 | self._buildid = buildid |
321 | 101 | self._slave = slave | 100 | self._slave = slave |
322 | 101 | if reactor is None: | ||
323 | 102 | reactor = default_reactor | ||
324 | 103 | self._reactor = reactor | ||
325 | 102 | self._preppath = slave._config.get("allmanagers", "preppath") | 104 | self._preppath = slave._config.get("allmanagers", "preppath") |
326 | 103 | self._unpackpath = slave._config.get("allmanagers", "unpackpath") | 105 | self._unpackpath = slave._config.get("allmanagers", "unpackpath") |
327 | 104 | self._cleanpath = slave._config.get("allmanagers", "cleanpath") | 106 | self._cleanpath = slave._config.get("allmanagers", "cleanpath") |
328 | 105 | self._mountpath = slave._config.get("allmanagers", "mountpath") | 107 | self._mountpath = slave._config.get("allmanagers", "mountpath") |
329 | 106 | self._umountpath = slave._config.get("allmanagers", "umountpath") | 108 | self._umountpath = slave._config.get("allmanagers", "umountpath") |
330 | 109 | self._scanpath = slave._config.get("allmanagers", "processscanpath") | ||
331 | 110 | self._subprocess = None | ||
332 | 111 | self._reaped_states = set() | ||
333 | 107 | self.is_archive_private = False | 112 | self.is_archive_private = False |
334 | 108 | self.home = os.environ['HOME'] | 113 | self.home = os.environ['HOME'] |
335 | 114 | self.abort_timeout = 120 | ||
336 | 109 | 115 | ||
338 | 110 | def runSubProcess(self, command, args): | 116 | def runSubProcess(self, command, args, iterate=None): |
339 | 111 | """Run a sub process capturing the results in the log.""" | 117 | """Run a sub process capturing the results in the log.""" |
341 | 112 | self._subprocess = RunCapture(self._slave, self.iterate) | 118 | if iterate is None: |
342 | 119 | iterate = self.iterate | ||
343 | 120 | self._subprocess = RunCapture(self._slave, iterate) | ||
344 | 113 | self._slave.log("RUN: %s %r\n" % (command, args)) | 121 | self._slave.log("RUN: %s %r\n" % (command, args)) |
345 | 114 | childfds = {0: devnull.fileno(), 1: "r", 2: "r"} | 122 | childfds = {0: devnull.fileno(), 1: "r", 2: "r"} |
347 | 115 | reactor.spawnProcess( | 123 | self._reactor.spawnProcess( |
348 | 116 | self._subprocess, command, args, env=os.environ, | 124 | self._subprocess, command, args, env=os.environ, |
349 | 117 | path=self.home, childFDs=childfds) | 125 | path=self.home, childFDs=childfds) |
350 | 118 | 126 | ||
351 | @@ -122,6 +130,25 @@ | |||
352 | 122 | self._unpackpath, | 130 | self._unpackpath, |
353 | 123 | ["unpack-chroot", self._buildid, self._chroottarfile]) | 131 | ["unpack-chroot", self._buildid, self._chroottarfile]) |
354 | 124 | 132 | ||
355 | 133 | def doReapProcesses(self, state, notify=True): | ||
356 | 134 | """Reap any processes left lying around in the chroot.""" | ||
357 | 135 | if state is not None and state in self._reaped_states: | ||
358 | 136 | # We've already reaped this state. To avoid a loop, proceed | ||
359 | 137 | # immediately to the next iterator. | ||
360 | 138 | self._slave.log("Already reaped from state %s" % state) | ||
361 | 139 | if notify: | ||
362 | 140 | self.iterateReap(state, 0) | ||
363 | 141 | else: | ||
364 | 142 | if state is not None: | ||
365 | 143 | self._reaped_states.add(state) | ||
366 | 144 | if notify: | ||
367 | 145 | iterate = partial(self.iterateReap, state) | ||
368 | 146 | else: | ||
369 | 147 | iterate = lambda success: None | ||
370 | 148 | self.runSubProcess( | ||
371 | 149 | self._scanpath, [self._scanpath, self._buildid], | ||
372 | 150 | iterate=iterate) | ||
373 | 151 | |||
374 | 125 | def doCleanup(self): | 152 | def doCleanup(self): |
375 | 126 | """Remove the build tree etc.""" | 153 | """Remove the build tree etc.""" |
376 | 127 | self.runSubProcess(self._cleanpath, ["remove-build", self._buildid]) | 154 | self.runSubProcess(self._cleanpath, ["remove-build", self._buildid]) |
377 | @@ -175,37 +202,66 @@ | |||
378 | 175 | raise NotImplementedError("BuildManager should be subclassed to be " | 202 | raise NotImplementedError("BuildManager should be subclassed to be " |
379 | 176 | "used") | 203 | "used") |
380 | 177 | 204 | ||
381 | 205 | def iterateReap(self, state, success): | ||
382 | 206 | """Perform an iteration of the slave following subprocess reaping. | ||
383 | 207 | |||
384 | 208 | Subprocess reaping is special, typically occurring at several | ||
385 | 209 | positions in a build manager's state machine. We therefore keep | ||
386 | 210 | track of the state being reaped so that we can select the | ||
387 | 211 | appropriate next state. | ||
388 | 212 | """ | ||
389 | 213 | raise NotImplementedError("BuildManager should be subclassed to be " | ||
390 | 214 | "used") | ||
391 | 215 | |||
392 | 216 | def abortReap(self): | ||
393 | 217 | """Abort by killing all processes in the chroot, as hard as we can. | ||
394 | 218 | |||
395 | 219 | We expect this to result in the main build process exiting non-zero | ||
396 | 220 | and giving us some useful logs. | ||
397 | 221 | |||
398 | 222 | This may be overridden in subclasses so that they can perform their | ||
399 | 223 | own state machine management. | ||
400 | 224 | """ | ||
401 | 225 | self.doReapProcesses(None, notify=False) | ||
402 | 226 | |||
403 | 178 | def abort(self): | 227 | def abort(self): |
404 | 179 | """Abort the build by killing the subprocess.""" | 228 | """Abort the build by killing the subprocess.""" |
405 | 180 | if self.alreadyfailed or self._subprocess is None: | 229 | if self.alreadyfailed or self._subprocess is None: |
406 | 181 | return | 230 | return |
407 | 182 | else: | 231 | else: |
408 | 183 | self.alreadyfailed = True | 232 | self.alreadyfailed = True |
430 | 184 | # Either SIGKILL and SIGTERM presents the same behavior, the | 233 | primary_subprocess = self._subprocess |
431 | 185 | # process is just killed some time after the signal was sent | 234 | self.abortReap() |
432 | 186 | # 10 s ~ 40 s, and returns None as exit_code, instead of the normal | 235 | # In extreme cases the build may be hung too badly for |
433 | 187 | # integer. See further info on DebianBuildermanager.iterate in | 236 | # scan-for-processes to manage to kill it (blocked on I/O, |
434 | 188 | # debian.py | 237 | # forkbombing test suite, etc.). In this case, fail the builder and |
435 | 189 | # XXX cprov 2005-09-02: | 238 | # let an admin sort it out. |
436 | 190 | # we may want to follow the canonical.tachandler kill process | 239 | self._subprocess.builderFailCall = self._reactor.callLater( |
437 | 191 | # style, which sends SIGTERM to the process wait a given timeout | 240 | self.abort_timeout, self.builderFail, |
438 | 192 | # and if was not killed sends a SIGKILL. IMO it only would be worth | 241 | "Failed to kill all processes.", primary_subprocess) |
439 | 193 | # if we found different behaviour than the previous described. | 242 | |
440 | 194 | self._subprocess.transport.signalProcess('TERM') | 243 | def builderFail(self, reason, primary_subprocess): |
441 | 195 | # alternatively to simply send SIGTERM, we can pend a request to | 244 | """Mark the builder as failed.""" |
442 | 196 | # send SIGKILL to the process if nothing happened in 10 seconds | 245 | self._slave.log("ABORTING: %s\n" % reason) |
443 | 197 | # see base class process | 246 | self._subprocess.builderFailCall = None |
444 | 198 | self._subprocess.killCall = reactor.callLater(10, self.kill) | 247 | self._slave.builderFail() |
445 | 199 | 248 | self.alreadyfailed = True | |
446 | 200 | def kill(self): | 249 | # If we failed to kill all processes in the chroot, then the primary |
447 | 201 | """Send SIGKILL to child process | 250 | # subprocess (i.e. the one running immediately before |
448 | 202 | 251 | # doReapProcesses was called) may not have exited. Kill it so that | |
449 | 203 | Mask exception generated when the child process has already exited. | 252 | # we can proceed. |
429 | 204 | """ | ||
450 | 205 | try: | 253 | try: |
452 | 206 | self._subprocess.transport.signalProcess('KILL') | 254 | primary_subprocess.transport.signalProcess('KILL') |
453 | 207 | except process.ProcessExitedAlready: | 255 | except process.ProcessExitedAlready: |
454 | 208 | self._slave.log("ABORTING: Process Exited Already\n") | 256 | self._slave.log("ABORTING: Process Exited Already\n") |
455 | 257 | primary_subprocess.transport.loseConnection() | ||
456 | 258 | # Leave the reaper running, but disconnect it from our state | ||
457 | 259 | # machine. Perhaps an admin can make something of it, and in any | ||
458 | 260 | # case scan-for-processes elevates itself to root so it's awkward to | ||
459 | 261 | # kill it. | ||
460 | 262 | self._subprocess.ignore = True | ||
461 | 263 | self._subprocess.transport.loseConnection() | ||
462 | 264 | |||
463 | 209 | 265 | ||
464 | 210 | class BuilderStatus: | 266 | class BuilderStatus: |
465 | 211 | """Status values for the builder.""" | 267 | """Status values for the builder.""" |
466 | @@ -229,6 +285,7 @@ | |||
467 | 229 | PACKAGEFAIL = "BuildStatus.PACKAGEFAIL" | 285 | PACKAGEFAIL = "BuildStatus.PACKAGEFAIL" |
468 | 230 | CHROOTFAIL = "BuildStatus.CHROOTFAIL" | 286 | CHROOTFAIL = "BuildStatus.CHROOTFAIL" |
469 | 231 | BUILDERFAIL = "BuildStatus.BUILDERFAIL" | 287 | BUILDERFAIL = "BuildStatus.BUILDERFAIL" |
470 | 288 | ABORTED = "BuildStatus.ABORTED" | ||
471 | 232 | 289 | ||
472 | 233 | 290 | ||
473 | 234 | class BuildDSlave(object): | 291 | class BuildDSlave(object): |
474 | @@ -355,6 +412,11 @@ | |||
475 | 355 | # XXX: dsilvers: 2005-01-21: Current abort mechanism doesn't wait | 412 | # XXX: dsilvers: 2005-01-21: Current abort mechanism doesn't wait |
476 | 356 | # for abort to complete. This is potentially an issue in a heavy | 413 | # for abort to complete. This is potentially an issue in a heavy |
477 | 357 | # load situation. | 414 | # load situation. |
478 | 415 | if self.builderstatus == BuilderStatus.ABORTING: | ||
479 | 416 | # This might happen if the master side restarts in the middle of | ||
480 | 417 | # an abort cycle. | ||
481 | 418 | self.log("Slave already ABORTING when asked to abort") | ||
482 | 419 | return | ||
483 | 358 | if self.builderstatus != BuilderStatus.BUILDING: | 420 | if self.builderstatus != BuilderStatus.BUILDING: |
484 | 359 | # XXX: Should raise a known Fault so that the client can make | 421 | # XXX: Should raise a known Fault so that the client can make |
485 | 360 | # useful decisions about the error! | 422 | # useful decisions about the error! |
486 | @@ -458,8 +520,10 @@ | |||
487 | 458 | 520 | ||
488 | 459 | def builderFail(self): | 521 | def builderFail(self): |
489 | 460 | """Cease building because the builder has a problem.""" | 522 | """Cease building because the builder has a problem.""" |
492 | 461 | if self.builderstatus != BuilderStatus.BUILDING: | 523 | if self.builderstatus not in (BuilderStatus.BUILDING, |
493 | 462 | raise ValueError("Slave is not BUILDING when set to BUILDERFAIL") | 524 | BuilderStatus.ABORTING): |
494 | 525 | raise ValueError("Slave is not BUILDING|ABORTING when set to " | ||
495 | 526 | "BUILDERFAIL") | ||
496 | 463 | self.buildstatus = BuildStatus.BUILDERFAIL | 527 | self.buildstatus = BuildStatus.BUILDERFAIL |
497 | 464 | 528 | ||
498 | 465 | def chrootFail(self): | 529 | def chrootFail(self): |
499 | @@ -497,14 +561,25 @@ | |||
500 | 497 | raise ValueError("Slave is not BUILDING when set to GIVENBACK") | 561 | raise ValueError("Slave is not BUILDING when set to GIVENBACK") |
501 | 498 | self.buildstatus = BuildStatus.GIVENBACK | 562 | self.buildstatus = BuildStatus.GIVENBACK |
502 | 499 | 563 | ||
503 | 564 | def buildAborted(self): | ||
504 | 565 | """Mark a build as aborted.""" | ||
505 | 566 | if self.builderstatus != BuilderStatus.ABORTING: | ||
506 | 567 | raise ValueError("Slave is not ABORTING when set to ABORTED") | ||
507 | 568 | if self.buildstatus != BuildStatus.BUILDERFAIL: | ||
508 | 569 | self.buildstatus = BuildStatus.ABORTED | ||
509 | 570 | |||
510 | 500 | def buildComplete(self): | 571 | def buildComplete(self): |
511 | 501 | """Mark the build as complete and waiting interaction from the build | 572 | """Mark the build as complete and waiting interaction from the build |
512 | 502 | daemon master. | 573 | daemon master. |
513 | 503 | """ | 574 | """ |
518 | 504 | if self.builderstatus != BuilderStatus.BUILDING: | 575 | if self.builderstatus == BuilderStatus.BUILDING: |
519 | 505 | raise ValueError("Slave is not BUILDING when told build is " | 576 | self.builderstatus = BuilderStatus.WAITING |
520 | 506 | "complete") | 577 | elif self.builderstatus == BuilderStatus.ABORTING: |
521 | 507 | self.builderstatus = BuilderStatus.WAITING | 578 | self.buildAborted() |
522 | 579 | self.builderstatus = BuilderStatus.WAITING | ||
523 | 580 | else: | ||
524 | 581 | raise ValueError("Slave is not BUILDING|ABORTING when told build " | ||
525 | 582 | "is complete") | ||
526 | 508 | 583 | ||
527 | 509 | def sanitizeBuildlog(self, log_path): | 584 | def sanitizeBuildlog(self, log_path): |
528 | 510 | """Removes passwords from buildlog URLs. | 585 | """Removes passwords from buildlog URLs. |
529 | 511 | 586 | ||
530 | === modified file 'lpbuildd/sourcepackagerecipe.py' | |||
531 | --- lpbuildd/sourcepackagerecipe.py 2011-11-10 04:55:02 +0000 | |||
532 | +++ lpbuildd/sourcepackagerecipe.py 2013-07-30 11:30:43 +0000 | |||
533 | @@ -124,8 +124,12 @@ | |||
534 | 124 | self._slave.builderFail() | 124 | self._slave.builderFail() |
535 | 125 | print("Returning build status: Builder failed.") | 125 | print("Returning build status: Builder failed.") |
536 | 126 | self.alreadyfailed = True | 126 | self.alreadyfailed = True |
539 | 127 | self._state = DebianBuildState.REAP | 127 | self.doReapProcesses(self._state) |
540 | 128 | self.doReapProcesses() | 128 | |
541 | 129 | def iterateReap_BUILD_RECIPE(self, retcode): | ||
542 | 130 | """Finished reaping after recipe building.""" | ||
543 | 131 | self._state = DebianBuildState.UMOUNT | ||
544 | 132 | self.doUnmounting() | ||
545 | 129 | 133 | ||
546 | 130 | def getChangesFilename(self): | 134 | def getChangesFilename(self): |
547 | 131 | """Return the path to the changes file.""" | 135 | """Return the path to the changes file.""" |
548 | 132 | 136 | ||
549 | === modified file 'lpbuildd/tests/buildd-slave-test.conf' | |||
550 | --- lpbuildd/tests/buildd-slave-test.conf 2011-12-08 22:13:03 +0000 | |||
551 | +++ lpbuildd/tests/buildd-slave-test.conf 2013-07-30 11:30:43 +0000 | |||
552 | @@ -12,17 +12,16 @@ | |||
553 | 12 | mountpath = /var/tmp/buildd/slavebin/mount-chroot | 12 | mountpath = /var/tmp/buildd/slavebin/mount-chroot |
554 | 13 | umountpath = /var/tmp/buildd/slavebin/umount-chroot | 13 | umountpath = /var/tmp/buildd/slavebin/umount-chroot |
555 | 14 | preppath = /var/tmp/buildd/slavebin/slave-prep | 14 | preppath = /var/tmp/buildd/slavebin/slave-prep |
556 | 15 | processscanpath = /var/tmp/buildd/slavebin/scan-for-processes | ||
557 | 15 | 16 | ||
558 | 16 | [debianmanager] | 17 | [debianmanager] |
559 | 17 | sbuildpath = /var/tmp/buildd/slavebin/sbuild-package | 18 | sbuildpath = /var/tmp/buildd/slavebin/sbuild-package |
560 | 18 | sbuildargs = -dautobuild --nolog --batch | 19 | sbuildargs = -dautobuild --nolog --batch |
561 | 19 | updatepath = /var/tmp/buildd/slavebin/update-debian-chroot | 20 | updatepath = /var/tmp/buildd/slavebin/update-debian-chroot |
562 | 20 | processscanpath = /var/tmp/buildd/slavebin/scan-for-processes | ||
563 | 21 | sourcespath = /usr/share/launchpad-buildd/slavebin/override-sources-list | 21 | sourcespath = /usr/share/launchpad-buildd/slavebin/override-sources-list |
564 | 22 | 22 | ||
565 | 23 | [binarypackagemanager] | 23 | [binarypackagemanager] |
566 | 24 | sbuildpath = /var/tmp/buildd/slavebin/sbuild-package | 24 | sbuildpath = /var/tmp/buildd/slavebin/sbuild-package |
567 | 25 | sbuildargs = -dautobuild --nolog --batch | 25 | sbuildargs = -dautobuild --nolog --batch |
568 | 26 | updatepath = /var/tmp/buildd/slavebin/update-debian-chroot | 26 | updatepath = /var/tmp/buildd/slavebin/update-debian-chroot |
569 | 27 | processscanpath = /var/tmp/buildd/slavebin/scan-for-processes | ||
570 | 28 | sourcespath = /usr/share/launchpad-buildd/slavebin/override-sources-list | 27 | sourcespath = /usr/share/launchpad-buildd/slavebin/override-sources-list |
571 | 29 | 28 | ||
572 | === added file 'lpbuildd/tests/fakeslave.py' | |||
573 | --- lpbuildd/tests/fakeslave.py 1970-01-01 00:00:00 +0000 | |||
574 | +++ lpbuildd/tests/fakeslave.py 2013-07-30 11:30:43 +0000 | |||
575 | @@ -0,0 +1,104 @@ | |||
576 | 1 | # Copyright 2013 Canonical Ltd. This software is licensed under the | ||
577 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | ||
578 | 3 | |||
579 | 4 | __metaclass__ = type | ||
580 | 5 | __all__ = [ | ||
581 | 6 | 'FakeMethod', | ||
582 | 7 | 'FakeSlave', | ||
583 | 8 | ] | ||
584 | 9 | |||
585 | 10 | import os | ||
586 | 11 | |||
587 | 12 | |||
588 | 13 | class FakeMethod: | ||
589 | 14 | """Catch any function or method call, and record the fact. | ||
590 | 15 | |||
591 | 16 | Use this for easy stubbing. The call operator can return a fixed | ||
592 | 17 | value, or raise a fixed exception object. | ||
593 | 18 | |||
594 | 19 | This is useful when unit-testing code that does things you don't | ||
595 | 20 | want to integration-test, e.g. because it wants to talk to remote | ||
596 | 21 | systems. | ||
597 | 22 | """ | ||
598 | 23 | |||
599 | 24 | def __init__(self, result=None, failure=None): | ||
600 | 25 | """Set up a fake function or method. | ||
601 | 26 | |||
602 | 27 | :param result: Value to return. | ||
603 | 28 | :param failure: Exception to raise. | ||
604 | 29 | """ | ||
605 | 30 | self.result = result | ||
606 | 31 | self.failure = failure | ||
607 | 32 | |||
608 | 33 | # A log of arguments for each call to this method. | ||
609 | 34 | self.calls = [] | ||
610 | 35 | |||
611 | 36 | def __call__(self, *args, **kwargs): | ||
612 | 37 | """Catch an invocation to the method. | ||
613 | 38 | |||
614 | 39 | Increment `call_count`, and adds the arguments to `calls`. | ||
615 | 40 | |||
616 | 41 | Accepts any and all parameters. Raises the failure passed to | ||
617 | 42 | the constructor, if any; otherwise, returns the result value | ||
618 | 43 | passed to the constructor. | ||
619 | 44 | """ | ||
620 | 45 | self.calls.append((args, kwargs)) | ||
621 | 46 | |||
622 | 47 | if self.failure is None: | ||
623 | 48 | return self.result | ||
624 | 49 | else: | ||
625 | 50 | # pylint thinks this raises None, which is clearly not | ||
626 | 51 | # possible. That's why this test disables pylint message | ||
627 | 52 | # E0702. | ||
628 | 53 | raise self.failure | ||
629 | 54 | |||
630 | 55 | @property | ||
631 | 56 | def call_count(self): | ||
632 | 57 | return len(self.calls) | ||
633 | 58 | |||
634 | 59 | def extract_args(self): | ||
635 | 60 | """Return just the calls' positional-arguments tuples.""" | ||
636 | 61 | return [args for args, kwargs in self.calls] | ||
637 | 62 | |||
638 | 63 | def extract_kwargs(self): | ||
639 | 64 | """Return just the calls' keyword-arguments dicts.""" | ||
640 | 65 | return [kwargs for args, kwargs in self.calls] | ||
641 | 66 | |||
642 | 67 | |||
643 | 68 | class FakeConfig: | ||
644 | 69 | def get(self, section, key): | ||
645 | 70 | return key | ||
646 | 71 | |||
647 | 72 | |||
648 | 73 | class FakeSlave: | ||
649 | 74 | def __init__(self, tempdir): | ||
650 | 75 | self._cachepath = tempdir | ||
651 | 76 | self._config = FakeConfig() | ||
652 | 77 | self._was_called = set() | ||
653 | 78 | self.waitingfiles = {} | ||
654 | 79 | |||
655 | 80 | def cachePath(self, file): | ||
656 | 81 | return os.path.join(self._cachepath, file) | ||
657 | 82 | |||
658 | 83 | def anyMethod(self, *args, **kwargs): | ||
659 | 84 | pass | ||
660 | 85 | |||
661 | 86 | fake_methods = [ | ||
662 | 87 | 'emptyLog', 'log', 'chrootFail', 'buildFail', 'builderFail', | ||
663 | 88 | ] | ||
664 | 89 | def __getattr__(self, name): | ||
665 | 90 | """Remember which fake methods were called.""" | ||
666 | 91 | if name not in self.fake_methods: | ||
667 | 92 | raise AttributeError( | ||
668 | 93 | "'%s' object has no attribute '%s'" % (self.__class__, name)) | ||
669 | 94 | self._was_called.add(name) | ||
670 | 95 | return self.anyMethod | ||
671 | 96 | |||
672 | 97 | def wasCalled(self, name): | ||
673 | 98 | return name in self._was_called | ||
674 | 99 | |||
675 | 100 | def getArch(self): | ||
676 | 101 | return 'i386' | ||
677 | 102 | |||
678 | 103 | storeFile = FakeMethod() | ||
679 | 104 | addWaitingFile = FakeMethod() | ||
680 | 0 | 105 | ||
681 | === modified file 'lpbuildd/tests/harness.py' | |||
682 | --- lpbuildd/tests/harness.py 2012-06-05 17:11:03 +0000 | |||
683 | +++ lpbuildd/tests/harness.py 2013-07-30 11:30:43 +0000 | |||
684 | @@ -4,6 +4,7 @@ | |||
685 | 4 | __metaclass__ = type | 4 | __metaclass__ = type |
686 | 5 | __all__ = [ | 5 | __all__ = [ |
687 | 6 | 'BuilddTestCase', | 6 | 'BuilddTestCase', |
688 | 7 | 'FakeSlave', | ||
689 | 7 | ] | 8 | ] |
690 | 8 | 9 | ||
691 | 9 | import os | 10 | import os |
692 | 10 | 11 | ||
693 | === added file 'lpbuildd/tests/test_binarypackage.py' | |||
694 | --- lpbuildd/tests/test_binarypackage.py 1970-01-01 00:00:00 +0000 | |||
695 | +++ lpbuildd/tests/test_binarypackage.py 2013-07-30 11:30:43 +0000 | |||
696 | @@ -0,0 +1,206 @@ | |||
697 | 1 | # Copyright 2013 Canonical Ltd. This software is licensed under the | ||
698 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | ||
699 | 3 | |||
700 | 4 | __metaclass__ = type | ||
701 | 5 | |||
702 | 6 | import tempfile | ||
703 | 7 | |||
704 | 8 | import os | ||
705 | 9 | import shutil | ||
706 | 10 | from testtools import TestCase | ||
707 | 11 | |||
708 | 12 | from twisted.internet.task import Clock | ||
709 | 13 | |||
710 | 14 | from lpbuildd.binarypackage import ( | ||
711 | 15 | BinaryPackageBuildManager, | ||
712 | 16 | BinaryPackageBuildState, | ||
713 | 17 | ) | ||
714 | 18 | from lpbuildd.tests.fakeslave import ( | ||
715 | 19 | FakeMethod, | ||
716 | 20 | FakeSlave, | ||
717 | 21 | ) | ||
718 | 22 | |||
719 | 23 | |||
720 | 24 | class MockTransport: | ||
721 | 25 | loseConnection = FakeMethod() | ||
722 | 26 | signalProcess = FakeMethod() | ||
723 | 27 | |||
724 | 28 | |||
725 | 29 | class MockSubprocess: | ||
726 | 30 | def __init__(self, path): | ||
727 | 31 | self.path = path | ||
728 | 32 | self.transport = MockTransport() | ||
729 | 33 | |||
730 | 34 | |||
731 | 35 | class MockBuildManager(BinaryPackageBuildManager): | ||
732 | 36 | def __init__(self, *args, **kwargs): | ||
733 | 37 | super(MockBuildManager, self).__init__(*args, **kwargs) | ||
734 | 38 | self.commands = [] | ||
735 | 39 | self.iterators = [] | ||
736 | 40 | |||
737 | 41 | def runSubProcess(self, path, command, iterate=None): | ||
738 | 42 | self.commands.append([path]+command) | ||
739 | 43 | if iterate is None: | ||
740 | 44 | iterate = self.iterate | ||
741 | 45 | self.iterators.append(iterate) | ||
742 | 46 | self._subprocess = MockSubprocess(path) | ||
743 | 47 | return 0 | ||
744 | 48 | |||
745 | 49 | |||
746 | 50 | class TestBinaryPackageBuildManagerIteration(TestCase): | ||
747 | 51 | """Run BinaryPackageBuildManager through its iteration steps.""" | ||
748 | 52 | def setUp(self): | ||
749 | 53 | super(TestBinaryPackageBuildManagerIteration, self).setUp() | ||
750 | 54 | self.working_dir = tempfile.mkdtemp() | ||
751 | 55 | self.addCleanup(lambda: shutil.rmtree(self.working_dir)) | ||
752 | 56 | slave_dir = os.path.join(self.working_dir, 'slave') | ||
753 | 57 | home_dir = os.path.join(self.working_dir, 'home') | ||
754 | 58 | for dir in (slave_dir, home_dir): | ||
755 | 59 | os.mkdir(dir) | ||
756 | 60 | self.slave = FakeSlave(slave_dir) | ||
757 | 61 | self.buildid = '123' | ||
758 | 62 | self.clock = Clock() | ||
759 | 63 | self.buildmanager = MockBuildManager( | ||
760 | 64 | self.slave, self.buildid, reactor=self.clock) | ||
761 | 65 | self.buildmanager.home = home_dir | ||
762 | 66 | self.buildmanager._cachepath = self.slave._cachepath | ||
763 | 67 | self.chrootdir = os.path.join( | ||
764 | 68 | home_dir, 'build-%s' % self.buildid, 'chroot-autobuild') | ||
765 | 69 | |||
766 | 70 | def getState(self): | ||
767 | 71 | """Retrieve build manager's state.""" | ||
768 | 72 | return self.buildmanager._state | ||
769 | 73 | |||
770 | 74 | def startBuild(self): | ||
771 | 75 | # The build manager's iterate() kicks off the consecutive states | ||
772 | 76 | # after INIT. | ||
773 | 77 | self.buildmanager.initiate( | ||
774 | 78 | {'foo_1.dsc': ''}, 'chroot.tar.gz', | ||
775 | 79 | {'suite': 'warty', 'ogrecomponent': 'main'}) | ||
776 | 80 | |||
777 | 81 | # Skip DebianBuildManager states to the state directly before | ||
778 | 82 | # SBUILD. | ||
779 | 83 | self.buildmanager._state = BinaryPackageBuildState.UPDATE | ||
780 | 84 | |||
781 | 85 | # SBUILD: Build the package. | ||
782 | 86 | self.buildmanager.iterate(0) | ||
783 | 87 | self.assertEqual(BinaryPackageBuildState.SBUILD, self.getState()) | ||
784 | 88 | expected_command = [ | ||
785 | 89 | 'sbuildpath', 'sbuild-package', self.buildid, 'i386', 'warty', | ||
786 | 90 | 'sbuildargs', '--dist=warty', '--architecture=i386', '--comp=main', | ||
787 | 91 | 'foo_1.dsc', | ||
788 | 92 | ] | ||
789 | 93 | self.assertEqual(expected_command, self.buildmanager.commands[-1]) | ||
790 | 94 | self.assertEqual( | ||
791 | 95 | self.buildmanager.iterate, self.buildmanager.iterators[-1]) | ||
792 | 96 | self.assertFalse(self.slave.wasCalled('chrootFail')) | ||
793 | 97 | |||
794 | 98 | def test_iterate(self): | ||
795 | 99 | # The build manager iterates a normal build from start to finish. | ||
796 | 100 | self.startBuild() | ||
797 | 101 | |||
798 | 102 | log_path = os.path.join(self.buildmanager._cachepath, 'buildlog') | ||
799 | 103 | log = open(log_path, 'w') | ||
800 | 104 | log.write("I am a build log.") | ||
801 | 105 | log.close() | ||
802 | 106 | |||
803 | 107 | changes_path = os.path.join( | ||
804 | 108 | self.buildmanager.home, 'build-%s' % self.buildid, | ||
805 | 109 | 'foo_1_i386.changes') | ||
806 | 110 | changes = open(changes_path, 'w') | ||
807 | 111 | changes.write("I am a changes file.") | ||
808 | 112 | changes.close() | ||
809 | 113 | |||
810 | 114 | # After building the package, reap processes. | ||
811 | 115 | self.buildmanager.iterate(0) | ||
812 | 116 | expected_command = [ | ||
813 | 117 | 'processscanpath', 'processscanpath', self.buildid, | ||
814 | 118 | ] | ||
815 | 119 | self.assertEqual(BinaryPackageBuildState.SBUILD, self.getState()) | ||
816 | 120 | self.assertEqual(expected_command, self.buildmanager.commands[-1]) | ||
817 | 121 | self.assertNotEqual( | ||
818 | 122 | self.buildmanager.iterate, self.buildmanager.iterators[-1]) | ||
819 | 123 | self.assertFalse(self.slave.wasCalled('buildFail')) | ||
820 | 124 | self.assertEqual([], self.slave.addWaitingFile.calls) | ||
821 | 125 | |||
822 | 126 | # Control returns to the DebianBuildManager in the UMOUNT state. | ||
823 | 127 | self.buildmanager.iterateReap(self.getState(), 0) | ||
824 | 128 | expected_command = [ | ||
825 | 129 | 'umountpath', 'umount-chroot', self.buildid | ||
826 | 130 | ] | ||
827 | 131 | self.assertEqual(BinaryPackageBuildState.UMOUNT, self.getState()) | ||
828 | 132 | self.assertEqual(expected_command, self.buildmanager.commands[-1]) | ||
829 | 133 | self.assertEqual( | ||
830 | 134 | self.buildmanager.iterate, self.buildmanager.iterators[-1]) | ||
831 | 135 | self.assertFalse(self.slave.wasCalled('buildFail')) | ||
832 | 136 | |||
833 | 137 | def test_abort_sbuild(self): | ||
834 | 138 | # Aborting sbuild kills processes in the chroot. | ||
835 | 139 | self.startBuild() | ||
836 | 140 | |||
837 | 141 | # Send an abort command. The build manager reaps processes. | ||
838 | 142 | self.buildmanager.abort() | ||
839 | 143 | expected_command = [ | ||
840 | 144 | 'processscanpath', 'processscanpath', self.buildid | ||
841 | 145 | ] | ||
842 | 146 | self.assertEqual(BinaryPackageBuildState.SBUILD, self.getState()) | ||
843 | 147 | self.assertEqual(expected_command, self.buildmanager.commands[-1]) | ||
844 | 148 | self.assertNotEqual( | ||
845 | 149 | self.buildmanager.iterate, self.buildmanager.iterators[-1]) | ||
846 | 150 | self.assertFalse(self.slave.wasCalled('buildFail')) | ||
847 | 151 | |||
848 | 152 | # If reaping completes successfully, the build manager returns | ||
849 | 153 | # control to the DebianBuildManager in the UMOUNT state. | ||
850 | 154 | self.buildmanager.iterateReap(self.getState(), 0) | ||
851 | 155 | expected_command = [ | ||
852 | 156 | 'umountpath', 'umount-chroot', self.buildid | ||
853 | 157 | ] | ||
854 | 158 | self.assertEqual(BinaryPackageBuildState.UMOUNT, self.getState()) | ||
855 | 159 | self.assertEqual(expected_command, self.buildmanager.commands[-1]) | ||
856 | 160 | self.assertEqual( | ||
857 | 161 | self.buildmanager.iterate, self.buildmanager.iterators[-1]) | ||
858 | 162 | self.assertFalse(self.slave.wasCalled('buildFail')) | ||
859 | 163 | |||
860 | 164 | def test_abort_sbuild_fail(self): | ||
861 | 165 | # If killing processes in the chroot hangs, the build manager does | ||
862 | 166 | # its best to clean up and fails the builder. | ||
863 | 167 | self.startBuild() | ||
864 | 168 | sbuild_subprocess = self.buildmanager._subprocess | ||
865 | 169 | |||
866 | 170 | # Send an abort command. The build manager reaps processes. | ||
867 | 171 | self.buildmanager.abort() | ||
868 | 172 | expected_command = [ | ||
869 | 173 | 'processscanpath', 'processscanpath', self.buildid | ||
870 | 174 | ] | ||
871 | 175 | self.assertEqual(BinaryPackageBuildState.SBUILD, self.getState()) | ||
872 | 176 | self.assertEqual(expected_command, self.buildmanager.commands[-1]) | ||
873 | 177 | self.assertNotEqual( | ||
874 | 178 | self.buildmanager.iterate, self.buildmanager.iterators[-1]) | ||
875 | 179 | self.assertFalse(self.slave.wasCalled('builderFail')) | ||
876 | 180 | reap_subprocess = self.buildmanager._subprocess | ||
877 | 181 | |||
878 | 182 | # If reaping fails, the builder is failed, sbuild is killed, and the | ||
879 | 183 | # reaper is disconnected. | ||
880 | 184 | self.clock.advance(120) | ||
881 | 185 | self.assertTrue(self.slave.wasCalled('builderFail')) | ||
882 | 186 | self.assertEqual( | ||
883 | 187 | [(('KILL',), {})], sbuild_subprocess.transport.signalProcess.calls) | ||
884 | 188 | self.assertNotEqual( | ||
885 | 189 | [], sbuild_subprocess.transport.loseConnection.calls) | ||
886 | 190 | self.assertNotEqual([], reap_subprocess.transport.loseConnection.calls) | ||
887 | 191 | |||
888 | 192 | log_path = os.path.join(self.buildmanager._cachepath, 'buildlog') | ||
889 | 193 | log = open(log_path, 'w') | ||
890 | 194 | log.write("I am a build log.") | ||
891 | 195 | log.close() | ||
892 | 196 | |||
893 | 197 | # When sbuild exits, it does not reap processes again, but proceeds | ||
894 | 198 | # directly to UMOUNT. | ||
895 | 199 | self.buildmanager.iterate(128 + 9) # SIGKILL | ||
896 | 200 | expected_command = [ | ||
897 | 201 | 'umountpath', 'umount-chroot', self.buildid | ||
898 | 202 | ] | ||
899 | 203 | self.assertEqual(BinaryPackageBuildState.UMOUNT, self.getState()) | ||
900 | 204 | self.assertEqual(expected_command, self.buildmanager.commands[-1]) | ||
901 | 205 | self.assertEqual( | ||
902 | 206 | self.buildmanager.iterate, self.buildmanager.iterators[-1]) | ||
903 | 0 | 207 | ||
904 | === modified file 'lpbuildd/tests/test_translationtemplatesbuildmanager.py' | |||
905 | --- lpbuildd/tests/test_translationtemplatesbuildmanager.py 2011-11-21 05:41:27 +0000 | |||
906 | +++ lpbuildd/tests/test_translationtemplatesbuildmanager.py 2013-07-30 11:30:43 +0000 | |||
907 | @@ -9,109 +9,24 @@ | |||
908 | 9 | 9 | ||
909 | 10 | from testtools import TestCase | 10 | from testtools import TestCase |
910 | 11 | 11 | ||
911 | 12 | from lpbuildd.tests.fakeslave import FakeSlave | ||
912 | 12 | from lpbuildd.translationtemplates import ( | 13 | from lpbuildd.translationtemplates import ( |
913 | 13 | TranslationTemplatesBuildManager, | 14 | TranslationTemplatesBuildManager, |
914 | 14 | TranslationTemplatesBuildState, | 15 | TranslationTemplatesBuildState, |
915 | 15 | ) | 16 | ) |
916 | 16 | 17 | ||
917 | 17 | 18 | ||
918 | 18 | class FakeMethod: | ||
919 | 19 | """Catch any function or method call, and record the fact. | ||
920 | 20 | |||
921 | 21 | Use this for easy stubbing. The call operator can return a fixed | ||
922 | 22 | value, or raise a fixed exception object. | ||
923 | 23 | |||
924 | 24 | This is useful when unit-testing code that does things you don't | ||
925 | 25 | want to integration-test, e.g. because it wants to talk to remote | ||
926 | 26 | systems. | ||
927 | 27 | """ | ||
928 | 28 | |||
929 | 29 | def __init__(self, result=None, failure=None): | ||
930 | 30 | """Set up a fake function or method. | ||
931 | 31 | |||
932 | 32 | :param result: Value to return. | ||
933 | 33 | :param failure: Exception to raise. | ||
934 | 34 | """ | ||
935 | 35 | self.result = result | ||
936 | 36 | self.failure = failure | ||
937 | 37 | |||
938 | 38 | # A log of arguments for each call to this method. | ||
939 | 39 | self.calls = [] | ||
940 | 40 | |||
941 | 41 | def __call__(self, *args, **kwargs): | ||
942 | 42 | """Catch an invocation to the method. | ||
943 | 43 | |||
944 | 44 | Increment `call_count`, and adds the arguments to `calls`. | ||
945 | 45 | |||
946 | 46 | Accepts any and all parameters. Raises the failure passed to | ||
947 | 47 | the constructor, if any; otherwise, returns the result value | ||
948 | 48 | passed to the constructor. | ||
949 | 49 | """ | ||
950 | 50 | self.calls.append((args, kwargs)) | ||
951 | 51 | |||
952 | 52 | if self.failure is None: | ||
953 | 53 | return self.result | ||
954 | 54 | else: | ||
955 | 55 | # pylint thinks this raises None, which is clearly not | ||
956 | 56 | # possible. That's why this test disables pylint message | ||
957 | 57 | # E0702. | ||
958 | 58 | raise self.failure | ||
959 | 59 | |||
960 | 60 | @property | ||
961 | 61 | def call_count(self): | ||
962 | 62 | return len(self.calls) | ||
963 | 63 | |||
964 | 64 | def extract_args(self): | ||
965 | 65 | """Return just the calls' positional-arguments tuples.""" | ||
966 | 66 | return [args for args, kwargs in self.calls] | ||
967 | 67 | |||
968 | 68 | def extract_kwargs(self): | ||
969 | 69 | """Return just the calls' keyword-arguments dicts.""" | ||
970 | 70 | return [kwargs for args, kwargs in self.calls] | ||
971 | 71 | |||
972 | 72 | |||
973 | 73 | class FakeConfig: | ||
974 | 74 | def get(self, section, key): | ||
975 | 75 | return key | ||
976 | 76 | |||
977 | 77 | |||
978 | 78 | class FakeSlave: | ||
979 | 79 | def __init__(self, tempdir): | ||
980 | 80 | self._cachepath = tempdir | ||
981 | 81 | self._config = FakeConfig() | ||
982 | 82 | self._was_called = set() | ||
983 | 83 | |||
984 | 84 | def cachePath(self, file): | ||
985 | 85 | return os.path.join(self._cachepath, file) | ||
986 | 86 | |||
987 | 87 | def anyMethod(self, *args, **kwargs): | ||
988 | 88 | pass | ||
989 | 89 | |||
990 | 90 | fake_methods = ['emptyLog', 'chrootFail', 'buildFail', 'builderFail',] | ||
991 | 91 | def __getattr__(self, name): | ||
992 | 92 | """Remember which fake methods were called.""" | ||
993 | 93 | if name not in self.fake_methods: | ||
994 | 94 | raise AttributeError( | ||
995 | 95 | "'%s' object has no attribute '%s'" % (self.__class__, name)) | ||
996 | 96 | self._was_called.add(name) | ||
997 | 97 | return self.anyMethod | ||
998 | 98 | |||
999 | 99 | def wasCalled(self, name): | ||
1000 | 100 | return name in self._was_called | ||
1001 | 101 | |||
1002 | 102 | def getArch(self): | ||
1003 | 103 | return 'i386' | ||
1004 | 104 | |||
1005 | 105 | addWaitingFile = FakeMethod() | ||
1006 | 106 | |||
1007 | 107 | |||
1008 | 108 | class MockBuildManager(TranslationTemplatesBuildManager): | 19 | class MockBuildManager(TranslationTemplatesBuildManager): |
1009 | 109 | def __init__(self, *args, **kwargs): | 20 | def __init__(self, *args, **kwargs): |
1010 | 110 | super(MockBuildManager, self).__init__(*args, **kwargs) | 21 | super(MockBuildManager, self).__init__(*args, **kwargs) |
1011 | 111 | self.commands = [] | 22 | self.commands = [] |
1012 | 23 | self.iterators = [] | ||
1013 | 112 | 24 | ||
1015 | 113 | def runSubProcess(self, path, command): | 25 | def runSubProcess(self, path, command, iterate=None): |
1016 | 114 | self.commands.append([path]+command) | 26 | self.commands.append([path]+command) |
1017 | 27 | if iterate is None: | ||
1018 | 28 | iterate = self.iterate | ||
1019 | 29 | self.iterators.append(iterate) | ||
1020 | 115 | return 0 | 30 | return 0 |
1021 | 116 | 31 | ||
1022 | 117 | 32 | ||
1023 | @@ -143,7 +58,7 @@ | |||
1024 | 143 | # after INIT. | 58 | # after INIT. |
1025 | 144 | self.buildmanager.initiate({}, 'chroot.tar.gz', {'branch_url': url}) | 59 | self.buildmanager.initiate({}, 'chroot.tar.gz', {'branch_url': url}) |
1026 | 145 | 60 | ||
1028 | 146 | # Skip states that are done in DebianBuldManager to the state | 61 | # Skip states that are done in DebianBuildManager to the state |
1029 | 147 | # directly before INSTALL. | 62 | # directly before INSTALL. |
1030 | 148 | self.buildmanager._state = TranslationTemplatesBuildState.UPDATE | 63 | self.buildmanager._state = TranslationTemplatesBuildState.UPDATE |
1031 | 149 | 64 | ||
1032 | @@ -158,6 +73,8 @@ | |||
1033 | 158 | 'apt-get', | 73 | 'apt-get', |
1034 | 159 | ] | 74 | ] |
1035 | 160 | self.assertEqual(expected_command, self.buildmanager.commands[-1][:5]) | 75 | self.assertEqual(expected_command, self.buildmanager.commands[-1][:5]) |
1036 | 76 | self.assertEqual( | ||
1037 | 77 | self.buildmanager.iterate, self.buildmanager.iterators[-1]) | ||
1038 | 161 | 78 | ||
1039 | 162 | # GENERATE: Run the slave's payload, the script that generates | 79 | # GENERATE: Run the slave's payload, the script that generates |
1040 | 163 | # templates. | 80 | # templates. |
1041 | @@ -168,6 +85,8 @@ | |||
1042 | 168 | 'generatepath', 'generatepath', self.buildid, url, 'resultarchive' | 85 | 'generatepath', 'generatepath', self.buildid, url, 'resultarchive' |
1043 | 169 | ] | 86 | ] |
1044 | 170 | self.assertEqual(expected_command, self.buildmanager.commands[-1]) | 87 | self.assertEqual(expected_command, self.buildmanager.commands[-1]) |
1045 | 88 | self.assertEqual( | ||
1046 | 89 | self.buildmanager.iterate, self.buildmanager.iterators[-1]) | ||
1047 | 171 | self.assertFalse(self.slave.wasCalled('chrootFail')) | 90 | self.assertFalse(self.slave.wasCalled('chrootFail')) |
1048 | 172 | 91 | ||
1049 | 173 | outfile_path = os.path.join( | 92 | outfile_path = os.path.join( |
1050 | @@ -179,18 +98,32 @@ | |||
1051 | 179 | outfile.write("I am a template tarball. Seriously.") | 98 | outfile.write("I am a template tarball. Seriously.") |
1052 | 180 | outfile.close() | 99 | outfile.close() |
1053 | 181 | 100 | ||
1055 | 182 | # The control returns to the DebianBuildManager in the REAP state. | 101 | # After generating templates, reap processes. |
1056 | 183 | self.buildmanager.iterate(0) | 102 | self.buildmanager.iterate(0) |
1057 | 184 | expected_command = [ | 103 | expected_command = [ |
1058 | 185 | 'processscanpath', 'processscanpath', self.buildid | 104 | 'processscanpath', 'processscanpath', self.buildid |
1059 | 186 | ] | 105 | ] |
1060 | 187 | self.assertEqual( | 106 | self.assertEqual( |
1062 | 188 | TranslationTemplatesBuildState.REAP, self.getState()) | 107 | TranslationTemplatesBuildState.GENERATE, self.getState()) |
1063 | 189 | self.assertEqual(expected_command, self.buildmanager.commands[-1]) | 108 | self.assertEqual(expected_command, self.buildmanager.commands[-1]) |
1064 | 109 | self.assertNotEqual( | ||
1065 | 110 | self.buildmanager.iterate, self.buildmanager.iterators[-1]) | ||
1066 | 190 | self.assertFalse(self.slave.wasCalled('buildFail')) | 111 | self.assertFalse(self.slave.wasCalled('buildFail')) |
1067 | 191 | self.assertEqual( | 112 | self.assertEqual( |
1068 | 192 | [((outfile_path,), {})], self.slave.addWaitingFile.calls) | 113 | [((outfile_path,), {})], self.slave.addWaitingFile.calls) |
1069 | 193 | 114 | ||
1070 | 115 | # The control returns to the DebianBuildManager in the UMOUNT state. | ||
1071 | 116 | self.buildmanager.iterateReap(self.getState(), 0) | ||
1072 | 117 | expected_command = [ | ||
1073 | 118 | 'umountpath', 'umount-chroot', self.buildid | ||
1074 | 119 | ] | ||
1075 | 120 | self.assertEqual( | ||
1076 | 121 | TranslationTemplatesBuildState.UMOUNT, self.getState()) | ||
1077 | 122 | self.assertEqual(expected_command, self.buildmanager.commands[-1]) | ||
1078 | 123 | self.assertEqual( | ||
1079 | 124 | self.buildmanager.iterate, self.buildmanager.iterators[-1]) | ||
1080 | 125 | self.assertFalse(self.slave.wasCalled('buildFail')) | ||
1081 | 126 | |||
1082 | 194 | def test_iterate_fail_INSTALL(self): | 127 | def test_iterate_fail_INSTALL(self): |
1083 | 195 | # See that a failing INSTALL is handled properly. | 128 | # See that a failing INSTALL is handled properly. |
1084 | 196 | url = 'lp:~my/branch' | 129 | url = 'lp:~my/branch' |
1085 | @@ -209,6 +142,8 @@ | |||
1086 | 209 | 'umountpath', 'umount-chroot', self.buildid | 142 | 'umountpath', 'umount-chroot', self.buildid |
1087 | 210 | ] | 143 | ] |
1088 | 211 | self.assertEqual(expected_command, self.buildmanager.commands[-1]) | 144 | self.assertEqual(expected_command, self.buildmanager.commands[-1]) |
1089 | 145 | self.assertEqual( | ||
1090 | 146 | self.buildmanager.iterate, self.buildmanager.iterators[-1]) | ||
1091 | 212 | self.assertTrue(self.slave.wasCalled('chrootFail')) | 147 | self.assertTrue(self.slave.wasCalled('chrootFail')) |
1092 | 213 | 148 | ||
1093 | 214 | def test_iterate_fail_GENERATE(self): | 149 | def test_iterate_fail_GENERATE(self): |
1094 | @@ -221,12 +156,25 @@ | |||
1095 | 221 | # Skip states to the INSTALL state. | 156 | # Skip states to the INSTALL state. |
1096 | 222 | self.buildmanager._state = TranslationTemplatesBuildState.GENERATE | 157 | self.buildmanager._state = TranslationTemplatesBuildState.GENERATE |
1097 | 223 | 158 | ||
1099 | 224 | # The buildmanager fails and iterates to the REAP state. | 159 | # The buildmanager fails and reaps processes. |
1100 | 225 | self.buildmanager.iterate(-1) | 160 | self.buildmanager.iterate(-1) |
1101 | 226 | expected_command = [ | 161 | expected_command = [ |
1102 | 227 | 'processscanpath', 'processscanpath', self.buildid | 162 | 'processscanpath', 'processscanpath', self.buildid |
1103 | 228 | ] | 163 | ] |
1104 | 229 | self.assertEqual( | 164 | self.assertEqual( |
1106 | 230 | TranslationTemplatesBuildState.REAP, self.getState()) | 165 | TranslationTemplatesBuildState.GENERATE, self.getState()) |
1107 | 231 | self.assertEqual(expected_command, self.buildmanager.commands[-1]) | 166 | self.assertEqual(expected_command, self.buildmanager.commands[-1]) |
1108 | 167 | self.assertNotEqual( | ||
1109 | 168 | self.buildmanager.iterate, self.buildmanager.iterators[-1]) | ||
1110 | 232 | self.assertTrue(self.slave.wasCalled('buildFail')) | 169 | self.assertTrue(self.slave.wasCalled('buildFail')) |
1111 | 170 | |||
1112 | 171 | # The buildmanager iterates to the UMOUNT state. | ||
1113 | 172 | self.buildmanager.iterateReap(self.getState(), 0) | ||
1114 | 173 | self.assertEqual( | ||
1115 | 174 | TranslationTemplatesBuildState.UMOUNT, self.getState()) | ||
1116 | 175 | expected_command = [ | ||
1117 | 176 | 'umountpath', 'umount-chroot', self.buildid | ||
1118 | 177 | ] | ||
1119 | 178 | self.assertEqual(expected_command, self.buildmanager.commands[-1]) | ||
1120 | 179 | self.assertEqual( | ||
1121 | 180 | self.buildmanager.iterate, self.buildmanager.iterators[-1]) | ||
1122 | 233 | 181 | ||
1123 | === modified file 'lpbuildd/translationtemplates.py' | |||
1124 | --- lpbuildd/translationtemplates.py 2011-11-10 04:55:02 +0000 | |||
1125 | +++ lpbuildd/translationtemplates.py 2013-07-30 11:30:43 +0000 | |||
1126 | @@ -88,12 +88,14 @@ | |||
1127 | 88 | if success == 0: | 88 | if success == 0: |
1128 | 89 | # It worked! Now let's bring in the harvest. | 89 | # It worked! Now let's bring in the harvest. |
1129 | 90 | self.gatherResults() | 90 | self.gatherResults() |
1132 | 91 | self._state = TranslationTemplatesBuildState.REAP | 91 | self.doReapProcesses(self._state) |
1131 | 92 | self.doReapProcesses() | ||
1133 | 93 | else: | 92 | else: |
1134 | 94 | if not self.alreadyfailed: | 93 | if not self.alreadyfailed: |
1135 | 95 | self._slave.buildFail() | 94 | self._slave.buildFail() |
1136 | 96 | self.alreadyfailed = True | 95 | self.alreadyfailed = True |
1139 | 97 | self._state = TranslationTemplatesBuildState.REAP | 96 | self.doReapProcesses(self._state) |
1138 | 98 | self.doReapProcesses() | ||
1140 | 99 | 97 | ||
1141 | 98 | def iterateReap_GENERATE(self, success): | ||
1142 | 99 | """Finished reaping after template generation.""" | ||
1143 | 100 | self._state = TranslationTemplatesBuildState.UMOUNT | ||
1144 | 101 | self.doUnmounting() | ||
1145 | 100 | 102 | ||
1146 | === modified file 'template-buildd-slave.conf' | |||
1147 | --- template-buildd-slave.conf 2011-12-05 02:39:05 +0000 | |||
1148 | +++ template-buildd-slave.conf 2013-07-30 11:30:43 +0000 | |||
1149 | @@ -15,10 +15,10 @@ | |||
1150 | 15 | cleanpath = /usr/share/launchpad-buildd/slavebin/remove-build | 15 | cleanpath = /usr/share/launchpad-buildd/slavebin/remove-build |
1151 | 16 | mountpath = /usr/share/launchpad-buildd/slavebin/mount-chroot | 16 | mountpath = /usr/share/launchpad-buildd/slavebin/mount-chroot |
1152 | 17 | umountpath = /usr/share/launchpad-buildd/slavebin/umount-chroot | 17 | umountpath = /usr/share/launchpad-buildd/slavebin/umount-chroot |
1153 | 18 | processscanpath = /usr/share/launchpad-buildd/slavebin/scan-for-processes | ||
1154 | 18 | 19 | ||
1155 | 19 | [debianmanager] | 20 | [debianmanager] |
1156 | 20 | updatepath = /usr/share/launchpad-buildd/slavebin/update-debian-chroot | 21 | updatepath = /usr/share/launchpad-buildd/slavebin/update-debian-chroot |
1157 | 21 | processscanpath = /usr/share/launchpad-buildd/slavebin/scan-for-processes | ||
1158 | 22 | sourcespath = /usr/share/launchpad-buildd/slavebin/override-sources-list | 22 | sourcespath = /usr/share/launchpad-buildd/slavebin/override-sources-list |
1159 | 23 | 23 | ||
1160 | 24 | [binarypackagemanager] | 24 | [binarypackagemanager] |
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.