launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #27953
[Merge] ~cjwatson/launchpad:refactor-updateBuild into launchpad:master
Colin Watson has proposed merging ~cjwatson/launchpad:refactor-updateBuild into launchpad:master with ~cjwatson/launchpad:rename-builder-interactor-slave as a prerequisite.
Commit message:
Push more of BuilderInteractor.updateBuild down to behaviours
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/+git/launchpad/+merge/414070
For upcoming work on CI jobs, we want to be able to do some build-type-specific work on non-terminal build states, namely incrementally fetching output files from builders. This requires `updateBuild` to call something build-type-specific in the `BUILDING` state. Since there's a small amount of code in common between `BUILDING`/`ABORTING` and `WAITING` already (the `updateStatus` call and the following commit), push this all down to `BuildFarmJobBehaviour.handleStatus`, which now handles everything except fetching the logtail.
No functional change is intended from this refactoring, although a number of tests need to change because `handleStatus` now requires a more complete version of launchpad-buildd's `status` response; previously some tests could get away with omitting some fields.
--
Your team Launchpad code reviewers is requested to review the proposed merge of ~cjwatson/launchpad:refactor-updateBuild into launchpad:master.
diff --git a/lib/lp/buildmaster/interactor.py b/lib/lp/buildmaster/interactor.py
index e8a1d7f..679d5c6 100644
--- a/lib/lp/buildmaster/interactor.py
+++ b/lib/lp/buildmaster/interactor.py
@@ -1,4 +1,4 @@
-# Copyright 2009-2021 Canonical Ltd. This software is licensed under the
+# Copyright 2009-2022 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
__all__ = [
@@ -526,19 +526,6 @@ class BuilderInteractor:
return candidate
@staticmethod
- def extractBuildStatus(worker_status):
- """Read build status name.
-
- :param worker_status: build status dict from BuilderWorker.status.
- :return: the unqualified status name, e.g. "OK".
- """
- status_string = worker_status['build_status']
- lead_string = 'BuildStatus.'
- assert status_string.startswith(lead_string), (
- "Malformed status string: '%s'" % status_string)
- return status_string[len(lead_string):]
-
- @staticmethod
def extractLogTail(worker_status):
"""Extract the log tail from a builder status response.
@@ -580,24 +567,20 @@ class BuilderInteractor:
# matches the DB, and this method isn't called unless the DB
# says there's a job.
builder_status = worker_status['builder_status']
+ if builder_status not in (
+ 'BuilderStatus.BUILDING', 'BuilderStatus.ABORTING',
+ 'BuilderStatus.WAITING'):
+ raise AssertionError("Unknown status %s" % builder_status)
+ builder = builder_factory[vitals.name]
+ behaviour = behaviour_factory(vitals.build_queue, builder, worker)
if builder_status in (
'BuilderStatus.BUILDING', 'BuilderStatus.ABORTING'):
logtail = cls.extractLogTail(worker_status)
if logtail is not None:
manager.addLogTail(vitals.build_queue.id, logtail)
- vitals.build_queue.specific_build.updateStatus(
- vitals.build_queue.specific_build.status,
- worker_status=worker_status)
- transaction.commit()
- elif builder_status == 'BuilderStatus.WAITING':
- # Build has finished. Delegate handling to the build itself.
- builder = builder_factory[vitals.name]
- behaviour = behaviour_factory(vitals.build_queue, builder, worker)
- yield behaviour.handleStatus(
- vitals.build_queue, cls.extractBuildStatus(worker_status),
- worker_status)
- else:
- raise AssertionError("Unknown status %s" % builder_status)
+ # Delegate the remaining handling to the build behaviour, which will
+ # commit the transaction.
+ yield behaviour.handleStatus(vitals.build_queue, worker_status)
@staticmethod
def _getWorkerScannerLogger():
diff --git a/lib/lp/buildmaster/interfaces/buildfarmjobbehaviour.py b/lib/lp/buildmaster/interfaces/buildfarmjobbehaviour.py
index 2d0eed4..c529327 100644
--- a/lib/lp/buildmaster/interfaces/buildfarmjobbehaviour.py
+++ b/lib/lp/buildmaster/interfaces/buildfarmjobbehaviour.py
@@ -1,4 +1,4 @@
-# Copyright 2009-2021 Canonical Ltd. This software is licensed under the
+# Copyright 2009-2022 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
"""Interface for build farm job behaviours."""
@@ -88,10 +88,9 @@ class IBuildFarmJobBehaviour(Interface):
def verifySuccessfulBuild():
"""Check that we are allowed to collect this successful build."""
- def handleStatus(bq, status, worker_status):
- """Update the build from a WAITING worker result.
+ def handleStatus(bq, worker_status):
+ """Update the build from a worker's status response.
:param bq: The `BuildQueue` currently being processed.
- :param status: The tail of the BuildStatus (eg. OK or PACKAGEFAIL).
:param worker_status: Worker status dict from `BuilderWorker.status`.
"""
diff --git a/lib/lp/buildmaster/model/buildfarmjobbehaviour.py b/lib/lp/buildmaster/model/buildfarmjobbehaviour.py
index 0315b52..a7537d3 100644
--- a/lib/lp/buildmaster/model/buildfarmjobbehaviour.py
+++ b/lib/lp/buildmaster/model/buildfarmjobbehaviour.py
@@ -1,4 +1,4 @@
-# Copyright 2009-2021 Canonical Ltd. This software is licensed under the
+# Copyright 2009-2022 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
"""Base and idle BuildFarmJobBehaviour classes."""
@@ -33,6 +33,7 @@ from lp.services.config import config
from lp.services.helpers import filenameToContentType
from lp.services.librarian.interfaces import ILibraryFileAliasSet
from lp.services.librarian.utils import copy_and_close
+from lp.services.propertycache import cachedproperty
from lp.services.statsd.interfaces.statsd_client import IStatsdClient
from lp.services.utils import sanitise_urls
from lp.services.webapp import canonical_url
@@ -53,7 +54,10 @@ class BuildFarmJobBehaviourBase:
"""Store a reference to the job_type with which we were created."""
self.build = build
self._builder = None
- self._authserver = xmlrpc.Proxy(
+
+ @cachedproperty
+ def _authserver(self):
+ return xmlrpc.Proxy(
config.builddmaster.authentication_endpoint.encode('UTF-8'),
connectTimeout=config.builddmaster.authentication_timeout)
@@ -255,6 +259,19 @@ class BuildFarmJobBehaviourBase:
"%s (%s) can not be built for pocket %s in %s: illegal status"
% (build.title, build.id, build.pocket.name, build.archive))
+ @staticmethod
+ def extractBuildStatus(worker_status):
+ """Read build status name.
+
+ :param worker_status: build status dict from BuilderWorker.status.
+ :return: the unqualified status name, e.g. "OK".
+ """
+ status_string = worker_status['build_status']
+ lead_string = 'BuildStatus.'
+ assert status_string.startswith(lead_string), (
+ "Malformed status string: '%s'" % status_string)
+ return status_string[len(lead_string):]
+
# The list of build status values for which email notifications are
# allowed to be sent. It is up to each callback as to whether it will
# consider sending a notification but it won't do so if the status is not
@@ -262,57 +279,65 @@ class BuildFarmJobBehaviourBase:
ALLOWED_STATUS_NOTIFICATIONS = ['PACKAGEFAIL', 'CHROOTFAIL']
@defer.inlineCallbacks
- def handleStatus(self, bq, status, worker_status):
+ def handleStatus(self, bq, worker_status):
"""See `IBuildFarmJobBehaviour`."""
if bq != self.build.buildqueue_record:
raise AssertionError(
"%r != %r" % (bq, self.build.buildqueue_record))
from lp.buildmaster.manager import BUILDD_MANAGER_LOG_NAME
logger = logging.getLogger(BUILDD_MANAGER_LOG_NAME)
- notify = status in self.ALLOWED_STATUS_NOTIFICATIONS
- fail_status_map = {
- 'PACKAGEFAIL': BuildStatus.FAILEDTOBUILD,
- 'DEPFAIL': BuildStatus.MANUALDEPWAIT,
- 'CHROOTFAIL': BuildStatus.CHROOTWAIT,
- }
- if self.build.status == BuildStatus.CANCELLING:
- fail_status_map['ABORTED'] = BuildStatus.CANCELLED
-
- logger.info(
- 'Processing finished job %s (%s) from builder %s: %s'
- % (self.build.build_cookie, self.build.title,
- self.build.buildqueue_record.builder.name, status))
- build_status = None
- if status == 'OK':
- yield self.storeLogFromWorker()
- # handleSuccess will sometimes perform write operations
- # outside the database transaction, so a failure between
- # here and the commit can cause duplicated results. For
- # example, a BinaryPackageBuild will end up in the upload
- # queue twice if notify() crashes.
- build_status = yield self.handleSuccess(worker_status, logger)
- elif status in fail_status_map:
- # XXX wgrant: The builder should be set long before here, but
- # currently isn't.
- yield self.storeLogFromWorker()
- build_status = fail_status_map[status]
+ builder_status = worker_status["builder_status"]
+
+ if builder_status == "BuilderStatus.WAITING":
+ # Build has finished.
+ status = self.extractBuildStatus(worker_status)
+ notify = status in self.ALLOWED_STATUS_NOTIFICATIONS
+ fail_status_map = {
+ 'PACKAGEFAIL': BuildStatus.FAILEDTOBUILD,
+ 'DEPFAIL': BuildStatus.MANUALDEPWAIT,
+ 'CHROOTFAIL': BuildStatus.CHROOTWAIT,
+ }
+ if self.build.status == BuildStatus.CANCELLING:
+ fail_status_map['ABORTED'] = BuildStatus.CANCELLED
+
+ logger.info(
+ 'Processing finished job %s (%s) from builder %s: %s'
+ % (self.build.build_cookie, self.build.title,
+ self.build.buildqueue_record.builder.name, status))
+ build_status = None
+ if status == 'OK':
+ yield self.storeLogFromWorker()
+ # handleSuccess will sometimes perform write operations
+ # outside the database transaction, so a failure between
+ # here and the commit can cause duplicated results. For
+ # example, a BinaryPackageBuild will end up in the upload
+ # queue twice if notify() crashes.
+ build_status = yield self.handleSuccess(worker_status, logger)
+ elif status in fail_status_map:
+ yield self.storeLogFromWorker()
+ build_status = fail_status_map[status]
+ else:
+ raise BuildDaemonError(
+ "Build returned unexpected status: %r" % status)
else:
- raise BuildDaemonError(
- "Build returned unexpected status: %r" % status)
+ # The build status remains unchanged.
+ build_status = bq.specific_build.status
- # Set the status and dequeue the build atomically. Setting the
- # status to UPLOADING constitutes handoff to process-upload, so
- # doing that before we've removed the BuildQueue causes races.
+ # Set the status and (if the build has finished) dequeue the build
+ # atomically. Setting the status to UPLOADING constitutes handoff to
+ # process-upload, so doing that before we've removed the BuildQueue
+ # causes races.
# XXX wgrant: The builder should be set long before here, but
# currently isn't.
self.build.updateStatus(
- build_status,
- builder=self.build.buildqueue_record.builder,
- worker_status=worker_status)
- if notify:
- self.build.notify()
- self.build.buildqueue_record.destroySelf()
+ build_status, builder=bq.builder, worker_status=worker_status)
+
+ if builder_status == "BuilderStatus.WAITING":
+ if notify:
+ self.build.notify()
+ self.build.buildqueue_record.destroySelf()
+
transaction.commit()
@defer.inlineCallbacks
diff --git a/lib/lp/buildmaster/tests/test_buildfarmjobbehaviour.py b/lib/lp/buildmaster/tests/test_buildfarmjobbehaviour.py
index ad05908..f36750e 100644
--- a/lib/lp/buildmaster/tests/test_buildfarmjobbehaviour.py
+++ b/lib/lp/buildmaster/tests/test_buildfarmjobbehaviour.py
@@ -1,4 +1,4 @@
-# Copyright 2010-2020 Canonical Ltd. This software is licensed under the
+# Copyright 2010-2022 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
"""Unit tests for BuildFarmJobBehaviourBase."""
@@ -30,6 +30,7 @@ from lp.buildmaster.interfaces.builder import BuildDaemonError
from lp.buildmaster.interfaces.buildfarmjobbehaviour import (
IBuildFarmJobBehaviour,
)
+from lp.buildmaster.interfaces.buildqueue import IBuildQueueSet
from lp.buildmaster.interfaces.processor import IProcessorSet
from lp.buildmaster.model.buildfarmjobbehaviour import (
BuildFarmJobBehaviourBase,
@@ -154,6 +155,22 @@ class TestBuildFarmJobBehaviourBase(TestCaseWithFactory):
behaviour.setBuilder(self.factory.makeBuilder(virtualized=False), None)
self.assertIs(False, behaviour.extraBuildArgs()["fast_cleanup"])
+ def test_extractBuildStatus_baseline(self):
+ # extractBuildStatus picks the name of the build status out of a
+ # dict describing the worker's status.
+ worker_status = {"build_status": "BuildStatus.BUILDING"}
+ self.assertEqual(
+ "BUILDING",
+ BuildFarmJobBehaviourBase.extractBuildStatus(worker_status))
+
+ def test_extractBuildStatus_malformed(self):
+ # extractBuildStatus errors out when the status string is not
+ # of the form it expects.
+ worker_status = {"build_status": "BUILDING"}
+ self.assertRaises(
+ AssertionError, BuildFarmJobBehaviourBase.extractBuildStatus,
+ worker_status)
+
class TestDispatchBuildToWorker(StatsMixin, TestCase):
@@ -383,19 +400,44 @@ class TestHandleStatusMixin:
1, len(os.listdir(os.path.join(self.upload_root, result))))
@defer.inlineCallbacks
- def test_handleStatus_OK_normal_file(self):
+ def test_handleStatus_BUILDING(self):
+ # If the builder is BUILDING (or any status other than WAITING),
+ # then the behaviour calls updateStatus but doesn't do anything
+ # else.
+ initial_status = self.build.status
+ bq_id = self.build.buildqueue_record.id
+ worker_status = {"builder_status": "BuilderStatus.BUILDING"}
+ removeSecurityProxy(self.build).updateStatus = FakeMethod()
+ with dbuser(config.builddmaster.dbuser):
+ yield self.behaviour.handleStatus(
+ self.build.buildqueue_record, worker_status)
+ self.assertEqual(None, self.build.log)
+ self.assertEqual(0, len(os.listdir(self.upload_root)))
+ self.assertEqual(
+ [((initial_status,),
+ {"builder": self.builder, "worker_status": worker_status})],
+ removeSecurityProxy(self.build).updateStatus.calls)
+ self.assertEqual(0, len(pop_notifications()), "Notifications received")
+ self.assertEqual(
+ self.build.buildqueue_record,
+ getUtility(IBuildQueueSet).get(bq_id))
+
+ @defer.inlineCallbacks
+ def test_handleStatus_WAITING_OK_normal_file(self):
# A filemap with plain filenames should not cause a problem.
# The call to handleStatus will attempt to get the file from
# the worker resulting in a URL error in this test case.
with dbuser(config.builddmaster.dbuser):
yield self.behaviour.handleStatus(
- self.build.buildqueue_record, 'OK',
- {'filemap': {'myfile.py': 'test_file_hash'}})
+ self.build.buildqueue_record,
+ {'builder_status': 'BuilderStatus.WAITING',
+ 'build_status': 'BuildStatus.OK',
+ 'filemap': {'myfile.py': 'test_file_hash'}})
self.assertEqual(BuildStatus.UPLOADING, self.build.status)
self.assertResultCount(1, "incoming")
@defer.inlineCallbacks
- def test_handleStatus_OK_absolute_filepath(self):
+ def test_handleStatus_WAITING_OK_absolute_filepath(self):
# A filemap that tries to write to files outside of the upload
# directory will not be collected.
with ExpectedException(
@@ -403,11 +445,13 @@ class TestHandleStatusMixin:
"Build returned a file named '/tmp/myfile.py'."):
with dbuser(config.builddmaster.dbuser):
yield self.behaviour.handleStatus(
- self.build.buildqueue_record, 'OK',
- {'filemap': {'/tmp/myfile.py': 'test_file_hash'}})
+ self.build.buildqueue_record,
+ {'builder_status': 'BuilderStatus.WAITING',
+ 'build_status': 'BuildStatus.OK',
+ 'filemap': {'/tmp/myfile.py': 'test_file_hash'}})
@defer.inlineCallbacks
- def test_handleStatus_OK_relative_filepath(self):
+ def test_handleStatus_WAITING_OK_relative_filepath(self):
# A filemap that tries to write to files outside of
# the upload directory will not be collected.
with ExpectedException(
@@ -415,21 +459,25 @@ class TestHandleStatusMixin:
"Build returned a file named '../myfile.py'."):
with dbuser(config.builddmaster.dbuser):
yield self.behaviour.handleStatus(
- self.build.buildqueue_record, 'OK',
- {'filemap': {'../myfile.py': 'test_file_hash'}})
+ self.build.buildqueue_record,
+ {'builder_status': 'BuilderStatus.WAITING',
+ 'build_status': 'BuildStatus.OK',
+ 'filemap': {'../myfile.py': 'test_file_hash'}})
@defer.inlineCallbacks
- def test_handleStatus_OK_sets_build_log(self):
+ def test_handleStatus_WAITING_OK_sets_build_log(self):
# The build log is set during handleStatus.
self.assertEqual(None, self.build.log)
with dbuser(config.builddmaster.dbuser):
yield self.behaviour.handleStatus(
- self.build.buildqueue_record, 'OK',
- {'filemap': {'myfile.py': 'test_file_hash'}})
+ self.build.buildqueue_record,
+ {'builder_status': 'BuilderStatus.WAITING',
+ 'build_status': 'BuildStatus.OK',
+ 'filemap': {'myfile.py': 'test_file_hash'}})
self.assertNotEqual(None, self.build.log)
@defer.inlineCallbacks
- def _test_handleStatus_notifies(self, status):
+ def _test_handleStatus_WAITING_notifies(self, status):
# An email notification is sent for a given build status if
# notifications are allowed for that status.
expected_notification = (
@@ -437,7 +485,9 @@ class TestHandleStatusMixin:
with dbuser(config.builddmaster.dbuser):
yield self.behaviour.handleStatus(
- self.build.buildqueue_record, status, {})
+ self.build.buildqueue_record,
+ {'builder_status': 'BuilderStatus.WAITING',
+ 'build_status': 'BuildStatus.%s' % status})
if expected_notification:
self.assertNotEqual(
@@ -446,26 +496,28 @@ class TestHandleStatusMixin:
self.assertEqual(
0, len(pop_notifications()), "Notifications received")
- def test_handleStatus_DEPFAIL_notifies(self):
- return self._test_handleStatus_notifies("DEPFAIL")
+ def test_handleStatus_WAITING_DEPFAIL_notifies(self):
+ return self._test_handleStatus_WAITING_notifies("DEPFAIL")
- def test_handleStatus_CHROOTFAIL_notifies(self):
- return self._test_handleStatus_notifies("CHROOTFAIL")
+ def test_handleStatus_WAITING_CHROOTFAIL_notifies(self):
+ return self._test_handleStatus_WAITING_notifies("CHROOTFAIL")
- def test_handleStatus_PACKAGEFAIL_notifies(self):
- return self._test_handleStatus_notifies("PACKAGEFAIL")
+ def test_handleStatus_WAITING_PACKAGEFAIL_notifies(self):
+ return self._test_handleStatus_WAITING_notifies("PACKAGEFAIL")
@defer.inlineCallbacks
- def test_handleStatus_ABORTED_cancels_cancelling(self):
+ def test_handleStatus_WAITING_ABORTED_cancels_cancelling(self):
with dbuser(config.builddmaster.dbuser):
self.build.updateStatus(BuildStatus.CANCELLING)
yield self.behaviour.handleStatus(
- self.build.buildqueue_record, "ABORTED", {})
+ self.build.buildqueue_record,
+ {"builder_status": "BuilderStatus.WAITING",
+ "build_status": "BuildStatus.ABORTED"})
self.assertEqual(0, len(pop_notifications()), "Notifications received")
self.assertEqual(BuildStatus.CANCELLED, self.build.status)
@defer.inlineCallbacks
- def test_handleStatus_ABORTED_illegal_when_building(self):
+ def test_handleStatus_WAITING_ABORTED_illegal_when_building(self):
self.builder.vm_host = "fake_vm_host"
self.behaviour = self.interactor.getBuildBehaviour(
self.build.buildqueue_record, self.builder, self.worker)
@@ -475,16 +527,20 @@ class TestHandleStatusMixin:
BuildDaemonError,
"Build returned unexpected status: %r" % 'ABORTED'):
yield self.behaviour.handleStatus(
- self.build.buildqueue_record, "ABORTED", {})
+ self.build.buildqueue_record,
+ {"builder_status": "BuilderStatus.WAITING",
+ "build_status": "BuildStatus.ABORTED"})
@defer.inlineCallbacks
- def test_handleStatus_ABORTED_cancelling_sets_build_log(self):
+ def test_handleStatus_WAITING_ABORTED_cancelling_sets_build_log(self):
# If a build is intentionally cancelled, the build log is set.
self.assertEqual(None, self.build.log)
with dbuser(config.builddmaster.dbuser):
self.build.updateStatus(BuildStatus.CANCELLING)
yield self.behaviour.handleStatus(
- self.build.buildqueue_record, "ABORTED", {})
+ self.build.buildqueue_record,
+ {"builder_status": "BuilderStatus.WAITING",
+ "build_status": "BuildStatus.ABORTED"})
self.assertNotEqual(None, self.build.log)
@defer.inlineCallbacks
@@ -493,8 +549,10 @@ class TestHandleStatusMixin:
self.assertEqual(None, self.build.date_finished)
with dbuser(config.builddmaster.dbuser):
yield self.behaviour.handleStatus(
- self.build.buildqueue_record, 'OK',
- {'filemap': {'myfile.py': 'test_file_hash'}})
+ self.build.buildqueue_record,
+ {'builder_status': 'BuilderStatus.WAITING',
+ 'build_status': 'BuildStatus.OK',
+ 'filemap': {'myfile.py': 'test_file_hash'}})
self.assertNotEqual(None, self.build.date_finished)
@defer.inlineCallbacks
@@ -504,7 +562,9 @@ class TestHandleStatusMixin:
"Build returned unexpected status: %r" % 'GIVENBACK'):
with dbuser(config.builddmaster.dbuser):
yield self.behaviour.handleStatus(
- self.build.buildqueue_record, "GIVENBACK", {})
+ self.build.buildqueue_record,
+ {"builder_status": "BuilderStatus.WAITING",
+ "build_status": "BuildStatus.GIVENBACK"})
@defer.inlineCallbacks
def test_builderfail_collection(self):
@@ -513,7 +573,9 @@ class TestHandleStatusMixin:
"Build returned unexpected status: %r" % 'BUILDERFAIL'):
with dbuser(config.builddmaster.dbuser):
yield self.behaviour.handleStatus(
- self.build.buildqueue_record, "BUILDERFAIL", {})
+ self.build.buildqueue_record,
+ {"builder_status": "BuilderStatus.WAITING",
+ "build_status": "BuildStatus.BUILDERFAIL"})
@defer.inlineCallbacks
def test_invalid_status_collection(self):
@@ -522,4 +584,6 @@ class TestHandleStatusMixin:
"Build returned unexpected status: %r" % 'BORKED'):
with dbuser(config.builddmaster.dbuser):
yield self.behaviour.handleStatus(
- self.build.buildqueue_record, "BORKED", {})
+ self.build.buildqueue_record,
+ {"builder_status": "BuilderStatus.WAITING",
+ "build_status": "BuildStatus.BORKED"})
diff --git a/lib/lp/buildmaster/tests/test_interactor.py b/lib/lp/buildmaster/tests/test_interactor.py
index 6e4a790..9aa025c 100644
--- a/lib/lp/buildmaster/tests/test_interactor.py
+++ b/lib/lp/buildmaster/tests/test_interactor.py
@@ -1,4 +1,4 @@
-# Copyright 2009-2020 Canonical Ltd. This software is licensed under the
+# Copyright 2009-2022 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
"""Test BuilderInteractor features."""
@@ -121,21 +121,6 @@ class TestBuilderInteractor(TestCase):
super().setUp()
self.addCleanup(shut_down_default_process_pool)
- def test_extractBuildStatus_baseline(self):
- # extractBuildStatus picks the name of the build status out of a
- # dict describing the worker's status.
- worker_status = {'build_status': 'BuildStatus.BUILDING'}
- self.assertEqual(
- 'BUILDING', BuilderInteractor.extractBuildStatus(worker_status))
-
- def test_extractBuildStatus_malformed(self):
- # extractBuildStatus errors out when the status string is not
- # of the form it expects.
- worker_status = {'build_status': 'BUILDING'}
- self.assertRaises(
- AssertionError, BuilderInteractor.extractBuildStatus,
- worker_status)
-
def resumeWorkerHost(self, builder):
vitals = extract_vitals_from_db(builder)
return BuilderInteractor.resumeWorkerHost(
diff --git a/lib/lp/code/model/tests/test_recipebuilder.py b/lib/lp/code/model/tests/test_recipebuilder.py
index 9077a08..9c75d68 100644
--- a/lib/lp/code/model/tests/test_recipebuilder.py
+++ b/lib/lp/code/model/tests/test_recipebuilder.py
@@ -1,4 +1,4 @@
-# Copyright 2010-2020 Canonical Ltd. This software is licensed under the
+# Copyright 2010-2022 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
"""Test RecipeBuildBehaviour."""
@@ -418,7 +418,11 @@ class TestBuildNotifications(TestCaseWithFactory):
self.queue_record, self.queue_record.builder, worker)
def assertDeferredNotifyCount(self, status, behaviour, expected_count):
- d = behaviour.handleStatus(self.queue_record, status, {'filemap': {}})
+ d = behaviour.handleStatus(
+ self.queue_record,
+ {'builder_status': 'BuilderStatus.WAITING',
+ 'build_status': 'BuildStatus.%s' % status,
+ 'filemap': {}})
def cb(result):
self.assertEqual(expected_count, len(pop_notifications()))
diff --git a/lib/lp/oci/tests/test_ocirecipebuildbehaviour.py b/lib/lp/oci/tests/test_ocirecipebuildbehaviour.py
index 7804583..cb5438f 100644
--- a/lib/lp/oci/tests/test_ocirecipebuildbehaviour.py
+++ b/lib/lp/oci/tests/test_ocirecipebuildbehaviour.py
@@ -1,4 +1,4 @@
-# Copyright 2015-2021 Canonical Ltd. This software is licensed under the
+# Copyright 2015-2022 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
"""Tests for `OCIRecipeBuildBehaviour`."""
@@ -55,6 +55,7 @@ from lp.buildmaster.interfaces.builder import (
from lp.buildmaster.interfaces.buildfarmjobbehaviour import (
IBuildFarmJobBehaviour,
)
+from lp.buildmaster.interfaces.buildqueue import IBuildQueueSet
from lp.buildmaster.interfaces.processor import IProcessorSet
from lp.buildmaster.tests.builderproxy import (
InProcessProxyAuthAPIFixture,
@@ -773,15 +774,40 @@ class TestHandleStatusForOCIRecipeBuild(MakeOCIBuildMixin,
1, len(os.listdir(os.path.join(self.upload_root, result))))
@defer.inlineCallbacks
- def test_handleStatus_OK_normal_image(self):
+ def test_handleStatus_BUILDING(self):
+ # If the builder is BUILDING (or any status other than WAITING),
+ # then the behaviour calls updateStatus but doesn't do anything
+ # else.
+ initial_status = self.build.status
+ bq_id = self.build.buildqueue_record.id
+ worker_status = {"builder_status": "BuilderStatus.BUILDING"}
+ removeSecurityProxy(self.build).updateStatus = FakeMethod()
+ with dbuser(config.builddmaster.dbuser):
+ yield self.behaviour.handleStatus(
+ self.build.buildqueue_record, worker_status)
+ self.assertEqual(None, self.build.log)
+ self.assertEqual(0, len(os.listdir(self.upload_root)))
+ self.assertEqual(
+ [((initial_status,),
+ {"builder": self.builder, "worker_status": worker_status})],
+ removeSecurityProxy(self.build).updateStatus.calls)
+ self.assertEqual(0, len(pop_notifications()), "Notifications received")
+ self.assertEqual(
+ self.build.buildqueue_record,
+ getUtility(IBuildQueueSet).get(bq_id))
+
+ @defer.inlineCallbacks
+ def test_handleStatus_WAITING_OK_normal_image(self):
now = datetime.now()
mock_datetime = self.useFixture(MockPatch(
'lp.buildmaster.model.buildfarmjobbehaviour.datetime')).mock
mock_datetime.now = lambda: now
with dbuser(config.builddmaster.dbuser):
yield self.behaviour.handleStatus(
- self.build.buildqueue_record, 'OK',
- {'filemap': self.filemap})
+ self.build.buildqueue_record,
+ {'builder_status': 'BuilderStatus.WAITING',
+ 'build_status': 'BuildStatus.OK',
+ 'filemap': self.filemap})
self.assertEqual(
['buildlog', 'manifest_hash', 'digests_hash', 'config_1_hash',
'layer_2_hash'],
@@ -805,7 +831,7 @@ class TestHandleStatusForOCIRecipeBuild(MakeOCIBuildMixin,
self.assertEqual(contents, b'retrieved from librarian')
@defer.inlineCallbacks
- def test_handleStatus_OK_reuse_from_other_build(self):
+ def test_handleStatus_WAITING_OK_reuse_from_other_build(self):
"""We should be able to reuse a layer file from a separate build."""
oci_file = self.factory.makeOCIFile(
layer_file_digest='digest_2',
@@ -820,8 +846,10 @@ class TestHandleStatusForOCIRecipeBuild(MakeOCIBuildMixin,
mock_oci_datetime.now = lambda tzinfo=None: now
with dbuser(config.builddmaster.dbuser):
yield self.behaviour.handleStatus(
- self.build.buildqueue_record, 'OK',
- {'filemap': self.filemap})
+ self.build.buildqueue_record,
+ {'builder_status': 'BuilderStatus.WAITING',
+ 'build_status': 'BuildStatus.OK',
+ 'filemap': self.filemap})
self.assertEqual(
['buildlog', 'manifest_hash', 'digests_hash', 'config_1_hash'],
self.worker._got_file_record)
@@ -844,7 +872,7 @@ class TestHandleStatusForOCIRecipeBuild(MakeOCIBuildMixin,
self.assertEqual(now, oci_file.date_last_used)
@defer.inlineCallbacks
- def test_handleStatus_OK_absolute_filepath(self):
+ def test_handleStatus_WAITING_OK_absolute_filepath(self):
self._createTestFile(
'manifest.json',
@@ -862,11 +890,13 @@ class TestHandleStatusForOCIRecipeBuild(MakeOCIBuildMixin,
"'/notvalid/config_file_1.json'."):
with dbuser(config.builddmaster.dbuser):
yield self.behaviour.handleStatus(
- self.build.buildqueue_record, 'OK',
- {'filemap': self.filemap})
+ self.build.buildqueue_record,
+ {'builder_status': 'BuilderStatus.WAITING',
+ 'build_status': 'BuildStatus.OK',
+ 'filemap': self.filemap})
@defer.inlineCallbacks
- def test_handleStatus_OK_relative_filepath(self):
+ def test_handleStatus_WAITING_OK_relative_filepath(self):
self._createTestFile(
'manifest.json',
@@ -882,30 +912,36 @@ class TestHandleStatusForOCIRecipeBuild(MakeOCIBuildMixin,
"Build returned a file named '../config_file_1.json'."):
with dbuser(config.builddmaster.dbuser):
yield self.behaviour.handleStatus(
- self.build.buildqueue_record, 'OK',
- {'filemap': self.filemap})
+ self.build.buildqueue_record,
+ {'builder_status': 'BuilderStatus.WAITING',
+ 'build_status': 'BuildStatus.OK',
+ 'filemap': self.filemap})
@defer.inlineCallbacks
- def test_handleStatus_OK_sets_build_log(self):
+ def test_handleStatus_WAITING_OK_sets_build_log(self):
# The build log is set during handleStatus.
self.assertEqual(None, self.build.log)
with dbuser(config.builddmaster.dbuser):
yield self.behaviour.handleStatus(
- self.build.buildqueue_record, 'OK',
- {'filemap': self.filemap})
+ self.build.buildqueue_record,
+ {'builder_status': 'BuilderStatus.WAITING',
+ 'build_status': 'BuildStatus.OK',
+ 'filemap': self.filemap})
self.assertNotEqual(None, self.build.log)
@defer.inlineCallbacks
- def test_handleStatus_ABORTED_cancels_cancelling(self):
+ def test_handleStatus_WAITING_ABORTED_cancels_cancelling(self):
with dbuser(config.builddmaster.dbuser):
self.build.updateStatus(BuildStatus.CANCELLING)
yield self.behaviour.handleStatus(
- self.build.buildqueue_record, "ABORTED", {})
+ self.build.buildqueue_record,
+ {"builder_status": "BuilderStatus.WAITING",
+ "build_status": "BuildStatus.ABORTED"})
self.assertEqual(0, len(pop_notifications()), "Notifications received")
self.assertEqual(BuildStatus.CANCELLED, self.build.status)
@defer.inlineCallbacks
- def test_handleStatus_ABORTED_illegal_when_building(self):
+ def test_handleStatus_WAITING_ABORTED_illegal_when_building(self):
self.builder.vm_host = "fake_vm_host"
self.behaviour = self.interactor.getBuildBehaviour(
self.build.buildqueue_record, self.builder, self.worker)
@@ -915,16 +951,20 @@ class TestHandleStatusForOCIRecipeBuild(MakeOCIBuildMixin,
BuildDaemonError,
"Build returned unexpected status: %r" % 'ABORTED'):
yield self.behaviour.handleStatus(
- self.build.buildqueue_record, "ABORTED", {})
+ self.build.buildqueue_record,
+ {"builder_status": "BuilderStatus.WAITING",
+ "build_status": "BuildStatus.ABORTED"})
@defer.inlineCallbacks
- def test_handleStatus_ABORTED_cancelling_sets_build_log(self):
+ def test_handleStatus_WAITING_ABORTED_cancelling_sets_build_log(self):
# If a build is intentionally cancelled, the build log is set.
self.assertEqual(None, self.build.log)
with dbuser(config.builddmaster.dbuser):
self.build.updateStatus(BuildStatus.CANCELLING)
yield self.behaviour.handleStatus(
- self.build.buildqueue_record, "ABORTED", {})
+ self.build.buildqueue_record,
+ {"builder_status": "BuilderStatus.WAITING",
+ "build_status": "BuildStatus.ABORTED"})
self.assertNotEqual(None, self.build.log)
@defer.inlineCallbacks
@@ -933,8 +973,10 @@ class TestHandleStatusForOCIRecipeBuild(MakeOCIBuildMixin,
self.assertEqual(None, self.build.date_finished)
with dbuser(config.builddmaster.dbuser):
yield self.behaviour.handleStatus(
- self.build.buildqueue_record, 'OK',
- {'filemap': self.filemap})
+ self.build.buildqueue_record,
+ {'builder_status': 'BuilderStatus.WAITING',
+ 'build_status': 'BuildStatus.OK',
+ 'filemap': self.filemap})
self.assertNotEqual(None, self.build.date_finished)
@defer.inlineCallbacks
@@ -944,7 +986,9 @@ class TestHandleStatusForOCIRecipeBuild(MakeOCIBuildMixin,
"Build returned unexpected status: %r" % 'GIVENBACK'):
with dbuser(config.builddmaster.dbuser):
yield self.behaviour.handleStatus(
- self.build.buildqueue_record, "GIVENBACK", {})
+ self.build.buildqueue_record,
+ {"builder_status": "BuilderStatus.WAITING",
+ "build_status": "BuildStatus.GIVENBACK"})
@defer.inlineCallbacks
def test_builderfail_collection(self):
@@ -953,7 +997,9 @@ class TestHandleStatusForOCIRecipeBuild(MakeOCIBuildMixin,
"Build returned unexpected status: %r" % 'BUILDERFAIL'):
with dbuser(config.builddmaster.dbuser):
yield self.behaviour.handleStatus(
- self.build.buildqueue_record, "BUILDERFAIL", {})
+ self.build.buildqueue_record,
+ {"builder_status": "BuilderStatus.WAITING",
+ "build_status": "BuildStatus.BUILDERFAIL"})
@defer.inlineCallbacks
def test_invalid_status_collection(self):
@@ -962,7 +1008,9 @@ class TestHandleStatusForOCIRecipeBuild(MakeOCIBuildMixin,
"Build returned unexpected status: %r" % 'BORKED'):
with dbuser(config.builddmaster.dbuser):
yield self.behaviour.handleStatus(
- self.build.buildqueue_record, "BORKED", {})
+ self.build.buildqueue_record,
+ {"builder_status": "BuilderStatus.WAITING",
+ "build_status": "BuildStatus.BORKED"})
class TestGetUploadMethodsForOCIRecipeBuild(
diff --git a/lib/lp/translations/tests/test_translationtemplatesbuildbehaviour.py b/lib/lp/translations/tests/test_translationtemplatesbuildbehaviour.py
index 0e446ec..93f9325 100644
--- a/lib/lp/translations/tests/test_translationtemplatesbuildbehaviour.py
+++ b/lib/lp/translations/tests/test_translationtemplatesbuildbehaviour.py
@@ -1,4 +1,4 @@
-# Copyright 2010-2020 Canonical Ltd. This software is licensed under the
+# Copyright 2010-2022 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
"""Unit tests for TranslationTemplatesBuildBehaviour."""
@@ -14,7 +14,6 @@ from zope.component import getUtility
from lp.app.interfaces.launchpad import ILaunchpadCelebrities
from lp.buildmaster.enums import BuildStatus
-from lp.buildmaster.interactor import BuilderInteractor
from lp.buildmaster.interfaces.buildfarmjobbehaviour import (
IBuildFarmJobBehaviour,
)
@@ -164,11 +163,7 @@ class TestTranslationTemplatesBuildBehaviour(
d = worker.status()
def got_status(status):
- return (
- behaviour.handleStatus(
- queue_item, BuilderInteractor.extractBuildStatus(status),
- status),
- worker.call_log)
+ return behaviour.handleStatus(queue_item, status), worker.call_log
def build_updated(ignored):
self.assertEqual(BuildStatus.FULLYBUILT, behaviour.build.status)
@@ -193,10 +188,7 @@ class TestTranslationTemplatesBuildBehaviour(
def got_status(status):
del status['filemap']
- return behaviour.handleStatus(
- queue_item,
- BuilderInteractor.extractBuildStatus(status),
- status),
+ return behaviour.handleStatus(queue_item, status),
def build_updated(ignored):
self.assertEqual(BuildStatus.FAILEDTOBUILD, behaviour.build.status)
@@ -222,10 +214,7 @@ class TestTranslationTemplatesBuildBehaviour(
def got_status(status):
del status['filemap']
- return behaviour.handleStatus(
- queue_item,
- BuilderInteractor.extractBuildStatus(status),
- status),
+ return behaviour.handleStatus(queue_item, status),
def build_updated(ignored):
self.assertEqual(BuildStatus.FULLYBUILT, behaviour.build.status)
@@ -257,10 +246,7 @@ class TestTranslationTemplatesBuildBehaviour(
d = worker.status()
def got_status(status):
- return behaviour.handleStatus(
- queue_item,
- BuilderInteractor.extractBuildStatus(status),
- status),
+ return behaviour.handleStatus(queue_item, status),
def build_updated(ignored):
self.assertEqual(BuildStatus.FULLYBUILT, behaviour.build.status)