Merge ~cjwatson/launchpad:charm-recipe-build-behaviour into launchpad:master
- Git
- lp:~cjwatson/launchpad
- charm-recipe-build-behaviour
- Merge into 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) |
Related bugs: |
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
Description of the change
To post a comment you must log in.
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
1 | diff --git a/lib/lp/charms/configure.zcml b/lib/lp/charms/configure.zcml |
2 | index 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" /> |
19 | diff --git a/lib/lp/charms/model/charmrecipebuildbehaviour.py b/lib/lp/charms/model/charmrecipebuildbehaviour.py |
20 | new file mode 100644 |
21 | index 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 |
136 | diff --git a/lib/lp/charms/tests/test_charmrecipebuildbehaviour.py b/lib/lp/charms/tests/test_charmrecipebuildbehaviour.py |
137 | new file mode 100644 |
138 | index 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.""" |
Looks good!