Merge ~cjwatson/launchpad:charm-recipe-build-behaviour into launchpad:master

Proposed by Colin Watson
Status: Merged
Approved by: Colin Watson
Approved revision: b7d4b47cd8bbafb401d41d534bd219a0bccf60be
Merge reported by: Otto Co-Pilot
Merged at revision: not available
Proposed branch: ~cjwatson/launchpad:charm-recipe-build-behaviour
Merge into: launchpad:master
Prerequisite: ~cjwatson/launchpad:charm-recipe-build-mailer
Diff against target: 564 lines (+541/-0)
3 files modified
lib/lp/charms/configure.zcml (+7/-0)
lib/lp/charms/model/charmrecipebuildbehaviour.py (+111/-0)
lib/lp/charms/tests/test_charmrecipebuildbehaviour.py (+423/-0)
Reviewer Review Type Date Requested Status
Ioana Lasc (community) Approve
Cristian Gonzalez (community) Approve
Review via email: mp+403732@code.launchpad.net

Commit message

Add a build behaviour for charm recipes

To post a comment you must log in.
Revision history for this message
Cristian Gonzalez (cristiangsp) wrote :

Looks good!

review: Approve
Revision history for this message
Ioana Lasc (ilasc) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/lib/lp/charms/configure.zcml b/lib/lp/charms/configure.zcml
2index f1deb96..68acb5e 100644
3--- a/lib/lp/charms/configure.zcml
4+++ b/lib/lp/charms/configure.zcml
5@@ -77,6 +77,13 @@
6 <allow interface="lp.charms.interfaces.charmrecipebuild.ICharmFile" />
7 </class>
8
9+ <!-- CharmRecipeBuildBehaviour -->
10+ <adapter
11+ for="lp.charms.interfaces.charmrecipebuild.ICharmRecipeBuild"
12+ provides="lp.buildmaster.interfaces.buildfarmjobbehaviour.IBuildFarmJobBehaviour"
13+ factory="lp.charms.model.charmrecipebuildbehaviour.CharmRecipeBuildBehaviour"
14+ permission="zope.Public" />
15+
16 <!-- Charm-related jobs -->
17 <class class="lp.charms.model.charmrecipejob.CharmRecipeJob">
18 <allow interface="lp.charms.interfaces.charmrecipejob.ICharmRecipeJob" />
19diff --git a/lib/lp/charms/model/charmrecipebuildbehaviour.py b/lib/lp/charms/model/charmrecipebuildbehaviour.py
20new file mode 100644
21index 0000000..31ef483
22--- /dev/null
23+++ b/lib/lp/charms/model/charmrecipebuildbehaviour.py
24@@ -0,0 +1,111 @@
25+# Copyright 2021 Canonical Ltd. This software is licensed under the
26+# GNU Affero General Public License version 3 (see the file LICENSE).
27+
28+"""An `IBuildFarmJobBehaviour` for `CharmRecipeBuild`.
29+
30+Dispatches charm recipe build jobs to build-farm slaves.
31+"""
32+
33+from __future__ import absolute_import, print_function, unicode_literals
34+
35+__metaclass__ = type
36+__all__ = [
37+ "CharmRecipeBuildBehaviour",
38+ ]
39+
40+from twisted.internet import defer
41+from zope.component import adapter
42+from zope.interface import implementer
43+from zope.security.proxy import removeSecurityProxy
44+
45+from lp.buildmaster.enums import BuildBaseImageType
46+from lp.buildmaster.interfaces.builder import CannotBuild
47+from lp.buildmaster.interfaces.buildfarmjobbehaviour import (
48+ IBuildFarmJobBehaviour,
49+ )
50+from lp.buildmaster.model.buildfarmjobbehaviour import (
51+ BuildFarmJobBehaviourBase,
52+ )
53+from lp.charms.interfaces.charmrecipebuild import ICharmRecipeBuild
54+from lp.registry.interfaces.series import SeriesStatus
55+from lp.soyuz.adapters.archivedependencies import (
56+ get_sources_list_for_building,
57+ )
58+
59+
60+@adapter(ICharmRecipeBuild)
61+@implementer(IBuildFarmJobBehaviour)
62+class CharmRecipeBuildBehaviour(BuildFarmJobBehaviourBase):
63+ """Dispatches `CharmRecipeBuild` jobs to slaves."""
64+
65+ builder_type = "charm"
66+ image_types = [BuildBaseImageType.LXD, BuildBaseImageType.CHROOT]
67+
68+ def getLogFileName(self):
69+ das = self.build.distro_arch_series
70+
71+ # Examples:
72+ # buildlog_charm_ubuntu_wily_amd64_name_FULLYBUILT.txt
73+ return "buildlog_charm_%s_%s_%s_%s_%s.txt" % (
74+ das.distroseries.distribution.name, das.distroseries.name,
75+ das.architecturetag, self.build.recipe.name,
76+ self.build.status.name)
77+
78+ def verifyBuildRequest(self, logger):
79+ """Assert some pre-build checks.
80+
81+ The build request is checked:
82+ * Virtualized builds can't build on a non-virtual builder
83+ * Ensure that we have a chroot
84+ """
85+ build = self.build
86+ if build.virtualized and not self._builder.virtualized:
87+ raise AssertionError(
88+ "Attempt to build virtual item on a non-virtual builder.")
89+
90+ chroot = build.distro_arch_series.getChroot()
91+ if chroot is None:
92+ raise CannotBuild(
93+ "Missing chroot for %s" % build.distro_arch_series.displayname)
94+
95+ @defer.inlineCallbacks
96+ def extraBuildArgs(self, logger=None):
97+ """
98+ Return the extra arguments required by the slave for the given build.
99+ """
100+ build = self.build
101+ args = yield super(CharmRecipeBuildBehaviour, self).extraBuildArgs(
102+ logger=logger)
103+ args["name"] = build.recipe.store_name or build.recipe.name
104+ channels = build.channels or {}
105+ # We have to remove the security proxy that Zope applies to this
106+ # dict, since otherwise we'll be unable to serialise it to XML-RPC.
107+ args["channels"] = removeSecurityProxy(channels)
108+ args["archives"], args["trusted_keys"] = (
109+ yield get_sources_list_for_building(
110+ self, build.distro_arch_series, None, logger=logger))
111+ if build.recipe.build_path is not None:
112+ args["build_path"] = build.recipe.build_path
113+ if build.recipe.git_ref is not None:
114+ args["git_repository"] = build.recipe.git_repository.git_https_url
115+ # "git clone -b" doesn't accept full ref names. If this becomes
116+ # a problem then we could change launchpad-buildd to do "git
117+ # clone" followed by "git checkout" instead.
118+ if build.recipe.git_path != "HEAD":
119+ args["git_path"] = build.recipe.git_ref.name
120+ else:
121+ raise CannotBuild(
122+ "Source repository for ~%s/%s/+charm/%s has been deleted." % (
123+ build.recipe.owner.name, build.recipe.project.name,
124+ build.recipe.name))
125+ args["private"] = build.is_private
126+ defer.returnValue(args)
127+
128+ def verifySuccessfulBuild(self):
129+ """See `IBuildFarmJobBehaviour`."""
130+ # The implementation in BuildFarmJobBehaviourBase checks whether the
131+ # target suite is modifiable in the target archive. However, a
132+ # `CharmRecipeBuild`'s archive is a source rather than a target, so
133+ # that check does not make sense. We do, however, refuse to build
134+ # for obsolete series.
135+ assert self.build.distro_series.status != SeriesStatus.OBSOLETE
136diff --git a/lib/lp/charms/tests/test_charmrecipebuildbehaviour.py b/lib/lp/charms/tests/test_charmrecipebuildbehaviour.py
137new file mode 100644
138index 0000000..cafabd1
139--- /dev/null
140+++ b/lib/lp/charms/tests/test_charmrecipebuildbehaviour.py
141@@ -0,0 +1,423 @@
142+# Copyright 2021 Canonical Ltd. This software is licensed under the
143+# GNU Affero General Public License version 3 (see the file LICENSE).
144+
145+"""Test charm recipe build behaviour."""
146+
147+from __future__ import absolute_import, print_function, unicode_literals
148+
149+__metaclass__ = type
150+
151+import os.path
152+
153+from pymacaroons import Macaroon
154+from testtools import ExpectedException
155+from testtools.matchers import (
156+ Equals,
157+ Is,
158+ IsInstance,
159+ MatchesDict,
160+ MatchesListwise,
161+ )
162+from testtools.twistedsupport import (
163+ AsynchronousDeferredRunTestForBrokenTwisted,
164+ )
165+import transaction
166+from twisted.internet import defer
167+from zope.component import getUtility
168+from zope.proxy import isProxy
169+from zope.security.proxy import removeSecurityProxy
170+
171+from lp.app.enums import InformationType
172+from lp.archivepublisher.interfaces.archivegpgsigningkey import (
173+ IArchiveGPGSigningKey,
174+ )
175+from lp.buildmaster.enums import (
176+ BuildBaseImageType,
177+ BuildStatus,
178+ )
179+from lp.buildmaster.interfaces.builder import CannotBuild
180+from lp.buildmaster.interfaces.buildfarmjobbehaviour import (
181+ IBuildFarmJobBehaviour,
182+ )
183+from lp.buildmaster.interfaces.processor import IProcessorSet
184+from lp.buildmaster.tests.mock_slaves import (
185+ MockBuilder,
186+ OkSlave,
187+ )
188+from lp.buildmaster.tests.test_buildfarmjobbehaviour import (
189+ TestGetUploadMethodsMixin,
190+ TestHandleStatusMixin,
191+ TestVerifySuccessfulBuildMixin,
192+ )
193+from lp.charms.interfaces.charmrecipe import (
194+ CHARM_RECIPE_ALLOW_CREATE,
195+ CHARM_RECIPE_PRIVATE_FEATURE_FLAG,
196+ )
197+from lp.charms.model.charmrecipebuildbehaviour import (
198+ CharmRecipeBuildBehaviour,
199+ )
200+from lp.registry.interfaces.series import SeriesStatus
201+from lp.services.config import config
202+from lp.services.features.testing import FeatureFixture
203+from lp.services.log.logger import (
204+ BufferLogger,
205+ DevNullLogger,
206+ )
207+from lp.services.statsd.tests import StatsMixin
208+from lp.services.webapp import canonical_url
209+from lp.soyuz.adapters.archivedependencies import (
210+ get_sources_list_for_building,
211+ )
212+from lp.soyuz.enums import PackagePublishingStatus
213+from lp.soyuz.tests.soyuz import Base64KeyMatches
214+from lp.testing import TestCaseWithFactory
215+from lp.testing.dbuser import dbuser
216+from lp.testing.gpgkeys import gpgkeysdir
217+from lp.testing.keyserver import InProcessKeyServerFixture
218+from lp.testing.layers import LaunchpadZopelessLayer
219+
220+
221+class TestCharmRecipeBuildBehaviourBase(TestCaseWithFactory):
222+ layer = LaunchpadZopelessLayer
223+
224+ def setUp(self):
225+ self.useFixture(FeatureFixture({CHARM_RECIPE_ALLOW_CREATE: "on"}))
226+ super(TestCharmRecipeBuildBehaviourBase, self).setUp()
227+
228+ def makeJob(self, distribution=None, with_builder=False, **kwargs):
229+ """Create a sample `ICharmRecipeBuildBehaviour`."""
230+ if distribution is None:
231+ distribution = self.factory.makeDistribution(name="distro")
232+ distroseries = self.factory.makeDistroSeries(
233+ distribution=distribution, name="unstable")
234+ processor = getUtility(IProcessorSet).getByName("386")
235+ distroarchseries = self.factory.makeDistroArchSeries(
236+ distroseries=distroseries, architecturetag="i386",
237+ processor=processor)
238+
239+ # Taken from test_archivedependencies.py
240+ for component_name in ("main", "universe"):
241+ self.factory.makeComponentSelection(distroseries, component_name)
242+
243+ build = self.factory.makeCharmRecipeBuild(
244+ distro_arch_series=distroarchseries, name="test-charm", **kwargs)
245+ job = IBuildFarmJobBehaviour(build)
246+ if with_builder:
247+ builder = MockBuilder()
248+ builder.processor = processor
249+ job.setBuilder(builder, None)
250+ return job
251+
252+
253+class TestCharmRecipeBuildBehaviour(TestCharmRecipeBuildBehaviourBase):
254+ layer = LaunchpadZopelessLayer
255+
256+ def test_provides_interface(self):
257+ # CharmRecipeBuildBehaviour provides IBuildFarmJobBehaviour.
258+ job = CharmRecipeBuildBehaviour(None)
259+ self.assertProvides(job, IBuildFarmJobBehaviour)
260+
261+ def test_adapts_ICharmRecipeBuild(self):
262+ # IBuildFarmJobBehaviour adapts an ICharmRecipeBuild.
263+ build = self.factory.makeCharmRecipeBuild()
264+ job = IBuildFarmJobBehaviour(build)
265+ self.assertProvides(job, IBuildFarmJobBehaviour)
266+
267+ def test_verifyBuildRequest_valid(self):
268+ # verifyBuildRequest doesn't raise any exceptions when called with a
269+ # valid builder set.
270+ job = self.makeJob()
271+ lfa = self.factory.makeLibraryFileAlias()
272+ transaction.commit()
273+ job.build.distro_arch_series.addOrUpdateChroot(lfa)
274+ builder = MockBuilder()
275+ job.setBuilder(builder, OkSlave())
276+ logger = BufferLogger()
277+ job.verifyBuildRequest(logger)
278+ self.assertEqual("", logger.getLogBuffer())
279+
280+ def test_verifyBuildRequest_virtual_mismatch(self):
281+ # verifyBuildRequest raises on an attempt to build a virtualized
282+ # build on a non-virtual builder.
283+ job = self.makeJob()
284+ lfa = self.factory.makeLibraryFileAlias()
285+ transaction.commit()
286+ job.build.distro_arch_series.addOrUpdateChroot(lfa)
287+ builder = MockBuilder(virtualized=False)
288+ job.setBuilder(builder, OkSlave())
289+ logger = BufferLogger()
290+ e = self.assertRaises(AssertionError, job.verifyBuildRequest, logger)
291+ self.assertEqual(
292+ "Attempt to build virtual item on a non-virtual builder.", str(e))
293+
294+ def test_verifyBuildRequest_no_chroot(self):
295+ # verifyBuildRequest raises when the DAS has no chroot.
296+ job = self.makeJob()
297+ builder = MockBuilder()
298+ job.setBuilder(builder, OkSlave())
299+ logger = BufferLogger()
300+ e = self.assertRaises(CannotBuild, job.verifyBuildRequest, logger)
301+ self.assertIn("Missing chroot", str(e))
302+
303+
304+class TestAsyncCharmRecipeBuildBehaviour(
305+ StatsMixin, TestCharmRecipeBuildBehaviourBase):
306+
307+ run_tests_with = AsynchronousDeferredRunTestForBrokenTwisted.make_factory(
308+ timeout=30)
309+
310+ def setUp(self):
311+ super(TestAsyncCharmRecipeBuildBehaviour, self).setUp()
312+ self.setUpStats()
313+
314+ @defer.inlineCallbacks
315+ def test_composeBuildRequest(self):
316+ job = self.makeJob(with_builder=True)
317+ lfa = self.factory.makeLibraryFileAlias(db_only=True)
318+ job.build.distro_arch_series.addOrUpdateChroot(lfa)
319+ build_request = yield job.composeBuildRequest(None)
320+ self.assertThat(build_request, MatchesListwise([
321+ Equals("charm"),
322+ Equals(job.build.distro_arch_series),
323+ Equals(job.build.pocket),
324+ Equals({}),
325+ IsInstance(dict),
326+ ]))
327+
328+ @defer.inlineCallbacks
329+ def test_extraBuildArgs_git(self):
330+ # extraBuildArgs returns appropriate arguments if asked to build a
331+ # job for a Git branch.
332+ [ref] = self.factory.makeGitRefs()
333+ job = self.makeJob(git_ref=ref, with_builder=True)
334+ expected_archives, expected_trusted_keys = (
335+ yield get_sources_list_for_building(
336+ job, job.build.distro_arch_series, None))
337+ for archive_line in expected_archives:
338+ self.assertIn("universe", archive_line)
339+ with dbuser(config.builddmaster.dbuser):
340+ args = yield job.extraBuildArgs()
341+ self.assertThat(args, MatchesDict({
342+ "archive_private": Is(False),
343+ "archives": Equals(expected_archives),
344+ "arch_tag": Equals("i386"),
345+ "build_url": Equals(canonical_url(job.build)),
346+ "channels": Equals({}),
347+ "fast_cleanup": Is(True),
348+ "git_repository": Equals(ref.repository.git_https_url),
349+ "git_path": Equals(ref.name),
350+ "name": Equals("test-charm"),
351+ "private": Is(False),
352+ "series": Equals("unstable"),
353+ "trusted_keys": Equals(expected_trusted_keys),
354+ }))
355+
356+ @defer.inlineCallbacks
357+ def test_extraBuildArgs_git_HEAD(self):
358+ # extraBuildArgs returns appropriate arguments if asked to build a
359+ # job for the default branch in a Launchpad-hosted Git repository.
360+ [ref] = self.factory.makeGitRefs()
361+ removeSecurityProxy(ref.repository)._default_branch = ref.path
362+ job = self.makeJob(
363+ git_ref=ref.repository.getRefByPath("HEAD"), with_builder=True)
364+ expected_archives, expected_trusted_keys = (
365+ yield get_sources_list_for_building(
366+ job, job.build.distro_arch_series, None))
367+ for archive_line in expected_archives:
368+ self.assertIn("universe", archive_line)
369+ with dbuser(config.builddmaster.dbuser):
370+ args = yield job.extraBuildArgs()
371+ self.assertThat(args, MatchesDict({
372+ "archive_private": Is(False),
373+ "archives": Equals(expected_archives),
374+ "arch_tag": Equals("i386"),
375+ "build_url": Equals(canonical_url(job.build)),
376+ "channels": Equals({}),
377+ "fast_cleanup": Is(True),
378+ "git_repository": Equals(ref.repository.git_https_url),
379+ "name": Equals("test-charm"),
380+ "private": Is(False),
381+ "series": Equals("unstable"),
382+ "trusted_keys": Equals(expected_trusted_keys),
383+ }))
384+
385+ @defer.inlineCallbacks
386+ def test_extraBuildArgs_prefers_store_name(self):
387+ # For the "name" argument, extraBuildArgs prefers
388+ # CharmRecipe.store_name over CharmRecipe.name if the former is set.
389+ job = self.makeJob(store_name="something-else", with_builder=True)
390+ with dbuser(config.builddmaster.dbuser):
391+ args = yield job.extraBuildArgs()
392+ self.assertEqual("something-else", args["name"])
393+
394+ @defer.inlineCallbacks
395+ def test_extraBuildArgs_archive_trusted_keys(self):
396+ # If the archive has a signing key, extraBuildArgs sends it.
397+ yield self.useFixture(InProcessKeyServerFixture()).start()
398+ distribution = self.factory.makeDistribution()
399+ key_path = os.path.join(gpgkeysdir, "ppa-sample@canonical.com.sec")
400+ yield IArchiveGPGSigningKey(distribution.main_archive).setSigningKey(
401+ key_path, async_keyserver=True)
402+ job = self.makeJob(distribution=distribution, with_builder=True)
403+ self.factory.makeBinaryPackagePublishingHistory(
404+ distroarchseries=job.build.distro_arch_series,
405+ pocket=job.build.pocket, archive=distribution.main_archive,
406+ status=PackagePublishingStatus.PUBLISHED)
407+ with dbuser(config.builddmaster.dbuser):
408+ args = yield job.extraBuildArgs()
409+ self.assertThat(args["trusted_keys"], MatchesListwise([
410+ Base64KeyMatches("0D57E99656BEFB0897606EE9A022DD1F5001B46D"),
411+ ]))
412+
413+ @defer.inlineCallbacks
414+ def test_extraBuildArgs_channels(self):
415+ # If the build needs particular channels, extraBuildArgs sends them.
416+ job = self.makeJob(channels={"charmcraft": "edge"}, with_builder=True)
417+ expected_archives, expected_trusted_keys = (
418+ yield get_sources_list_for_building(
419+ job, job.build.distro_arch_series, None))
420+ with dbuser(config.builddmaster.dbuser):
421+ args = yield job.extraBuildArgs()
422+ self.assertFalse(isProxy(args["channels"]))
423+ self.assertEqual({"charmcraft": "edge"}, args["channels"])
424+
425+ @defer.inlineCallbacks
426+ def test_extraBuildArgs_archives_primary(self):
427+ # The build uses the release, security, and updates pockets from the
428+ # primary archive.
429+ job = self.makeJob(with_builder=True)
430+ expected_archives = [
431+ "deb %s %s main universe" % (
432+ job.archive.archive_url, job.build.distro_series.name),
433+ "deb %s %s-security main universe" % (
434+ job.archive.archive_url, job.build.distro_series.name),
435+ "deb %s %s-updates main universe" % (
436+ job.archive.archive_url, job.build.distro_series.name),
437+ ]
438+ with dbuser(config.builddmaster.dbuser):
439+ extra_args = yield job.extraBuildArgs()
440+ self.assertEqual(expected_archives, extra_args["archives"])
441+
442+ @defer.inlineCallbacks
443+ def test_extraBuildArgs_build_path(self):
444+ # If the recipe specifies a build path, extraBuildArgs sends it.
445+ job = self.makeJob(build_path="src", with_builder=True)
446+ expected_archives, expected_trusted_keys = (
447+ yield get_sources_list_for_building(
448+ job, job.build.distro_arch_series, None))
449+ with dbuser(config.builddmaster.dbuser):
450+ args = yield job.extraBuildArgs()
451+ self.assertEqual("src", args["build_path"])
452+
453+ @defer.inlineCallbacks
454+ def test_extraBuildArgs_private(self):
455+ # If the recipe is private, extraBuildArgs sends the appropriate
456+ # arguments.
457+ self.useFixture(FeatureFixture({
458+ CHARM_RECIPE_ALLOW_CREATE: "on",
459+ CHARM_RECIPE_PRIVATE_FEATURE_FLAG: "on",
460+ }))
461+ job = self.makeJob(
462+ information_type=InformationType.PROPRIETARY, with_builder=True)
463+ with dbuser(config.builddmaster.dbuser):
464+ args = yield job.extraBuildArgs()
465+ self.assertTrue(args["private"])
466+
467+ @defer.inlineCallbacks
468+ def test_composeBuildRequest_git_ref_deleted(self):
469+ # If the source Git reference has been deleted, composeBuildRequest
470+ # raises CannotBuild.
471+ repository = self.factory.makeGitRepository()
472+ [ref] = self.factory.makeGitRefs(repository=repository)
473+ owner = self.factory.makePerson(name="charm-owner")
474+ project = self.factory.makeProduct(name="charm-project")
475+ job = self.makeJob(
476+ registrant=owner, owner=owner, project=project, git_ref=ref,
477+ with_builder=True)
478+ repository.removeRefs([ref.path])
479+ self.assertIsNone(job.build.recipe.git_ref)
480+ expected_exception_msg = (
481+ r"Source repository for "
482+ r"~charm-owner/charm-project/\+charm/test-charm has been deleted.")
483+ with ExpectedException(CannotBuild, expected_exception_msg):
484+ yield job.composeBuildRequest(None)
485+
486+ @defer.inlineCallbacks
487+ def test_dispatchBuildToSlave_prefers_lxd(self):
488+ job = self.makeJob()
489+ builder = MockBuilder()
490+ builder.processor = job.build.processor
491+ slave = OkSlave()
492+ job.setBuilder(builder, slave)
493+ chroot_lfa = self.factory.makeLibraryFileAlias(db_only=True)
494+ job.build.distro_arch_series.addOrUpdateChroot(
495+ chroot_lfa, image_type=BuildBaseImageType.CHROOT)
496+ lxd_lfa = self.factory.makeLibraryFileAlias(db_only=True)
497+ job.build.distro_arch_series.addOrUpdateChroot(
498+ lxd_lfa, image_type=BuildBaseImageType.LXD)
499+ yield job.dispatchBuildToSlave(DevNullLogger())
500+ self.assertEqual(
501+ ("ensurepresent", lxd_lfa.http_url, "", ""), slave.call_log[0])
502+ self.assertEqual(1, self.stats_client.incr.call_count)
503+ self.assertEqual(
504+ self.stats_client.incr.call_args_list[0][0],
505+ ("build.count,builder_name={},env=test,"
506+ "job_type=CHARMRECIPEBUILD".format(builder.name),))
507+
508+ @defer.inlineCallbacks
509+ def test_dispatchBuildToSlave_falls_back_to_chroot(self):
510+ job = self.makeJob()
511+ builder = MockBuilder()
512+ builder.processor = job.build.processor
513+ slave = OkSlave()
514+ job.setBuilder(builder, slave)
515+ chroot_lfa = self.factory.makeLibraryFileAlias(db_only=True)
516+ job.build.distro_arch_series.addOrUpdateChroot(
517+ chroot_lfa, image_type=BuildBaseImageType.CHROOT)
518+ yield job.dispatchBuildToSlave(DevNullLogger())
519+ self.assertEqual(
520+ ("ensurepresent", chroot_lfa.http_url, "", ""), slave.call_log[0])
521+
522+
523+class MakeCharmRecipeBuildMixin:
524+ """Provide the common makeBuild method returning a queued build."""
525+
526+ def makeCharmRecipe(self):
527+ self.useFixture(FeatureFixture({CHARM_RECIPE_ALLOW_CREATE: "on"}))
528+ return self.factory.makeCharmRecipe(
529+ store_upload=True, store_name=self.factory.getUniqueUnicode(),
530+ store_secrets={"root": Macaroon().serialize()})
531+
532+ def makeBuild(self):
533+ recipe = self.makeCharmRecipe()
534+ build = self.factory.makeCharmRecipeBuild(
535+ requester=recipe.registrant, recipe=recipe,
536+ status=BuildStatus.BUILDING)
537+ build.queueBuild()
538+ return build
539+
540+ def makeUnmodifiableBuild(self):
541+ recipe = self.makeCharmRecipe()
542+ build = self.factory.makeCharmRecipeBuild(
543+ requester=recipe.registrant, recipe=recipe,
544+ status=BuildStatus.BUILDING)
545+ build.distro_series.status = SeriesStatus.OBSOLETE
546+ build.queueBuild()
547+ return build
548+
549+
550+class TestGetUploadMethodsForCharmRecipeBuild(
551+ MakeCharmRecipeBuildMixin, TestGetUploadMethodsMixin,
552+ TestCaseWithFactory):
553+ """IPackageBuild.getUpload* methods work with charm recipe builds."""
554+
555+
556+class TestVerifySuccessfulBuildForCharmRecipeBuild(
557+ MakeCharmRecipeBuildMixin, TestVerifySuccessfulBuildMixin,
558+ TestCaseWithFactory):
559+ """IBuildFarmJobBehaviour.verifySuccessfulBuild works."""
560+
561+
562+class TestHandleStatusForCharmRecipeBuild(
563+ MakeCharmRecipeBuildMixin, TestHandleStatusMixin, TestCaseWithFactory):
564+ """IPackageBuild.handleStatus works with charm recipe builds."""

Subscribers

People subscribed via source and target branches

to status/vote changes: