launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #31464
[Merge] ~jugmac00/launchpad:implement-RockRecipeRequestsBuildsJob into launchpad:master
Jürgen Gmach has proposed merging ~jugmac00/launchpad:implement-RockRecipeRequestsBuildsJob into launchpad:master with ~jugmac00/launchpad:add-a-rockraft.yaml-parser-like-charms as a prerequisite.
Commit message:
Implement RockRecipeRequestBuildsJob
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~jugmac00/launchpad/+git/launchpad/+merge/473246
--
Your team Launchpad code reviewers is requested to review the proposed merge of ~jugmac00/launchpad:implement-RockRecipeRequestsBuildsJob into launchpad:master.
diff --git a/lib/lp/rocks/adapters/buildarch.py b/lib/lp/rocks/adapters/buildarch.py
index 7f0cd5a..d2a1569 100644
--- a/lib/lp/rocks/adapters/buildarch.py
+++ b/lib/lp/rocks/adapters/buildarch.py
@@ -122,42 +122,17 @@ class RockBaseConfiguration:
return cls(build_on, run_on=run_on)
-def determine_instances_to_build(
- rockcraft_data, supported_arches, default_distro_series
-):
+def determine_instances_to_build(rockcraft_data, supported_arches):
"""Return a list of instances to build based on rockcraft.yaml.
:param rockcraft_data: A parsed rockcraft.yaml.
:param supported_arches: An ordered list of all `DistroArchSeries` that
we can create builds for. Note that these may span multiple
`DistroSeries`.
- :param default_distro_series: The default `DistroSeries` to use if
- rockcraft.yaml does not explicitly declare any bases.
:return: A list of `DistroArchSeries`.
"""
bases_list = rockcraft_data.get("bases")
-
- if bases_list:
- configs = [
- RockBaseConfiguration.from_dict(item) for item in bases_list
- ]
- else:
- # If no bases are specified, build one for each supported
- # architecture for the default series.
- configs = [
- RockBaseConfiguration(
- [
- RockBase(
- default_distro_series.distribution.name,
- default_distro_series.version,
- das.architecturetag,
- ),
- ]
- )
- for das in supported_arches
- if das.distroseries == default_distro_series
- ]
-
+ configs = [RockBaseConfiguration.from_dict(item) for item in bases_list]
# Ensure that multiple `run-on` items don't overlap; this is ambiguous
# and forbidden by rockcraft.
run_ons = Counter()
diff --git a/lib/lp/rocks/interfaces/rockrecipe.py b/lib/lp/rocks/interfaces/rockrecipe.py
index 18b5099..b803488 100644
--- a/lib/lp/rocks/interfaces/rockrecipe.py
+++ b/lib/lp/rocks/interfaces/rockrecipe.py
@@ -6,10 +6,14 @@
__all__ = [
"BadRockRecipeSource",
"BadRockRecipeSearchContext",
+<<<<<<< lib/lp/rocks/interfaces/rockrecipe.py
"ROCK_RECIPE_ALLOW_CREATE",
"ROCK_RECIPE_PRIVATE_FEATURE_FLAG",
-<<<<<<< lib/lp/rocks/interfaces/rockrecipe.py
=======
+ "CannotFetchRockcraftYaml",
+ "CannotParseRockcraftYaml",
+ "ROCK_RECIPE_ALLOW_CREATE",
+ "ROCK_RECIPE_PRIVATE_FEATURE_FLAG",
"RockRecipeBuildAlreadyPending",
"RockRecipeBuildDisallowedArchitecture",
"RockRecipeBuildRequestStatus",
@@ -21,10 +25,12 @@ __all__ = [
"DuplicateRockRecipeName",
"IRockRecipe",
<<<<<<< lib/lp/rocks/interfaces/rockrecipe.py
+ "IRockRecipeSet",
=======
"IRockRecipeBuildRequest",
->>>>>>> lib/lp/rocks/interfaces/rockrecipe.py
"IRockRecipeSet",
+ "MissingRockcraftYaml",
+>>>>>>> lib/lp/rocks/interfaces/rockrecipe.py
"NoSourceForRockRecipe",
"NoSuchRockRecipe",
]
@@ -147,6 +153,21 @@ class BadRockRecipeSearchContext(Exception):
<<<<<<< lib/lp/rocks/interfaces/rockrecipe.py
=======
+class MissingRockcraftYaml(Exception):
+ """The repository for this rock recipe does not have a rockcraft.yaml."""
+
+ def __init__(self, branch_name):
+ super().__init__("Cannot find rockcraft.yaml in %s" % branch_name)
+
+
+class CannotFetchRockcraftYaml(Exception):
+ """Launchpad cannot fetch this rock recipe's rockcraft.yaml."""
+
+
+class CannotParseRockcraftYaml(Exception):
+ """Launchpad cannot parse this rock recipe's rockcraft.yaml."""
+
+
@error_status(http.client.BAD_REQUEST)
class RockRecipeBuildAlreadyPending(Exception):
"""A build was requested when an identical build was already pending."""
@@ -331,6 +352,31 @@ class IRockRecipeView(Interface):
:return: An `IRockRecipeBuildRequest`.
"""
+ def requestBuildsFromJob(
+ build_request,
+ channels=None,
+ architectures=None,
+ allow_failures=False,
+ logger=None,
+ ):
+ """Synchronous part of `RockRecipe.requestBuilds`.
+
+ Request that the rock recipe be built for relevant architectures.
+
+ :param build_request: The `IRockRecipeBuildRequest` job being
+ processed.
+ :param channels: A dictionary mapping snap names to channels to use
+ for these builds.
+ :param architectures: If not None, limit builds to architectures
+ with these architecture tags (in addition to any other
+ applicable constraints).
+ :param allow_failures: If True, log exceptions other than "already
+ pending" from individual build requests; if False, raise them to
+ the caller.
+ :param logger: An optional logger.
+ :return: A sequence of `IRockRecipeBuild` instances.
+ """
+
def getBuildRequest(job_id):
"""Get an asynchronous build request by ID.
@@ -564,6 +610,23 @@ class IRockRecipeSet(Interface):
def preloadDataForRecipes(recipes, user):
"""Load the data related to a list of rock recipes."""
+ def getRockcraftYaml(context, logger=None):
+ """Fetch a recipe's rockcraft.yaml from code hosting, if possible.
+
+ :param context: Either an `IRockRecipe` or the source branch for a
+ rock recipe.
+ :param logger: An optional logger.
+
+ :return: The recipe's parsed rockcraft.yaml.
+ :raises MissingRockcraftYaml: if this recipe has no
+ rockcraft.yaml.
+ :raises CannotFetchRockcraftYaml: if it was not possible to fetch
+ rockcraft.yaml from the code hosting backend for some other
+ reason.
+ :raises CannotParseRockcraftYaml: if the fetched rockcraft.yaml
+ cannot be parsed.
+ """
+
>>>>>>> lib/lp/rocks/interfaces/rockrecipe.py
def findByGitRepository(repository, paths=None):
"""Return all rock recipes for the given Git repository.
diff --git a/lib/lp/rocks/model/rockrecipe.py b/lib/lp/rocks/model/rockrecipe.py
index 4ae64c7..a9de221 100644
--- a/lib/lp/rocks/model/rockrecipe.py
+++ b/lib/lp/rocks/model/rockrecipe.py
@@ -6,16 +6,18 @@
__all__ = [
"RockRecipe",
]
+<<<<<<< lib/lp/rocks/model/rockrecipe.py
from datetime import timezone
-<<<<<<< lib/lp/rocks/model/rockrecipe.py
from storm.databases.postgres import JSON
from storm.locals import Bool, DateTime, Int, Reference, Unicode
from zope.component import getUtility
=======
-from operator import itemgetter
+from datetime import timezone
+from operator import attrgetter, itemgetter
+import yaml
from lazr.lifecycle.event import ObjectCreatedEvent
from storm.databases.postgres import JSON
from storm.locals import (
@@ -43,8 +45,16 @@ from lp.app.enums import (
from lp.code.model.gitrepository import GitRepository
from lp.registry.errors import PrivatePersonLinkageError
from lp.registry.interfaces.person import validate_public_person
+from lp.rocks.interfaces.rockrecipe import (
+ ROCK_RECIPE_ALLOW_CREATE,
+ ROCK_RECIPE_PRIVATE_FEATURE_FLAG,
+ DuplicateRockRecipeName,
+ IRockRecipe,
+ IRockRecipeSet,
+ NoSourceForRockRecipe,
=======
from lp.buildmaster.enums import BuildStatus
+from lp.code.errors import GitRepositoryBlobNotFound, GitRepositoryScanFault
from lp.code.model.gitcollection import GenericGitCollection
from lp.code.model.gitrepository import GitRepository
from lp.registry.errors import PrivatePersonLinkageError
@@ -52,18 +62,17 @@ from lp.registry.interfaces.person import IPersonSet, validate_public_person
from lp.registry.model.distribution import Distribution
from lp.registry.model.distroseries import DistroSeries
from lp.registry.model.series import ACTIVE_STATUSES
->>>>>>> lib/lp/rocks/model/rockrecipe.py
+from lp.rocks.adapters.buildarch import determine_instances_to_build
from lp.rocks.interfaces.rockrecipe import (
ROCK_RECIPE_ALLOW_CREATE,
ROCK_RECIPE_PRIVATE_FEATURE_FLAG,
+ CannotFetchRockcraftYaml,
+ CannotParseRockcraftYaml,
DuplicateRockRecipeName,
IRockRecipe,
-<<<<<<< lib/lp/rocks/model/rockrecipe.py
- IRockRecipeSet,
- NoSourceForRockRecipe,
-=======
IRockRecipeBuildRequest,
IRockRecipeSet,
+ MissingRockcraftYaml,
NoSourceForRockRecipe,
RockRecipeBuildAlreadyPending,
RockRecipeBuildDisallowedArchitecture,
@@ -472,6 +481,88 @@ class RockRecipe(StormBase):
)
return self.getBuildRequest(job.job_id)
+ def requestBuildsFromJob(
+ self,
+ build_request,
+ channels=None,
+ architectures=None,
+ allow_failures=False,
+ logger=None,
+ ):
+ """See `IRockRecipe`."""
+ try:
+ rockcraft_data = removeSecurityProxy(
+ getUtility(IRockRecipeSet).getRockcraftYaml(self)
+ )
+
+ # Sort by (Distribution.id, DistroSeries.id, Processor.id) for
+ # determinism. This is chosen to be a similar order as in
+ # BinaryPackageBuildSet.createForSource, to minimize confusion.
+ supported_arches = [
+ das
+ for das in sorted(
+ self.getAllowedArchitectures(),
+ key=attrgetter(
+ "distroseries.distribution.id",
+ "distroseries.id",
+ "processor.id",
+ ),
+ )
+ if (
+ architectures is None
+ or das.architecturetag in architectures
+ )
+ ]
+ instances_to_build = determine_instances_to_build(
+ rockcraft_data, supported_arches
+ )
+ except Exception as e:
+ if not allow_failures:
+ raise
+ elif logger is not None:
+ logger.exception(
+ " - %s/%s/%s: %s",
+ self.owner.name,
+ self.project.name,
+ self.name,
+ e,
+ )
+
+ builds = []
+ for das in instances_to_build:
+ try:
+ build = self.requestBuild(
+ build_request, das, channels=channels
+ )
+ if logger is not None:
+ logger.debug(
+ " - %s/%s/%s %s/%s/%s: Build requested.",
+ self.owner.name,
+ self.project.name,
+ self.name,
+ das.distroseries.distribution.name,
+ das.distroseries.name,
+ das.architecturetag,
+ )
+ builds.append(build)
+ except RockRecipeBuildAlreadyPending:
+ pass
+ except Exception as e:
+ if not allow_failures:
+ raise
+ elif logger is not None:
+ logger.exception(
+ " - %s/%s/%s %s/%s/%s: %s",
+ self.owner.name,
+ self.project.name,
+ self.name,
+ das.distroseries.distribution.name,
+ das.distroseries.name,
+ das.architecturetag,
+ e,
+ )
+ return builds
+
def getBuildRequest(self, job_id):
"""See `IRockRecipe`."""
return RockRecipeBuildRequest(self, job_id)
@@ -608,6 +699,53 @@ class RockRecipeSet:
)
)
+ def getRockcraftYaml(self, context, logger=None):
+ """See `IRockRecipeSet`."""
+ if IRockRecipe.providedBy(context):
+ recipe = context
+ source = context.git_ref
+ else:
+ recipe = None
+ source = context
+ if source is None:
+ raise CannotFetchRockcraftYaml("Rock source is not defined")
+ try:
+ path = "rockcraft.yaml"
+ if recipe is not None and recipe.build_path is not None:
+ path = "/".join((recipe.build_path, path))
+ try:
+ blob = source.getBlob(path)
+ except GitRepositoryBlobNotFound:
+ if logger is not None:
+ logger.exception(
+ "Cannot find rockcraft.yaml in %s", source.unique_name
+ )
+ raise MissingRockcraftYaml(source.unique_name)
+ except GitRepositoryScanFault as e:
+ msg = "Failed to get rockcraft.yaml from %s"
+ if logger is not None:
+ logger.exception(msg, source.unique_name)
+ raise CannotFetchRockcraftYaml(
+ "%s: %s" % (msg % source.unique_name, e)
+ )
+
+ try:
+ rockcraft_data = yaml.safe_load(blob)
+ except Exception as e:
+ # Don't bother logging parsing errors from user-supplied YAML.
+ raise CannotParseRockcraftYaml(
+ "Cannot parse rockcraft.yaml from %s: %s"
+ % (source.unique_name, e)
+ )
+
+ if not isinstance(rockcraft_data, dict):
+ raise CannotParseRockcraftYaml(
+ "The top level of rockcraft.yaml from %s is not a mapping"
+ % source.unique_name
+ )
+
+ return rockcraft_data
+
>>>>>>> lib/lp/rocks/model/rockrecipe.py
def findByGitRepository(self, repository, paths=None):
"""See `IRockRecipeSet`."""
diff --git a/lib/lp/rocks/model/rockrecipejob.py b/lib/lp/rocks/model/rockrecipejob.py
index b0211af..f21df96 100644
--- a/lib/lp/rocks/model/rockrecipejob.py
+++ b/lib/lp/rocks/model/rockrecipejob.py
@@ -20,6 +20,11 @@ from zope.interface import implementer, provider
from lp.app.errors import NotFoundError
from lp.registry.interfaces.person import IPersonSet
+from lp.rocks.interfaces.rockrecipe import (
+ CannotFetchRockcraftYaml,
+ CannotParseRockcraftYaml,
+ MissingRockcraftYaml,
+)
from lp.rocks.interfaces.rockrecipejob import (
IRockRecipeJob,
IRockRecipeRequestBuildsJob,
@@ -156,6 +161,12 @@ class RockRecipeRequestBuildsJob(RockRecipeJobDerived):
class_job_type = RockRecipeJobType.REQUEST_BUILDS
+ user_error_types = (
+ CannotParseRockcraftYaml,
+ MissingRockcraftYaml,
+ )
+ retry_error_types = (CannotFetchRockcraftYaml,)
+
max_retries = 5
config = config.IRockRecipeRequestBuildsJobSource
@@ -304,9 +315,13 @@ class RockRecipeRequestBuildsJob(RockRecipeJobDerived):
)
return
try:
- # XXX jugmac00 2024-09-06: Implement this once we have a
- # RockRecipeBuild model.
- raise NotImplementedError
+ self.builds = self.recipe.requestBuildsFromJob(
+ self.build_request,
+ channels=self.channels,
+ architectures=self.architectures,
+ logger=log,
+ )
+ self.error_message = None
except Exception as e:
self.error_message = str(e)
# The normal job infrastructure will abort the transaction, but
diff --git a/lib/lp/rocks/tests/test_rockrecipe.py b/lib/lp/rocks/tests/test_rockrecipe.py
index 8f55598..50f8271 100644
--- a/lib/lp/rocks/tests/test_rockrecipe.py
+++ b/lib/lp/rocks/tests/test_rockrecipe.py
@@ -5,6 +5,9 @@
<<<<<<< lib/lp/rocks/tests/test_rockrecipe.py
=======
+from textwrap import dedent
+
+import transaction
from storm.locals import Store
from testtools.matchers import (
Equals,
@@ -20,6 +23,7 @@ from zope.security.proxy import removeSecurityProxy
from lp.app.enums import InformationType
<<<<<<< lib/lp/rocks/tests/test_rockrecipe.py
=======
+from lp.app.interfaces.launchpad import ILaunchpadCelebrities
from lp.buildmaster.enums import BuildQueueStatus, BuildStatus
from lp.buildmaster.interfaces.buildqueue import IBuildQueue
from lp.buildmaster.interfaces.processor import (
@@ -27,6 +31,7 @@ from lp.buildmaster.interfaces.processor import (
ProcessorNotFound,
)
from lp.buildmaster.model.buildqueue import BuildQueue
+from lp.code.tests.helpers import GitHostingFixture
>>>>>>> lib/lp/rocks/tests/test_rockrecipe.py
from lp.rocks.interfaces.rockrecipe import (
ROCK_RECIPE_ALLOW_CREATE,
@@ -220,6 +225,129 @@ class TestRockRecipe(TestCaseWithFactory):
),
)
+ def makeRequestBuildsJob(
+ self, distro_series_version, arch_tags, git_ref=None
+ ):
+ recipe = self.factory.makeRockRecipe(git_ref=git_ref)
+ distro_series = self.factory.makeDistroSeries(
+ distribution=getUtility(ILaunchpadCelebrities).ubuntu,
+ version=distro_series_version,
+ )
+ for arch_tag in arch_tags:
+ self.makeBuildableDistroArchSeries(
+ distroseries=distro_series, architecturetag=arch_tag
+ )
+ return getUtility(IRockRecipeRequestBuildsJobSource).create(
+ recipe, recipe.owner.teamowner, {"rockcraft": "edge"}
+ )
+
+ def assertRequestedBuildsMatch(
+ self, builds, job, distro_series_version, arch_tags, channels
+ ):
+ self.assertThat(
+ builds,
+ MatchesSetwise(
+ *(
+ MatchesStructure(
+ requester=Equals(job.requester),
+ recipe=Equals(job.recipe),
+ distro_arch_series=MatchesStructure(
+ distroseries=MatchesStructure.byEquality(
+ version=distro_series_version
+ ),
+ architecturetag=Equals(arch_tag),
+ ),
+ channels=Equals(channels),
+ )
+ for arch_tag in arch_tags
+ )
+ ),
+ )
+
+ def test_requestBuildsFromJob_restricts_explicit_list(self):
+ # requestBuildsFromJob limits builds targeted at an explicit list of
+ # architectures to those allowed for the recipe.
+ self.useFixture(
+ GitHostingFixture(
+ blob=dedent(
+ """\
+ bases:
+ - build-on:
+ - name: ubuntu
+ channel: "20.04"
+ architectures: [sparc]
+ - build-on:
+ - name: ubuntu
+ channel: "20.04"
+ architectures: [i386]
+ - build-on:
+ - name: ubuntu
+ channel: "20.04"
+ architectures: [avr]
+ """
+ )
+ )
+ )
+ job = self.makeRequestBuildsJob("20.04", ["sparc", "avr", "mips64el"])
+ self.assertEqual(
+ get_transaction_timestamp(IStore(job.recipe)), job.date_created
+ )
+ transaction.commit()
+ with person_logged_in(job.requester):
+ builds = job.recipe.requestBuildsFromJob(
+ job.build_request, channels=removeSecurityProxy(job.channels)
+ )
+ self.assertRequestedBuildsMatch(
+ builds, job, "20.04", ["sparc", "avr"], job.channels
+ )
+
+ def test_requestBuildsFromJob_architectures_parameter(self):
+ # If an explicit set of architectures was given as a parameter,
+ # requestBuildsFromJob intersects those with any other constraints
+ # when requesting builds.
+ # self.useFixture(GitHostingFixture(blob="name: foo\n"))
+ self.useFixture(
+ GitHostingFixture(
+ blob=dedent(
+ """\
+ bases:
+ - build-on:
+ - name: ubuntu
+ channel: "20.04"
+ architectures: [sparc]
+ - build-on:
+ - name: ubuntu
+ channel: "20.04"
+ architectures: [i386]
+ - build-on:
+ - name: ubuntu
+ channel: "20.04"
+ architectures: [avr]
+ - build-on:
+ - name: ubuntu
+ channel: "20.04"
+ architectures: [riscv64]
+ """
+ )
+ )
+ )
+ job = self.makeRequestBuildsJob(
+ "20.04", ["avr", "mips64el", "riscv64"]
+ )
+ self.assertEqual(
+ get_transaction_timestamp(IStore(job.recipe)), job.date_created
+ )
+ transaction.commit()
+ with person_logged_in(job.requester):
+ builds = job.recipe.requestBuildsFromJob(
+ job.build_request,
+ channels=removeSecurityProxy(job.channels),
+ architectures={"avr", "riscv64"},
+ )
+ self.assertRequestedBuildsMatch(
+ builds, job, "20.04", ["avr", "riscv64"], job.channels
+ )
+
>>>>>>> lib/lp/rocks/tests/test_rockrecipe.py
def test_delete_without_builds(self):
# A rock recipe with no builds can be deleted.
diff --git a/lib/lp/rocks/tests/test_rockrecipejob.py b/lib/lp/rocks/tests/test_rockrecipejob.py
index f5da198..d3aee31 100644
--- a/lib/lp/rocks/tests/test_rockrecipejob.py
+++ b/lib/lp/rocks/tests/test_rockrecipejob.py
@@ -3,7 +3,28 @@
"""Tests for rock recipe jobs."""
-from lp.rocks.interfaces.rockrecipe import ROCK_RECIPE_ALLOW_CREATE
+from textwrap import dedent
+
+import six
+from testtools.matchers import (
+ AfterPreprocessing,
+ ContainsDict,
+ Equals,
+ GreaterThan,
+ Is,
+ LessThan,
+ MatchesAll,
+ MatchesSetwise,
+ MatchesStructure,
+)
+from zope.component import getUtility
+
+from lp.app.interfaces.launchpad import ILaunchpadCelebrities
+from lp.code.tests.helpers import GitHostingFixture
+from lp.rocks.interfaces.rockrecipe import (
+ ROCK_RECIPE_ALLOW_CREATE,
+ CannotParseRockcraftYaml,
+)
from lp.rocks.interfaces.rockrecipejob import (
IRockRecipeJob,
IRockRecipeRequestBuildsJob,
@@ -13,8 +34,15 @@ from lp.rocks.model.rockrecipejob import (
RockRecipeJobType,
RockRecipeRequestBuildsJob,
)
+from lp.services.config import config
+from lp.services.database.interfaces import IStore
+from lp.services.database.sqlbase import get_transaction_timestamp
from lp.services.features.testing import FeatureFixture
+from lp.services.job.interfaces.job import JobStatus
+from lp.services.job.runner import JobRunner
+from lp.services.mail.sendmail import format_address_for_person
from lp.testing import TestCaseWithFactory
+from lp.testing.dbuser import dbuser
from lp.testing.layers import ZopelessDatabaseLayer
@@ -59,3 +87,192 @@ class TestRockRecipeRequestBuildsJob(TestCaseWithFactory):
% (recipe.owner.name, recipe.project.name, recipe.name),
repr(job),
)
+
+ def makeSeriesAndProcessors(self, distro_series_version, arch_tags):
+ distroseries = self.factory.makeDistroSeries(
+ distribution=getUtility(ILaunchpadCelebrities).ubuntu,
+ version=distro_series_version,
+ )
+ processors = [
+ self.factory.makeProcessor(
+ name=arch_tag, supports_virtualized=True
+ )
+ for arch_tag in arch_tags
+ ]
+ for processor in processors:
+ das = self.factory.makeDistroArchSeries(
+ distroseries=distroseries,
+ architecturetag=processor.name,
+ processor=processor,
+ )
+ das.addOrUpdateChroot(
+ self.factory.makeLibraryFileAlias(
+ filename="fake_chroot.tar.gz", db_only=True
+ )
+ )
+ return distroseries, processors
+
+ def test_run(self):
+ # The job requests builds and records the result.
+ distroseries, _ = self.makeSeriesAndProcessors(
+ "20.04", ["avr2001", "sparc64", "x32"]
+ )
+ [git_ref] = self.factory.makeGitRefs()
+ recipe = self.factory.makeRockRecipe(git_ref=git_ref)
+ expected_date_created = get_transaction_timestamp(IStore(recipe))
+ job = RockRecipeRequestBuildsJob.create(
+ recipe, recipe.registrant, channels={"core": "stable"}
+ )
+ rockcraft_yaml = dedent(
+ """\
+ bases:
+ - build-on:
+ - name: ubuntu
+ channel: "20.04"
+ architectures: [avr2001]
+ - build-on:
+ - name: ubuntu
+ channel: "20.04"
+ architectures: [x32]
+ """
+ )
+ self.useFixture(GitHostingFixture(blob=rockcraft_yaml))
+ with dbuser(config.IRockRecipeRequestBuildsJobSource.dbuser):
+ JobRunner([job]).runAll()
+ now = get_transaction_timestamp(IStore(recipe))
+ self.assertEmailQueueLength(0)
+ self.assertThat(
+ job,
+ MatchesStructure(
+ job=MatchesStructure.byEquality(status=JobStatus.COMPLETED),
+ date_created=Equals(expected_date_created),
+ date_finished=MatchesAll(
+ GreaterThan(expected_date_created), LessThan(now)
+ ),
+ error_message=Is(None),
+ builds=AfterPreprocessing(
+ set,
+ MatchesSetwise(
+ *[
+ MatchesStructure(
+ build_request=MatchesStructure.byEquality(
+ id=job.job.id
+ ),
+ requester=Equals(recipe.registrant),
+ recipe=Equals(recipe),
+ distro_arch_series=Equals(distroseries[arch]),
+ channels=Equals({"core": "stable"}),
+ )
+ for arch in ("avr2001", "x32")
+ ]
+ ),
+ ),
+ ),
+ )
+
+ def test_run_with_architectures(self):
+ # If the user explicitly requested architectures, the job passes
+ # those through when requesting builds, intersecting them with other
+ # constraints.
+ distroseries, _ = self.makeSeriesAndProcessors(
+ "20.04", ["avr2001", "sparc64", "x32"]
+ )
+ [git_ref] = self.factory.makeGitRefs()
+ recipe = self.factory.makeRockRecipe(git_ref=git_ref)
+ expected_date_created = get_transaction_timestamp(IStore(recipe))
+ job = RockRecipeRequestBuildsJob.create(
+ recipe,
+ recipe.registrant,
+ channels={"core": "stable"},
+ architectures=["sparc64", "x32"],
+ )
+ rockcraft_yaml = dedent(
+ """\
+ bases:
+ - build-on:
+ - name: ubuntu
+ channel: "20.04"
+ architectures: [avr2001]
+ - build-on:
+ - name: ubuntu
+ channel: "20.04"
+ architectures: [x32]
+ """
+ )
+ self.useFixture(GitHostingFixture(blob=rockcraft_yaml))
+ with dbuser(config.IRockRecipeRequestBuildsJobSource.dbuser):
+ JobRunner([job]).runAll()
+ now = get_transaction_timestamp(IStore(recipe))
+ self.assertEmailQueueLength(0)
+ self.assertThat(
+ job,
+ MatchesStructure(
+ job=MatchesStructure.byEquality(status=JobStatus.COMPLETED),
+ date_created=Equals(expected_date_created),
+ date_finished=MatchesAll(
+ GreaterThan(expected_date_created), LessThan(now)
+ ),
+ error_message=Is(None),
+ builds=AfterPreprocessing(
+ set,
+ MatchesSetwise(
+ MatchesStructure(
+ build_request=MatchesStructure.byEquality(
+ id=job.job.id
+ ),
+ requester=Equals(recipe.registrant),
+ recipe=Equals(recipe),
+ distro_arch_series=Equals(distroseries["x32"]),
+ channels=Equals({"core": "stable"}),
+ )
+ ),
+ ),
+ ),
+ )
+
+ def test_run_failed(self):
+ # A failed run sets the job status to FAILED and records the error
+ # message.
+ [git_ref] = self.factory.makeGitRefs()
+ recipe = self.factory.makeRockRecipe(git_ref=git_ref)
+ expected_date_created = get_transaction_timestamp(IStore(recipe))
+ job = RockRecipeRequestBuildsJob.create(
+ recipe, recipe.registrant, channels={"core": "stable"}
+ )
+ self.useFixture(GitHostingFixture()).getBlob.failure = (
+ CannotParseRockcraftYaml("Nonsense on stilts")
+ )
+ with dbuser(config.IRockRecipeRequestBuildsJobSource.dbuser):
+ JobRunner([job]).runAll()
+ now = get_transaction_timestamp(IStore(recipe))
+ [notification] = self.assertEmailQueueLength(1)
+ self.assertThat(
+ dict(notification),
+ ContainsDict(
+ {
+ "From": Equals(config.canonical.noreply_from_address),
+ "To": Equals(format_address_for_person(recipe.registrant)),
+ "Subject": Equals(
+ "Launchpad error while requesting builds of %s"
+ % recipe.name
+ ),
+ }
+ ),
+ )
+ self.assertEqual(
+ "Launchpad encountered an error during the following operation: "
+ "requesting builds of %s. Nonsense on stilts" % recipe.name,
+ six.ensure_text(notification.get_payload(decode=True)),
+ )
+ self.assertThat(
+ job,
+ MatchesStructure(
+ job=MatchesStructure.byEquality(status=JobStatus.FAILED),
+ date_created=Equals(expected_date_created),
+ date_finished=MatchesAll(
+ GreaterThan(expected_date_created), LessThan(now)
+ ),
+ error_message=Equals("Nonsense on stilts"),
+ builds=AfterPreprocessing(set, MatchesSetwise()),
+ ),
+ )
Follow ups