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