Merge lp:~wgrant/launchpad/ppa-reset-2.0-basics into lp:launchpad

Proposed by William Grant
Status: Merged
Merged at revision: 17050
Proposed branch: lp:~wgrant/launchpad/ppa-reset-2.0-basics
Merge into: lp:launchpad
Prerequisite: lp:~wgrant/launchpad/ppa-reset-2.0-model
Diff against target: 985 lines (+361/-297)
6 files modified
lib/lp/buildmaster/interactor.py (+56/-54)
lib/lp/buildmaster/manager.py (+76/-54)
lib/lp/buildmaster/tests/mock_slaves.py (+6/-1)
lib/lp/buildmaster/tests/test_interactor.py (+96/-100)
lib/lp/buildmaster/tests/test_manager.py (+118/-72)
lib/lp/soyuz/tests/test_binarypackagebuildbehaviour.py (+9/-16)
To merge this branch: bzr merge lp:~wgrant/launchpad/ppa-reset-2.0-basics
Reviewer Review Type Date Requested Status
Colin Watson (community) Approve
Review via email: mp+223397@code.launchpad.net

Commit message

Reset virtual builders a cycle before any dispatch attempt, and persist the clean status in the DB. Preparation for the new Scalingstack reset protocol.

To post a comment you must log in.
Revision history for this message
Colin Watson (cjwatson) wrote :

Looks good to me; just one minor suggestion.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/lp/buildmaster/interactor.py'
--- lib/lp/buildmaster/interactor.py 2014-05-10 18:40:36 +0000
+++ lib/lp/buildmaster/interactor.py 2014-06-18 08:49:42 +0000
@@ -21,6 +21,7 @@
21 removeSecurityProxy,21 removeSecurityProxy,
22 )22 )
2323
24from lp.buildmaster.enums import BuilderCleanStatus
24from lp.buildmaster.interfaces.builder import (25from lp.buildmaster.interfaces.builder import (
25 BuildDaemonError,26 BuildDaemonError,
26 CannotFetchFile,27 CannotFetchFile,
@@ -216,7 +217,7 @@
216BuilderVitals = namedtuple(217BuilderVitals = namedtuple(
217 'BuilderVitals',218 'BuilderVitals',
218 ('name', 'url', 'virtualized', 'vm_host', 'builderok', 'manual',219 ('name', 'url', 'virtualized', 'vm_host', 'builderok', 'manual',
219 'build_queue', 'version'))220 'build_queue', 'version', 'clean_status'))
220221
221_BQ_UNSPECIFIED = object()222_BQ_UNSPECIFIED = object()
222223
@@ -226,7 +227,8 @@
226 build_queue = builder.currentjob227 build_queue = builder.currentjob
227 return BuilderVitals(228 return BuilderVitals(
228 builder.name, builder.url, builder.virtualized, builder.vm_host,229 builder.name, builder.url, builder.virtualized, builder.vm_host,
229 builder.builderok, builder.manual, build_queue, builder.version)230 builder.builderok, builder.manual, build_queue, builder.version,
231 builder.clean_status)
230232
231233
232class BuilderInteractor(object):234class BuilderInteractor(object):
@@ -249,44 +251,6 @@
249 return behaviour251 return behaviour
250252
251 @classmethod253 @classmethod
252 @defer.inlineCallbacks
253 def rescueIfLost(cls, vitals, slave, slave_status, expected_cookie,
254 logger=None):
255 """Reset the slave if its job information doesn't match the DB.
256
257 This checks the build ID reported in the slave status against
258 the given cookie. If it isn't building what we think it should
259 be, the current build will be aborted and the slave cleaned in
260 preparation for a new task.
261
262 :return: A Deferred that fires when the dialog with the slave is
263 finished. Its return value is True if the slave is lost,
264 False otherwise.
265 """
266 # Determine the slave's current build cookie.
267 status = slave_status['builder_status']
268 slave_cookie = slave_status.get('build_id')
269
270 if slave_cookie == expected_cookie:
271 # The master and slave agree about the current job. Continue.
272 defer.returnValue(False)
273 else:
274 # The master and slave disagree. The master is our master,
275 # so try to rescue the slave.
276 # An IDLE slave doesn't need rescuing (SlaveScanner.scan
277 # will rescue the DB side instead), and we just have to wait
278 # out an ABORTING one.
279 if status == 'BuilderStatus.WAITING':
280 yield slave.clean()
281 elif status == 'BuilderStatus.BUILDING':
282 yield slave.abort()
283 if logger:
284 logger.info(
285 "Builder slave '%s' rescued from %r (expected %r)." %
286 (vitals.name, slave_cookie, expected_cookie))
287 defer.returnValue(True)
288
289 @classmethod
290 def resumeSlaveHost(cls, vitals, slave):254 def resumeSlaveHost(cls, vitals, slave):
291 """Resume the slave host to a known good condition.255 """Resume the slave host to a known good condition.
292256
@@ -323,6 +287,49 @@
323287
324 @classmethod288 @classmethod
325 @defer.inlineCallbacks289 @defer.inlineCallbacks
290 def cleanSlave(cls, vitals, slave):
291 """Prepare a slave for a new build.
292
293 :return: A Deferred that fires when this stage of the resume
294 operations finishes. If the value is True, the slave is now clean.
295 If it's False, the clean is still in progress and this must be
296 called again later.
297 """
298 if vitals.virtualized:
299 # If we are building a virtual build, resume the virtual
300 # machine. Before we try and contact the resumed slave,
301 # we're going to send it a message. This is to ensure
302 # it's accepting packets from the outside world, because
303 # testing has shown that the first packet will randomly
304 # fail for no apparent reason. This could be a quirk of
305 # the Xen guest, we're not sure. We also don't care
306 # about the result from this message, just that it's
307 # sent, hence the "addBoth". See bug 586359.
308 yield cls.resumeSlaveHost(vitals, slave)
309 yield slave.echo("ping")
310 defer.returnValue(True)
311 else:
312 slave_status = yield slave.status()
313 status = slave_status.get('builder_status', None)
314 if status == 'BuilderStatus.IDLE':
315 # This is as clean as we can get it.
316 defer.returnValue(True)
317 elif status == 'BuilderStatus.BUILDING':
318 # Asynchronously abort() the slave and wait until WAITING.
319 yield slave.abort()
320 defer.returnValue(False)
321 elif status == 'BuilderStatus.ABORTING':
322 # Wait it out until WAITING.
323 defer.returnValue(False)
324 elif status == 'BuilderStatus.WAITING':
325 # Just a synchronous clean() call and we'll be idle.
326 yield slave.clean()
327 defer.returnValue(True)
328 raise BuildDaemonError(
329 "Invalid status during clean: %r" % status)
330
331 @classmethod
332 @defer.inlineCallbacks
326 def _startBuild(cls, build_queue_item, vitals, builder, slave, behaviour,333 def _startBuild(cls, build_queue_item, vitals, builder, slave, behaviour,
327 logger):334 logger):
328 """Start a build on this builder.335 """Start a build on this builder.
@@ -342,17 +349,12 @@
342 raise BuildDaemonError(349 raise BuildDaemonError(
343 "Attempted to start a build on a known-bad builder.")350 "Attempted to start a build on a known-bad builder.")
344351
345 # If we are building a virtual build, resume the virtual352 if builder.clean_status != BuilderCleanStatus.CLEAN:
346 # machine. Before we try and contact the resumed slave, we're353 raise BuildDaemonError(
347 # going to send it a message. This is to ensure it's accepting354 "Attempted to start build on a dirty slave.")
348 # packets from the outside world, because testing has shown that355
349 # the first packet will randomly fail for no apparent reason.356 builder.setCleanStatus(BuilderCleanStatus.DIRTY)
350 # This could be a quirk of the Xen guest, we're not sure. We357 transaction.commit()
351 # also don't care about the result from this message, just that
352 # it's sent, hence the "addBoth". See bug 586359.
353 if builder.virtualized:
354 yield cls.resumeSlaveHost(vitals, slave)
355 yield slave.echo("ping")
356358
357 yield behaviour.dispatchBuildToSlave(build_queue_item.id, logger)359 yield behaviour.dispatchBuildToSlave(build_queue_item.id, logger)
358360
@@ -448,9 +450,9 @@
448 :return: A Deferred that fires when the slave dialog is finished.450 :return: A Deferred that fires when the slave dialog is finished.
449 """451 """
450 # IDLE is deliberately not handled here, because it should be452 # IDLE is deliberately not handled here, because it should be
451 # impossible to get past rescueIfLost unless the slave matches453 # impossible to get past the cookie check unless the slave
452 # the DB, and this method isn't called unless the DB says454 # matches the DB, and this method isn't called unless the DB
453 # there's a job.455 # says there's a job.
454 builder_status = slave_status['builder_status']456 builder_status = slave_status['builder_status']
455 if builder_status == 'BuilderStatus.BUILDING':457 if builder_status == 'BuilderStatus.BUILDING':
456 # Build still building, collect the logtail.458 # Build still building, collect the logtail.
457459
=== modified file 'lib/lp/buildmaster/manager.py'
--- lib/lp/buildmaster/manager.py 2014-02-05 23:22:30 +0000
+++ lib/lp/buildmaster/manager.py 2014-06-18 08:49:42 +0000
@@ -26,6 +26,7 @@
26from zope.component import getUtility26from zope.component import getUtility
2727
28from lp.buildmaster.enums import (28from lp.buildmaster.enums import (
29 BuilderCleanStatus,
29 BuildQueueStatus,30 BuildQueueStatus,
30 BuildStatus,31 BuildStatus,
31 )32 )
@@ -413,70 +414,91 @@
413 vitals = self.builder_factory.getVitals(self.builder_name)414 vitals = self.builder_factory.getVitals(self.builder_name)
414 interactor = self.interactor_factory()415 interactor = self.interactor_factory()
415 slave = self.slave_factory(vitals)416 slave = self.slave_factory(vitals)
416 if vitals.builderok:417
417 self.logger.debug("Scanning %s." % self.builder_name)418 if vitals.build_queue is not None:
418 slave_status = yield slave.status()419 if vitals.clean_status != BuilderCleanStatus.DIRTY:
419 else:420 # This is probably a grave bug with security implications,
420 slave_status = None421 # as a slave that has a job must be cleaned afterwards.
421422 raise BuildDaemonError("Non-dirty builder allegedly building.")
422 # Confirm that the DB and slave sides are in a valid, mutually423
423 # agreeable state.424 lost_reason = None
424 lost_reason = None425 if not vitals.builderok:
425 if not vitals.builderok:426 lost_reason = '%s is disabled' % vitals.name
426 lost_reason = '%s is disabled' % vitals.name427 else:
427 else:428 slave_status = yield slave.status()
428 self.updateVersion(vitals, slave_status)429 # Ensure that the slave has the job that we think it
429 cancelled = yield self.checkCancellation(vitals, slave, interactor)430 # should.
430 if cancelled:431 slave_cookie = slave_status.get('build_id')
431 return432 expected_cookie = self.getExpectedCookie(vitals)
432 assert slave_status is not None433 if slave_cookie != expected_cookie:
433 lost = yield interactor.rescueIfLost(434 lost_reason = (
434 vitals, slave, slave_status, self.getExpectedCookie(vitals),435 '%s is lost (expected %r, got %r)' % (
435 self.logger)436 vitals.name, expected_cookie, slave_cookie))
436 if lost:437
437 lost_reason = '%s is lost' % vitals.name438 if lost_reason is not None:
438439 # The slave is either confused or disabled, so reset and
439 # The slave is lost or the builder is disabled. We can't440 # requeue the job. The next scan cycle will clean up the
440 # continue to update the job status or dispatch a new job, so441 # slave if appropriate.
441 # just rescue the assigned job, if any, so it can be dispatched
442 # to another slave.
443 if lost_reason is not None:
444 if vitals.build_queue is not None:
445 self.logger.warn(442 self.logger.warn(
446 "%s. Resetting BuildQueue %d.", lost_reason,443 "%s. Resetting BuildQueue %d.", lost_reason,
447 vitals.build_queue.id)444 vitals.build_queue.id)
448 vitals.build_queue.reset()445 vitals.build_queue.reset()
449 transaction.commit()446 transaction.commit()
450 return447 return
451448
452 # We've confirmed that the slave state matches the DB. Continue449 cancelled = yield self.checkCancellation(
453 # with updating the job status, or dispatching a new job if the450 vitals, slave, interactor)
454 # builder is idle.451 if cancelled:
455 if vitals.build_queue is not None:452 return
456 # Scan the slave and get the logtail, or collect the build453
457 # if it's ready. Yes, "updateBuild" is a bad name.454 # The slave and DB agree on the builder's state. Scan the
455 # slave and get the logtail, or collect the build if it's
456 # ready. Yes, "updateBuild" is a bad name.
458 assert slave_status is not None457 assert slave_status is not None
459 yield interactor.updateBuild(458 yield interactor.updateBuild(
460 vitals, slave, slave_status, self.builder_factory,459 vitals, slave, slave_status, self.builder_factory,
461 self.behaviour_factory)460 self.behaviour_factory)
462 elif vitals.manual:
463 # If the builder is in manual mode, don't dispatch anything.
464 self.logger.debug(
465 '%s is in manual mode, not dispatching.' % vitals.name)
466 else:461 else:
467 # See if there is a job we can dispatch to the builder slave.462 if not vitals.builderok:
468 builder = self.builder_factory[self.builder_name]463 return
469 # Try to dispatch the job. If it fails, don't attempt to464 # We think the builder is idle. If it's clean, dispatch. If
470 # just retry the scan; we need to reset the job so the465 # it's dirty, clean.
471 # dispatch will be reattempted.466 if vitals.clean_status == BuilderCleanStatus.CLEAN:
472 d = interactor.findAndStartJob(vitals, builder, slave)467 slave_status = yield slave.status()
473 d.addErrback(functools.partial(self._scanFailed, False))468 if slave_status.get('builder_status') != 'BuilderStatus.IDLE':
474 yield d469 raise BuildDaemonError(
475 if builder.currentjob is not None:470 'Allegedly clean slave not idle (%r instead)'
476 # After a successful dispatch we can reset the471 % slave_status.get('builder_status'))
477 # failure_count.472 self.updateVersion(vitals, slave_status)
478 builder.resetFailureCount()473 if vitals.manual:
479 transaction.commit()474 # If the builder is in manual mode, don't dispatch
475 # anything.
476 self.logger.debug(
477 '%s is in manual mode, not dispatching.', vitals.name)
478 return
479 # Try to find and dispatch a job. If it fails, don't
480 # attempt to just retry the scan; we need to reset
481 # the job so the dispatch will be reattempted.
482 builder = self.builder_factory[self.builder_name]
483 d = interactor.findAndStartJob(vitals, builder, slave)
484 d.addErrback(functools.partial(self._scanFailed, False))
485 yield d
486 if builder.currentjob is not None:
487 # After a successful dispatch we can reset the
488 # failure_count.
489 builder.resetFailureCount()
490 transaction.commit()
491 else:
492 # Ask the BuilderInteractor to clean the slave. It might
493 # be immediately cleaned on return, in which case we go
494 # straight back to CLEAN, or we might have to spin
495 # through another few cycles.
496 done = yield interactor.cleanSlave(vitals, slave)
497 if done:
498 builder = self.builder_factory[self.builder_name]
499 builder.setCleanStatus(BuilderCleanStatus.CLEAN)
500 self.logger.debug('%s has been cleaned.', vitals.name)
501 transaction.commit()
480502
481503
482class NewBuildersScanner:504class NewBuildersScanner:
483505
=== modified file 'lib/lp/buildmaster/tests/mock_slaves.py'
--- lib/lp/buildmaster/tests/mock_slaves.py 2014-06-06 10:32:55 +0000
+++ lib/lp/buildmaster/tests/mock_slaves.py 2014-06-18 08:49:42 +0000
@@ -30,6 +30,7 @@
30from twisted.internet import defer30from twisted.internet import defer
31from twisted.web import xmlrpc31from twisted.web import xmlrpc
3232
33from lp.buildmaster.enums import BuilderCleanStatus
33from lp.buildmaster.interactor import BuilderSlave34from lp.buildmaster.interactor import BuilderSlave
34from lp.buildmaster.interfaces.builder import CannotFetchFile35from lp.buildmaster.interfaces.builder import CannotFetchFile
35from lp.services.config import config36from lp.services.config import config
@@ -48,7 +49,7 @@
4849
49 def __init__(self, name='mock-builder', builderok=True, manual=False,50 def __init__(self, name='mock-builder', builderok=True, manual=False,
50 virtualized=True, vm_host=None, url='http://fake:0000',51 virtualized=True, vm_host=None, url='http://fake:0000',
51 version=None):52 version=None, clean_status=BuilderCleanStatus.DIRTY):
52 self.currentjob = None53 self.currentjob = None
53 self.builderok = builderok54 self.builderok = builderok
54 self.manual = manual55 self.manual = manual
@@ -58,6 +59,10 @@
58 self.vm_host = vm_host59 self.vm_host = vm_host
59 self.failnotes = None60 self.failnotes = None
60 self.version = version61 self.version = version
62 self.clean_status = clean_status
63
64 def setCleanStatus(self, clean_status):
65 self.clean_status = clean_status
6166
62 def failBuilder(self, reason):67 def failBuilder(self, reason):
63 self.builderok = False68 self.builderok = False
6469
=== modified file 'lib/lp/buildmaster/tests/test_interactor.py'
--- lib/lp/buildmaster/tests/test_interactor.py 2014-06-06 11:15:29 +0000
+++ lib/lp/buildmaster/tests/test_interactor.py 2014-06-18 08:49:42 +0000
@@ -16,19 +16,24 @@
16 SynchronousDeferredRunTest,16 SynchronousDeferredRunTest,
17 )17 )
18from testtools.matchers import ContainsAll18from testtools.matchers import ContainsAll
19from testtools.testcase import ExpectedException
19from twisted.internet import defer20from twisted.internet import defer
20from twisted.internet.task import Clock21from twisted.internet.task import Clock
21from twisted.python.failure import Failure22from twisted.python.failure import Failure
22from twisted.web.client import getPage23from twisted.web.client import getPage
23from zope.security.proxy import removeSecurityProxy24from zope.security.proxy import removeSecurityProxy
2425
25from lp.buildmaster.enums import BuildStatus26from lp.buildmaster.enums import (
27 BuilderCleanStatus,
28 BuildStatus,
29 )
26from lp.buildmaster.interactor import (30from lp.buildmaster.interactor import (
27 BuilderInteractor,31 BuilderInteractor,
28 BuilderSlave,32 BuilderSlave,
29 extract_vitals_from_db,33 extract_vitals_from_db,
30 )34 )
31from lp.buildmaster.interfaces.builder import (35from lp.buildmaster.interfaces.builder import (
36 BuildDaemonError,
32 CannotFetchFile,37 CannotFetchFile,
33 CannotResumeHost,38 CannotResumeHost,
34 )39 )
@@ -150,94 +155,75 @@
150 slave = BuilderInteractor.makeSlaveFromVitals(vitals)155 slave = BuilderInteractor.makeSlaveFromVitals(vitals)
151 self.assertEqual(5, slave.timeout)156 self.assertEqual(5, slave.timeout)
152157
153 @defer.inlineCallbacks158
154 def test_rescueIfLost_aborts_lost_and_broken_slave(self):159class TestBuilderInteractorCleanSlave(TestCase):
155 # A slave that's 'lost' should be aborted; when the slave is160
156 # broken then abort() should also throw a fault.161 run_tests_with = AsynchronousDeferredRunTest
162
163 @defer.inlineCallbacks
164 def assertCleanCalls(self, builder, slave, calls, done):
165 actually_done = yield BuilderInteractor.cleanSlave(
166 extract_vitals_from_db(builder), slave)
167 self.assertEqual(done, actually_done)
168 self.assertEqual(calls, slave.method_log)
169
170 @defer.inlineCallbacks
171 def test_virtual(self):
172 # We don't care what the status of a virtual builder is; we just
173 # reset its VM and check that it's back.
174 yield self.assertCleanCalls(
175 MockBuilder(
176 virtualized=True, vm_host='lol',
177 clean_status=BuilderCleanStatus.DIRTY),
178 OkSlave(), ['resume', 'echo'], True)
179
180 @defer.inlineCallbacks
181 def test_nonvirtual_idle(self):
182 # An IDLE non-virtual slave is already as clean as we can get it.
183 yield self.assertCleanCalls(
184 MockBuilder(
185 virtualized=False, clean_status=BuilderCleanStatus.DIRTY),
186 OkSlave(), ['status'], True)
187
188 @defer.inlineCallbacks
189 def test_nonvirtual_building(self):
190 # A BUILDING non-virtual slave needs to be aborted. It'll go
191 # through ABORTING and eventually be picked up from WAITING.
192 yield self.assertCleanCalls(
193 MockBuilder(
194 virtualized=False, clean_status=BuilderCleanStatus.DIRTY),
195 BuildingSlave(), ['status', 'abort'], False)
196
197 @defer.inlineCallbacks
198 def test_nonvirtual_aborting(self):
199 # An ABORTING non-virtual slave must be waited out. It should
200 # hit WAITING eventually.
201 yield self.assertCleanCalls(
202 MockBuilder(
203 virtualized=False, clean_status=BuilderCleanStatus.DIRTY),
204 AbortingSlave(), ['status'], False)
205
206 @defer.inlineCallbacks
207 def test_nonvirtual_waiting(self):
208 # A WAITING non-virtual slave just needs clean() called.
209 yield self.assertCleanCalls(
210 MockBuilder(
211 virtualized=False, clean_status=BuilderCleanStatus.DIRTY),
212 WaitingSlave(), ['status', 'clean'], True)
213
214 @defer.inlineCallbacks
215 def test_nonvirtual_broken(self):
216 # A broken non-virtual builder is probably unrecoverable, so the
217 # method just crashes.
218 vitals = extract_vitals_from_db(MockBuilder(
219 virtualized=False, clean_status=BuilderCleanStatus.DIRTY))
157 slave = LostBuildingBrokenSlave()220 slave = LostBuildingBrokenSlave()
158 slave_status = yield slave.status()
159 try:221 try:
160 yield BuilderInteractor.rescueIfLost(222 yield BuilderInteractor.cleanSlave(vitals, slave)
161 extract_vitals_from_db(MockBuilder()), slave, slave_status,
162 'trivial')
163 except xmlrpclib.Fault:223 except xmlrpclib.Fault:
164 self.assertIn('abort', slave.call_log)224 self.assertEqual(['status', 'abort'], slave.call_log)
165 else:225 else:
166 self.fail("xmlrpclib.Fault not raised")226 self.fail("abort() should crash.")
167
168 @defer.inlineCallbacks
169 def test_recover_idle_slave(self):
170 # An idle slave is not rescued, even if it's not meant to be
171 # idle. SlaveScanner.scan() will clean up the DB side, because
172 # we still report that it's lost.
173 slave = OkSlave()
174 slave_status = yield slave.status()
175 lost = yield BuilderInteractor.rescueIfLost(
176 extract_vitals_from_db(MockBuilder()), slave, slave_status,
177 'trivial')
178 self.assertTrue(lost)
179 self.assertEqual(['status'], slave.call_log)
180
181 @defer.inlineCallbacks
182 def test_recover_ok_slave(self):
183 # An idle slave that's meant to be idle is not rescued.
184 slave = OkSlave()
185 slave_status = yield slave.status()
186 lost = yield BuilderInteractor.rescueIfLost(
187 extract_vitals_from_db(MockBuilder()), slave, slave_status, None)
188 self.assertFalse(lost)
189 self.assertEqual(['status'], slave.call_log)
190
191 @defer.inlineCallbacks
192 def test_recover_waiting_slave_with_good_id(self):
193 # rescueIfLost does not attempt to abort or clean a builder that is
194 # WAITING.
195 waiting_slave = WaitingSlave(build_id='trivial')
196 slave_status = yield waiting_slave.status()
197 lost = yield BuilderInteractor.rescueIfLost(
198 extract_vitals_from_db(MockBuilder()), waiting_slave, slave_status,
199 'trivial')
200 self.assertFalse(lost)
201 self.assertEqual(['status'], waiting_slave.call_log)
202
203 @defer.inlineCallbacks
204 def test_recover_waiting_slave_with_bad_id(self):
205 # If a slave is WAITING with a build for us to get, and the build
206 # cookie cannot be verified, which means we don't recognize the build,
207 # then rescueBuilderIfLost should attempt to abort it, so that the
208 # builder is reset for a new build, and the corrupt build is
209 # discarded.
210 waiting_slave = WaitingSlave(build_id='non-trivial')
211 slave_status = yield waiting_slave.status()
212 lost = yield BuilderInteractor.rescueIfLost(
213 extract_vitals_from_db(MockBuilder()), waiting_slave, slave_status,
214 'trivial')
215 self.assertTrue(lost)
216 self.assertEqual(['status', 'clean'], waiting_slave.call_log)
217
218 @defer.inlineCallbacks
219 def test_recover_building_slave_with_good_id(self):
220 # rescueIfLost does not attempt to abort or clean a builder that is
221 # BUILDING.
222 building_slave = BuildingSlave(build_id='trivial')
223 slave_status = yield building_slave.status()
224 lost = yield BuilderInteractor.rescueIfLost(
225 extract_vitals_from_db(MockBuilder()), building_slave,
226 slave_status, 'trivial')
227 self.assertFalse(lost)
228 self.assertEqual(['status'], building_slave.call_log)
229
230 @defer.inlineCallbacks
231 def test_recover_building_slave_with_bad_id(self):
232 # If a slave is BUILDING with a build id we don't recognize, then we
233 # abort the build, thus stopping it in its tracks.
234 building_slave = BuildingSlave(build_id='non-trivial')
235 slave_status = yield building_slave.status()
236 lost = yield BuilderInteractor.rescueIfLost(
237 extract_vitals_from_db(MockBuilder()), building_slave,
238 slave_status, 'trivial')
239 self.assertTrue(lost)
240 self.assertEqual(['status', 'abort'], building_slave.call_log)
241227
242228
243class TestBuilderSlaveStatus(TestCase):229class TestBuilderSlaveStatus(TestCase):
@@ -321,6 +307,7 @@
321 processor = self.factory.makeProcessor(name="i386")307 processor = self.factory.makeProcessor(name="i386")
322 builder = self.factory.makeBuilder(308 builder = self.factory.makeBuilder(
323 processors=[processor], virtualized=True, vm_host="bladh")309 processors=[processor], virtualized=True, vm_host="bladh")
310 builder.setCleanStatus(BuilderCleanStatus.CLEAN)
324 self.patch(BuilderSlave, 'makeBuilderSlave', FakeMethod(OkSlave()))311 self.patch(BuilderSlave, 'makeBuilderSlave', FakeMethod(OkSlave()))
325 distroseries = self.factory.makeDistroSeries()312 distroseries = self.factory.makeDistroSeries()
326 das = self.factory.makeDistroArchSeries(313 das = self.factory.makeDistroArchSeries(
@@ -377,21 +364,30 @@
377364
378 return d.addCallback(check_build_started)365 return d.addCallback(check_build_started)
379366
380 def test_virtual_job_dispatch_pings_before_building(self):367 @defer.inlineCallbacks
381 # We need to send a ping to the builder to work around a bug368 def test_findAndStartJob_requires_clean_slave(self):
382 # where sometimes the first network packet sent is dropped.369 # findAndStartJob ensures that its slave starts CLEAN.
383 builder, build = self._setupBinaryBuildAndBuilder()370 builder, build = self._setupBinaryBuildAndBuilder()
384 candidate = build.queueBuild()371 builder.setCleanStatus(BuilderCleanStatus.DIRTY)
385 removeSecurityProxy(builder)._findBuildCandidate = FakeMethod(372 candidate = build.queueBuild()
386 result=candidate)373 removeSecurityProxy(builder)._findBuildCandidate = FakeMethod(
387 vitals = extract_vitals_from_db(builder)374 result=candidate)
388 slave = OkSlave()375 vitals = extract_vitals_from_db(builder)
389 d = BuilderInteractor.findAndStartJob(vitals, builder, slave)376 with ExpectedException(
390377 BuildDaemonError,
391 def check_build_started(candidate):378 "Attempted to start build on a dirty slave."):
392 self.assertIn(('echo', 'ping'), slave.call_log)379 yield BuilderInteractor.findAndStartJob(vitals, builder, OkSlave())
393380
394 return d.addCallback(check_build_started)381 @defer.inlineCallbacks
382 def test_findAndStartJob_dirties_slave(self):
383 # findAndStartJob marks its builder DIRTY before dispatching.
384 builder, build = self._setupBinaryBuildAndBuilder()
385 candidate = build.queueBuild()
386 removeSecurityProxy(builder)._findBuildCandidate = FakeMethod(
387 result=candidate)
388 vitals = extract_vitals_from_db(builder)
389 yield BuilderInteractor.findAndStartJob(vitals, builder, OkSlave())
390 self.assertEqual(BuilderCleanStatus.DIRTY, builder.clean_status)
395391
396392
397class TestSlave(TestCase):393class TestSlave(TestCase):
398394
=== modified file 'lib/lp/buildmaster/tests/test_manager.py'
--- lib/lp/buildmaster/tests/test_manager.py 2014-06-06 10:32:55 +0000
+++ lib/lp/buildmaster/tests/test_manager.py 2014-06-18 08:49:42 +0000
@@ -13,6 +13,7 @@
13 AsynchronousDeferredRunTest,13 AsynchronousDeferredRunTest,
14 )14 )
15from testtools.matchers import Equals15from testtools.matchers import Equals
16from testtools.testcase import ExpectedException
16import transaction17import transaction
17from twisted.internet import (18from twisted.internet import (
18 defer,19 defer,
@@ -25,6 +26,7 @@
25from zope.security.proxy import removeSecurityProxy26from zope.security.proxy import removeSecurityProxy
2627
27from lp.buildmaster.enums import (28from lp.buildmaster.enums import (
29 BuilderCleanStatus,
28 BuildQueueStatus,30 BuildQueueStatus,
29 BuildStatus,31 BuildStatus,
30 )32 )
@@ -33,7 +35,10 @@
33 BuilderSlave,35 BuilderSlave,
34 extract_vitals_from_db,36 extract_vitals_from_db,
35 )37 )
36from lp.buildmaster.interfaces.builder import IBuilderSet38from lp.buildmaster.interfaces.builder import (
39 BuildDaemonError,
40 IBuilderSet,
41 )
37from lp.buildmaster.interfaces.buildfarmjobbehaviour import (42from lp.buildmaster.interfaces.buildfarmjobbehaviour import (
38 IBuildFarmJobBehaviour,43 IBuildFarmJobBehaviour,
39 )44 )
@@ -113,6 +118,7 @@
113 job = builder.currentjob118 job = builder.currentjob
114 if job is not None:119 if job is not None:
115 job.reset()120 job.reset()
121 builder.setCleanStatus(BuilderCleanStatus.CLEAN)
116122
117 transaction.commit()123 transaction.commit()
118124
@@ -155,6 +161,7 @@
155 # Set this to 1 here so that _checkDispatch can make sure it's161 # Set this to 1 here so that _checkDispatch can make sure it's
156 # reset to 0 after a successful dispatch.162 # reset to 0 after a successful dispatch.
157 builder.failure_count = 1163 builder.failure_count = 1
164 builder.setCleanStatus(BuilderCleanStatus.CLEAN)
158165
159 # Run 'scan' and check its result.166 # Run 'scan' and check its result.
160 switch_dbuser(config.builddmaster.dbuser)167 switch_dbuser(config.builddmaster.dbuser)
@@ -258,6 +265,7 @@
258 def test_scan_with_nothing_to_dispatch(self):265 def test_scan_with_nothing_to_dispatch(self):
259 factory = LaunchpadObjectFactory()266 factory = LaunchpadObjectFactory()
260 builder = factory.makeBuilder()267 builder = factory.makeBuilder()
268 builder.setCleanStatus(BuilderCleanStatus.CLEAN)
261 self.patch(BuilderSlave, 'makeBuilderSlave', FakeMethod(OkSlave()))269 self.patch(BuilderSlave, 'makeBuilderSlave', FakeMethod(OkSlave()))
262 transaction.commit()270 transaction.commit()
263 scanner = self._getScanner(builder_name=builder.name)271 scanner = self._getScanner(builder_name=builder.name)
@@ -422,9 +430,9 @@
422 self.assertFalse(builder.builderok)430 self.assertFalse(builder.builderok)
423431
424 @defer.inlineCallbacks432 @defer.inlineCallbacks
425 def test_fail_to_resume_slave_resets_job(self):433 def test_fail_to_resume_leaves_it_dirty(self):
426 # If an attempt to resume and dispatch a slave fails, it should434 # If an attempt to resume a slave fails, its failure count is
427 # reset the job via job.reset()435 # incremented and it is left DIRTY.
428436
429 # Make a slave with a failing resume() method.437 # Make a slave with a failing resume() method.
430 slave = OkSlave()438 slave = OkSlave()
@@ -435,26 +443,21 @@
435 builder = removeSecurityProxy(443 builder = removeSecurityProxy(
436 getUtility(IBuilderSet)[BOB_THE_BUILDER_NAME])444 getUtility(IBuilderSet)[BOB_THE_BUILDER_NAME])
437 self._resetBuilder(builder)445 self._resetBuilder(builder)
446 builder.setCleanStatus(BuilderCleanStatus.DIRTY)
447 builder.virtualized = True
438 self.assertEqual(0, builder.failure_count)448 self.assertEqual(0, builder.failure_count)
439 self.patch(BuilderSlave, 'makeBuilderSlave', FakeMethod(slave))449 self.patch(BuilderSlave, 'makeBuilderSlave', FakeMethod(slave))
440 builder.vm_host = "fake_vm_host"450 builder.vm_host = "fake_vm_host"
441
442 scanner = self._getScanner()
443
444 # Get the next job that will be dispatched.
445 job = removeSecurityProxy(builder._findBuildCandidate())
446 job.virtualized = True
447 builder.virtualized = True
448 transaction.commit()451 transaction.commit()
449 yield scanner.singleCycle()452
450453 # A spin of the scanner will see the DIRTY builder and reset it.
451 # The failure_count will have been incremented on the builder, we454 # Our patched reset will fail.
452 # can check that to see that a dispatch attempt did indeed occur.455 yield self._getScanner().singleCycle()
456
457 # The failure_count will have been incremented on the builder,
458 # and it will be left DIRTY.
453 self.assertEqual(1, builder.failure_count)459 self.assertEqual(1, builder.failure_count)
454 # There should also be no builder set on the job.460 self.assertEqual(BuilderCleanStatus.DIRTY, builder.clean_status)
455 self.assertIsNone(job.builder)
456 build = getUtility(IBinaryPackageBuildSet).getByQueueEntry(job)
457 self.assertEqual(build.status, BuildStatus.NEEDSBUILD)
458461
459 @defer.inlineCallbacks462 @defer.inlineCallbacks
460 def test_update_slave_version(self):463 def test_update_slave_version(self):
@@ -569,15 +572,25 @@
569 builder.name, BuilderFactory(), BufferLogger(),572 builder.name, BuilderFactory(), BufferLogger(),
570 slave_factory=get_slave, clock=clock)573 slave_factory=get_slave, clock=clock)
571574
572 # The slave is idle and there's a build candidate, so the first575 # The slave is idle and dirty, so the first scan will clean it
573 # scan will reset the builder and dispatch the build.576 # with a reset.
577 self.assertEqual(BuilderCleanStatus.DIRTY, builder.clean_status)
578 yield scanner.scan()
579 self.assertEqual(['resume', 'echo'], get_slave.result.method_log)
580 self.assertEqual(BuilderCleanStatus.CLEAN, builder.clean_status)
581 self.assertIs(None, builder.currentjob)
582
583 # The slave is idle and clean, and there's a build candidate, so
584 # the next scan will dispatch the build.
585 get_slave.result = OkSlave()
574 yield scanner.scan()586 yield scanner.scan()
575 self.assertEqual(587 self.assertEqual(
576 ['status', 'resume', 'echo', 'ensurepresent', 'build'],588 ['status', 'ensurepresent', 'build'],
577 get_slave.result.method_log)589 get_slave.result.method_log)
578 self.assertEqual(bq, builder.currentjob)590 self.assertEqual(bq, builder.currentjob)
579 self.assertEqual(BuildQueueStatus.RUNNING, bq.status)591 self.assertEqual(BuildQueueStatus.RUNNING, bq.status)
580 self.assertEqual(BuildStatus.BUILDING, build.status)592 self.assertEqual(BuildStatus.BUILDING, build.status)
593 self.assertEqual(BuilderCleanStatus.DIRTY, builder.clean_status)
581594
582 # build() has been called, so switch in a BUILDING slave.595 # build() has been called, so switch in a BUILDING slave.
583 # Scans will now just do a status() each, as the logtail is596 # Scans will now just do a status() each, as the logtail is
@@ -596,7 +609,8 @@
596 # When the build finishes, the scanner will notice, call609 # When the build finishes, the scanner will notice, call
597 # handleStatus(), and then clean the builder. Our fake610 # handleStatus(), and then clean the builder. Our fake
598 # _handleStatus_OK doesn't do anything special, but there'd611 # _handleStatus_OK doesn't do anything special, but there'd
599 # usually be file retrievals in the middle.612 # usually be file retrievals in the middle. The builder remains
613 # dirty afterward.
600 get_slave.result = WaitingSlave(614 get_slave.result = WaitingSlave(
601 build_id=IBuildFarmJobBehaviour(build).getBuildCookie())615 build_id=IBuildFarmJobBehaviour(build).getBuildCookie())
602 yield scanner.scan()616 yield scanner.scan()
@@ -604,13 +618,15 @@
604 self.assertIs(None, builder.currentjob)618 self.assertIs(None, builder.currentjob)
605 self.assertEqual(BuildStatus.UPLOADING, build.status)619 self.assertEqual(BuildStatus.UPLOADING, build.status)
606 self.assertEqual(builder, build.builder)620 self.assertEqual(builder, build.builder)
621 self.assertEqual(BuilderCleanStatus.DIRTY, builder.clean_status)
607622
608 # We're clean, so let's flip back to an idle slave and623 # We're idle and dirty, so let's flip back to an idle slave and
609 # confirm that a scan does nothing special.624 # confirm that the slave gets cleaned.
610 get_slave.result = OkSlave()625 get_slave.result = OkSlave()
611 yield scanner.scan()626 yield scanner.scan()
612 self.assertEqual(['status'], get_slave.result.method_log)627 self.assertEqual(['resume', 'echo'], get_slave.result.method_log)
613 self.assertIs(None, builder.currentjob)628 self.assertIs(None, builder.currentjob)
629 self.assertEqual(BuilderCleanStatus.CLEAN, builder.clean_status)
614630
615631
616class TestPrefetchedBuilderFactory(TestCaseWithFactory):632class TestPrefetchedBuilderFactory(TestCaseWithFactory):
@@ -740,74 +756,104 @@
740756
741 run_tests_with = AsynchronousDeferredRunTest757 run_tests_with = AsynchronousDeferredRunTest
742758
759 def getScanner(self, builder_factory=None, interactor=None, slave=None,
760 behaviour=None):
761 if builder_factory is None:
762 builder_factory = MockBuilderFactory(
763 MockBuilder(virtualized=False), None)
764 if interactor is None:
765 interactor = BuilderInteractor()
766 interactor.updateBuild = FakeMethod()
767 if slave is None:
768 slave = OkSlave()
769 if behaviour is None:
770 behaviour = TrivialBehaviour()
771 return SlaveScanner(
772 'mock', builder_factory, BufferLogger(),
773 interactor_factory=FakeMethod(interactor),
774 slave_factory=FakeMethod(slave),
775 behaviour_factory=FakeMethod(behaviour))
776
743 @defer.inlineCallbacks777 @defer.inlineCallbacks
744 def test_scan_with_job(self):778 def test_scan_with_job(self):
745 # SlaveScanner.scan calls updateBuild() when a job is building.779 # SlaveScanner.scan calls updateBuild() when a job is building.
746 slave = BuildingSlave('trivial')780 slave = BuildingSlave('trivial')
747 bq = FakeBuildQueue()781 bq = FakeBuildQueue()
748782 scanner = self.getScanner(
749 # Instrument updateBuild.783 builder_factory=MockBuilderFactory(MockBuilder(), bq),
750 interactor = BuilderInteractor()784 slave=slave)
751 interactor.updateBuild = FakeMethod()
752
753 scanner = SlaveScanner(
754 'mock', MockBuilderFactory(MockBuilder(), bq), BufferLogger(),
755 interactor_factory=FakeMethod(interactor),
756 slave_factory=FakeMethod(slave),
757 behaviour_factory=FakeMethod(TrivialBehaviour()))
758785
759 yield scanner.scan()786 yield scanner.scan()
760 self.assertEqual(['status'], slave.call_log)787 self.assertEqual(['status'], slave.call_log)
761 self.assertEqual(1, interactor.updateBuild.call_count)788 self.assertEqual(
789 1, scanner.interactor_factory.result.updateBuild.call_count)
762 self.assertEqual(0, bq.reset.call_count)790 self.assertEqual(0, bq.reset.call_count)
763791
764 @defer.inlineCallbacks792 @defer.inlineCallbacks
765 def test_scan_aborts_lost_slave_with_job(self):793 def test_scan_recovers_lost_slave_with_job(self):
766 # SlaveScanner.scan uses BuilderInteractor.rescueIfLost to abort794 # SlaveScanner.scan identifies slaves that aren't building what
767 # slaves that don't have the expected job.795 # they should be, resets the jobs, and then aborts the slaves.
768 slave = BuildingSlave('nontrivial')796 slave = BuildingSlave('nontrivial')
769 bq = FakeBuildQueue()797 bq = FakeBuildQueue()
770798 builder = MockBuilder(virtualized=False)
771 # Instrument updateBuild.799 scanner = self.getScanner(
772 interactor = BuilderInteractor()800 builder_factory=MockBuilderFactory(builder, bq),
773 interactor.updateBuild = FakeMethod()801 slave=slave)
774
775 scanner = SlaveScanner(
776 'mock', MockBuilderFactory(MockBuilder(), bq), BufferLogger(),
777 interactor_factory=FakeMethod(interactor),
778 slave_factory=FakeMethod(slave),
779 behaviour_factory=FakeMethod(TrivialBehaviour()))
780802
781 # A single scan will call status(), notice that the slave is lost,803 # A single scan will call status(), notice that the slave is lost,
782 # abort() the slave, then reset() the job without calling804 # and reset() the job without calling updateBuild().
783 # updateBuild().
784 yield scanner.scan()805 yield scanner.scan()
785 self.assertEqual(['status', 'abort'], slave.call_log)806 self.assertEqual(['status'], slave.call_log)
786 self.assertEqual(0, interactor.updateBuild.call_count)807 self.assertEqual(
808 0, scanner.interactor_factory.result.updateBuild.call_count)
787 self.assertEqual(1, bq.reset.call_count)809 self.assertEqual(1, bq.reset.call_count)
810 # The reset would normally have unset build_queue.
811 scanner.builder_factory.updateTestData(builder, None)
812
813 # The next scan will see a dirty idle builder with a BUILDING
814 # slave, and abort() it.
815 yield scanner.scan()
816 self.assertEqual(['status', 'status', 'abort'], slave.call_log)
788817
789 @defer.inlineCallbacks818 @defer.inlineCallbacks
790 def test_scan_aborts_lost_slave_when_idle(self):819 def test_scan_recovers_lost_slave_when_idle(self):
791 # SlaveScanner.scan uses BuilderInteractor.rescueIfLost to abort820 # SlaveScanner.scan identifies slaves that are building when
792 # slaves that aren't meant to have a job.821 # they shouldn't be and aborts them.
793 slave = BuildingSlave()822 slave = BuildingSlave()
794823 scanner = self.getScanner(slave=slave)
795 # Instrument updateBuild.
796 interactor = BuilderInteractor()
797 interactor.updateBuild = FakeMethod()
798
799 scanner = SlaveScanner(
800 'mock', MockBuilderFactory(MockBuilder(), None), BufferLogger(),
801 interactor_factory=FakeMethod(interactor),
802 slave_factory=FakeMethod(slave),
803 behaviour_factory=FakeMethod(None))
804
805 # A single scan will call status(), notice that the slave is lost,
806 # abort() the slave, then reset() the job without calling
807 # updateBuild().
808 yield scanner.scan()824 yield scanner.scan()
809 self.assertEqual(['status', 'abort'], slave.call_log)825 self.assertEqual(['status', 'abort'], slave.call_log)
810 self.assertEqual(0, interactor.updateBuild.call_count)826
827 @defer.inlineCallbacks
828 def test_scan_building_but_not_dirty_builder_explodes(self):
829 # Builders with a build assigned must be dirty for safety
830 # reasons. If we run into one that's clean, we blow up.
831 slave = BuildingSlave()
832 builder = MockBuilder(clean_status=BuilderCleanStatus.CLEAN)
833 bq = FakeBuildQueue()
834 scanner = self.getScanner(
835 slave=slave, builder_factory=MockBuilderFactory(builder, bq))
836
837 with ExpectedException(
838 BuildDaemonError, "Non-dirty builder allegedly building."):
839 yield scanner.scan()
840 self.assertEqual([], slave.call_log)
841
842 @defer.inlineCallbacks
843 def test_scan_clean_but_not_idle_slave_explodes(self):
844 # Clean builders by definition have slaves that are idle. If
845 # an ostensibly clean slave isn't idle, blow up.
846 slave = BuildingSlave()
847 builder = MockBuilder(clean_status=BuilderCleanStatus.CLEAN)
848 scanner = self.getScanner(
849 slave=slave, builder_factory=MockBuilderFactory(builder, None))
850
851 with ExpectedException(
852 BuildDaemonError,
853 "Allegedly clean slave not idle "
854 "\('BuilderStatus.BUILDING' instead\)"):
855 yield scanner.scan()
856 self.assertEqual(['status'], slave.call_log)
811857
812 def test_getExpectedCookie_caches(self):858 def test_getExpectedCookie_caches(self):
813 bf = MockBuilderFactory(MockBuilder(), FakeBuildQueue())859 bf = MockBuilderFactory(MockBuilder(), FakeBuildQueue())
814860
=== modified file 'lib/lp/soyuz/tests/test_binarypackagebuildbehaviour.py'
--- lib/lp/soyuz/tests/test_binarypackagebuildbehaviour.py 2014-06-06 10:32:55 +0000
+++ lib/lp/soyuz/tests/test_binarypackagebuildbehaviour.py 2014-06-18 08:49:42 +0000
@@ -19,6 +19,7 @@
19from zope.security.proxy import removeSecurityProxy19from zope.security.proxy import removeSecurityProxy
2020
21from lp.buildmaster.enums import (21from lp.buildmaster.enums import (
22 BuilderCleanStatus,
22 BuildQueueStatus,23 BuildQueueStatus,
23 BuildStatus,24 BuildStatus,
24 )25 )
@@ -131,10 +132,7 @@
131 build_log = [132 build_log = [
132 ('build', cookie, 'binarypackage', chroot.content.sha1,133 ('build', cookie, 'binarypackage', chroot.content.sha1,
133 filemap_names, extra_args)]134 filemap_names, extra_args)]
134 if builder.virtualized:135 result = upload_logs + build_log
135 result = [('echo', 'ping')] + upload_logs + build_log
136 else:
137 result = upload_logs + build_log
138 return result136 return result
139137
140 def test_non_virtual_ppa_dispatch(self):138 def test_non_virtual_ppa_dispatch(self):
@@ -146,6 +144,7 @@
146 archive = self.factory.makeArchive(virtualized=False)144 archive = self.factory.makeArchive(virtualized=False)
147 slave = OkSlave()145 slave = OkSlave()
148 builder = self.factory.makeBuilder(virtualized=False)146 builder = self.factory.makeBuilder(virtualized=False)
147 builder.setCleanStatus(BuilderCleanStatus.CLEAN)
149 vitals = extract_vitals_from_db(builder)148 vitals = extract_vitals_from_db(builder)
150 build = self.factory.makeBinaryPackageBuild(149 build = self.factory.makeBinaryPackageBuild(
151 builder=builder, archive=archive)150 builder=builder, archive=archive)
@@ -164,12 +163,11 @@
164 return d163 return d
165164
166 def test_virtual_ppa_dispatch(self):165 def test_virtual_ppa_dispatch(self):
167 # Make sure the builder slave gets reset before a build is
168 # dispatched to it.
169 archive = self.factory.makeArchive(virtualized=True)166 archive = self.factory.makeArchive(virtualized=True)
170 slave = OkSlave()167 slave = OkSlave()
171 builder = self.factory.makeBuilder(168 builder = self.factory.makeBuilder(
172 virtualized=True, vm_host="foohost")169 virtualized=True, vm_host="foohost")
170 builder.setCleanStatus(BuilderCleanStatus.CLEAN)
173 vitals = extract_vitals_from_db(builder)171 vitals = extract_vitals_from_db(builder)
174 build = self.factory.makeBinaryPackageBuild(172 build = self.factory.makeBinaryPackageBuild(
175 builder=builder, archive=archive)173 builder=builder, archive=archive)
@@ -182,22 +180,17 @@
182 d = interactor._startBuild(180 d = interactor._startBuild(
183 bq, vitals, builder, slave,181 bq, vitals, builder, slave,
184 interactor.getBuildBehaviour(bq, builder, slave), BufferLogger())182 interactor.getBuildBehaviour(bq, builder, slave), BufferLogger())
185183 d.addCallback(
186 def check_build(ignored):184 self.assertExpectedInteraction, slave.call_log, builder, build,
187 # We expect the first call to the slave to be a resume call,185 lf, archive, ArchivePurpose.PPA)
188 # followed by the rest of the usual calls we expect.186 return d
189 expected_resume_call = slave.call_log.pop(0)
190 self.assertEqual('resume', expected_resume_call)
191 self.assertExpectedInteraction(
192 ignored, slave.call_log, builder, build, lf, archive,
193 ArchivePurpose.PPA)
194 return d.addCallback(check_build)
195187
196 def test_partner_dispatch_no_publishing_history(self):188 def test_partner_dispatch_no_publishing_history(self):
197 archive = self.factory.makeArchive(189 archive = self.factory.makeArchive(
198 virtualized=False, purpose=ArchivePurpose.PARTNER)190 virtualized=False, purpose=ArchivePurpose.PARTNER)
199 slave = OkSlave()191 slave = OkSlave()
200 builder = self.factory.makeBuilder(virtualized=False)192 builder = self.factory.makeBuilder(virtualized=False)
193 builder.setCleanStatus(BuilderCleanStatus.CLEAN)
201 vitals = extract_vitals_from_db(builder)194 vitals = extract_vitals_from_db(builder)
202 build = self.factory.makeBinaryPackageBuild(195 build = self.factory.makeBinaryPackageBuild(
203 builder=builder, archive=archive)196 builder=builder, archive=archive)