Merge ~jugmac00/launchpad:create-lpcraft-jobs-on-push into launchpad:master
- Git
- lp:~jugmac00/launchpad
- create-lpcraft-jobs-on-push
- Merge into master
Proposed by
Jürgen Gmach
Status: | Merged |
---|---|
Approved by: | Jürgen Gmach |
Approved revision: | 4195ec844f18ceef354f7309cc1abcbc2d0f2dff |
Merge reported by: | Otto Co-Pilot |
Merged at revision: | not available |
Proposed branch: | ~jugmac00/launchpad:create-lpcraft-jobs-on-push |
Merge into: | launchpad:master |
Diff against target: |
1163 lines (+821/-10) 6 files modified
lib/lp/code/interfaces/cibuild.py (+56/-0) lib/lp/code/model/cibuild.py (+138/-7) lib/lp/code/model/tests/test_cibuild.py (+487/-1) lib/lp/code/model/tests/test_gitrepository.py (+99/-0) lib/lp/code/subscribers/git.py (+8/-1) lib/lp/testing/factory.py (+33/-1) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Colin Watson (community) | Approve | ||
Review via email: mp+416223@code.launchpad.net |
Commit message
WIP: Create lpcraft jobs on push
Description of the change
To post a comment you must log in.
Revision history for this message
Jürgen Gmach (jugmac00) wrote : | # |
Revision history for this message
Colin Watson (cjwatson) : | # |
Revision history for this message
Jürgen Gmach (jugmac00) wrote : | # |
Thanks for the feedback! I have applied most suggestions, and now I am ready to implement the missing test cases.
Revision history for this message
Jürgen Gmach (jugmac00) wrote : | # |
I will rebase/squash the many commits once the MP is approved - I kept them for now as I may need to revert something.
Revision history for this message
Jürgen Gmach (jugmac00) wrote : | # |
Ready for the next review!
Revision history for this message
Colin Watson (cjwatson) : | # |
review:
Approve
Revision history for this message
Jürgen Gmach (jugmac00) wrote : | # |
Thanks a lot for the detailed review!
Revision history for this message
Colin Watson (cjwatson) : | # |
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | diff --git a/lib/lp/code/interfaces/cibuild.py b/lib/lp/code/interfaces/cibuild.py | |||
2 | index 4537b1c..e1abcf8 100644 | |||
3 | --- a/lib/lp/code/interfaces/cibuild.py | |||
4 | +++ b/lib/lp/code/interfaces/cibuild.py | |||
5 | @@ -6,11 +6,16 @@ | |||
6 | 6 | __all__ = [ | 6 | __all__ = [ |
7 | 7 | "CannotFetchConfiguration", | 7 | "CannotFetchConfiguration", |
8 | 8 | "CannotParseConfiguration", | 8 | "CannotParseConfiguration", |
9 | 9 | "CIBuildAlreadyRequested", | ||
10 | 10 | "CIBuildDisallowedArchitecture", | ||
11 | 9 | "ICIBuild", | 11 | "ICIBuild", |
12 | 10 | "ICIBuildSet", | 12 | "ICIBuildSet", |
13 | 11 | "MissingConfiguration", | 13 | "MissingConfiguration", |
14 | 12 | ] | 14 | ] |
15 | 13 | 15 | ||
16 | 16 | import http.client | ||
17 | 17 | |||
18 | 18 | from lazr.restful.declarations import error_status | ||
19 | 14 | from lazr.restful.fields import Reference | 19 | from lazr.restful.fields import Reference |
20 | 15 | from zope.schema import ( | 20 | from zope.schema import ( |
21 | 16 | Bool, | 21 | Bool, |
22 | @@ -53,6 +58,26 @@ class CannotParseConfiguration(Exception): | |||
23 | 53 | """Launchpad cannot parse this CI build's .launchpad.yaml.""" | 58 | """Launchpad cannot parse this CI build's .launchpad.yaml.""" |
24 | 54 | 59 | ||
25 | 55 | 60 | ||
26 | 61 | @error_status(http.client.BAD_REQUEST) | ||
27 | 62 | class CIBuildDisallowedArchitecture(Exception): | ||
28 | 63 | """A build was requested for a disallowed architecture.""" | ||
29 | 64 | |||
30 | 65 | def __init__(self, das, pocket): | ||
31 | 66 | super().__init__( | ||
32 | 67 | "Builds for %s/%s are not allowed." % ( | ||
33 | 68 | das.distroseries.getSuite(pocket), das.architecturetag) | ||
34 | 69 | ) | ||
35 | 70 | |||
36 | 71 | |||
37 | 72 | @error_status(http.client.BAD_REQUEST) | ||
38 | 73 | class CIBuildAlreadyRequested(Exception): | ||
39 | 74 | """An identical build was requested more than once.""" | ||
40 | 75 | |||
41 | 76 | def __init__(self): | ||
42 | 77 | super().__init__( | ||
43 | 78 | "An identical build for this commit was already requested.") | ||
44 | 79 | |||
45 | 80 | |||
46 | 56 | class ICIBuildView(IPackageBuildView): | 81 | class ICIBuildView(IPackageBuildView): |
47 | 57 | """`ICIBuild` attributes that require launchpad.View.""" | 82 | """`ICIBuild` attributes that require launchpad.View.""" |
48 | 58 | 83 | ||
49 | @@ -133,6 +158,37 @@ class ICIBuildSet(ISpecificBuildFarmJobSource): | |||
50 | 133 | these Git commit IDs. | 158 | these Git commit IDs. |
51 | 134 | """ | 159 | """ |
52 | 135 | 160 | ||
53 | 161 | def requestBuild(git_repository, commit_sha1, distro_arch_series): | ||
54 | 162 | """Request a CI build. | ||
55 | 163 | |||
56 | 164 | This checks that the architecture is allowed and that there isn't | ||
57 | 165 | already a matching pending build. | ||
58 | 166 | |||
59 | 167 | :param git_repository: The `IGitRepository` for the new build. | ||
60 | 168 | :param commit_sha1: The Git commit ID for the new build. | ||
61 | 169 | :param distro_arch_series: The `IDistroArchSeries` that the new | ||
62 | 170 | build should run on. | ||
63 | 171 | :raises CIBuildDisallowedArchitecture: if builds on | ||
64 | 172 | `distro_arch_series` are not allowed. | ||
65 | 173 | :raises CIBuildAlreadyRequested: if a matching build was already | ||
66 | 174 | requested. | ||
67 | 175 | :return: `ICIBuild`. | ||
68 | 176 | """ | ||
69 | 177 | |||
70 | 178 | def requestBuildsForRefs(git_repository, ref_paths, logger=None): | ||
71 | 179 | """Request CI builds for a collection of refs. | ||
72 | 180 | |||
73 | 181 | This fetches `.launchpad.yaml` from the repository and parses it to | ||
74 | 182 | work out which series/architectures need builds. | ||
75 | 183 | |||
76 | 184 | :param git_repository: The `IGitRepository` for which to request | ||
77 | 185 | builds. | ||
78 | 186 | :param ref_paths: A collection of Git reference paths within | ||
79 | 187 | `git_repository`; builds will be requested for the commits that | ||
80 | 188 | each of them points to. | ||
81 | 189 | :param logger: An optional logger. | ||
82 | 190 | """ | ||
83 | 191 | |||
84 | 136 | def deleteByGitRepository(git_repository): | 192 | def deleteByGitRepository(git_repository): |
85 | 137 | """Delete all CI builds for the given Git repository. | 193 | """Delete all CI builds for the given Git repository. |
86 | 138 | 194 | ||
87 | diff --git a/lib/lp/code/model/cibuild.py b/lib/lp/code/model/cibuild.py | |||
88 | index f9c9031..bdc570c 100644 | |||
89 | --- a/lib/lp/code/model/cibuild.py | |||
90 | +++ b/lib/lp/code/model/cibuild.py | |||
91 | @@ -9,6 +9,7 @@ __all__ = [ | |||
92 | 9 | 9 | ||
93 | 10 | from datetime import timedelta | 10 | from datetime import timedelta |
94 | 11 | 11 | ||
95 | 12 | from lazr.lifecycle.event import ObjectCreatedEvent | ||
96 | 12 | import pytz | 13 | import pytz |
97 | 13 | from storm.locals import ( | 14 | from storm.locals import ( |
98 | 14 | Bool, | 15 | Bool, |
99 | @@ -21,8 +22,11 @@ from storm.locals import ( | |||
100 | 21 | ) | 22 | ) |
101 | 22 | from storm.store import EmptyResultSet | 23 | from storm.store import EmptyResultSet |
102 | 23 | from zope.component import getUtility | 24 | from zope.component import getUtility |
103 | 25 | from zope.event import notify | ||
104 | 24 | from zope.interface import implementer | 26 | from zope.interface import implementer |
105 | 25 | 27 | ||
106 | 28 | from lp.app.errors import NotFoundError | ||
107 | 29 | from lp.app.interfaces.launchpad import ILaunchpadCelebrities | ||
108 | 26 | from lp.buildmaster.enums import ( | 30 | from lp.buildmaster.enums import ( |
109 | 27 | BuildFarmJobType, | 31 | BuildFarmJobType, |
110 | 28 | BuildQueueStatus, | 32 | BuildQueueStatus, |
111 | @@ -38,10 +42,14 @@ from lp.code.errors import ( | |||
112 | 38 | from lp.code.interfaces.cibuild import ( | 42 | from lp.code.interfaces.cibuild import ( |
113 | 39 | CannotFetchConfiguration, | 43 | CannotFetchConfiguration, |
114 | 40 | CannotParseConfiguration, | 44 | CannotParseConfiguration, |
115 | 45 | CIBuildAlreadyRequested, | ||
116 | 46 | CIBuildDisallowedArchitecture, | ||
117 | 41 | ICIBuild, | 47 | ICIBuild, |
118 | 42 | ICIBuildSet, | 48 | ICIBuildSet, |
119 | 43 | MissingConfiguration, | 49 | MissingConfiguration, |
120 | 44 | ) | 50 | ) |
121 | 51 | from lp.code.interfaces.githosting import IGitHostingClient | ||
122 | 52 | from lp.code.model.gitref import GitRef | ||
123 | 45 | from lp.code.model.lpcraft import load_configuration | 53 | from lp.code.model.lpcraft import load_configuration |
124 | 46 | from lp.registry.interfaces.pocket import PackagePublishingPocket | 54 | from lp.registry.interfaces.pocket import PackagePublishingPocket |
125 | 47 | from lp.registry.interfaces.series import SeriesStatus | 55 | from lp.registry.interfaces.series import SeriesStatus |
126 | @@ -65,6 +73,53 @@ from lp.services.propertycache import cachedproperty | |||
127 | 65 | from lp.soyuz.model.distroarchseries import DistroArchSeries | 73 | from lp.soyuz.model.distroarchseries import DistroArchSeries |
128 | 66 | 74 | ||
129 | 67 | 75 | ||
130 | 76 | def determine_DASes_to_build(configuration, logger=None): | ||
131 | 77 | """Generate distroarchseries to build for this configuration.""" | ||
132 | 78 | architectures_by_series = {} | ||
133 | 79 | for stage in configuration.pipeline: | ||
134 | 80 | for job_name in stage: | ||
135 | 81 | if job_name not in configuration.jobs: | ||
136 | 82 | if logger is not None: | ||
137 | 83 | logger.error("No job definition for %r", job_name) | ||
138 | 84 | continue | ||
139 | 85 | for job in configuration.jobs[job_name]: | ||
140 | 86 | for architecture in job["architectures"]: | ||
141 | 87 | architectures_by_series.setdefault( | ||
142 | 88 | job["series"], set()).add(architecture) | ||
143 | 89 | # XXX cjwatson 2022-01-21: We have to hardcode Ubuntu for now, since | ||
144 | 90 | # the .launchpad.yaml format doesn't currently support other | ||
145 | 91 | # distributions (although nor does the Launchpad build farm). | ||
146 | 92 | distribution = getUtility(ILaunchpadCelebrities).ubuntu | ||
147 | 93 | for series_name, architecture_names in architectures_by_series.items(): | ||
148 | 94 | try: | ||
149 | 95 | series = distribution[series_name] | ||
150 | 96 | except NotFoundError: | ||
151 | 97 | if logger is not None: | ||
152 | 98 | logger.error("Unknown Ubuntu series name %s" % series_name) | ||
153 | 99 | continue | ||
154 | 100 | architectures = { | ||
155 | 101 | das.architecturetag: das | ||
156 | 102 | for das in series.buildable_architectures} | ||
157 | 103 | for architecture_name in architecture_names: | ||
158 | 104 | try: | ||
159 | 105 | das = architectures[architecture_name] | ||
160 | 106 | except KeyError: | ||
161 | 107 | if logger is not None: | ||
162 | 108 | logger.error( | ||
163 | 109 | "%s is not a buildable architecture name in " | ||
164 | 110 | "Ubuntu %s" % (architecture_name, series_name)) | ||
165 | 111 | continue | ||
166 | 112 | yield das | ||
167 | 113 | |||
168 | 114 | |||
169 | 115 | def get_all_commits_for_paths(git_repository, paths): | ||
170 | 116 | return [ | ||
171 | 117 | ref.commit_sha1 | ||
172 | 118 | for ref in GitRef.findByReposAndPaths( | ||
173 | 119 | [(git_repository, ref_path) | ||
174 | 120 | for ref_path in paths]).values()] | ||
175 | 121 | |||
176 | 122 | |||
177 | 68 | def parse_configuration(git_repository, blob): | 123 | def parse_configuration(git_repository, blob): |
178 | 69 | try: | 124 | try: |
179 | 70 | return load_configuration(blob) | 125 | return load_configuration(blob) |
180 | @@ -329,6 +384,89 @@ class CIBuildSet(SpecificBuildFarmJobSourceMixin): | |||
181 | 329 | store.flush() | 384 | store.flush() |
182 | 330 | return cibuild | 385 | return cibuild |
183 | 331 | 386 | ||
184 | 387 | def findByGitRepository(self, git_repository, commit_sha1s=None): | ||
185 | 388 | """See `ICIBuildSet`.""" | ||
186 | 389 | clauses = [CIBuild.git_repository == git_repository] | ||
187 | 390 | if commit_sha1s is not None: | ||
188 | 391 | clauses.append(CIBuild.commit_sha1.is_in(commit_sha1s)) | ||
189 | 392 | return IStore(CIBuild).find(CIBuild, *clauses) | ||
190 | 393 | |||
191 | 394 | def _isBuildableArchitectureAllowed(self, das): | ||
192 | 395 | """Check whether we may build for a buildable `DistroArchSeries`. | ||
193 | 396 | |||
194 | 397 | The caller is assumed to have already checked that a suitable chroot | ||
195 | 398 | is available (either directly or via | ||
196 | 399 | `DistroSeries.buildable_architectures`). | ||
197 | 400 | """ | ||
198 | 401 | return ( | ||
199 | 402 | das.enabled | ||
200 | 403 | # We only support builds on virtualized builders at the moment. | ||
201 | 404 | and das.processor.supports_virtualized) | ||
202 | 405 | |||
203 | 406 | def _isArchitectureAllowed(self, das, pocket, snap_base=None): | ||
204 | 407 | return ( | ||
205 | 408 | das.getChroot(pocket=pocket) is not None | ||
206 | 409 | and self._isBuildableArchitectureAllowed(das)) | ||
207 | 410 | |||
208 | 411 | def requestBuild(self, git_repository, commit_sha1, distro_arch_series): | ||
209 | 412 | """See `ICIBuildSet`.""" | ||
210 | 413 | pocket = PackagePublishingPocket.UPDATES | ||
211 | 414 | if not self._isArchitectureAllowed(distro_arch_series, pocket): | ||
212 | 415 | raise CIBuildDisallowedArchitecture(distro_arch_series, pocket) | ||
213 | 416 | |||
214 | 417 | result = IStore(CIBuild).find( | ||
215 | 418 | CIBuild, | ||
216 | 419 | CIBuild.git_repository == git_repository, | ||
217 | 420 | CIBuild.commit_sha1 == commit_sha1, | ||
218 | 421 | CIBuild.distro_arch_series == distro_arch_series) | ||
219 | 422 | if not result.is_empty(): | ||
220 | 423 | raise CIBuildAlreadyRequested | ||
221 | 424 | |||
222 | 425 | build = self.new(git_repository, commit_sha1, distro_arch_series) | ||
223 | 426 | build.queueBuild() | ||
224 | 427 | notify(ObjectCreatedEvent(build)) | ||
225 | 428 | return build | ||
226 | 429 | |||
227 | 430 | def _tryToRequestBuild(self, git_repository, commit_sha1, das, logger): | ||
228 | 431 | try: | ||
229 | 432 | if logger is not None: | ||
230 | 433 | logger.info( | ||
231 | 434 | "Requesting CI build for %s on %s/%s", | ||
232 | 435 | commit_sha1, das.distroseries.name, das.architecturetag, | ||
233 | 436 | ) | ||
234 | 437 | self.requestBuild(git_repository, commit_sha1, das) | ||
235 | 438 | except CIBuildAlreadyRequested: | ||
236 | 439 | pass | ||
237 | 440 | except Exception as e: | ||
238 | 441 | if logger is not None: | ||
239 | 442 | logger.error( | ||
240 | 443 | "Failed to request CI build for %s on %s/%s: %s", | ||
241 | 444 | commit_sha1, das.distroseries.name, das.architecturetag, e | ||
242 | 445 | ) | ||
243 | 446 | |||
244 | 447 | def requestBuildsForRefs(self, git_repository, ref_paths, logger=None): | ||
245 | 448 | """See `ICIBuildSet`.""" | ||
246 | 449 | commit_sha1s = get_all_commits_for_paths(git_repository, ref_paths) | ||
247 | 450 | # getCommits performs a web request! | ||
248 | 451 | commits = getUtility(IGitHostingClient).getCommits( | ||
249 | 452 | git_repository.getInternalPath(), commit_sha1s, | ||
250 | 453 | # XXX cjwatson 2022-01-19: We should also fetch | ||
251 | 454 | # debian/.launchpad.yaml (or perhaps make the path a property of | ||
252 | 455 | # the repository) once lpcraft and launchpad-buildd support | ||
253 | 456 | # using alternative paths for builds. | ||
254 | 457 | filter_paths=[".launchpad.yaml"]) | ||
255 | 458 | for commit in commits: | ||
256 | 459 | try: | ||
257 | 460 | configuration = parse_configuration( | ||
258 | 461 | git_repository, commit["blobs"][".launchpad.yaml"]) | ||
259 | 462 | except CannotParseConfiguration as e: | ||
260 | 463 | if logger is not None: | ||
261 | 464 | logger.error(e) | ||
262 | 465 | continue | ||
263 | 466 | for das in determine_DASes_to_build(configuration): | ||
264 | 467 | self._tryToRequestBuild( | ||
265 | 468 | git_repository, commit["sha1"], das, logger) | ||
266 | 469 | |||
267 | 332 | def getByID(self, build_id): | 470 | def getByID(self, build_id): |
268 | 333 | """See `ISpecificBuildFarmJobSource`.""" | 471 | """See `ISpecificBuildFarmJobSource`.""" |
269 | 334 | store = IMasterStore(CIBuild) | 472 | store = IMasterStore(CIBuild) |
270 | @@ -357,13 +495,6 @@ class CIBuildSet(SpecificBuildFarmJobSourceMixin): | |||
271 | 357 | bfj.id for bfj in build_farm_jobs)) | 495 | bfj.id for bfj in build_farm_jobs)) |
272 | 358 | return DecoratedResultSet(rows, pre_iter_hook=self.preloadBuildsData) | 496 | return DecoratedResultSet(rows, pre_iter_hook=self.preloadBuildsData) |
273 | 359 | 497 | ||
274 | 360 | def findByGitRepository(self, git_repository, commit_sha1s=None): | ||
275 | 361 | """See `ICIBuildSet`.""" | ||
276 | 362 | clauses = [CIBuild.git_repository == git_repository] | ||
277 | 363 | if commit_sha1s is not None: | ||
278 | 364 | clauses.append(CIBuild.commit_sha1.is_in(commit_sha1s)) | ||
279 | 365 | return IStore(CIBuild).find(CIBuild, *clauses) | ||
280 | 366 | |||
281 | 367 | def deleteByGitRepository(self, git_repository): | 498 | def deleteByGitRepository(self, git_repository): |
282 | 368 | """See `ICIBuildSet`.""" | 499 | """See `ICIBuildSet`.""" |
283 | 369 | self.findByGitRepository(git_repository).remove() | 500 | self.findByGitRepository(git_repository).remove() |
284 | diff --git a/lib/lp/code/model/tests/test_cibuild.py b/lib/lp/code/model/tests/test_cibuild.py | |||
285 | index 133bed2..bd2c5e9 100644 | |||
286 | --- a/lib/lp/code/model/tests/test_cibuild.py | |||
287 | +++ b/lib/lp/code/model/tests/test_cibuild.py | |||
288 | @@ -7,9 +7,13 @@ from datetime import ( | |||
289 | 7 | datetime, | 7 | datetime, |
290 | 8 | timedelta, | 8 | timedelta, |
291 | 9 | ) | 9 | ) |
292 | 10 | import hashlib | ||
293 | 10 | from textwrap import dedent | 11 | from textwrap import dedent |
294 | 12 | from unittest.mock import Mock | ||
295 | 11 | 13 | ||
296 | 14 | from fixtures import MockPatchObject | ||
297 | 12 | import pytz | 15 | import pytz |
298 | 16 | from storm.locals import Store | ||
299 | 13 | from testtools.matchers import ( | 17 | from testtools.matchers import ( |
300 | 14 | Equals, | 18 | Equals, |
301 | 15 | MatchesStructure, | 19 | MatchesStructure, |
302 | @@ -18,9 +22,14 @@ from zope.component import getUtility | |||
303 | 18 | from zope.security.proxy import removeSecurityProxy | 22 | from zope.security.proxy import removeSecurityProxy |
304 | 19 | 23 | ||
305 | 20 | from lp.app.enums import InformationType | 24 | from lp.app.enums import InformationType |
307 | 21 | from lp.buildmaster.enums import BuildStatus | 25 | from lp.app.interfaces.launchpad import ILaunchpadCelebrities |
308 | 26 | from lp.buildmaster.enums import ( | ||
309 | 27 | BuildQueueStatus, | ||
310 | 28 | BuildStatus, | ||
311 | 29 | ) | ||
312 | 22 | from lp.buildmaster.interfaces.buildqueue import IBuildQueue | 30 | from lp.buildmaster.interfaces.buildqueue import IBuildQueue |
313 | 23 | from lp.buildmaster.interfaces.packagebuild import IPackageBuild | 31 | from lp.buildmaster.interfaces.packagebuild import IPackageBuild |
314 | 32 | from lp.buildmaster.model.buildqueue import BuildQueue | ||
315 | 24 | from lp.code.errors import ( | 33 | from lp.code.errors import ( |
316 | 25 | GitRepositoryBlobNotFound, | 34 | GitRepositoryBlobNotFound, |
317 | 26 | GitRepositoryScanFault, | 35 | GitRepositoryScanFault, |
318 | @@ -28,12 +37,20 @@ from lp.code.errors import ( | |||
319 | 28 | from lp.code.interfaces.cibuild import ( | 37 | from lp.code.interfaces.cibuild import ( |
320 | 29 | CannotFetchConfiguration, | 38 | CannotFetchConfiguration, |
321 | 30 | CannotParseConfiguration, | 39 | CannotParseConfiguration, |
322 | 40 | CIBuildAlreadyRequested, | ||
323 | 41 | CIBuildDisallowedArchitecture, | ||
324 | 31 | ICIBuild, | 42 | ICIBuild, |
325 | 32 | ICIBuildSet, | 43 | ICIBuildSet, |
326 | 33 | MissingConfiguration, | 44 | MissingConfiguration, |
327 | 34 | ) | 45 | ) |
328 | 46 | from lp.code.model.cibuild import ( | ||
329 | 47 | determine_DASes_to_build, | ||
330 | 48 | get_all_commits_for_paths, | ||
331 | 49 | ) | ||
332 | 50 | from lp.code.model.lpcraft import load_configuration | ||
333 | 35 | from lp.code.tests.helpers import GitHostingFixture | 51 | from lp.code.tests.helpers import GitHostingFixture |
334 | 36 | from lp.registry.interfaces.series import SeriesStatus | 52 | from lp.registry.interfaces.series import SeriesStatus |
335 | 53 | from lp.services.log.logger import BufferLogger | ||
336 | 37 | from lp.services.propertycache import clear_property_cache | 54 | from lp.services.propertycache import clear_property_cache |
337 | 38 | from lp.testing import ( | 55 | from lp.testing import ( |
338 | 39 | person_logged_in, | 56 | person_logged_in, |
339 | @@ -44,6 +61,39 @@ from lp.testing.layers import LaunchpadZopelessLayer | |||
340 | 44 | from lp.testing.matchers import HasQueryCount | 61 | from lp.testing.matchers import HasQueryCount |
341 | 45 | 62 | ||
342 | 46 | 63 | ||
343 | 64 | class TestGetAllCommitsForPaths(TestCaseWithFactory): | ||
344 | 65 | |||
345 | 66 | layer = LaunchpadZopelessLayer | ||
346 | 67 | |||
347 | 68 | def test_no_refs(self): | ||
348 | 69 | repository = self.factory.makeGitRepository() | ||
349 | 70 | ref_paths = ['refs/heads/master'] | ||
350 | 71 | |||
351 | 72 | rv = get_all_commits_for_paths(repository, ref_paths) | ||
352 | 73 | |||
353 | 74 | self.assertEqual([], rv) | ||
354 | 75 | |||
355 | 76 | def test_one_ref_one_path(self): | ||
356 | 77 | repository = self.factory.makeGitRepository() | ||
357 | 78 | ref_paths = ['refs/heads/master'] | ||
358 | 79 | [ref] = self.factory.makeGitRefs(repository, ref_paths) | ||
359 | 80 | |||
360 | 81 | rv = get_all_commits_for_paths(repository, ref_paths) | ||
361 | 82 | |||
362 | 83 | self.assertEqual(1, len(rv)) | ||
363 | 84 | self.assertEqual(ref.commit_sha1, rv[0]) | ||
364 | 85 | |||
365 | 86 | def test_multiple_refs_and_paths(self): | ||
366 | 87 | repository = self.factory.makeGitRepository() | ||
367 | 88 | ref_paths = ['refs/heads/master', "refs/heads/dev"] | ||
368 | 89 | refs = self.factory.makeGitRefs(repository, ref_paths) | ||
369 | 90 | |||
370 | 91 | rv = get_all_commits_for_paths(repository, ref_paths) | ||
371 | 92 | |||
372 | 93 | self.assertEqual(2, len(rv)) | ||
373 | 94 | self.assertEqual({ref.commit_sha1 for ref in refs}, set(rv)) | ||
374 | 95 | |||
375 | 96 | |||
376 | 47 | class TestCIBuild(TestCaseWithFactory): | 97 | class TestCIBuild(TestCaseWithFactory): |
377 | 48 | 98 | ||
378 | 49 | layer = LaunchpadZopelessLayer | 99 | layer = LaunchpadZopelessLayer |
379 | @@ -336,6 +386,329 @@ class TestCIBuildSet(TestCaseWithFactory): | |||
380 | 336 | self.assertContentEqual( | 386 | self.assertContentEqual( |
381 | 337 | builds[2:], ci_build_set.findByGitRepository(repositories[1])) | 387 | builds[2:], ci_build_set.findByGitRepository(repositories[1])) |
382 | 338 | 388 | ||
383 | 389 | def test_requestCIBuild(self): | ||
384 | 390 | # requestBuild creates a new CIBuild. | ||
385 | 391 | repository = self.factory.makeGitRepository() | ||
386 | 392 | commit_sha1 = hashlib.sha1(self.factory.getUniqueBytes()).hexdigest() | ||
387 | 393 | das = self.factory.makeBuildableDistroArchSeries() | ||
388 | 394 | |||
389 | 395 | build = getUtility(ICIBuildSet).requestBuild( | ||
390 | 396 | repository, commit_sha1, das) | ||
391 | 397 | |||
392 | 398 | self.assertTrue(ICIBuild.providedBy(build)) | ||
393 | 399 | self.assertThat(build, MatchesStructure.byEquality( | ||
394 | 400 | git_repository=repository, | ||
395 | 401 | commit_sha1=commit_sha1, | ||
396 | 402 | distro_arch_series=das, | ||
397 | 403 | status=BuildStatus.NEEDSBUILD, | ||
398 | 404 | )) | ||
399 | 405 | store = Store.of(build) | ||
400 | 406 | store.flush() | ||
401 | 407 | build_queue = store.find( | ||
402 | 408 | BuildQueue, | ||
403 | 409 | BuildQueue._build_farm_job_id == | ||
404 | 410 | removeSecurityProxy(build).build_farm_job_id).one() | ||
405 | 411 | self.assertProvides(build_queue, IBuildQueue) | ||
406 | 412 | self.assertTrue(build_queue.virtualized) | ||
407 | 413 | self.assertEqual(BuildQueueStatus.WAITING, build_queue.status) | ||
408 | 414 | |||
409 | 415 | def test_requestBuild_score(self): | ||
410 | 416 | # CI builds have an initial queue score of 2600. | ||
411 | 417 | repository = self.factory.makeGitRepository() | ||
412 | 418 | commit_sha1 = hashlib.sha1(self.factory.getUniqueBytes()).hexdigest() | ||
413 | 419 | das = self.factory.makeBuildableDistroArchSeries() | ||
414 | 420 | build = getUtility(ICIBuildSet).requestBuild( | ||
415 | 421 | repository, commit_sha1, das) | ||
416 | 422 | queue_record = build.buildqueue_record | ||
417 | 423 | queue_record.score() | ||
418 | 424 | self.assertEqual(2600, queue_record.lastscore) | ||
419 | 425 | |||
420 | 426 | def test_requestBuild_rejects_repeats(self): | ||
421 | 427 | # requestBuild refuses if an identical build was already requested. | ||
422 | 428 | repository = self.factory.makeGitRepository() | ||
423 | 429 | commit_sha1 = hashlib.sha1(self.factory.getUniqueBytes()).hexdigest() | ||
424 | 430 | distro_series = self.factory.makeDistroSeries() | ||
425 | 431 | arches = [ | ||
426 | 432 | self.factory.makeBuildableDistroArchSeries( | ||
427 | 433 | distroseries=distro_series) | ||
428 | 434 | for _ in range(2)] | ||
429 | 435 | old_build = getUtility(ICIBuildSet).requestBuild( | ||
430 | 436 | repository, commit_sha1, arches[0]) | ||
431 | 437 | self.assertRaises( | ||
432 | 438 | CIBuildAlreadyRequested, getUtility(ICIBuildSet).requestBuild, | ||
433 | 439 | repository, commit_sha1, arches[0]) | ||
434 | 440 | # We can build for a different distroarchseries. | ||
435 | 441 | getUtility(ICIBuildSet).requestBuild( | ||
436 | 442 | repository, commit_sha1, arches[1]) | ||
437 | 443 | # Changing the status of the old build does not allow a new build. | ||
438 | 444 | old_build.updateStatus(BuildStatus.BUILDING) | ||
439 | 445 | old_build.updateStatus(BuildStatus.FULLYBUILT) | ||
440 | 446 | self.assertRaises( | ||
441 | 447 | CIBuildAlreadyRequested, getUtility(ICIBuildSet).requestBuild, | ||
442 | 448 | repository, commit_sha1, arches[0]) | ||
443 | 449 | |||
444 | 450 | def test_requestBuild_virtualization(self): | ||
445 | 451 | # New builds are virtualized. | ||
446 | 452 | repository = self.factory.makeGitRepository() | ||
447 | 453 | commit_sha1 = hashlib.sha1(self.factory.getUniqueBytes()).hexdigest() | ||
448 | 454 | distro_series = self.factory.makeDistroSeries() | ||
449 | 455 | for proc_nonvirt in True, False: | ||
450 | 456 | das = self.factory.makeBuildableDistroArchSeries( | ||
451 | 457 | distroseries=distro_series, supports_virtualized=True, | ||
452 | 458 | supports_nonvirtualized=proc_nonvirt) | ||
453 | 459 | build = getUtility(ICIBuildSet).requestBuild( | ||
454 | 460 | repository, commit_sha1, das) | ||
455 | 461 | self.assertTrue(build.virtualized) | ||
456 | 462 | |||
457 | 463 | def test_requestBuild_nonvirtualized(self): | ||
458 | 464 | # A non-virtualized processor cannot run a CI build. | ||
459 | 465 | repository = self.factory.makeGitRepository() | ||
460 | 466 | commit_sha1 = hashlib.sha1(self.factory.getUniqueBytes()).hexdigest() | ||
461 | 467 | distro_series = self.factory.makeDistroSeries() | ||
462 | 468 | das = self.factory.makeBuildableDistroArchSeries( | ||
463 | 469 | distroseries=distro_series, supports_virtualized=False, | ||
464 | 470 | supports_nonvirtualized=True) | ||
465 | 471 | self.assertRaises( | ||
466 | 472 | CIBuildDisallowedArchitecture, | ||
467 | 473 | getUtility(ICIBuildSet).requestBuild, repository, commit_sha1, das) | ||
468 | 474 | |||
469 | 475 | def test_requestBuildsForRefs_triggers_builds(self): | ||
470 | 476 | ubuntu = getUtility(ILaunchpadCelebrities).ubuntu | ||
471 | 477 | series = self.factory.makeDistroSeries( | ||
472 | 478 | distribution=ubuntu, | ||
473 | 479 | name="focal", | ||
474 | 480 | ) | ||
475 | 481 | self.factory.makeBuildableDistroArchSeries( | ||
476 | 482 | distroseries=series, | ||
477 | 483 | architecturetag="amd64" | ||
478 | 484 | ) | ||
479 | 485 | configuration = dedent("""\ | ||
480 | 486 | pipeline: | ||
481 | 487 | - test | ||
482 | 488 | |||
483 | 489 | jobs: | ||
484 | 490 | test: | ||
485 | 491 | series: focal | ||
486 | 492 | architectures: amd64 | ||
487 | 493 | run: echo hello world >output | ||
488 | 494 | """).encode() | ||
489 | 495 | repository = self.factory.makeGitRepository() | ||
490 | 496 | ref_paths = ['refs/heads/master'] | ||
491 | 497 | [ref] = self.factory.makeGitRefs(repository, ref_paths) | ||
492 | 498 | encoded_commit_json = { | ||
493 | 499 | "sha1": ref.commit_sha1, | ||
494 | 500 | "blobs": {".launchpad.yaml": configuration}, | ||
495 | 501 | } | ||
496 | 502 | hosting_fixture = self.useFixture( | ||
497 | 503 | GitHostingFixture(commits=[encoded_commit_json]) | ||
498 | 504 | ) | ||
499 | 505 | |||
500 | 506 | getUtility(ICIBuildSet).requestBuildsForRefs(repository, ref_paths) | ||
501 | 507 | |||
502 | 508 | self.assertEqual( | ||
503 | 509 | [((repository.getInternalPath(), [ref.commit_sha1]), | ||
504 | 510 | {"filter_paths": [".launchpad.yaml"]})], | ||
505 | 511 | hosting_fixture.getCommits.calls | ||
506 | 512 | ) | ||
507 | 513 | |||
508 | 514 | build = getUtility(ICIBuildSet).findByGitRepository(repository).one() | ||
509 | 515 | |||
510 | 516 | # check that a build was created | ||
511 | 517 | self.assertEqual(ref.commit_sha1, build.commit_sha1) | ||
512 | 518 | self.assertEqual("focal", build.distro_arch_series.distroseries.name) | ||
513 | 519 | self.assertEqual("amd64", build.distro_arch_series.architecturetag) | ||
514 | 520 | |||
515 | 521 | def test_requestBuildsForRefs_no_commits_at_all(self): | ||
516 | 522 | repository = self.factory.makeGitRepository() | ||
517 | 523 | ref_paths = ['refs/heads/master'] | ||
518 | 524 | hosting_fixture = self.useFixture(GitHostingFixture(commits=[])) | ||
519 | 525 | |||
520 | 526 | getUtility(ICIBuildSet).requestBuildsForRefs(repository, ref_paths) | ||
521 | 527 | |||
522 | 528 | self.assertEqual( | ||
523 | 529 | [((repository.getInternalPath(), []), | ||
524 | 530 | {"filter_paths": [".launchpad.yaml"]})], | ||
525 | 531 | hosting_fixture.getCommits.calls | ||
526 | 532 | ) | ||
527 | 533 | |||
528 | 534 | self.assertTrue( | ||
529 | 535 | getUtility(ICIBuildSet).findByGitRepository(repository).is_empty() | ||
530 | 536 | ) | ||
531 | 537 | |||
532 | 538 | def test_requestBuildsForRefs_no_matching_commits(self): | ||
533 | 539 | repository = self.factory.makeGitRepository() | ||
534 | 540 | ref_paths = ['refs/heads/master'] | ||
535 | 541 | [ref] = self.factory.makeGitRefs(repository, ref_paths) | ||
536 | 542 | hosting_fixture = self.useFixture( | ||
537 | 543 | GitHostingFixture(commits=[]) | ||
538 | 544 | ) | ||
539 | 545 | |||
540 | 546 | getUtility(ICIBuildSet).requestBuildsForRefs(repository, ref_paths) | ||
541 | 547 | |||
542 | 548 | self.assertEqual( | ||
543 | 549 | [((repository.getInternalPath(), [ref.commit_sha1]), | ||
544 | 550 | {"filter_paths": [".launchpad.yaml"]})], | ||
545 | 551 | hosting_fixture.getCommits.calls | ||
546 | 552 | ) | ||
547 | 553 | |||
548 | 554 | self.assertTrue( | ||
549 | 555 | getUtility(ICIBuildSet).findByGitRepository(repository).is_empty() | ||
550 | 556 | ) | ||
551 | 557 | |||
552 | 558 | def test_requestBuildsForRefs_configuration_parse_error(self): | ||
553 | 559 | ubuntu = getUtility(ILaunchpadCelebrities).ubuntu | ||
554 | 560 | series = self.factory.makeDistroSeries( | ||
555 | 561 | distribution=ubuntu, | ||
556 | 562 | name="focal", | ||
557 | 563 | ) | ||
558 | 564 | self.factory.makeBuildableDistroArchSeries( | ||
559 | 565 | distroseries=series, | ||
560 | 566 | architecturetag="amd64" | ||
561 | 567 | ) | ||
562 | 568 | configuration = dedent("""\ | ||
563 | 569 | no - valid - configuration - file | ||
564 | 570 | """).encode() | ||
565 | 571 | repository = self.factory.makeGitRepository() | ||
566 | 572 | ref_paths = ['refs/heads/master'] | ||
567 | 573 | [ref] = self.factory.makeGitRefs(repository, ref_paths) | ||
568 | 574 | encoded_commit_json = { | ||
569 | 575 | "sha1": ref.commit_sha1, | ||
570 | 576 | "blobs": {".launchpad.yaml": configuration}, | ||
571 | 577 | } | ||
572 | 578 | hosting_fixture = self.useFixture( | ||
573 | 579 | GitHostingFixture(commits=[encoded_commit_json]) | ||
574 | 580 | ) | ||
575 | 581 | logger = BufferLogger() | ||
576 | 582 | |||
577 | 583 | getUtility(ICIBuildSet).requestBuildsForRefs( | ||
578 | 584 | repository, ref_paths, logger) | ||
579 | 585 | |||
580 | 586 | self.assertEqual( | ||
581 | 587 | [((repository.getInternalPath(), [ref.commit_sha1]), | ||
582 | 588 | {"filter_paths": [".launchpad.yaml"]})], | ||
583 | 589 | hosting_fixture.getCommits.calls | ||
584 | 590 | ) | ||
585 | 591 | |||
586 | 592 | self.assertTrue( | ||
587 | 593 | getUtility(ICIBuildSet).findByGitRepository(repository).is_empty() | ||
588 | 594 | ) | ||
589 | 595 | |||
590 | 596 | self.assertEqual( | ||
591 | 597 | "ERROR Cannot parse .launchpad.yaml from %s: " | ||
592 | 598 | "Configuration file does not declare 'pipeline'\n" % ( | ||
593 | 599 | repository.unique_name,), | ||
594 | 600 | logger.getLogBuffer() | ||
595 | 601 | ) | ||
596 | 602 | |||
597 | 603 | def test_requestBuildsForRefs_build_already_scheduled(self): | ||
598 | 604 | ubuntu = getUtility(ILaunchpadCelebrities).ubuntu | ||
599 | 605 | series = self.factory.makeDistroSeries( | ||
600 | 606 | distribution=ubuntu, | ||
601 | 607 | name="focal", | ||
602 | 608 | ) | ||
603 | 609 | self.factory.makeBuildableDistroArchSeries( | ||
604 | 610 | distroseries=series, | ||
605 | 611 | architecturetag="amd64" | ||
606 | 612 | ) | ||
607 | 613 | configuration = dedent("""\ | ||
608 | 614 | pipeline: | ||
609 | 615 | - test | ||
610 | 616 | |||
611 | 617 | jobs: | ||
612 | 618 | test: | ||
613 | 619 | series: focal | ||
614 | 620 | architectures: amd64 | ||
615 | 621 | run: echo hello world >output | ||
616 | 622 | """).encode() | ||
617 | 623 | repository = self.factory.makeGitRepository() | ||
618 | 624 | ref_paths = ['refs/heads/master'] | ||
619 | 625 | [ref] = self.factory.makeGitRefs(repository, ref_paths) | ||
620 | 626 | encoded_commit_json = { | ||
621 | 627 | "sha1": ref.commit_sha1, | ||
622 | 628 | "blobs": {".launchpad.yaml": configuration}, | ||
623 | 629 | } | ||
624 | 630 | hosting_fixture = self.useFixture( | ||
625 | 631 | GitHostingFixture(commits=[encoded_commit_json]) | ||
626 | 632 | ) | ||
627 | 633 | build_set = removeSecurityProxy(getUtility(ICIBuildSet)) | ||
628 | 634 | mock = Mock(side_effect=CIBuildAlreadyRequested) | ||
629 | 635 | self.useFixture(MockPatchObject(build_set, "requestBuild", mock)) | ||
630 | 636 | logger = BufferLogger() | ||
631 | 637 | |||
632 | 638 | build_set.requestBuildsForRefs(repository, ref_paths, logger) | ||
633 | 639 | |||
634 | 640 | self.assertEqual( | ||
635 | 641 | [((repository.getInternalPath(), [ref.commit_sha1]), | ||
636 | 642 | {"filter_paths": [".launchpad.yaml"]})], | ||
637 | 643 | hosting_fixture.getCommits.calls | ||
638 | 644 | ) | ||
639 | 645 | |||
640 | 646 | self.assertTrue( | ||
641 | 647 | getUtility(ICIBuildSet).findByGitRepository(repository).is_empty() | ||
642 | 648 | ) | ||
643 | 649 | self.assertEqual( | ||
644 | 650 | "INFO Requesting CI build " | ||
645 | 651 | "for %s on focal/amd64\n" % ref.commit_sha1, | ||
646 | 652 | logger.getLogBuffer() | ||
647 | 653 | ) | ||
648 | 654 | |||
649 | 655 | def test_requestBuildsForRefs_unexpected_exception(self): | ||
650 | 656 | ubuntu = getUtility(ILaunchpadCelebrities).ubuntu | ||
651 | 657 | series = self.factory.makeDistroSeries( | ||
652 | 658 | distribution=ubuntu, | ||
653 | 659 | name="focal", | ||
654 | 660 | ) | ||
655 | 661 | self.factory.makeBuildableDistroArchSeries( | ||
656 | 662 | distroseries=series, | ||
657 | 663 | architecturetag="amd64" | ||
658 | 664 | ) | ||
659 | 665 | configuration = dedent("""\ | ||
660 | 666 | pipeline: | ||
661 | 667 | - test | ||
662 | 668 | |||
663 | 669 | jobs: | ||
664 | 670 | test: | ||
665 | 671 | series: focal | ||
666 | 672 | architectures: amd64 | ||
667 | 673 | run: echo hello world >output | ||
668 | 674 | """).encode() | ||
669 | 675 | repository = self.factory.makeGitRepository() | ||
670 | 676 | ref_paths = ['refs/heads/master'] | ||
671 | 677 | [ref] = self.factory.makeGitRefs(repository, ref_paths) | ||
672 | 678 | encoded_commit_json = { | ||
673 | 679 | "sha1": ref.commit_sha1, | ||
674 | 680 | "blobs": {".launchpad.yaml": configuration}, | ||
675 | 681 | } | ||
676 | 682 | hosting_fixture = self.useFixture( | ||
677 | 683 | GitHostingFixture(commits=[encoded_commit_json]) | ||
678 | 684 | ) | ||
679 | 685 | build_set = removeSecurityProxy(getUtility(ICIBuildSet)) | ||
680 | 686 | mock = Mock(side_effect=Exception("some unexpected error")) | ||
681 | 687 | self.useFixture(MockPatchObject(build_set, "requestBuild", mock)) | ||
682 | 688 | logger = BufferLogger() | ||
683 | 689 | |||
684 | 690 | build_set.requestBuildsForRefs(repository, ref_paths, logger) | ||
685 | 691 | |||
686 | 692 | self.assertEqual( | ||
687 | 693 | [((repository.getInternalPath(), [ref.commit_sha1]), | ||
688 | 694 | {"filter_paths": [".launchpad.yaml"]})], | ||
689 | 695 | hosting_fixture.getCommits.calls | ||
690 | 696 | ) | ||
691 | 697 | |||
692 | 698 | self.assertTrue( | ||
693 | 699 | getUtility(ICIBuildSet).findByGitRepository(repository).is_empty() | ||
694 | 700 | ) | ||
695 | 701 | |||
696 | 702 | log_line1, log_line2 = logger.getLogBuffer().splitlines() | ||
697 | 703 | self.assertEqual( | ||
698 | 704 | "INFO Requesting CI build for %s on focal/amd64" % ref.commit_sha1, | ||
699 | 705 | log_line1) | ||
700 | 706 | self.assertEqual( | ||
701 | 707 | "ERROR Failed to request CI build for %s on focal/amd64: " | ||
702 | 708 | "some unexpected error" % (ref.commit_sha1,), | ||
703 | 709 | log_line2 | ||
704 | 710 | ) | ||
705 | 711 | |||
706 | 339 | def test_deleteByGitRepository(self): | 712 | def test_deleteByGitRepository(self): |
707 | 340 | repositories = [self.factory.makeGitRepository() for _ in range(2)] | 713 | repositories = [self.factory.makeGitRepository() for _ in range(2)] |
708 | 341 | builds = [] | 714 | builds = [] |
709 | @@ -350,3 +723,116 @@ class TestCIBuildSet(TestCaseWithFactory): | |||
710 | 350 | [], ci_build_set.findByGitRepository(repositories[0])) | 723 | [], ci_build_set.findByGitRepository(repositories[0])) |
711 | 351 | self.assertContentEqual( | 724 | self.assertContentEqual( |
712 | 352 | builds[2:], ci_build_set.findByGitRepository(repositories[1])) | 725 | builds[2:], ci_build_set.findByGitRepository(repositories[1])) |
713 | 726 | |||
714 | 727 | |||
715 | 728 | class TestDetermineDASesToBuild(TestCaseWithFactory): | ||
716 | 729 | |||
717 | 730 | layer = LaunchpadZopelessLayer | ||
718 | 731 | |||
719 | 732 | def test_returns_expected_DASes(self): | ||
720 | 733 | ubuntu = getUtility(ILaunchpadCelebrities).ubuntu | ||
721 | 734 | distro_serieses = [ | ||
722 | 735 | self.factory.makeDistroSeries(ubuntu) for _ in range(2)] | ||
723 | 736 | dases = [] | ||
724 | 737 | for distro_series in distro_serieses: | ||
725 | 738 | for _ in range(2): | ||
726 | 739 | dases.append(self.factory.makeBuildableDistroArchSeries( | ||
727 | 740 | distroseries=distro_series)) | ||
728 | 741 | configuration = load_configuration(dedent("""\ | ||
729 | 742 | pipeline: | ||
730 | 743 | - [build] | ||
731 | 744 | - [test] | ||
732 | 745 | jobs: | ||
733 | 746 | build: | ||
734 | 747 | series: {distro_serieses[1].name} | ||
735 | 748 | architectures: | ||
736 | 749 | - {dases[2].architecturetag} | ||
737 | 750 | - {dases[3].architecturetag} | ||
738 | 751 | test: | ||
739 | 752 | series: {distro_serieses[1].name} | ||
740 | 753 | architectures: | ||
741 | 754 | - {dases[2].architecturetag} | ||
742 | 755 | """.format(distro_serieses=distro_serieses, dases=dases))) | ||
743 | 756 | logger = BufferLogger() | ||
744 | 757 | |||
745 | 758 | dases_to_build = list( | ||
746 | 759 | determine_DASes_to_build(configuration, logger=logger)) | ||
747 | 760 | |||
748 | 761 | self.assertContentEqual(dases[2:], dases_to_build) | ||
749 | 762 | self.assertEqual("", logger.getLogBuffer()) | ||
750 | 763 | |||
751 | 764 | |||
752 | 765 | def test_logs_missing_job_definition(self): | ||
753 | 766 | ubuntu = getUtility(ILaunchpadCelebrities).ubuntu | ||
754 | 767 | distro_series = self.factory.makeDistroSeries(ubuntu) | ||
755 | 768 | das = self.factory.makeBuildableDistroArchSeries( | ||
756 | 769 | distroseries=distro_series) | ||
757 | 770 | configuration = load_configuration(dedent("""\ | ||
758 | 771 | pipeline: | ||
759 | 772 | - [test] | ||
760 | 773 | jobs: | ||
761 | 774 | build: | ||
762 | 775 | series: {distro_series.name} | ||
763 | 776 | architectures: | ||
764 | 777 | - {das.architecturetag} | ||
765 | 778 | """.format(distro_series=distro_series, das=das))) | ||
766 | 779 | logger = BufferLogger() | ||
767 | 780 | |||
768 | 781 | dases_to_build = list( | ||
769 | 782 | determine_DASes_to_build(configuration, logger=logger)) | ||
770 | 783 | |||
771 | 784 | self.assertEqual(0, len(dases_to_build)) | ||
772 | 785 | self.assertEqual( | ||
773 | 786 | "ERROR No job definition for 'test'\n", logger.getLogBuffer() | ||
774 | 787 | ) | ||
775 | 788 | |||
776 | 789 | |||
777 | 790 | def test_logs_missing_series(self): | ||
778 | 791 | ubuntu = getUtility(ILaunchpadCelebrities).ubuntu | ||
779 | 792 | distro_series = self.factory.makeDistroSeries(ubuntu) | ||
780 | 793 | das = self.factory.makeBuildableDistroArchSeries( | ||
781 | 794 | distroseries=distro_series) | ||
782 | 795 | configuration = load_configuration(dedent("""\ | ||
783 | 796 | pipeline: | ||
784 | 797 | - [build] | ||
785 | 798 | jobs: | ||
786 | 799 | build: | ||
787 | 800 | series: unknown-series | ||
788 | 801 | architectures: | ||
789 | 802 | - {das.architecturetag} | ||
790 | 803 | """.format(das=das))) | ||
791 | 804 | logger = BufferLogger() | ||
792 | 805 | |||
793 | 806 | dases_to_build = list( | ||
794 | 807 | determine_DASes_to_build(configuration, logger=logger)) | ||
795 | 808 | |||
796 | 809 | self.assertEqual(0, len(dases_to_build)) | ||
797 | 810 | self.assertEqual( | ||
798 | 811 | "ERROR Unknown Ubuntu series name unknown-series\n", | ||
799 | 812 | logger.getLogBuffer() | ||
800 | 813 | ) | ||
801 | 814 | |||
802 | 815 | |||
803 | 816 | def test_logs_non_buildable_architecture(self): | ||
804 | 817 | ubuntu = getUtility(ILaunchpadCelebrities).ubuntu | ||
805 | 818 | distro_series = self.factory.makeDistroSeries(ubuntu) | ||
806 | 819 | configuration = load_configuration(dedent("""\ | ||
807 | 820 | pipeline: | ||
808 | 821 | - [build] | ||
809 | 822 | jobs: | ||
810 | 823 | build: | ||
811 | 824 | series: {distro_series.name} | ||
812 | 825 | architectures: | ||
813 | 826 | - non-buildable-architecture | ||
814 | 827 | """.format(distro_series=distro_series))) | ||
815 | 828 | logger = BufferLogger() | ||
816 | 829 | |||
817 | 830 | dases_to_build = list( | ||
818 | 831 | determine_DASes_to_build(configuration, logger=logger)) | ||
819 | 832 | |||
820 | 833 | self.assertEqual(0, len(dases_to_build)) | ||
821 | 834 | self.assertEqual( | ||
822 | 835 | "ERROR non-buildable-architecture is not a buildable architecture " | ||
823 | 836 | "name in Ubuntu %s\n" % distro_series.name, | ||
824 | 837 | logger.getLogBuffer() | ||
825 | 838 | ) | ||
826 | diff --git a/lib/lp/code/model/tests/test_gitrepository.py b/lib/lp/code/model/tests/test_gitrepository.py | |||
827 | index 46781f2..fd1f9eb 100644 | |||
828 | --- a/lib/lp/code/model/tests/test_gitrepository.py | |||
829 | +++ b/lib/lp/code/model/tests/test_gitrepository.py | |||
830 | @@ -11,6 +11,7 @@ import email | |||
831 | 11 | from functools import partial | 11 | from functools import partial |
832 | 12 | import hashlib | 12 | import hashlib |
833 | 13 | import json | 13 | import json |
834 | 14 | from textwrap import dedent | ||
835 | 14 | 15 | ||
836 | 15 | from breezy import urlutils | 16 | from breezy import urlutils |
837 | 16 | from fixtures import MockPatch | 17 | from fixtures import MockPatch |
838 | @@ -85,6 +86,10 @@ from lp.code.event.git import GitRefsUpdatedEvent | |||
839 | 85 | from lp.code.interfaces.branchmergeproposal import ( | 86 | from lp.code.interfaces.branchmergeproposal import ( |
840 | 86 | BRANCH_MERGE_PROPOSAL_FINAL_STATES as FINAL_STATES, | 87 | BRANCH_MERGE_PROPOSAL_FINAL_STATES as FINAL_STATES, |
841 | 87 | ) | 88 | ) |
842 | 89 | from lp.code.interfaces.cibuild import ( | ||
843 | 90 | ICIBuild, | ||
844 | 91 | ICIBuildSet, | ||
845 | 92 | ) | ||
846 | 88 | from lp.code.interfaces.codeimport import ICodeImportSet | 93 | from lp.code.interfaces.codeimport import ICodeImportSet |
847 | 89 | from lp.code.interfaces.defaultgit import ICanHasDefaultGitRepository | 94 | from lp.code.interfaces.defaultgit import ICanHasDefaultGitRepository |
848 | 90 | from lp.code.interfaces.gitjob import ( | 95 | from lp.code.interfaces.gitjob import ( |
849 | @@ -169,6 +174,7 @@ from lp.services.identity.interfaces.account import AccountStatus | |||
850 | 169 | from lp.services.job.interfaces.job import JobStatus | 174 | from lp.services.job.interfaces.job import JobStatus |
851 | 170 | from lp.services.job.model.job import Job | 175 | from lp.services.job.model.job import Job |
852 | 171 | from lp.services.job.runner import JobRunner | 176 | from lp.services.job.runner import JobRunner |
853 | 177 | from lp.services.log.logger import BufferLogger | ||
854 | 172 | from lp.services.macaroons.interfaces import IMacaroonIssuer | 178 | from lp.services.macaroons.interfaces import IMacaroonIssuer |
855 | 173 | from lp.services.macaroons.testing import ( | 179 | from lp.services.macaroons.testing import ( |
856 | 174 | find_caveats_by_name, | 180 | find_caveats_by_name, |
857 | @@ -1469,6 +1475,7 @@ class TestGitRepositoryModifications(TestCaseWithFactory): | |||
858 | 1469 | repository, "date_last_modified", UTC_NOW) | 1475 | repository, "date_last_modified", UTC_NOW) |
859 | 1470 | 1476 | ||
860 | 1471 | def test_create_ref_sets_date_last_modified(self): | 1477 | def test_create_ref_sets_date_last_modified(self): |
861 | 1478 | self.useFixture(GitHostingFixture()) | ||
862 | 1472 | repository = self.factory.makeGitRepository( | 1479 | repository = self.factory.makeGitRepository( |
863 | 1473 | date_created=datetime(2015, 6, 1, tzinfo=pytz.UTC)) | 1480 | date_created=datetime(2015, 6, 1, tzinfo=pytz.UTC)) |
864 | 1474 | [ref] = self.factory.makeGitRefs(repository=repository) | 1481 | [ref] = self.factory.makeGitRefs(repository=repository) |
865 | @@ -1882,6 +1889,7 @@ class TestGitRepositoryRefs(TestCaseWithFactory): | |||
866 | 1882 | repository.refs, repository, ["refs/heads/master"]) | 1889 | repository.refs, repository, ["refs/heads/master"]) |
867 | 1883 | 1890 | ||
868 | 1884 | def test_update(self): | 1891 | def test_update(self): |
869 | 1892 | self.useFixture(GitHostingFixture()) | ||
870 | 1885 | repository = self.factory.makeGitRepository() | 1893 | repository = self.factory.makeGitRepository() |
871 | 1886 | paths = ("refs/heads/master", "refs/tags/1.0") | 1894 | paths = ("refs/heads/master", "refs/tags/1.0") |
872 | 1887 | self.factory.makeGitRefs(repository=repository, paths=paths) | 1895 | self.factory.makeGitRefs(repository=repository, paths=paths) |
873 | @@ -1913,6 +1921,7 @@ class TestGitRepositoryRefs(TestCaseWithFactory): | |||
874 | 1913 | return [UpdatePreviewDiffJob(job) for job in jobs] | 1921 | return [UpdatePreviewDiffJob(job) for job in jobs] |
875 | 1914 | 1922 | ||
876 | 1915 | def test_update_schedules_diff_update(self): | 1923 | def test_update_schedules_diff_update(self): |
877 | 1924 | self.useFixture(GitHostingFixture()) | ||
878 | 1916 | repository = self.factory.makeGitRepository() | 1925 | repository = self.factory.makeGitRepository() |
879 | 1917 | [ref] = self.factory.makeGitRefs(repository=repository) | 1926 | [ref] = self.factory.makeGitRefs(repository=repository) |
880 | 1918 | self.assertRefsMatch(repository.refs, repository, [ref.path]) | 1927 | self.assertRefsMatch(repository.refs, repository, [ref.path]) |
881 | @@ -2223,6 +2232,7 @@ class TestGitRepositoryRefs(TestCaseWithFactory): | |||
882 | 2223 | def test_synchroniseRefs(self): | 2232 | def test_synchroniseRefs(self): |
883 | 2224 | # synchroniseRefs copes with synchronising a repository where some | 2233 | # synchroniseRefs copes with synchronising a repository where some |
884 | 2225 | # refs have been created, some deleted, and some changed. | 2234 | # refs have been created, some deleted, and some changed. |
885 | 2235 | self.useFixture(GitHostingFixture()) | ||
886 | 2226 | repository = self.factory.makeGitRepository() | 2236 | repository = self.factory.makeGitRepository() |
887 | 2227 | paths = ("refs/heads/master", "refs/heads/foo", "refs/heads/bar") | 2237 | paths = ("refs/heads/master", "refs/heads/foo", "refs/heads/bar") |
888 | 2228 | self.factory.makeGitRefs(repository=repository, paths=paths) | 2238 | self.factory.makeGitRefs(repository=repository, paths=paths) |
889 | @@ -2992,6 +3002,7 @@ class TestGitRepositoryMarkRecipesStale(TestCaseWithFactory): | |||
890 | 2992 | 3002 | ||
891 | 2993 | def test_base_repository_recipe(self): | 3003 | def test_base_repository_recipe(self): |
892 | 2994 | # On ref changes, recipes where this ref is the base become stale. | 3004 | # On ref changes, recipes where this ref is the base become stale. |
893 | 3005 | self.useFixture(GitHostingFixture()) | ||
894 | 2995 | [ref] = self.factory.makeGitRefs() | 3006 | [ref] = self.factory.makeGitRefs() |
895 | 2996 | recipe = self.factory.makeSourcePackageRecipe(branches=[ref]) | 3007 | recipe = self.factory.makeSourcePackageRecipe(branches=[ref]) |
896 | 2997 | removeSecurityProxy(recipe).is_stale = False | 3008 | removeSecurityProxy(recipe).is_stale = False |
897 | @@ -3002,6 +3013,7 @@ class TestGitRepositoryMarkRecipesStale(TestCaseWithFactory): | |||
898 | 3002 | def test_base_repository_different_ref_recipe(self): | 3013 | def test_base_repository_different_ref_recipe(self): |
899 | 3003 | # On ref changes, recipes where a different ref in the same | 3014 | # On ref changes, recipes where a different ref in the same |
900 | 3004 | # repository is the base are left alone. | 3015 | # repository is the base are left alone. |
901 | 3016 | self.useFixture(GitHostingFixture()) | ||
902 | 3005 | ref1, ref2 = self.factory.makeGitRefs( | 3017 | ref1, ref2 = self.factory.makeGitRefs( |
903 | 3006 | paths=["refs/heads/a", "refs/heads/b"]) | 3018 | paths=["refs/heads/a", "refs/heads/b"]) |
904 | 3007 | recipe = self.factory.makeSourcePackageRecipe(branches=[ref1]) | 3019 | recipe = self.factory.makeSourcePackageRecipe(branches=[ref1]) |
905 | @@ -3013,6 +3025,7 @@ class TestGitRepositoryMarkRecipesStale(TestCaseWithFactory): | |||
906 | 3013 | def test_base_repository_default_branch_recipe(self): | 3025 | def test_base_repository_default_branch_recipe(self): |
907 | 3014 | # On ref changes to the default branch, recipes where this | 3026 | # On ref changes to the default branch, recipes where this |
908 | 3015 | # repository is the base with no explicit revspec become stale. | 3027 | # repository is the base with no explicit revspec become stale. |
909 | 3028 | self.useFixture(GitHostingFixture()) | ||
910 | 3016 | repository = self.factory.makeGitRepository() | 3029 | repository = self.factory.makeGitRepository() |
911 | 3017 | ref1, ref2 = self.factory.makeGitRefs( | 3030 | ref1, ref2 = self.factory.makeGitRefs( |
912 | 3018 | repository=repository, paths=["refs/heads/a", "refs/heads/b"]) | 3031 | repository=repository, paths=["refs/heads/a", "refs/heads/b"]) |
913 | @@ -3028,6 +3041,7 @@ class TestGitRepositoryMarkRecipesStale(TestCaseWithFactory): | |||
914 | 3028 | 3041 | ||
915 | 3029 | def test_instruction_repository_recipe(self): | 3042 | def test_instruction_repository_recipe(self): |
916 | 3030 | # On ref changes, recipes including this ref become stale. | 3043 | # On ref changes, recipes including this ref become stale. |
917 | 3044 | self.useFixture(GitHostingFixture()) | ||
918 | 3031 | [base_ref] = self.factory.makeGitRefs() | 3045 | [base_ref] = self.factory.makeGitRefs() |
919 | 3032 | [ref] = self.factory.makeGitRefs() | 3046 | [ref] = self.factory.makeGitRefs() |
920 | 3033 | recipe = self.factory.makeSourcePackageRecipe(branches=[base_ref, ref]) | 3047 | recipe = self.factory.makeSourcePackageRecipe(branches=[base_ref, ref]) |
921 | @@ -3039,6 +3053,7 @@ class TestGitRepositoryMarkRecipesStale(TestCaseWithFactory): | |||
922 | 3039 | def test_instruction_repository_different_ref_recipe(self): | 3053 | def test_instruction_repository_different_ref_recipe(self): |
923 | 3040 | # On ref changes, recipes including a different ref in the same | 3054 | # On ref changes, recipes including a different ref in the same |
924 | 3041 | # repository are left alone. | 3055 | # repository are left alone. |
925 | 3056 | self.useFixture(GitHostingFixture()) | ||
926 | 3042 | [base_ref] = self.factory.makeGitRefs() | 3057 | [base_ref] = self.factory.makeGitRefs() |
927 | 3043 | ref1, ref2 = self.factory.makeGitRefs( | 3058 | ref1, ref2 = self.factory.makeGitRefs( |
928 | 3044 | paths=["refs/heads/a", "refs/heads/b"]) | 3059 | paths=["refs/heads/a", "refs/heads/b"]) |
929 | @@ -3052,6 +3067,7 @@ class TestGitRepositoryMarkRecipesStale(TestCaseWithFactory): | |||
930 | 3052 | def test_instruction_repository_default_branch_recipe(self): | 3067 | def test_instruction_repository_default_branch_recipe(self): |
931 | 3053 | # On ref changes to the default branch, recipes including this | 3068 | # On ref changes to the default branch, recipes including this |
932 | 3054 | # repository with no explicit revspec become stale. | 3069 | # repository with no explicit revspec become stale. |
933 | 3070 | self.useFixture(GitHostingFixture()) | ||
934 | 3055 | [base_ref] = self.factory.makeGitRefs() | 3071 | [base_ref] = self.factory.makeGitRefs() |
935 | 3056 | repository = self.factory.makeGitRepository() | 3072 | repository = self.factory.makeGitRepository() |
936 | 3057 | ref1, ref2 = self.factory.makeGitRefs( | 3073 | ref1, ref2 = self.factory.makeGitRefs( |
937 | @@ -3069,6 +3085,7 @@ class TestGitRepositoryMarkRecipesStale(TestCaseWithFactory): | |||
938 | 3069 | 3085 | ||
939 | 3070 | def test_unrelated_repository_recipe(self): | 3086 | def test_unrelated_repository_recipe(self): |
940 | 3071 | # On ref changes, unrelated recipes are left alone. | 3087 | # On ref changes, unrelated recipes are left alone. |
941 | 3088 | self.useFixture(GitHostingFixture()) | ||
942 | 3072 | [ref] = self.factory.makeGitRefs() | 3089 | [ref] = self.factory.makeGitRefs() |
943 | 3073 | recipe = self.factory.makeSourcePackageRecipe( | 3090 | recipe = self.factory.makeSourcePackageRecipe( |
944 | 3074 | branches=self.factory.makeGitRefs()) | 3091 | branches=self.factory.makeGitRefs()) |
945 | @@ -3084,6 +3101,7 @@ class TestGitRepositoryMarkSnapsStale(TestCaseWithFactory): | |||
946 | 3084 | 3101 | ||
947 | 3085 | def test_same_repository(self): | 3102 | def test_same_repository(self): |
948 | 3086 | # On ref changes, snap packages using this ref become stale. | 3103 | # On ref changes, snap packages using this ref become stale. |
949 | 3104 | self.useFixture(GitHostingFixture()) | ||
950 | 3087 | [ref] = self.factory.makeGitRefs() | 3105 | [ref] = self.factory.makeGitRefs() |
951 | 3088 | snap = self.factory.makeSnap(git_ref=ref) | 3106 | snap = self.factory.makeSnap(git_ref=ref) |
952 | 3089 | removeSecurityProxy(snap).is_stale = False | 3107 | removeSecurityProxy(snap).is_stale = False |
953 | @@ -3094,6 +3112,7 @@ class TestGitRepositoryMarkSnapsStale(TestCaseWithFactory): | |||
954 | 3094 | def test_same_repository_different_ref(self): | 3112 | def test_same_repository_different_ref(self): |
955 | 3095 | # On ref changes, snap packages using a different ref in the same | 3113 | # On ref changes, snap packages using a different ref in the same |
956 | 3096 | # repository are left alone. | 3114 | # repository are left alone. |
957 | 3115 | self.useFixture(GitHostingFixture()) | ||
958 | 3097 | ref1, ref2 = self.factory.makeGitRefs( | 3116 | ref1, ref2 = self.factory.makeGitRefs( |
959 | 3098 | paths=["refs/heads/a", "refs/heads/b"]) | 3117 | paths=["refs/heads/a", "refs/heads/b"]) |
960 | 3099 | snap = self.factory.makeSnap(git_ref=ref1) | 3118 | snap = self.factory.makeSnap(git_ref=ref1) |
961 | @@ -3104,6 +3123,7 @@ class TestGitRepositoryMarkSnapsStale(TestCaseWithFactory): | |||
962 | 3104 | 3123 | ||
963 | 3105 | def test_different_repository(self): | 3124 | def test_different_repository(self): |
964 | 3106 | # On ref changes, unrelated snap packages are left alone. | 3125 | # On ref changes, unrelated snap packages are left alone. |
965 | 3126 | self.useFixture(GitHostingFixture()) | ||
966 | 3107 | [ref] = self.factory.makeGitRefs() | 3127 | [ref] = self.factory.makeGitRefs() |
967 | 3108 | snap = self.factory.makeSnap(git_ref=self.factory.makeGitRefs()[0]) | 3128 | snap = self.factory.makeSnap(git_ref=self.factory.makeGitRefs()[0]) |
968 | 3109 | removeSecurityProxy(snap).is_stale = False | 3129 | removeSecurityProxy(snap).is_stale = False |
969 | @@ -3113,6 +3133,7 @@ class TestGitRepositoryMarkSnapsStale(TestCaseWithFactory): | |||
970 | 3113 | 3133 | ||
971 | 3114 | def test_private_snap(self): | 3134 | def test_private_snap(self): |
972 | 3115 | # A private snap should be able to be marked stale | 3135 | # A private snap should be able to be marked stale |
973 | 3136 | self.useFixture(GitHostingFixture()) | ||
974 | 3116 | self.useFixture(FeatureFixture(SNAP_TESTING_FLAGS)) | 3137 | self.useFixture(FeatureFixture(SNAP_TESTING_FLAGS)) |
975 | 3117 | [ref] = self.factory.makeGitRefs() | 3138 | [ref] = self.factory.makeGitRefs() |
976 | 3118 | snap = self.factory.makeSnap(git_ref=ref, private=True) | 3139 | snap = self.factory.makeSnap(git_ref=ref, private=True) |
977 | @@ -3135,6 +3156,7 @@ class TestGitRepositoryMarkCharmRecipesStale(TestCaseWithFactory): | |||
978 | 3135 | 3156 | ||
979 | 3136 | def test_same_repository(self): | 3157 | def test_same_repository(self): |
980 | 3137 | # On ref changes, charm recipes using this ref become stale. | 3158 | # On ref changes, charm recipes using this ref become stale. |
981 | 3159 | self.useFixture(GitHostingFixture()) | ||
982 | 3138 | [ref] = self.factory.makeGitRefs() | 3160 | [ref] = self.factory.makeGitRefs() |
983 | 3139 | recipe = self.factory.makeCharmRecipe(git_ref=ref) | 3161 | recipe = self.factory.makeCharmRecipe(git_ref=ref) |
984 | 3140 | removeSecurityProxy(recipe).is_stale = False | 3162 | removeSecurityProxy(recipe).is_stale = False |
985 | @@ -3145,6 +3167,7 @@ class TestGitRepositoryMarkCharmRecipesStale(TestCaseWithFactory): | |||
986 | 3145 | def test_same_repository_different_ref(self): | 3167 | def test_same_repository_different_ref(self): |
987 | 3146 | # On ref changes, charm recipes using a different ref in the same | 3168 | # On ref changes, charm recipes using a different ref in the same |
988 | 3147 | # repository are left alone. | 3169 | # repository are left alone. |
989 | 3170 | self.useFixture(GitHostingFixture()) | ||
990 | 3148 | ref1, ref2 = self.factory.makeGitRefs( | 3171 | ref1, ref2 = self.factory.makeGitRefs( |
991 | 3149 | paths=["refs/heads/a", "refs/heads/b"]) | 3172 | paths=["refs/heads/a", "refs/heads/b"]) |
992 | 3150 | recipe = self.factory.makeCharmRecipe(git_ref=ref1) | 3173 | recipe = self.factory.makeCharmRecipe(git_ref=ref1) |
993 | @@ -3155,6 +3178,7 @@ class TestGitRepositoryMarkCharmRecipesStale(TestCaseWithFactory): | |||
994 | 3155 | 3178 | ||
995 | 3156 | def test_different_repository(self): | 3179 | def test_different_repository(self): |
996 | 3157 | # On ref changes, unrelated charm recipes are left alone. | 3180 | # On ref changes, unrelated charm recipes are left alone. |
997 | 3181 | self.useFixture(GitHostingFixture()) | ||
998 | 3158 | [ref] = self.factory.makeGitRefs() | 3182 | [ref] = self.factory.makeGitRefs() |
999 | 3159 | recipe = self.factory.makeCharmRecipe( | 3183 | recipe = self.factory.makeCharmRecipe( |
1000 | 3160 | git_ref=self.factory.makeGitRefs()[0]) | 3184 | git_ref=self.factory.makeGitRefs()[0]) |
1001 | @@ -3355,6 +3379,81 @@ class TestGitRepositoryDetectMerges(TestCaseWithFactory): | |||
1002 | 3355 | for event in events[:2]}) | 3379 | for event in events[:2]}) |
1003 | 3356 | 3380 | ||
1004 | 3357 | 3381 | ||
1005 | 3382 | class TestGitRepositoryRequestCIBuilds(TestCaseWithFactory): | ||
1006 | 3383 | |||
1007 | 3384 | layer = ZopelessDatabaseLayer | ||
1008 | 3385 | |||
1009 | 3386 | def test_findByGitRepository_with_configuration(self): | ||
1010 | 3387 | # If a changed ref has CI configuration, we request CI builds. | ||
1011 | 3388 | logger = BufferLogger() | ||
1012 | 3389 | [ref] = self.factory.makeGitRefs() | ||
1013 | 3390 | ubuntu = getUtility(ILaunchpadCelebrities).ubuntu | ||
1014 | 3391 | distroseries = self.factory.makeDistroSeries(distribution=ubuntu) | ||
1015 | 3392 | dases = [ | ||
1016 | 3393 | self.factory.makeBuildableDistroArchSeries( | ||
1017 | 3394 | distroseries=distroseries) | ||
1018 | 3395 | for _ in range(2)] | ||
1019 | 3396 | configuration = dedent("""\ | ||
1020 | 3397 | pipeline: [test] | ||
1021 | 3398 | jobs: | ||
1022 | 3399 | test: | ||
1023 | 3400 | series: {series} | ||
1024 | 3401 | architectures: [{architectures}] | ||
1025 | 3402 | """.format( | ||
1026 | 3403 | series=distroseries.name, | ||
1027 | 3404 | architectures=", ".join( | ||
1028 | 3405 | das.architecturetag for das in dases))).encode() | ||
1029 | 3406 | new_commit = hashlib.sha1(self.factory.getUniqueBytes()).hexdigest() | ||
1030 | 3407 | self.useFixture(GitHostingFixture(commits=[ | ||
1031 | 3408 | { | ||
1032 | 3409 | "sha1": new_commit, | ||
1033 | 3410 | "blobs": {".launchpad.yaml": configuration}, | ||
1034 | 3411 | }, | ||
1035 | 3412 | ])) | ||
1036 | 3413 | with dbuser("branchscanner"): | ||
1037 | 3414 | ref.repository.createOrUpdateRefs( | ||
1038 | 3415 | {ref.path: {"sha1": new_commit, "type": GitObjectType.COMMIT}}, | ||
1039 | 3416 | logger=logger) | ||
1040 | 3417 | |||
1041 | 3418 | results = getUtility(ICIBuildSet).findByGitRepository(ref.repository) | ||
1042 | 3419 | for result in results: | ||
1043 | 3420 | self.assertTrue(ICIBuild.providedBy(result)) | ||
1044 | 3421 | |||
1045 | 3422 | self.assertThat( | ||
1046 | 3423 | results, | ||
1047 | 3424 | MatchesSetwise(*( | ||
1048 | 3425 | MatchesStructure.byEquality( | ||
1049 | 3426 | git_repository=ref.repository, | ||
1050 | 3427 | commit_sha1=new_commit, | ||
1051 | 3428 | distro_arch_series=das) | ||
1052 | 3429 | for das in dases))) | ||
1053 | 3430 | self.assertContentEqual( | ||
1054 | 3431 | [ | ||
1055 | 3432 | "INFO Requesting CI build for {commit} on " | ||
1056 | 3433 | "{series}/{arch}".format( | ||
1057 | 3434 | commit=new_commit, series=distroseries.name, | ||
1058 | 3435 | arch=das.architecturetag) | ||
1059 | 3436 | for das in dases], | ||
1060 | 3437 | logger.getLogBuffer().splitlines()) | ||
1061 | 3438 | |||
1062 | 3439 | def test_findByGitRepository_without_configuration(self): | ||
1063 | 3440 | # If a changed ref has no CI configuration, we do not request CI | ||
1064 | 3441 | # builds. | ||
1065 | 3442 | logger = BufferLogger() | ||
1066 | 3443 | [ref] = self.factory.makeGitRefs() | ||
1067 | 3444 | new_commit = hashlib.sha1(self.factory.getUniqueBytes()).hexdigest() | ||
1068 | 3445 | self.useFixture(GitHostingFixture(commits=[])) | ||
1069 | 3446 | with dbuser("branchscanner"): | ||
1070 | 3447 | ref.repository.createOrUpdateRefs( | ||
1071 | 3448 | {ref.path: {"sha1": new_commit, "type": GitObjectType.COMMIT}}, | ||
1072 | 3449 | logger=logger) | ||
1073 | 3450 | self.assertTrue( | ||
1074 | 3451 | getUtility( | ||
1075 | 3452 | ICIBuildSet).findByGitRepository(ref.repository).is_empty() | ||
1076 | 3453 | ) | ||
1077 | 3454 | self.assertEqual("", logger.getLogBuffer()) | ||
1078 | 3455 | |||
1079 | 3456 | |||
1080 | 3358 | class TestGitRepositoryGetBlob(TestCaseWithFactory): | 3457 | class TestGitRepositoryGetBlob(TestCaseWithFactory): |
1081 | 3359 | """Tests for retrieving files from a Git repository.""" | 3458 | """Tests for retrieving files from a Git repository.""" |
1082 | 3360 | 3459 | ||
1083 | diff --git a/lib/lp/code/subscribers/git.py b/lib/lp/code/subscribers/git.py | |||
1084 | index 30b432e..831ac49 100644 | |||
1085 | --- a/lib/lp/code/subscribers/git.py | |||
1086 | +++ b/lib/lp/code/subscribers/git.py | |||
1087 | @@ -1,8 +1,13 @@ | |||
1089 | 1 | # Copyright 2015-2016 Canonical Ltd. This software is licensed under the | 1 | # Copyright 2015-2022 Canonical Ltd. This software is licensed under the |
1090 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). |
1091 | 3 | 3 | ||
1092 | 4 | """Event subscribers for Git repositories.""" | 4 | """Event subscribers for Git repositories.""" |
1093 | 5 | 5 | ||
1094 | 6 | from zope.component import getUtility | ||
1095 | 7 | |||
1096 | 8 | from lp.code.interfaces.cibuild import ICIBuildSet | ||
1097 | 9 | |||
1098 | 10 | |||
1099 | 6 | def refs_updated(repository, event): | 11 | def refs_updated(repository, event): |
1100 | 7 | """Some references in a Git repository have been updated.""" | 12 | """Some references in a Git repository have been updated.""" |
1101 | 8 | repository.updateMergeCommitIDs(event.paths) | 13 | repository.updateMergeCommitIDs(event.paths) |
1102 | @@ -11,3 +16,5 @@ def refs_updated(repository, event): | |||
1103 | 11 | repository.markSnapsStale(event.paths) | 16 | repository.markSnapsStale(event.paths) |
1104 | 12 | repository.markCharmRecipesStale(event.paths) | 17 | repository.markCharmRecipesStale(event.paths) |
1105 | 13 | repository.detectMerges(event.paths, logger=event.logger) | 18 | repository.detectMerges(event.paths, logger=event.logger) |
1106 | 19 | getUtility(ICIBuildSet).requestBuildsForRefs( | ||
1107 | 20 | repository, event.paths, logger=event.logger) | ||
1108 | diff --git a/lib/lp/testing/factory.py b/lib/lp/testing/factory.py | |||
1109 | index 8e0ec69..e145133 100644 | |||
1110 | --- a/lib/lp/testing/factory.py | |||
1111 | +++ b/lib/lp/testing/factory.py | |||
1112 | @@ -97,11 +97,15 @@ from lp.bugs.interfaces.cve import ( | |||
1113 | 97 | ) | 97 | ) |
1114 | 98 | from lp.bugs.model.bug import FileBugData | 98 | from lp.bugs.model.bug import FileBugData |
1115 | 99 | from lp.buildmaster.enums import ( | 99 | from lp.buildmaster.enums import ( |
1116 | 100 | BuildBaseImageType, | ||
1117 | 100 | BuilderResetProtocol, | 101 | BuilderResetProtocol, |
1118 | 101 | BuildStatus, | 102 | BuildStatus, |
1119 | 102 | ) | 103 | ) |
1120 | 103 | from lp.buildmaster.interfaces.builder import IBuilderSet | 104 | from lp.buildmaster.interfaces.builder import IBuilderSet |
1122 | 104 | from lp.buildmaster.interfaces.processor import IProcessorSet | 105 | from lp.buildmaster.interfaces.processor import ( |
1123 | 106 | IProcessorSet, | ||
1124 | 107 | ProcessorNotFound, | ||
1125 | 108 | ) | ||
1126 | 105 | from lp.charms.interfaces.charmbase import ICharmBaseSet | 109 | from lp.charms.interfaces.charmbase import ICharmBaseSet |
1127 | 106 | from lp.charms.interfaces.charmrecipe import ICharmRecipeSet | 110 | from lp.charms.interfaces.charmrecipe import ICharmRecipeSet |
1128 | 107 | from lp.charms.interfaces.charmrecipebuild import ICharmRecipeBuildSet | 111 | from lp.charms.interfaces.charmrecipebuild import ICharmRecipeBuildSet |
1129 | @@ -2932,6 +2936,34 @@ class BareLaunchpadObjectFactory(ObjectFactory): | |||
1130 | 2932 | return distroseries.newArch( | 2936 | return distroseries.newArch( |
1131 | 2933 | architecturetag, processor, official, owner, enabled) | 2937 | architecturetag, processor, official, owner, enabled) |
1132 | 2934 | 2938 | ||
1133 | 2939 | def makeBuildableDistroArchSeries(self, architecturetag=None, | ||
1134 | 2940 | processor=None, | ||
1135 | 2941 | supports_virtualized=True, | ||
1136 | 2942 | supports_nonvirtualized=True, **kwargs): | ||
1137 | 2943 | if architecturetag is None: | ||
1138 | 2944 | architecturetag = self.getUniqueUnicode("arch") | ||
1139 | 2945 | if processor is None: | ||
1140 | 2946 | try: | ||
1141 | 2947 | processor = getUtility(IProcessorSet).getByName( | ||
1142 | 2948 | architecturetag) | ||
1143 | 2949 | except ProcessorNotFound: | ||
1144 | 2950 | processor = self.makeProcessor( | ||
1145 | 2951 | name=architecturetag, | ||
1146 | 2952 | supports_virtualized=supports_virtualized, | ||
1147 | 2953 | supports_nonvirtualized=supports_nonvirtualized) | ||
1148 | 2954 | das = self.makeDistroArchSeries( | ||
1149 | 2955 | architecturetag=architecturetag, processor=processor, **kwargs) | ||
1150 | 2956 | # Add both a chroot and a LXD image to test that | ||
1151 | 2957 | # getAllowedArchitectures doesn't get confused by multiple | ||
1152 | 2958 | # PocketChroot rows for a single DistroArchSeries. | ||
1153 | 2959 | fake_chroot = self.makeLibraryFileAlias( | ||
1154 | 2960 | filename="fake_chroot.tar.gz", db_only=True) | ||
1155 | 2961 | das.addOrUpdateChroot(fake_chroot) | ||
1156 | 2962 | fake_lxd = self.makeLibraryFileAlias( | ||
1157 | 2963 | filename="fake_lxd.tar.gz", db_only=True) | ||
1158 | 2964 | das.addOrUpdateChroot(fake_lxd, image_type=BuildBaseImageType.LXD) | ||
1159 | 2965 | return das | ||
1160 | 2966 | |||
1161 | 2935 | def makeComponent(self, name=None): | 2967 | def makeComponent(self, name=None): |
1162 | 2936 | """Make a new `IComponent`.""" | 2968 | """Make a new `IComponent`.""" |
1163 | 2937 | if name is None: | 2969 | if name is None: |
@Colin: While the biggest part of the code is still the one you prepared, maybe you can have a look at what I added and give me feedback on my suggestions.