Merge ~jugmac00/launchpad:create-lpcraft-jobs-on-push into launchpad: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)
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

To post a comment you must log in.
Revision history for this message
Jürgen Gmach (jugmac00) wrote :

@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.

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
diff --git a/lib/lp/code/interfaces/cibuild.py b/lib/lp/code/interfaces/cibuild.py
index 4537b1c..e1abcf8 100644
--- a/lib/lp/code/interfaces/cibuild.py
+++ b/lib/lp/code/interfaces/cibuild.py
@@ -6,11 +6,16 @@
6__all__ = [6__all__ = [
7 "CannotFetchConfiguration",7 "CannotFetchConfiguration",
8 "CannotParseConfiguration",8 "CannotParseConfiguration",
9 "CIBuildAlreadyRequested",
10 "CIBuildDisallowedArchitecture",
9 "ICIBuild",11 "ICIBuild",
10 "ICIBuildSet",12 "ICIBuildSet",
11 "MissingConfiguration",13 "MissingConfiguration",
12 ]14 ]
1315
16import http.client
17
18from lazr.restful.declarations import error_status
14from lazr.restful.fields import Reference19from lazr.restful.fields import Reference
15from zope.schema import (20from zope.schema import (
16 Bool,21 Bool,
@@ -53,6 +58,26 @@ class CannotParseConfiguration(Exception):
53 """Launchpad cannot parse this CI build's .launchpad.yaml."""58 """Launchpad cannot parse this CI build's .launchpad.yaml."""
5459
5560
61@error_status(http.client.BAD_REQUEST)
62class CIBuildDisallowedArchitecture(Exception):
63 """A build was requested for a disallowed architecture."""
64
65 def __init__(self, das, pocket):
66 super().__init__(
67 "Builds for %s/%s are not allowed." % (
68 das.distroseries.getSuite(pocket), das.architecturetag)
69 )
70
71
72@error_status(http.client.BAD_REQUEST)
73class CIBuildAlreadyRequested(Exception):
74 """An identical build was requested more than once."""
75
76 def __init__(self):
77 super().__init__(
78 "An identical build for this commit was already requested.")
79
80
56class ICIBuildView(IPackageBuildView):81class ICIBuildView(IPackageBuildView):
57 """`ICIBuild` attributes that require launchpad.View."""82 """`ICIBuild` attributes that require launchpad.View."""
5883
@@ -133,6 +158,37 @@ class ICIBuildSet(ISpecificBuildFarmJobSource):
133 these Git commit IDs.158 these Git commit IDs.
134 """159 """
135160
161 def requestBuild(git_repository, commit_sha1, distro_arch_series):
162 """Request a CI build.
163
164 This checks that the architecture is allowed and that there isn't
165 already a matching pending build.
166
167 :param git_repository: The `IGitRepository` for the new build.
168 :param commit_sha1: The Git commit ID for the new build.
169 :param distro_arch_series: The `IDistroArchSeries` that the new
170 build should run on.
171 :raises CIBuildDisallowedArchitecture: if builds on
172 `distro_arch_series` are not allowed.
173 :raises CIBuildAlreadyRequested: if a matching build was already
174 requested.
175 :return: `ICIBuild`.
176 """
177
178 def requestBuildsForRefs(git_repository, ref_paths, logger=None):
179 """Request CI builds for a collection of refs.
180
181 This fetches `.launchpad.yaml` from the repository and parses it to
182 work out which series/architectures need builds.
183
184 :param git_repository: The `IGitRepository` for which to request
185 builds.
186 :param ref_paths: A collection of Git reference paths within
187 `git_repository`; builds will be requested for the commits that
188 each of them points to.
189 :param logger: An optional logger.
190 """
191
136 def deleteByGitRepository(git_repository):192 def deleteByGitRepository(git_repository):
137 """Delete all CI builds for the given Git repository.193 """Delete all CI builds for the given Git repository.
138194
diff --git a/lib/lp/code/model/cibuild.py b/lib/lp/code/model/cibuild.py
index f9c9031..bdc570c 100644
--- a/lib/lp/code/model/cibuild.py
+++ b/lib/lp/code/model/cibuild.py
@@ -9,6 +9,7 @@ __all__ = [
99
10from datetime import timedelta10from datetime import timedelta
1111
12from lazr.lifecycle.event import ObjectCreatedEvent
12import pytz13import pytz
13from storm.locals import (14from storm.locals import (
14 Bool,15 Bool,
@@ -21,8 +22,11 @@ from storm.locals import (
21 )22 )
22from storm.store import EmptyResultSet23from storm.store import EmptyResultSet
23from zope.component import getUtility24from zope.component import getUtility
25from zope.event import notify
24from zope.interface import implementer26from zope.interface import implementer
2527
28from lp.app.errors import NotFoundError
29from lp.app.interfaces.launchpad import ILaunchpadCelebrities
26from lp.buildmaster.enums import (30from lp.buildmaster.enums import (
27 BuildFarmJobType,31 BuildFarmJobType,
28 BuildQueueStatus,32 BuildQueueStatus,
@@ -38,10 +42,14 @@ from lp.code.errors import (
38from lp.code.interfaces.cibuild import (42from lp.code.interfaces.cibuild import (
39 CannotFetchConfiguration,43 CannotFetchConfiguration,
40 CannotParseConfiguration,44 CannotParseConfiguration,
45 CIBuildAlreadyRequested,
46 CIBuildDisallowedArchitecture,
41 ICIBuild,47 ICIBuild,
42 ICIBuildSet,48 ICIBuildSet,
43 MissingConfiguration,49 MissingConfiguration,
44 )50 )
51from lp.code.interfaces.githosting import IGitHostingClient
52from lp.code.model.gitref import GitRef
45from lp.code.model.lpcraft import load_configuration53from lp.code.model.lpcraft import load_configuration
46from lp.registry.interfaces.pocket import PackagePublishingPocket54from lp.registry.interfaces.pocket import PackagePublishingPocket
47from lp.registry.interfaces.series import SeriesStatus55from lp.registry.interfaces.series import SeriesStatus
@@ -65,6 +73,53 @@ from lp.services.propertycache import cachedproperty
65from lp.soyuz.model.distroarchseries import DistroArchSeries73from lp.soyuz.model.distroarchseries import DistroArchSeries
6674
6775
76def determine_DASes_to_build(configuration, logger=None):
77 """Generate distroarchseries to build for this configuration."""
78 architectures_by_series = {}
79 for stage in configuration.pipeline:
80 for job_name in stage:
81 if job_name not in configuration.jobs:
82 if logger is not None:
83 logger.error("No job definition for %r", job_name)
84 continue
85 for job in configuration.jobs[job_name]:
86 for architecture in job["architectures"]:
87 architectures_by_series.setdefault(
88 job["series"], set()).add(architecture)
89 # XXX cjwatson 2022-01-21: We have to hardcode Ubuntu for now, since
90 # the .launchpad.yaml format doesn't currently support other
91 # distributions (although nor does the Launchpad build farm).
92 distribution = getUtility(ILaunchpadCelebrities).ubuntu
93 for series_name, architecture_names in architectures_by_series.items():
94 try:
95 series = distribution[series_name]
96 except NotFoundError:
97 if logger is not None:
98 logger.error("Unknown Ubuntu series name %s" % series_name)
99 continue
100 architectures = {
101 das.architecturetag: das
102 for das in series.buildable_architectures}
103 for architecture_name in architecture_names:
104 try:
105 das = architectures[architecture_name]
106 except KeyError:
107 if logger is not None:
108 logger.error(
109 "%s is not a buildable architecture name in "
110 "Ubuntu %s" % (architecture_name, series_name))
111 continue
112 yield das
113
114
115def get_all_commits_for_paths(git_repository, paths):
116 return [
117 ref.commit_sha1
118 for ref in GitRef.findByReposAndPaths(
119 [(git_repository, ref_path)
120 for ref_path in paths]).values()]
121
122
68def parse_configuration(git_repository, blob):123def parse_configuration(git_repository, blob):
69 try:124 try:
70 return load_configuration(blob)125 return load_configuration(blob)
@@ -329,6 +384,89 @@ class CIBuildSet(SpecificBuildFarmJobSourceMixin):
329 store.flush()384 store.flush()
330 return cibuild385 return cibuild
331386
387 def findByGitRepository(self, git_repository, commit_sha1s=None):
388 """See `ICIBuildSet`."""
389 clauses = [CIBuild.git_repository == git_repository]
390 if commit_sha1s is not None:
391 clauses.append(CIBuild.commit_sha1.is_in(commit_sha1s))
392 return IStore(CIBuild).find(CIBuild, *clauses)
393
394 def _isBuildableArchitectureAllowed(self, das):
395 """Check whether we may build for a buildable `DistroArchSeries`.
396
397 The caller is assumed to have already checked that a suitable chroot
398 is available (either directly or via
399 `DistroSeries.buildable_architectures`).
400 """
401 return (
402 das.enabled
403 # We only support builds on virtualized builders at the moment.
404 and das.processor.supports_virtualized)
405
406 def _isArchitectureAllowed(self, das, pocket, snap_base=None):
407 return (
408 das.getChroot(pocket=pocket) is not None
409 and self._isBuildableArchitectureAllowed(das))
410
411 def requestBuild(self, git_repository, commit_sha1, distro_arch_series):
412 """See `ICIBuildSet`."""
413 pocket = PackagePublishingPocket.UPDATES
414 if not self._isArchitectureAllowed(distro_arch_series, pocket):
415 raise CIBuildDisallowedArchitecture(distro_arch_series, pocket)
416
417 result = IStore(CIBuild).find(
418 CIBuild,
419 CIBuild.git_repository == git_repository,
420 CIBuild.commit_sha1 == commit_sha1,
421 CIBuild.distro_arch_series == distro_arch_series)
422 if not result.is_empty():
423 raise CIBuildAlreadyRequested
424
425 build = self.new(git_repository, commit_sha1, distro_arch_series)
426 build.queueBuild()
427 notify(ObjectCreatedEvent(build))
428 return build
429
430 def _tryToRequestBuild(self, git_repository, commit_sha1, das, logger):
431 try:
432 if logger is not None:
433 logger.info(
434 "Requesting CI build for %s on %s/%s",
435 commit_sha1, das.distroseries.name, das.architecturetag,
436 )
437 self.requestBuild(git_repository, commit_sha1, das)
438 except CIBuildAlreadyRequested:
439 pass
440 except Exception as e:
441 if logger is not None:
442 logger.error(
443 "Failed to request CI build for %s on %s/%s: %s",
444 commit_sha1, das.distroseries.name, das.architecturetag, e
445 )
446
447 def requestBuildsForRefs(self, git_repository, ref_paths, logger=None):
448 """See `ICIBuildSet`."""
449 commit_sha1s = get_all_commits_for_paths(git_repository, ref_paths)
450 # getCommits performs a web request!
451 commits = getUtility(IGitHostingClient).getCommits(
452 git_repository.getInternalPath(), commit_sha1s,
453 # XXX cjwatson 2022-01-19: We should also fetch
454 # debian/.launchpad.yaml (or perhaps make the path a property of
455 # the repository) once lpcraft and launchpad-buildd support
456 # using alternative paths for builds.
457 filter_paths=[".launchpad.yaml"])
458 for commit in commits:
459 try:
460 configuration = parse_configuration(
461 git_repository, commit["blobs"][".launchpad.yaml"])
462 except CannotParseConfiguration as e:
463 if logger is not None:
464 logger.error(e)
465 continue
466 for das in determine_DASes_to_build(configuration):
467 self._tryToRequestBuild(
468 git_repository, commit["sha1"], das, logger)
469
332 def getByID(self, build_id):470 def getByID(self, build_id):
333 """See `ISpecificBuildFarmJobSource`."""471 """See `ISpecificBuildFarmJobSource`."""
334 store = IMasterStore(CIBuild)472 store = IMasterStore(CIBuild)
@@ -357,13 +495,6 @@ class CIBuildSet(SpecificBuildFarmJobSourceMixin):
357 bfj.id for bfj in build_farm_jobs))495 bfj.id for bfj in build_farm_jobs))
358 return DecoratedResultSet(rows, pre_iter_hook=self.preloadBuildsData)496 return DecoratedResultSet(rows, pre_iter_hook=self.preloadBuildsData)
359497
360 def findByGitRepository(self, git_repository, commit_sha1s=None):
361 """See `ICIBuildSet`."""
362 clauses = [CIBuild.git_repository == git_repository]
363 if commit_sha1s is not None:
364 clauses.append(CIBuild.commit_sha1.is_in(commit_sha1s))
365 return IStore(CIBuild).find(CIBuild, *clauses)
366
367 def deleteByGitRepository(self, git_repository):498 def deleteByGitRepository(self, git_repository):
368 """See `ICIBuildSet`."""499 """See `ICIBuildSet`."""
369 self.findByGitRepository(git_repository).remove()500 self.findByGitRepository(git_repository).remove()
diff --git a/lib/lp/code/model/tests/test_cibuild.py b/lib/lp/code/model/tests/test_cibuild.py
index 133bed2..bd2c5e9 100644
--- a/lib/lp/code/model/tests/test_cibuild.py
+++ b/lib/lp/code/model/tests/test_cibuild.py
@@ -7,9 +7,13 @@ from datetime import (
7 datetime,7 datetime,
8 timedelta,8 timedelta,
9 )9 )
10import hashlib
10from textwrap import dedent11from textwrap import dedent
12from unittest.mock import Mock
1113
14from fixtures import MockPatchObject
12import pytz15import pytz
16from storm.locals import Store
13from testtools.matchers import (17from testtools.matchers import (
14 Equals,18 Equals,
15 MatchesStructure,19 MatchesStructure,
@@ -18,9 +22,14 @@ from zope.component import getUtility
18from zope.security.proxy import removeSecurityProxy22from zope.security.proxy import removeSecurityProxy
1923
20from lp.app.enums import InformationType24from lp.app.enums import InformationType
21from lp.buildmaster.enums import BuildStatus25from lp.app.interfaces.launchpad import ILaunchpadCelebrities
26from lp.buildmaster.enums import (
27 BuildQueueStatus,
28 BuildStatus,
29 )
22from lp.buildmaster.interfaces.buildqueue import IBuildQueue30from lp.buildmaster.interfaces.buildqueue import IBuildQueue
23from lp.buildmaster.interfaces.packagebuild import IPackageBuild31from lp.buildmaster.interfaces.packagebuild import IPackageBuild
32from lp.buildmaster.model.buildqueue import BuildQueue
24from lp.code.errors import (33from lp.code.errors import (
25 GitRepositoryBlobNotFound,34 GitRepositoryBlobNotFound,
26 GitRepositoryScanFault,35 GitRepositoryScanFault,
@@ -28,12 +37,20 @@ from lp.code.errors import (
28from lp.code.interfaces.cibuild import (37from lp.code.interfaces.cibuild import (
29 CannotFetchConfiguration,38 CannotFetchConfiguration,
30 CannotParseConfiguration,39 CannotParseConfiguration,
40 CIBuildAlreadyRequested,
41 CIBuildDisallowedArchitecture,
31 ICIBuild,42 ICIBuild,
32 ICIBuildSet,43 ICIBuildSet,
33 MissingConfiguration,44 MissingConfiguration,
34 )45 )
46from lp.code.model.cibuild import (
47 determine_DASes_to_build,
48 get_all_commits_for_paths,
49 )
50from lp.code.model.lpcraft import load_configuration
35from lp.code.tests.helpers import GitHostingFixture51from lp.code.tests.helpers import GitHostingFixture
36from lp.registry.interfaces.series import SeriesStatus52from lp.registry.interfaces.series import SeriesStatus
53from lp.services.log.logger import BufferLogger
37from lp.services.propertycache import clear_property_cache54from lp.services.propertycache import clear_property_cache
38from lp.testing import (55from lp.testing import (
39 person_logged_in,56 person_logged_in,
@@ -44,6 +61,39 @@ from lp.testing.layers import LaunchpadZopelessLayer
44from lp.testing.matchers import HasQueryCount61from lp.testing.matchers import HasQueryCount
4562
4663
64class TestGetAllCommitsForPaths(TestCaseWithFactory):
65
66 layer = LaunchpadZopelessLayer
67
68 def test_no_refs(self):
69 repository = self.factory.makeGitRepository()
70 ref_paths = ['refs/heads/master']
71
72 rv = get_all_commits_for_paths(repository, ref_paths)
73
74 self.assertEqual([], rv)
75
76 def test_one_ref_one_path(self):
77 repository = self.factory.makeGitRepository()
78 ref_paths = ['refs/heads/master']
79 [ref] = self.factory.makeGitRefs(repository, ref_paths)
80
81 rv = get_all_commits_for_paths(repository, ref_paths)
82
83 self.assertEqual(1, len(rv))
84 self.assertEqual(ref.commit_sha1, rv[0])
85
86 def test_multiple_refs_and_paths(self):
87 repository = self.factory.makeGitRepository()
88 ref_paths = ['refs/heads/master', "refs/heads/dev"]
89 refs = self.factory.makeGitRefs(repository, ref_paths)
90
91 rv = get_all_commits_for_paths(repository, ref_paths)
92
93 self.assertEqual(2, len(rv))
94 self.assertEqual({ref.commit_sha1 for ref in refs}, set(rv))
95
96
47class TestCIBuild(TestCaseWithFactory):97class TestCIBuild(TestCaseWithFactory):
4898
49 layer = LaunchpadZopelessLayer99 layer = LaunchpadZopelessLayer
@@ -336,6 +386,329 @@ class TestCIBuildSet(TestCaseWithFactory):
336 self.assertContentEqual(386 self.assertContentEqual(
337 builds[2:], ci_build_set.findByGitRepository(repositories[1]))387 builds[2:], ci_build_set.findByGitRepository(repositories[1]))
338388
389 def test_requestCIBuild(self):
390 # requestBuild creates a new CIBuild.
391 repository = self.factory.makeGitRepository()
392 commit_sha1 = hashlib.sha1(self.factory.getUniqueBytes()).hexdigest()
393 das = self.factory.makeBuildableDistroArchSeries()
394
395 build = getUtility(ICIBuildSet).requestBuild(
396 repository, commit_sha1, das)
397
398 self.assertTrue(ICIBuild.providedBy(build))
399 self.assertThat(build, MatchesStructure.byEquality(
400 git_repository=repository,
401 commit_sha1=commit_sha1,
402 distro_arch_series=das,
403 status=BuildStatus.NEEDSBUILD,
404 ))
405 store = Store.of(build)
406 store.flush()
407 build_queue = store.find(
408 BuildQueue,
409 BuildQueue._build_farm_job_id ==
410 removeSecurityProxy(build).build_farm_job_id).one()
411 self.assertProvides(build_queue, IBuildQueue)
412 self.assertTrue(build_queue.virtualized)
413 self.assertEqual(BuildQueueStatus.WAITING, build_queue.status)
414
415 def test_requestBuild_score(self):
416 # CI builds have an initial queue score of 2600.
417 repository = self.factory.makeGitRepository()
418 commit_sha1 = hashlib.sha1(self.factory.getUniqueBytes()).hexdigest()
419 das = self.factory.makeBuildableDistroArchSeries()
420 build = getUtility(ICIBuildSet).requestBuild(
421 repository, commit_sha1, das)
422 queue_record = build.buildqueue_record
423 queue_record.score()
424 self.assertEqual(2600, queue_record.lastscore)
425
426 def test_requestBuild_rejects_repeats(self):
427 # requestBuild refuses if an identical build was already requested.
428 repository = self.factory.makeGitRepository()
429 commit_sha1 = hashlib.sha1(self.factory.getUniqueBytes()).hexdigest()
430 distro_series = self.factory.makeDistroSeries()
431 arches = [
432 self.factory.makeBuildableDistroArchSeries(
433 distroseries=distro_series)
434 for _ in range(2)]
435 old_build = getUtility(ICIBuildSet).requestBuild(
436 repository, commit_sha1, arches[0])
437 self.assertRaises(
438 CIBuildAlreadyRequested, getUtility(ICIBuildSet).requestBuild,
439 repository, commit_sha1, arches[0])
440 # We can build for a different distroarchseries.
441 getUtility(ICIBuildSet).requestBuild(
442 repository, commit_sha1, arches[1])
443 # Changing the status of the old build does not allow a new build.
444 old_build.updateStatus(BuildStatus.BUILDING)
445 old_build.updateStatus(BuildStatus.FULLYBUILT)
446 self.assertRaises(
447 CIBuildAlreadyRequested, getUtility(ICIBuildSet).requestBuild,
448 repository, commit_sha1, arches[0])
449
450 def test_requestBuild_virtualization(self):
451 # New builds are virtualized.
452 repository = self.factory.makeGitRepository()
453 commit_sha1 = hashlib.sha1(self.factory.getUniqueBytes()).hexdigest()
454 distro_series = self.factory.makeDistroSeries()
455 for proc_nonvirt in True, False:
456 das = self.factory.makeBuildableDistroArchSeries(
457 distroseries=distro_series, supports_virtualized=True,
458 supports_nonvirtualized=proc_nonvirt)
459 build = getUtility(ICIBuildSet).requestBuild(
460 repository, commit_sha1, das)
461 self.assertTrue(build.virtualized)
462
463 def test_requestBuild_nonvirtualized(self):
464 # A non-virtualized processor cannot run a CI build.
465 repository = self.factory.makeGitRepository()
466 commit_sha1 = hashlib.sha1(self.factory.getUniqueBytes()).hexdigest()
467 distro_series = self.factory.makeDistroSeries()
468 das = self.factory.makeBuildableDistroArchSeries(
469 distroseries=distro_series, supports_virtualized=False,
470 supports_nonvirtualized=True)
471 self.assertRaises(
472 CIBuildDisallowedArchitecture,
473 getUtility(ICIBuildSet).requestBuild, repository, commit_sha1, das)
474
475 def test_requestBuildsForRefs_triggers_builds(self):
476 ubuntu = getUtility(ILaunchpadCelebrities).ubuntu
477 series = self.factory.makeDistroSeries(
478 distribution=ubuntu,
479 name="focal",
480 )
481 self.factory.makeBuildableDistroArchSeries(
482 distroseries=series,
483 architecturetag="amd64"
484 )
485 configuration = dedent("""\
486 pipeline:
487 - test
488
489 jobs:
490 test:
491 series: focal
492 architectures: amd64
493 run: echo hello world >output
494 """).encode()
495 repository = self.factory.makeGitRepository()
496 ref_paths = ['refs/heads/master']
497 [ref] = self.factory.makeGitRefs(repository, ref_paths)
498 encoded_commit_json = {
499 "sha1": ref.commit_sha1,
500 "blobs": {".launchpad.yaml": configuration},
501 }
502 hosting_fixture = self.useFixture(
503 GitHostingFixture(commits=[encoded_commit_json])
504 )
505
506 getUtility(ICIBuildSet).requestBuildsForRefs(repository, ref_paths)
507
508 self.assertEqual(
509 [((repository.getInternalPath(), [ref.commit_sha1]),
510 {"filter_paths": [".launchpad.yaml"]})],
511 hosting_fixture.getCommits.calls
512 )
513
514 build = getUtility(ICIBuildSet).findByGitRepository(repository).one()
515
516 # check that a build was created
517 self.assertEqual(ref.commit_sha1, build.commit_sha1)
518 self.assertEqual("focal", build.distro_arch_series.distroseries.name)
519 self.assertEqual("amd64", build.distro_arch_series.architecturetag)
520
521 def test_requestBuildsForRefs_no_commits_at_all(self):
522 repository = self.factory.makeGitRepository()
523 ref_paths = ['refs/heads/master']
524 hosting_fixture = self.useFixture(GitHostingFixture(commits=[]))
525
526 getUtility(ICIBuildSet).requestBuildsForRefs(repository, ref_paths)
527
528 self.assertEqual(
529 [((repository.getInternalPath(), []),
530 {"filter_paths": [".launchpad.yaml"]})],
531 hosting_fixture.getCommits.calls
532 )
533
534 self.assertTrue(
535 getUtility(ICIBuildSet).findByGitRepository(repository).is_empty()
536 )
537
538 def test_requestBuildsForRefs_no_matching_commits(self):
539 repository = self.factory.makeGitRepository()
540 ref_paths = ['refs/heads/master']
541 [ref] = self.factory.makeGitRefs(repository, ref_paths)
542 hosting_fixture = self.useFixture(
543 GitHostingFixture(commits=[])
544 )
545
546 getUtility(ICIBuildSet).requestBuildsForRefs(repository, ref_paths)
547
548 self.assertEqual(
549 [((repository.getInternalPath(), [ref.commit_sha1]),
550 {"filter_paths": [".launchpad.yaml"]})],
551 hosting_fixture.getCommits.calls
552 )
553
554 self.assertTrue(
555 getUtility(ICIBuildSet).findByGitRepository(repository).is_empty()
556 )
557
558 def test_requestBuildsForRefs_configuration_parse_error(self):
559 ubuntu = getUtility(ILaunchpadCelebrities).ubuntu
560 series = self.factory.makeDistroSeries(
561 distribution=ubuntu,
562 name="focal",
563 )
564 self.factory.makeBuildableDistroArchSeries(
565 distroseries=series,
566 architecturetag="amd64"
567 )
568 configuration = dedent("""\
569 no - valid - configuration - file
570 """).encode()
571 repository = self.factory.makeGitRepository()
572 ref_paths = ['refs/heads/master']
573 [ref] = self.factory.makeGitRefs(repository, ref_paths)
574 encoded_commit_json = {
575 "sha1": ref.commit_sha1,
576 "blobs": {".launchpad.yaml": configuration},
577 }
578 hosting_fixture = self.useFixture(
579 GitHostingFixture(commits=[encoded_commit_json])
580 )
581 logger = BufferLogger()
582
583 getUtility(ICIBuildSet).requestBuildsForRefs(
584 repository, ref_paths, logger)
585
586 self.assertEqual(
587 [((repository.getInternalPath(), [ref.commit_sha1]),
588 {"filter_paths": [".launchpad.yaml"]})],
589 hosting_fixture.getCommits.calls
590 )
591
592 self.assertTrue(
593 getUtility(ICIBuildSet).findByGitRepository(repository).is_empty()
594 )
595
596 self.assertEqual(
597 "ERROR Cannot parse .launchpad.yaml from %s: "
598 "Configuration file does not declare 'pipeline'\n" % (
599 repository.unique_name,),
600 logger.getLogBuffer()
601 )
602
603 def test_requestBuildsForRefs_build_already_scheduled(self):
604 ubuntu = getUtility(ILaunchpadCelebrities).ubuntu
605 series = self.factory.makeDistroSeries(
606 distribution=ubuntu,
607 name="focal",
608 )
609 self.factory.makeBuildableDistroArchSeries(
610 distroseries=series,
611 architecturetag="amd64"
612 )
613 configuration = dedent("""\
614 pipeline:
615 - test
616
617 jobs:
618 test:
619 series: focal
620 architectures: amd64
621 run: echo hello world >output
622 """).encode()
623 repository = self.factory.makeGitRepository()
624 ref_paths = ['refs/heads/master']
625 [ref] = self.factory.makeGitRefs(repository, ref_paths)
626 encoded_commit_json = {
627 "sha1": ref.commit_sha1,
628 "blobs": {".launchpad.yaml": configuration},
629 }
630 hosting_fixture = self.useFixture(
631 GitHostingFixture(commits=[encoded_commit_json])
632 )
633 build_set = removeSecurityProxy(getUtility(ICIBuildSet))
634 mock = Mock(side_effect=CIBuildAlreadyRequested)
635 self.useFixture(MockPatchObject(build_set, "requestBuild", mock))
636 logger = BufferLogger()
637
638 build_set.requestBuildsForRefs(repository, ref_paths, logger)
639
640 self.assertEqual(
641 [((repository.getInternalPath(), [ref.commit_sha1]),
642 {"filter_paths": [".launchpad.yaml"]})],
643 hosting_fixture.getCommits.calls
644 )
645
646 self.assertTrue(
647 getUtility(ICIBuildSet).findByGitRepository(repository).is_empty()
648 )
649 self.assertEqual(
650 "INFO Requesting CI build "
651 "for %s on focal/amd64\n" % ref.commit_sha1,
652 logger.getLogBuffer()
653 )
654
655 def test_requestBuildsForRefs_unexpected_exception(self):
656 ubuntu = getUtility(ILaunchpadCelebrities).ubuntu
657 series = self.factory.makeDistroSeries(
658 distribution=ubuntu,
659 name="focal",
660 )
661 self.factory.makeBuildableDistroArchSeries(
662 distroseries=series,
663 architecturetag="amd64"
664 )
665 configuration = dedent("""\
666 pipeline:
667 - test
668
669 jobs:
670 test:
671 series: focal
672 architectures: amd64
673 run: echo hello world >output
674 """).encode()
675 repository = self.factory.makeGitRepository()
676 ref_paths = ['refs/heads/master']
677 [ref] = self.factory.makeGitRefs(repository, ref_paths)
678 encoded_commit_json = {
679 "sha1": ref.commit_sha1,
680 "blobs": {".launchpad.yaml": configuration},
681 }
682 hosting_fixture = self.useFixture(
683 GitHostingFixture(commits=[encoded_commit_json])
684 )
685 build_set = removeSecurityProxy(getUtility(ICIBuildSet))
686 mock = Mock(side_effect=Exception("some unexpected error"))
687 self.useFixture(MockPatchObject(build_set, "requestBuild", mock))
688 logger = BufferLogger()
689
690 build_set.requestBuildsForRefs(repository, ref_paths, logger)
691
692 self.assertEqual(
693 [((repository.getInternalPath(), [ref.commit_sha1]),
694 {"filter_paths": [".launchpad.yaml"]})],
695 hosting_fixture.getCommits.calls
696 )
697
698 self.assertTrue(
699 getUtility(ICIBuildSet).findByGitRepository(repository).is_empty()
700 )
701
702 log_line1, log_line2 = logger.getLogBuffer().splitlines()
703 self.assertEqual(
704 "INFO Requesting CI build for %s on focal/amd64" % ref.commit_sha1,
705 log_line1)
706 self.assertEqual(
707 "ERROR Failed to request CI build for %s on focal/amd64: "
708 "some unexpected error" % (ref.commit_sha1,),
709 log_line2
710 )
711
339 def test_deleteByGitRepository(self):712 def test_deleteByGitRepository(self):
340 repositories = [self.factory.makeGitRepository() for _ in range(2)]713 repositories = [self.factory.makeGitRepository() for _ in range(2)]
341 builds = []714 builds = []
@@ -350,3 +723,116 @@ class TestCIBuildSet(TestCaseWithFactory):
350 [], ci_build_set.findByGitRepository(repositories[0]))723 [], ci_build_set.findByGitRepository(repositories[0]))
351 self.assertContentEqual(724 self.assertContentEqual(
352 builds[2:], ci_build_set.findByGitRepository(repositories[1]))725 builds[2:], ci_build_set.findByGitRepository(repositories[1]))
726
727
728class TestDetermineDASesToBuild(TestCaseWithFactory):
729
730 layer = LaunchpadZopelessLayer
731
732 def test_returns_expected_DASes(self):
733 ubuntu = getUtility(ILaunchpadCelebrities).ubuntu
734 distro_serieses = [
735 self.factory.makeDistroSeries(ubuntu) for _ in range(2)]
736 dases = []
737 for distro_series in distro_serieses:
738 for _ in range(2):
739 dases.append(self.factory.makeBuildableDistroArchSeries(
740 distroseries=distro_series))
741 configuration = load_configuration(dedent("""\
742 pipeline:
743 - [build]
744 - [test]
745 jobs:
746 build:
747 series: {distro_serieses[1].name}
748 architectures:
749 - {dases[2].architecturetag}
750 - {dases[3].architecturetag}
751 test:
752 series: {distro_serieses[1].name}
753 architectures:
754 - {dases[2].architecturetag}
755 """.format(distro_serieses=distro_serieses, dases=dases)))
756 logger = BufferLogger()
757
758 dases_to_build = list(
759 determine_DASes_to_build(configuration, logger=logger))
760
761 self.assertContentEqual(dases[2:], dases_to_build)
762 self.assertEqual("", logger.getLogBuffer())
763
764
765 def test_logs_missing_job_definition(self):
766 ubuntu = getUtility(ILaunchpadCelebrities).ubuntu
767 distro_series = self.factory.makeDistroSeries(ubuntu)
768 das = self.factory.makeBuildableDistroArchSeries(
769 distroseries=distro_series)
770 configuration = load_configuration(dedent("""\
771 pipeline:
772 - [test]
773 jobs:
774 build:
775 series: {distro_series.name}
776 architectures:
777 - {das.architecturetag}
778 """.format(distro_series=distro_series, das=das)))
779 logger = BufferLogger()
780
781 dases_to_build = list(
782 determine_DASes_to_build(configuration, logger=logger))
783
784 self.assertEqual(0, len(dases_to_build))
785 self.assertEqual(
786 "ERROR No job definition for 'test'\n", logger.getLogBuffer()
787 )
788
789
790 def test_logs_missing_series(self):
791 ubuntu = getUtility(ILaunchpadCelebrities).ubuntu
792 distro_series = self.factory.makeDistroSeries(ubuntu)
793 das = self.factory.makeBuildableDistroArchSeries(
794 distroseries=distro_series)
795 configuration = load_configuration(dedent("""\
796 pipeline:
797 - [build]
798 jobs:
799 build:
800 series: unknown-series
801 architectures:
802 - {das.architecturetag}
803 """.format(das=das)))
804 logger = BufferLogger()
805
806 dases_to_build = list(
807 determine_DASes_to_build(configuration, logger=logger))
808
809 self.assertEqual(0, len(dases_to_build))
810 self.assertEqual(
811 "ERROR Unknown Ubuntu series name unknown-series\n",
812 logger.getLogBuffer()
813 )
814
815
816 def test_logs_non_buildable_architecture(self):
817 ubuntu = getUtility(ILaunchpadCelebrities).ubuntu
818 distro_series = self.factory.makeDistroSeries(ubuntu)
819 configuration = load_configuration(dedent("""\
820 pipeline:
821 - [build]
822 jobs:
823 build:
824 series: {distro_series.name}
825 architectures:
826 - non-buildable-architecture
827 """.format(distro_series=distro_series)))
828 logger = BufferLogger()
829
830 dases_to_build = list(
831 determine_DASes_to_build(configuration, logger=logger))
832
833 self.assertEqual(0, len(dases_to_build))
834 self.assertEqual(
835 "ERROR non-buildable-architecture is not a buildable architecture "
836 "name in Ubuntu %s\n" % distro_series.name,
837 logger.getLogBuffer()
838 )
diff --git a/lib/lp/code/model/tests/test_gitrepository.py b/lib/lp/code/model/tests/test_gitrepository.py
index 46781f2..fd1f9eb 100644
--- a/lib/lp/code/model/tests/test_gitrepository.py
+++ b/lib/lp/code/model/tests/test_gitrepository.py
@@ -11,6 +11,7 @@ import email
11from functools import partial11from functools import partial
12import hashlib12import hashlib
13import json13import json
14from textwrap import dedent
1415
15from breezy import urlutils16from breezy import urlutils
16from fixtures import MockPatch17from fixtures import MockPatch
@@ -85,6 +86,10 @@ from lp.code.event.git import GitRefsUpdatedEvent
85from lp.code.interfaces.branchmergeproposal import (86from lp.code.interfaces.branchmergeproposal import (
86 BRANCH_MERGE_PROPOSAL_FINAL_STATES as FINAL_STATES,87 BRANCH_MERGE_PROPOSAL_FINAL_STATES as FINAL_STATES,
87 )88 )
89from lp.code.interfaces.cibuild import (
90 ICIBuild,
91 ICIBuildSet,
92 )
88from lp.code.interfaces.codeimport import ICodeImportSet93from lp.code.interfaces.codeimport import ICodeImportSet
89from lp.code.interfaces.defaultgit import ICanHasDefaultGitRepository94from lp.code.interfaces.defaultgit import ICanHasDefaultGitRepository
90from lp.code.interfaces.gitjob import (95from lp.code.interfaces.gitjob import (
@@ -169,6 +174,7 @@ from lp.services.identity.interfaces.account import AccountStatus
169from lp.services.job.interfaces.job import JobStatus174from lp.services.job.interfaces.job import JobStatus
170from lp.services.job.model.job import Job175from lp.services.job.model.job import Job
171from lp.services.job.runner import JobRunner176from lp.services.job.runner import JobRunner
177from lp.services.log.logger import BufferLogger
172from lp.services.macaroons.interfaces import IMacaroonIssuer178from lp.services.macaroons.interfaces import IMacaroonIssuer
173from lp.services.macaroons.testing import (179from lp.services.macaroons.testing import (
174 find_caveats_by_name,180 find_caveats_by_name,
@@ -1469,6 +1475,7 @@ class TestGitRepositoryModifications(TestCaseWithFactory):
1469 repository, "date_last_modified", UTC_NOW)1475 repository, "date_last_modified", UTC_NOW)
14701476
1471 def test_create_ref_sets_date_last_modified(self):1477 def test_create_ref_sets_date_last_modified(self):
1478 self.useFixture(GitHostingFixture())
1472 repository = self.factory.makeGitRepository(1479 repository = self.factory.makeGitRepository(
1473 date_created=datetime(2015, 6, 1, tzinfo=pytz.UTC))1480 date_created=datetime(2015, 6, 1, tzinfo=pytz.UTC))
1474 [ref] = self.factory.makeGitRefs(repository=repository)1481 [ref] = self.factory.makeGitRefs(repository=repository)
@@ -1882,6 +1889,7 @@ class TestGitRepositoryRefs(TestCaseWithFactory):
1882 repository.refs, repository, ["refs/heads/master"])1889 repository.refs, repository, ["refs/heads/master"])
18831890
1884 def test_update(self):1891 def test_update(self):
1892 self.useFixture(GitHostingFixture())
1885 repository = self.factory.makeGitRepository()1893 repository = self.factory.makeGitRepository()
1886 paths = ("refs/heads/master", "refs/tags/1.0")1894 paths = ("refs/heads/master", "refs/tags/1.0")
1887 self.factory.makeGitRefs(repository=repository, paths=paths)1895 self.factory.makeGitRefs(repository=repository, paths=paths)
@@ -1913,6 +1921,7 @@ class TestGitRepositoryRefs(TestCaseWithFactory):
1913 return [UpdatePreviewDiffJob(job) for job in jobs]1921 return [UpdatePreviewDiffJob(job) for job in jobs]
19141922
1915 def test_update_schedules_diff_update(self):1923 def test_update_schedules_diff_update(self):
1924 self.useFixture(GitHostingFixture())
1916 repository = self.factory.makeGitRepository()1925 repository = self.factory.makeGitRepository()
1917 [ref] = self.factory.makeGitRefs(repository=repository)1926 [ref] = self.factory.makeGitRefs(repository=repository)
1918 self.assertRefsMatch(repository.refs, repository, [ref.path])1927 self.assertRefsMatch(repository.refs, repository, [ref.path])
@@ -2223,6 +2232,7 @@ class TestGitRepositoryRefs(TestCaseWithFactory):
2223 def test_synchroniseRefs(self):2232 def test_synchroniseRefs(self):
2224 # synchroniseRefs copes with synchronising a repository where some2233 # synchroniseRefs copes with synchronising a repository where some
2225 # refs have been created, some deleted, and some changed.2234 # refs have been created, some deleted, and some changed.
2235 self.useFixture(GitHostingFixture())
2226 repository = self.factory.makeGitRepository()2236 repository = self.factory.makeGitRepository()
2227 paths = ("refs/heads/master", "refs/heads/foo", "refs/heads/bar")2237 paths = ("refs/heads/master", "refs/heads/foo", "refs/heads/bar")
2228 self.factory.makeGitRefs(repository=repository, paths=paths)2238 self.factory.makeGitRefs(repository=repository, paths=paths)
@@ -2992,6 +3002,7 @@ class TestGitRepositoryMarkRecipesStale(TestCaseWithFactory):
29923002
2993 def test_base_repository_recipe(self):3003 def test_base_repository_recipe(self):
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.
3005 self.useFixture(GitHostingFixture())
2995 [ref] = self.factory.makeGitRefs()3006 [ref] = self.factory.makeGitRefs()
2996 recipe = self.factory.makeSourcePackageRecipe(branches=[ref])3007 recipe = self.factory.makeSourcePackageRecipe(branches=[ref])
2997 removeSecurityProxy(recipe).is_stale = False3008 removeSecurityProxy(recipe).is_stale = False
@@ -3002,6 +3013,7 @@ class TestGitRepositoryMarkRecipesStale(TestCaseWithFactory):
3002 def test_base_repository_different_ref_recipe(self):3013 def test_base_repository_different_ref_recipe(self):
3003 # On ref changes, recipes where a different ref in the same3014 # On ref changes, recipes where a different ref in the same
3004 # repository is the base are left alone.3015 # repository is the base are left alone.
3016 self.useFixture(GitHostingFixture())
3005 ref1, ref2 = self.factory.makeGitRefs(3017 ref1, ref2 = self.factory.makeGitRefs(
3006 paths=["refs/heads/a", "refs/heads/b"])3018 paths=["refs/heads/a", "refs/heads/b"])
3007 recipe = self.factory.makeSourcePackageRecipe(branches=[ref1])3019 recipe = self.factory.makeSourcePackageRecipe(branches=[ref1])
@@ -3013,6 +3025,7 @@ class TestGitRepositoryMarkRecipesStale(TestCaseWithFactory):
3013 def test_base_repository_default_branch_recipe(self):3025 def test_base_repository_default_branch_recipe(self):
3014 # On ref changes to the default branch, recipes where this3026 # On ref changes to the default branch, recipes where this
3015 # repository is the base with no explicit revspec become stale.3027 # repository is the base with no explicit revspec become stale.
3028 self.useFixture(GitHostingFixture())
3016 repository = self.factory.makeGitRepository()3029 repository = self.factory.makeGitRepository()
3017 ref1, ref2 = self.factory.makeGitRefs(3030 ref1, ref2 = self.factory.makeGitRefs(
3018 repository=repository, paths=["refs/heads/a", "refs/heads/b"])3031 repository=repository, paths=["refs/heads/a", "refs/heads/b"])
@@ -3028,6 +3041,7 @@ class TestGitRepositoryMarkRecipesStale(TestCaseWithFactory):
30283041
3029 def test_instruction_repository_recipe(self):3042 def test_instruction_repository_recipe(self):
3030 # On ref changes, recipes including this ref become stale.3043 # On ref changes, recipes including this ref become stale.
3044 self.useFixture(GitHostingFixture())
3031 [base_ref] = self.factory.makeGitRefs()3045 [base_ref] = self.factory.makeGitRefs()
3032 [ref] = self.factory.makeGitRefs()3046 [ref] = self.factory.makeGitRefs()
3033 recipe = self.factory.makeSourcePackageRecipe(branches=[base_ref, ref])3047 recipe = self.factory.makeSourcePackageRecipe(branches=[base_ref, ref])
@@ -3039,6 +3053,7 @@ class TestGitRepositoryMarkRecipesStale(TestCaseWithFactory):
3039 def test_instruction_repository_different_ref_recipe(self):3053 def test_instruction_repository_different_ref_recipe(self):
3040 # On ref changes, recipes including a different ref in the same3054 # On ref changes, recipes including a different ref in the same
3041 # repository are left alone.3055 # repository are left alone.
3056 self.useFixture(GitHostingFixture())
3042 [base_ref] = self.factory.makeGitRefs()3057 [base_ref] = self.factory.makeGitRefs()
3043 ref1, ref2 = self.factory.makeGitRefs(3058 ref1, ref2 = self.factory.makeGitRefs(
3044 paths=["refs/heads/a", "refs/heads/b"])3059 paths=["refs/heads/a", "refs/heads/b"])
@@ -3052,6 +3067,7 @@ class TestGitRepositoryMarkRecipesStale(TestCaseWithFactory):
3052 def test_instruction_repository_default_branch_recipe(self):3067 def test_instruction_repository_default_branch_recipe(self):
3053 # On ref changes to the default branch, recipes including this3068 # On ref changes to the default branch, recipes including this
3054 # repository with no explicit revspec become stale.3069 # repository with no explicit revspec become stale.
3070 self.useFixture(GitHostingFixture())
3055 [base_ref] = self.factory.makeGitRefs()3071 [base_ref] = self.factory.makeGitRefs()
3056 repository = self.factory.makeGitRepository()3072 repository = self.factory.makeGitRepository()
3057 ref1, ref2 = self.factory.makeGitRefs(3073 ref1, ref2 = self.factory.makeGitRefs(
@@ -3069,6 +3085,7 @@ class TestGitRepositoryMarkRecipesStale(TestCaseWithFactory):
30693085
3070 def test_unrelated_repository_recipe(self):3086 def test_unrelated_repository_recipe(self):
3071 # On ref changes, unrelated recipes are left alone.3087 # On ref changes, unrelated recipes are left alone.
3088 self.useFixture(GitHostingFixture())
3072 [ref] = self.factory.makeGitRefs()3089 [ref] = self.factory.makeGitRefs()
3073 recipe = self.factory.makeSourcePackageRecipe(3090 recipe = self.factory.makeSourcePackageRecipe(
3074 branches=self.factory.makeGitRefs())3091 branches=self.factory.makeGitRefs())
@@ -3084,6 +3101,7 @@ class TestGitRepositoryMarkSnapsStale(TestCaseWithFactory):
30843101
3085 def test_same_repository(self):3102 def test_same_repository(self):
3086 # On ref changes, snap packages using this ref become stale.3103 # On ref changes, snap packages using this ref become stale.
3104 self.useFixture(GitHostingFixture())
3087 [ref] = self.factory.makeGitRefs()3105 [ref] = self.factory.makeGitRefs()
3088 snap = self.factory.makeSnap(git_ref=ref)3106 snap = self.factory.makeSnap(git_ref=ref)
3089 removeSecurityProxy(snap).is_stale = False3107 removeSecurityProxy(snap).is_stale = False
@@ -3094,6 +3112,7 @@ class TestGitRepositoryMarkSnapsStale(TestCaseWithFactory):
3094 def test_same_repository_different_ref(self):3112 def test_same_repository_different_ref(self):
3095 # On ref changes, snap packages using a different ref in the same3113 # On ref changes, snap packages using a different ref in the same
3096 # repository are left alone.3114 # repository are left alone.
3115 self.useFixture(GitHostingFixture())
3097 ref1, ref2 = self.factory.makeGitRefs(3116 ref1, ref2 = self.factory.makeGitRefs(
3098 paths=["refs/heads/a", "refs/heads/b"])3117 paths=["refs/heads/a", "refs/heads/b"])
3099 snap = self.factory.makeSnap(git_ref=ref1)3118 snap = self.factory.makeSnap(git_ref=ref1)
@@ -3104,6 +3123,7 @@ class TestGitRepositoryMarkSnapsStale(TestCaseWithFactory):
31043123
3105 def test_different_repository(self):3124 def test_different_repository(self):
3106 # On ref changes, unrelated snap packages are left alone.3125 # On ref changes, unrelated snap packages are left alone.
3126 self.useFixture(GitHostingFixture())
3107 [ref] = self.factory.makeGitRefs()3127 [ref] = self.factory.makeGitRefs()
3108 snap = self.factory.makeSnap(git_ref=self.factory.makeGitRefs()[0])3128 snap = self.factory.makeSnap(git_ref=self.factory.makeGitRefs()[0])
3109 removeSecurityProxy(snap).is_stale = False3129 removeSecurityProxy(snap).is_stale = False
@@ -3113,6 +3133,7 @@ class TestGitRepositoryMarkSnapsStale(TestCaseWithFactory):
31133133
3114 def test_private_snap(self):3134 def test_private_snap(self):
3115 # A private snap should be able to be marked stale3135 # A private snap should be able to be marked stale
3136 self.useFixture(GitHostingFixture())
3116 self.useFixture(FeatureFixture(SNAP_TESTING_FLAGS))3137 self.useFixture(FeatureFixture(SNAP_TESTING_FLAGS))
3117 [ref] = self.factory.makeGitRefs()3138 [ref] = self.factory.makeGitRefs()
3118 snap = self.factory.makeSnap(git_ref=ref, private=True)3139 snap = self.factory.makeSnap(git_ref=ref, private=True)
@@ -3135,6 +3156,7 @@ class TestGitRepositoryMarkCharmRecipesStale(TestCaseWithFactory):
31353156
3136 def test_same_repository(self):3157 def test_same_repository(self):
3137 # On ref changes, charm recipes using this ref become stale.3158 # On ref changes, charm recipes using this ref become stale.
3159 self.useFixture(GitHostingFixture())
3138 [ref] = self.factory.makeGitRefs()3160 [ref] = self.factory.makeGitRefs()
3139 recipe = self.factory.makeCharmRecipe(git_ref=ref)3161 recipe = self.factory.makeCharmRecipe(git_ref=ref)
3140 removeSecurityProxy(recipe).is_stale = False3162 removeSecurityProxy(recipe).is_stale = False
@@ -3145,6 +3167,7 @@ class TestGitRepositoryMarkCharmRecipesStale(TestCaseWithFactory):
3145 def test_same_repository_different_ref(self):3167 def test_same_repository_different_ref(self):
3146 # On ref changes, charm recipes using a different ref in the same3168 # On ref changes, charm recipes using a different ref in the same
3147 # repository are left alone.3169 # repository are left alone.
3170 self.useFixture(GitHostingFixture())
3148 ref1, ref2 = self.factory.makeGitRefs(3171 ref1, ref2 = self.factory.makeGitRefs(
3149 paths=["refs/heads/a", "refs/heads/b"])3172 paths=["refs/heads/a", "refs/heads/b"])
3150 recipe = self.factory.makeCharmRecipe(git_ref=ref1)3173 recipe = self.factory.makeCharmRecipe(git_ref=ref1)
@@ -3155,6 +3178,7 @@ class TestGitRepositoryMarkCharmRecipesStale(TestCaseWithFactory):
31553178
3156 def test_different_repository(self):3179 def test_different_repository(self):
3157 # On ref changes, unrelated charm recipes are left alone.3180 # On ref changes, unrelated charm recipes are left alone.
3181 self.useFixture(GitHostingFixture())
3158 [ref] = self.factory.makeGitRefs()3182 [ref] = self.factory.makeGitRefs()
3159 recipe = self.factory.makeCharmRecipe(3183 recipe = self.factory.makeCharmRecipe(
3160 git_ref=self.factory.makeGitRefs()[0])3184 git_ref=self.factory.makeGitRefs()[0])
@@ -3355,6 +3379,81 @@ class TestGitRepositoryDetectMerges(TestCaseWithFactory):
3355 for event in events[:2]})3379 for event in events[:2]})
33563380
33573381
3382class TestGitRepositoryRequestCIBuilds(TestCaseWithFactory):
3383
3384 layer = ZopelessDatabaseLayer
3385
3386 def test_findByGitRepository_with_configuration(self):
3387 # If a changed ref has CI configuration, we request CI builds.
3388 logger = BufferLogger()
3389 [ref] = self.factory.makeGitRefs()
3390 ubuntu = getUtility(ILaunchpadCelebrities).ubuntu
3391 distroseries = self.factory.makeDistroSeries(distribution=ubuntu)
3392 dases = [
3393 self.factory.makeBuildableDistroArchSeries(
3394 distroseries=distroseries)
3395 for _ in range(2)]
3396 configuration = dedent("""\
3397 pipeline: [test]
3398 jobs:
3399 test:
3400 series: {series}
3401 architectures: [{architectures}]
3402 """.format(
3403 series=distroseries.name,
3404 architectures=", ".join(
3405 das.architecturetag for das in dases))).encode()
3406 new_commit = hashlib.sha1(self.factory.getUniqueBytes()).hexdigest()
3407 self.useFixture(GitHostingFixture(commits=[
3408 {
3409 "sha1": new_commit,
3410 "blobs": {".launchpad.yaml": configuration},
3411 },
3412 ]))
3413 with dbuser("branchscanner"):
3414 ref.repository.createOrUpdateRefs(
3415 {ref.path: {"sha1": new_commit, "type": GitObjectType.COMMIT}},
3416 logger=logger)
3417
3418 results = getUtility(ICIBuildSet).findByGitRepository(ref.repository)
3419 for result in results:
3420 self.assertTrue(ICIBuild.providedBy(result))
3421
3422 self.assertThat(
3423 results,
3424 MatchesSetwise(*(
3425 MatchesStructure.byEquality(
3426 git_repository=ref.repository,
3427 commit_sha1=new_commit,
3428 distro_arch_series=das)
3429 for das in dases)))
3430 self.assertContentEqual(
3431 [
3432 "INFO Requesting CI build for {commit} on "
3433 "{series}/{arch}".format(
3434 commit=new_commit, series=distroseries.name,
3435 arch=das.architecturetag)
3436 for das in dases],
3437 logger.getLogBuffer().splitlines())
3438
3439 def test_findByGitRepository_without_configuration(self):
3440 # If a changed ref has no CI configuration, we do not request CI
3441 # builds.
3442 logger = BufferLogger()
3443 [ref] = self.factory.makeGitRefs()
3444 new_commit = hashlib.sha1(self.factory.getUniqueBytes()).hexdigest()
3445 self.useFixture(GitHostingFixture(commits=[]))
3446 with dbuser("branchscanner"):
3447 ref.repository.createOrUpdateRefs(
3448 {ref.path: {"sha1": new_commit, "type": GitObjectType.COMMIT}},
3449 logger=logger)
3450 self.assertTrue(
3451 getUtility(
3452 ICIBuildSet).findByGitRepository(ref.repository).is_empty()
3453 )
3454 self.assertEqual("", logger.getLogBuffer())
3455
3456
3358class TestGitRepositoryGetBlob(TestCaseWithFactory):3457class TestGitRepositoryGetBlob(TestCaseWithFactory):
3359 """Tests for retrieving files from a Git repository."""3458 """Tests for retrieving files from a Git repository."""
33603459
diff --git a/lib/lp/code/subscribers/git.py b/lib/lp/code/subscribers/git.py
index 30b432e..831ac49 100644
--- a/lib/lp/code/subscribers/git.py
+++ b/lib/lp/code/subscribers/git.py
@@ -1,8 +1,13 @@
1# Copyright 2015-2016 Canonical Ltd. This software is licensed under the1# Copyright 2015-2022 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Event subscribers for Git repositories."""4"""Event subscribers for Git repositories."""
55
6from zope.component import getUtility
7
8from lp.code.interfaces.cibuild import ICIBuildSet
9
10
6def refs_updated(repository, event):11def refs_updated(repository, event):
7 """Some references in a Git repository have been updated."""12 """Some references in a Git repository have been updated."""
8 repository.updateMergeCommitIDs(event.paths)13 repository.updateMergeCommitIDs(event.paths)
@@ -11,3 +16,5 @@ def refs_updated(repository, event):
11 repository.markSnapsStale(event.paths)16 repository.markSnapsStale(event.paths)
12 repository.markCharmRecipesStale(event.paths)17 repository.markCharmRecipesStale(event.paths)
13 repository.detectMerges(event.paths, logger=event.logger)18 repository.detectMerges(event.paths, logger=event.logger)
19 getUtility(ICIBuildSet).requestBuildsForRefs(
20 repository, event.paths, logger=event.logger)
diff --git a/lib/lp/testing/factory.py b/lib/lp/testing/factory.py
index 8e0ec69..e145133 100644
--- a/lib/lp/testing/factory.py
+++ b/lib/lp/testing/factory.py
@@ -97,11 +97,15 @@ from lp.bugs.interfaces.cve import (
97 )97 )
98from lp.bugs.model.bug import FileBugData98from lp.bugs.model.bug import FileBugData
99from lp.buildmaster.enums import (99from lp.buildmaster.enums import (
100 BuildBaseImageType,
100 BuilderResetProtocol,101 BuilderResetProtocol,
101 BuildStatus,102 BuildStatus,
102 )103 )
103from lp.buildmaster.interfaces.builder import IBuilderSet104from lp.buildmaster.interfaces.builder import IBuilderSet
104from lp.buildmaster.interfaces.processor import IProcessorSet105from lp.buildmaster.interfaces.processor import (
106 IProcessorSet,
107 ProcessorNotFound,
108 )
105from lp.charms.interfaces.charmbase import ICharmBaseSet109from lp.charms.interfaces.charmbase import ICharmBaseSet
106from lp.charms.interfaces.charmrecipe import ICharmRecipeSet110from lp.charms.interfaces.charmrecipe import ICharmRecipeSet
107from lp.charms.interfaces.charmrecipebuild import ICharmRecipeBuildSet111from lp.charms.interfaces.charmrecipebuild import ICharmRecipeBuildSet
@@ -2932,6 +2936,34 @@ class BareLaunchpadObjectFactory(ObjectFactory):
2932 return distroseries.newArch(2936 return distroseries.newArch(
2933 architecturetag, processor, official, owner, enabled)2937 architecturetag, processor, official, owner, enabled)
29342938
2939 def makeBuildableDistroArchSeries(self, architecturetag=None,
2940 processor=None,
2941 supports_virtualized=True,
2942 supports_nonvirtualized=True, **kwargs):
2943 if architecturetag is None:
2944 architecturetag = self.getUniqueUnicode("arch")
2945 if processor is None:
2946 try:
2947 processor = getUtility(IProcessorSet).getByName(
2948 architecturetag)
2949 except ProcessorNotFound:
2950 processor = self.makeProcessor(
2951 name=architecturetag,
2952 supports_virtualized=supports_virtualized,
2953 supports_nonvirtualized=supports_nonvirtualized)
2954 das = self.makeDistroArchSeries(
2955 architecturetag=architecturetag, processor=processor, **kwargs)
2956 # Add both a chroot and a LXD image to test that
2957 # getAllowedArchitectures doesn't get confused by multiple
2958 # PocketChroot rows for a single DistroArchSeries.
2959 fake_chroot = self.makeLibraryFileAlias(
2960 filename="fake_chroot.tar.gz", db_only=True)
2961 das.addOrUpdateChroot(fake_chroot)
2962 fake_lxd = self.makeLibraryFileAlias(
2963 filename="fake_lxd.tar.gz", db_only=True)
2964 das.addOrUpdateChroot(fake_lxd, image_type=BuildBaseImageType.LXD)
2965 return das
2966
2935 def makeComponent(self, name=None):2967 def makeComponent(self, name=None):
2936 """Make a new `IComponent`."""2968 """Make a new `IComponent`."""
2937 if name is None:2969 if name is None:

Subscribers

People subscribed via source and target branches

to status/vote changes: