launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #31617
[Merge] ~ruinedyourlife/launchpad:completing-craft-api into launchpad:master
Quentin Debhi has proposed merging ~ruinedyourlife/launchpad:completing-craft-api into launchpad:master with ~ruinedyourlife/launchpad:delete-craft-recipe-builds-and-jobs-when-deleting-recipes as a prerequisite.
Commit message:
Complete craft api without frontend integration
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~ruinedyourlife/launchpad/+git/launchpad/+merge/474279
Implementing the changes similar to the rock ones but without all the frontend elements to speed up the process
--
Your team Launchpad code reviewers is requested to review the proposed merge of ~ruinedyourlife/launchpad:completing-craft-api into launchpad:master.
diff --git a/lib/lp/code/model/gitrepository.py b/lib/lp/code/model/gitrepository.py
index 724a9eb..440b270 100644
--- a/lib/lp/code/model/gitrepository.py
+++ b/lib/lp/code/model/gitrepository.py
@@ -103,6 +103,7 @@ from lp.code.model.gitrule import GitRule, GitRuleGrant
from lp.code.model.gitsubscription import GitSubscription
from lp.code.model.reciperegistry import recipe_registry
from lp.code.model.revisionstatus import RevisionStatusReport
+from lp.crafts.interfaces.craftrecipe import ICraftRecipeSet
from lp.registry.enums import PersonVisibility
from lp.registry.errors import CannotChangeInformationType
from lp.registry.interfaces.accesspolicy import (
@@ -932,6 +933,10 @@ class GitRepository(
self, paths=paths
):
get_property_cache(recipe)._git_ref = None
+ for recipe in getUtility(ICraftRecipeSet).findByGitRepository(
+ self, paths=paths
+ ):
+ get_property_cache(recipe)._git_ref = None
self.date_last_modified = UTC_NOW
def planRefChanges(self, hosting_path, logger=None):
diff --git a/lib/lp/crafts/interfaces/craftrecipe.py b/lib/lp/crafts/interfaces/craftrecipe.py
index 0b9b69b..1fa3858 100644
--- a/lib/lp/crafts/interfaces/craftrecipe.py
+++ b/lib/lp/crafts/interfaces/craftrecipe.py
@@ -31,7 +31,7 @@ import http.client
from lazr.enum import EnumeratedType, Item
from lazr.restful.declarations import error_status, exported
from lazr.restful.fields import CollectionField, Reference, ReferenceChoice
-from zope.interface import Interface
+from zope.interface import Attribute, Interface
from zope.schema import (
Bool,
Choice,
@@ -285,6 +285,8 @@ class ICraftRecipeView(Interface):
description=_("The person who registered this craft recipe."),
)
+ source = Attribute("The source branch for this craft recipe.")
+
private = Bool(
title=_("Private"),
required=False,
@@ -363,6 +365,53 @@ class ICraftRecipeView(Interface):
:return: `ICraftRecipeBuildRequest`.
"""
+ pending_build_requests = CollectionField(
+ title=_("Pending build requests for this craft recipe."),
+ value_type=Reference(ICraftRecipeBuildRequest),
+ required=True,
+ readonly=True,
+ )
+
+ failed_build_requests = CollectionField(
+ title=_("Failed build requests for this craft recipe."),
+ value_type=Reference(ICraftRecipeBuildRequest),
+ required=True,
+ readonly=True,
+ )
+
+ builds = CollectionField(
+ title=_("All builds of this craft recipe."),
+ description=_(
+ "All builds of this craft recipe, sorted in descending order "
+ "of finishing (or starting if not completed successfully)."
+ ),
+ # Really ICraftRecipeBuild.
+ value_type=Reference(schema=Interface),
+ readonly=True,
+ )
+
+ completed_builds = CollectionField(
+ title=_("Completed builds of this craft recipe."),
+ description=_(
+ "Completed builds of this craft recipe, sorted in descending "
+ "order of finishing."
+ ),
+ # Really ICraftRecipeBuild.
+ value_type=Reference(schema=Interface),
+ readonly=True,
+ )
+
+ pending_builds = CollectionField(
+ title=_("Pending builds of this craft recipe."),
+ description=_(
+ "Pending builds of this craft recipe, sorted in descending "
+ "order of creation."
+ ),
+ # Really ICraftRecipeBuild.
+ value_type=Reference(schema=Interface),
+ readonly=True,
+ )
+
class ICraftRecipeEdit(Interface):
"""`ICraftRecipe` methods that require launchpad.Edit permission."""
@@ -424,7 +473,7 @@ class ICraftRecipeEditableAttributes(Interface):
git_path = TextLine(
title=_("Git branch path"),
required=False,
- readonly=False,
+ readonly=True,
description=_(
"The path of the Git branch containing a craft.yaml recipe."
),
@@ -583,12 +632,13 @@ class ICraftRecipeSet(Interface):
def isValidInformationType(information_type, owner, git_ref=None):
"""Whether the information type context is valid."""
- def findByGitRepository(repository, paths=None):
+ def findByGitRepository(repository, paths=None, check_permissions=True):
"""Return all craft recipes for the given Git repository.
:param repository: An `IGitRepository`.
:param paths: If not None, only return craft recipes for one of
these Git reference paths.
+ :param check_permissions: If True, check the user's permissions.
"""
def findByOwner(owner):
@@ -620,3 +670,38 @@ class ICraftRecipeSet(Interface):
:raises CannotParseSourcecraftYaml: if the fetched sourcecraft.yaml
cannot be parsed.
"""
+
+ def findByPerson(person, visible_by_user=None):
+ """Return all craft recipes relevant to `person`.
+
+ This returns craft recipes for Git branches owned by `person`, or
+ where `person` is the owner of the craft recipe.
+
+ :param person: An `IPerson`.
+ :param visible_by_user: If not None, only return recipes visible by
+ this user; otherwise, only return publicly-visible recipes.
+ """
+
+ def findByProject(project, visible_by_user=None):
+ """Return all craft recipes for the given project.
+
+ :param project: An `IProduct`.
+ :param visible_by_user: If not None, only return recipes visible by
+ this user; otherwise, only return publicly-visible recipes.
+ """
+
+ def findByGitRef(ref):
+ """Return all craft recipes for the given Git reference."""
+
+ def findByContext(context, visible_by_user=None, order_by_date=True):
+ """Return all craft recipes for the given context.
+
+ :param context: An `IPerson`, `IProduct`, `IGitRepository`, or
+ `IGitRef`.
+ :param visible_by_user: If not None, only return recipes visible by
+ this user; otherwise, only return publicly-visible recipes.
+ :param order_by_date: If True, order recipes by descending
+ modification date.
+ :raises BadCraftRecipeSearchContext: if the context is not
+ understood.
+ """
diff --git a/lib/lp/crafts/model/craftrecipe.py b/lib/lp/crafts/model/craftrecipe.py
index a0af625..cf4670c 100644
--- a/lib/lp/crafts/model/craftrecipe.py
+++ b/lib/lp/crafts/model/craftrecipe.py
@@ -5,6 +5,7 @@
__all__ = [
"CraftRecipe",
+ "get_craft_recipe_privacy_filter",
]
from datetime import timezone
@@ -17,8 +18,10 @@ from storm.locals import (
And,
Bool,
DateTime,
+ Desc,
Int,
Join,
+ Not,
Or,
Reference,
Select,
@@ -36,16 +39,26 @@ from lp.app.enums import (
InformationType,
)
from lp.buildmaster.enums import BuildStatus
+from lp.buildmaster.interfaces.buildqueue import IBuildQueueSet
+from lp.buildmaster.model.builder import Builder
from lp.buildmaster.model.buildfarmjob import BuildFarmJob
from lp.buildmaster.model.buildqueue import BuildQueue
from lp.code.errors import GitRepositoryBlobNotFound, GitRepositoryScanFault
+from lp.code.interfaces.gitcollection import (
+ IAllGitRepositories,
+ IGitCollection,
+)
+from lp.code.interfaces.gitref import IGitRef
+from lp.code.interfaces.gitrepository import IGitRepository
from lp.code.model.gitcollection import GenericGitCollection
+from lp.code.model.gitref import GitRef
from lp.code.model.gitrepository import GitRepository
from lp.code.model.reciperegistry import recipe_registry
from lp.crafts.adapters.buildarch import determine_instances_to_build
from lp.crafts.interfaces.craftrecipe import (
CRAFT_RECIPE_ALLOW_CREATE,
CRAFT_RECIPE_PRIVATE_FEATURE_FLAG,
+ BadCraftRecipeSearchContext,
CannotFetchSourcecraftYaml,
CannotParseSourcecraftYaml,
CraftRecipeBuildAlreadyPending,
@@ -70,9 +83,15 @@ from lp.crafts.interfaces.craftrecipejob import (
from lp.crafts.model.craftrecipebuild import CraftRecipeBuild
from lp.crafts.model.craftrecipejob import CraftRecipeJob
from lp.registry.errors import PrivatePersonLinkageError
-from lp.registry.interfaces.person import IPersonSet, validate_public_person
+from lp.registry.interfaces.person import (
+ IPerson,
+ IPersonSet,
+ validate_public_person,
+)
+from lp.registry.interfaces.product import IProduct
from lp.registry.model.distribution import Distribution
from lp.registry.model.distroseries import DistroSeries
+from lp.registry.model.product import Product
from lp.registry.model.series import ACTIVE_STATUSES
from lp.services.database.bulk import load_related
from lp.services.database.constants import DEFAULT, UTC_NOW
@@ -80,6 +99,7 @@ from lp.services.database.decoratedresultset import DecoratedResultSet
from lp.services.database.enumcol import DBEnum
from lp.services.database.interfaces import IPrimaryStore, IStore
from lp.services.database.stormbase import StormBase
+from lp.services.database.stormexpr import Greatest, NullsLast
from lp.services.features import getFeatureFlag
from lp.services.job.interfaces.job import JobStatus
from lp.services.job.model.job import Job
@@ -241,14 +261,18 @@ class CraftRecipe(StormBase):
"""See `ICraftRecipe`."""
return self.information_type not in PUBLIC_INFORMATION_TYPES
- @property
- def git_ref(self):
- """See `ICraftRecipe`."""
+ @cachedproperty
+ def _git_ref(self):
if self.git_repository is not None:
return self.git_repository.getRefByPath(self.git_path)
else:
return None
+ @property
+ def git_ref(self):
+ """See `ICraftRecipe`."""
+ return self._git_ref
+
@git_ref.setter
def git_ref(self, value):
"""See `ICraftRecipe`."""
@@ -258,6 +282,12 @@ class CraftRecipe(StormBase):
else:
self.git_repository = None
self.git_path = None
+ get_property_cache(self)._git_ref = value
+
+ @property
+ def source(self):
+ """See `ICraftRecipe`."""
+ return self.git_ref
@property
def store_channels(self):
@@ -279,9 +309,17 @@ class CraftRecipe(StormBase):
"""See `ICraftRecipe`."""
if self.information_type in PUBLIC_INFORMATION_TYPES:
return True
- # XXX ruinedyourlife 2024-09-24: Finish implementing this once we have
- # more privacy infrastructure.
- return False
+ if user is None:
+ return False
+ return (
+ not IStore(CraftRecipe)
+ .find(
+ CraftRecipe,
+ CraftRecipe.id == self.id,
+ get_craft_recipe_privacy_filter(user),
+ )
+ .is_empty()
+ )
def _isBuildableArchitectureAllowed(self, das):
"""Check whether we may build for a buildable `DistroArchSeries`.
@@ -515,6 +553,98 @@ class CraftRecipe(StormBase):
"""See `ICraftRecipe`."""
return CraftRecipeBuildRequest(self, job_id)
+ @property
+ def pending_build_requests(self):
+ """See `ICraftRecipe`."""
+ job_source = getUtility(ICraftRecipeRequestBuildsJobSource)
+ # The returned jobs are ordered by descending ID.
+ jobs = job_source.findByRecipe(
+ self, statuses=(JobStatus.WAITING, JobStatus.RUNNING)
+ )
+ return DecoratedResultSet(
+ jobs, result_decorator=CraftRecipeBuildRequest.fromJob
+ )
+
+ @property
+ def failed_build_requests(self):
+ """See `ICraftRecipe`."""
+ job_source = getUtility(ICraftRecipeRequestBuildsJobSource)
+ # The returned jobs are ordered by descending ID.
+ jobs = job_source.findByRecipe(self, statuses=(JobStatus.FAILED,))
+ return DecoratedResultSet(
+ jobs, result_decorator=CraftRecipeBuildRequest.fromJob
+ )
+
+ def _getBuilds(self, filter_term, order_by):
+ """The actual query to get the builds."""
+ query_args = [
+ CraftRecipeBuild.recipe == self,
+ ]
+ if filter_term is not None:
+ query_args.append(filter_term)
+ result = Store.of(self).find(CraftRecipeBuild, *query_args)
+ result.order_by(order_by)
+
+ def eager_load(rows):
+ getUtility(ICraftRecipeBuildSet).preloadBuildsData(rows)
+ getUtility(IBuildQueueSet).preloadForBuildFarmJobs(rows)
+ load_related(Builder, rows, ["builder_id"])
+
+ return DecoratedResultSet(result, pre_iter_hook=eager_load)
+
+ @property
+ def builds(self):
+ """See `ICraftRecipe`."""
+ order_by = (
+ NullsLast(
+ Desc(
+ Greatest(
+ CraftRecipeBuild.date_started,
+ CraftRecipeBuild.date_finished,
+ )
+ )
+ ),
+ Desc(CraftRecipeBuild.date_created),
+ Desc(CraftRecipeBuild.id),
+ )
+ return self._getBuilds(None, order_by)
+
+ @property
+ def _pending_states(self):
+ """All the build states we consider pending (non-final)."""
+ return [
+ BuildStatus.NEEDSBUILD,
+ BuildStatus.BUILDING,
+ BuildStatus.UPLOADING,
+ BuildStatus.CANCELLING,
+ ]
+
+ @property
+ def completed_builds(self):
+ """See `ICraftRecipe`."""
+ filter_term = Not(CraftRecipeBuild.status.is_in(self._pending_states))
+ order_by = (
+ NullsLast(
+ Desc(
+ Greatest(
+ CraftRecipeBuild.date_started,
+ CraftRecipeBuild.date_finished,
+ )
+ )
+ ),
+ Desc(CraftRecipeBuild.id),
+ )
+ return self._getBuilds(filter_term, order_by)
+
+ @property
+ def pending_builds(self):
+ """See `ICraftRecipe`."""
+ filter_term = CraftRecipeBuild.status.is_in(self._pending_states)
+ # We want to order by date_created but this is the same as ordering
+ # by id (since id increases monotonically) and is less expensive.
+ order_by = Desc(CraftRecipeBuild.id)
+ return self._getBuilds(filter_term, order_by)
+
@recipe_registry.register_recipe_type(
ICraftRecipeSet, "Some craft recipes build from this repository."
@@ -627,28 +757,109 @@ class CraftRecipeSet:
return True
- def findByGitRepository(self, repository, paths=None):
+ def _getRecipesFromCollection(
+ self, collection, owner=None, visible_by_user=None
+ ):
+ id_column = CraftRecipe.git_repository_id
+ ids = collection.getRepositoryIds()
+ expressions = [id_column.is_in(ids._get_select())]
+ if owner is not None:
+ expressions.append(CraftRecipe.owner == owner)
+ expressions.append(get_craft_recipe_privacy_filter(visible_by_user))
+ return IStore(CraftRecipe).find(CraftRecipe, *expressions)
+
+ def findByPerson(self, person, visible_by_user=None):
+ """See `ICraftRecipeSet`."""
+
+ def _getRecipes(collection):
+ collection = collection.visibleByUser(visible_by_user)
+ owned = self._getRecipesFromCollection(
+ collection.ownedBy(person), visible_by_user=visible_by_user
+ )
+ packaged = self._getRecipesFromCollection(
+ collection, owner=person, visible_by_user=visible_by_user
+ )
+ return owned.union(packaged)
+
+ git_collection = removeSecurityProxy(getUtility(IAllGitRepositories))
+ git_recipes = _getRecipes(git_collection)
+ return git_recipes
+
+ def findByProject(self, project, visible_by_user=None):
+ """See `ICraftRecipeSet`."""
+
+ def _getRecipes(collection):
+ return self._getRecipesFromCollection(
+ collection.visibleByUser(visible_by_user),
+ visible_by_user=visible_by_user,
+ )
+
+ recipes_for_project = IStore(CraftRecipe).find(
+ CraftRecipe,
+ CraftRecipe.project == project,
+ get_craft_recipe_privacy_filter(visible_by_user),
+ )
+ git_collection = removeSecurityProxy(IGitCollection(project))
+ return recipes_for_project.union(_getRecipes(git_collection))
+
+ def findByGitRepository(
+ self,
+ repository,
+ paths=None,
+ visible_by_user=None,
+ check_permissions=True,
+ ):
"""See `ICraftRecipeSet`."""
clauses = [CraftRecipe.git_repository == repository]
if paths is not None:
clauses.append(CraftRecipe.git_path.is_in(paths))
- # XXX ruinedyourlife 2024-09-24: Check permissions once we have some
- # privacy infrastructure.
+ if check_permissions:
+ clauses.append(get_craft_recipe_privacy_filter(visible_by_user))
return IStore(CraftRecipe).find(CraftRecipe, *clauses)
- def findByOwner(self, owner):
+ def findByGitRef(self, ref, visible_by_user=None):
"""See `ICraftRecipeSet`."""
- return IStore(CraftRecipe).find(CraftRecipe, owner=owner)
+ return IStore(CraftRecipe).find(
+ CraftRecipe,
+ CraftRecipe.git_repository == ref.repository,
+ CraftRecipe.git_path == ref.path,
+ get_craft_recipe_privacy_filter(visible_by_user),
+ )
- def detachFromGitRepository(self, repository):
+ def findByContext(self, context, visible_by_user=None, order_by_date=True):
"""See `ICraftRecipeSet`."""
- self.findByGitRepository(repository).set(
- git_repository_id=None, git_path=None, date_last_modified=UTC_NOW
- )
+ if IPerson.providedBy(context):
+ recipes = self.findByPerson(
+ context, visible_by_user=visible_by_user
+ )
+ elif IProduct.providedBy(context):
+ recipes = self.findByProject(
+ context, visible_by_user=visible_by_user
+ )
+ elif IGitRepository.providedBy(context):
+ recipes = self.findByGitRepository(
+ context, visible_by_user=visible_by_user
+ )
+ elif IGitRef.providedBy(context):
+ recipes = self.findByGitRef(
+ context, visible_by_user=visible_by_user
+ )
+ else:
+ raise BadCraftRecipeSearchContext(context)
+ if order_by_date:
+ recipes = recipes.order_by(Desc(CraftRecipe.date_last_modified))
+ return recipes
+
+ def findByOwner(self, owner):
+ """See `ICraftRecipeSet`."""
+ return IStore(CraftRecipe).find(CraftRecipe, owner=owner)
def preloadDataForRecipes(self, recipes, user=None):
"""See `ICraftRecipeSet`."""
recipes = [removeSecurityProxy(recipe) for recipe in recipes]
+
+ load_related(Product, recipes, ["project_id"])
+
person_ids = set()
for recipe in recipes:
person_ids.add(recipe.registrant_id)
@@ -660,6 +871,14 @@ class CraftRecipeSet:
if repositories:
GenericGitCollection.preloadDataForRepositories(repositories)
+ git_refs = GitRef.findByReposAndPaths(
+ [(recipe.git_repository, recipe.git_path) for recipe in recipes]
+ )
+ for recipe in recipes:
+ git_ref = git_refs.get((recipe.git_repository, recipe.git_path))
+ if git_ref is not None:
+ get_property_cache(recipe)._git_ref = git_ref
+
# Add repository owners to the list of pre-loaded persons. We need
# the target repository owner as well, since repository unique names
# aren't trigger-maintained.
@@ -671,6 +890,15 @@ class CraftRecipeSet:
)
)
+ def detachFromGitRepository(self, repository):
+ """See `ICraftRecipeSet`."""
+ recipes = self.findByGitRepository(repository)
+ for recipe in recipes:
+ get_property_cache(recipe)._git_ref = None
+ recipes.set(
+ git_repository_id=None, git_path=None, date_last_modified=UTC_NOW
+ )
+
def getSourcecraftYaml(self, context, logger=None):
"""See `ICraftRecipeSet`."""
if ICraftRecipe.providedBy(context):
@@ -790,3 +1018,14 @@ class CraftRecipeBuildRequest:
def architectures(self):
"""See `ICraftRecipeBuildRequest`."""
return self._job.architectures
+
+
+def get_craft_recipe_privacy_filter(user):
+ """Return a Storm query filter to find craft recipes visible to `user`."""
+ public_filter = CraftRecipe.information_type.is_in(
+ PUBLIC_INFORMATION_TYPES
+ )
+
+ # XXX ruinedyourlife 2024-10-02: Flesh this out once we have more privacy
+ # infrastructure.
+ return [public_filter]
diff --git a/lib/lp/crafts/model/craftrecipebuild.py b/lib/lp/crafts/model/craftrecipebuild.py
index 6f989e9..e859c6b 100644
--- a/lib/lp/crafts/model/craftrecipebuild.py
+++ b/lib/lp/crafts/model/craftrecipebuild.py
@@ -35,6 +35,8 @@ from lp.crafts.interfaces.craftrecipebuild import (
from lp.crafts.mail.craftrecipebuild import CraftRecipeBuildMailer
from lp.registry.interfaces.pocket import PackagePublishingPocket
from lp.registry.interfaces.series import SeriesStatus
+from lp.registry.model.distribution import Distribution
+from lp.registry.model.distroseries import DistroSeries
from lp.registry.model.person import Person
from lp.services.config import config
from lp.services.database.bulk import load_related
@@ -46,6 +48,7 @@ from lp.services.database.stormbase import StormBase
from lp.services.librarian.model import LibraryFileAlias, LibraryFileContent
from lp.services.propertycache import cachedproperty, get_property_cache
from lp.services.webapp.snapshot import notify_modified
+from lp.soyuz.model.distroarchseries import DistroArchSeries
@implementer(ICraftRecipeBuild)
@@ -408,6 +411,13 @@ class CraftRecipeBuildSet(SpecificBuildFarmJobSourceMixin):
load_related(Person, builds, ["requester_id"])
lfas = load_related(LibraryFileAlias, builds, ["log_id"])
load_related(LibraryFileContent, lfas, ["contentID"])
+ distroarchserieses = load_related(
+ DistroArchSeries, builds, ["distro_arch_series_id"]
+ )
+ distroserieses = load_related(
+ DistroSeries, distroarchserieses, ["distroseries_id"]
+ )
+ load_related(Distribution, distroserieses, ["distribution_id"])
recipes = load_related(CraftRecipe, builds, ["recipe_id"])
getUtility(ICraftRecipeSet).preloadDataForRecipes(recipes)
diff --git a/lib/lp/crafts/tests/test_craftrecipe.py b/lib/lp/crafts/tests/test_craftrecipe.py
index afd74b3..cbff62a 100644
--- a/lib/lp/crafts/tests/test_craftrecipe.py
+++ b/lib/lp/crafts/tests/test_craftrecipe.py
@@ -30,6 +30,7 @@ from lp.buildmaster.model.buildqueue import BuildQueue
from lp.code.tests.helpers import GitHostingFixture
from lp.crafts.interfaces.craftrecipe import (
CRAFT_RECIPE_ALLOW_CREATE,
+ BadCraftRecipeSearchContext,
CraftRecipeBuildAlreadyPending,
CraftRecipeBuildDisallowedArchitecture,
CraftRecipeBuildRequestStatus,
@@ -692,6 +693,111 @@ class TestCraftRecipeSet(TestCaseWithFactory):
recipe, "date_last_modified", UTC_NOW
)
+ def test_findByPerson(self):
+ # ICraftRecipeSet.findByPerson returns all craft recipes with the
+ # given owner or based on repositories with the given owner.
+ owners = [self.factory.makePerson() for i in range(2)]
+ recipes = []
+ for owner in owners:
+ recipes.append(
+ self.factory.makeCraftRecipe(registrant=owner, owner=owner)
+ )
+ [ref] = self.factory.makeGitRefs(owner=owner)
+ recipes.append(self.factory.makeCraftRecipe(git_ref=ref))
+ recipe_set = getUtility(ICraftRecipeSet)
+ self.assertContentEqual(
+ recipes[:2], recipe_set.findByPerson(owners[0])
+ )
+ self.assertContentEqual(
+ recipes[2:], recipe_set.findByPerson(owners[1])
+ )
+
+ def test_findByProject(self):
+ # ICraftRecipeSet.findByProject returns all craft recipes based on
+ # repositories for the given project, and craft recipes associated
+ # directly with the project.
+ projects = [self.factory.makeProduct() for i in range(2)]
+ recipes = []
+ for project in projects:
+ [ref] = self.factory.makeGitRefs(target=project)
+ recipes.append(self.factory.makeCraftRecipe(git_ref=ref))
+ recipes.append(self.factory.makeCraftRecipe(project=project))
+ [ref] = self.factory.makeGitRefs(target=None)
+ recipes.append(self.factory.makeCraftRecipe(git_ref=ref))
+ recipe_set = getUtility(ICraftRecipeSet)
+ self.assertContentEqual(
+ recipes[:2], recipe_set.findByProject(projects[0])
+ )
+ self.assertContentEqual(
+ recipes[2:4], recipe_set.findByProject(projects[1])
+ )
+
+ def test_findByGitRef(self):
+ # ICraftRecipeSet.findByGitRef returns all craft recipes with the
+ # given Git reference.
+ repositories = [self.factory.makeGitRepository() for i in range(2)]
+ refs = []
+ recipes = []
+ for _ in repositories:
+ refs.extend(
+ self.factory.makeGitRefs(
+ paths=["refs/heads/master", "refs/heads/other"]
+ )
+ )
+ recipes.append(self.factory.makeCraftRecipe(git_ref=refs[-2]))
+ recipes.append(self.factory.makeCraftRecipe(git_ref=refs[-1]))
+ recipe_set = getUtility(ICraftRecipeSet)
+ for ref, recipe in zip(refs, recipes):
+ self.assertContentEqual([recipe], recipe_set.findByGitRef(ref))
+
+ def test_findByContext(self):
+ # ICraftRecipeSet.findByContext returns all craft recipes with the
+ # given context.
+ person = self.factory.makePerson()
+ project = self.factory.makeProduct()
+ repository = self.factory.makeGitRepository(
+ owner=person, target=project
+ )
+ refs = self.factory.makeGitRefs(
+ repository=repository,
+ paths=["refs/heads/master", "refs/heads/other"],
+ )
+ other_repository = self.factory.makeGitRepository()
+ other_refs = self.factory.makeGitRefs(
+ repository=other_repository,
+ paths=["refs/heads/master", "refs/heads/other"],
+ )
+ recipes = []
+ recipes.append(self.factory.makeCraftRecipe(git_ref=refs[0]))
+ recipes.append(self.factory.makeCraftRecipe(git_ref=refs[1]))
+ recipes.append(
+ self.factory.makeCraftRecipe(
+ registrant=person, owner=person, git_ref=other_refs[0]
+ )
+ )
+ recipes.append(
+ self.factory.makeCraftRecipe(
+ project=project, git_ref=other_refs[1]
+ )
+ )
+ recipe_set = getUtility(ICraftRecipeSet)
+ self.assertContentEqual(recipes[:3], recipe_set.findByContext(person))
+ self.assertContentEqual(
+ [recipes[0], recipes[1], recipes[3]],
+ recipe_set.findByContext(project),
+ )
+ self.assertContentEqual(
+ recipes[:2], recipe_set.findByContext(repository)
+ )
+ self.assertContentEqual(
+ [recipes[0]], recipe_set.findByContext(refs[0])
+ )
+ self.assertRaises(
+ BadCraftRecipeSearchContext,
+ recipe_set.findByContext,
+ self.factory.makeDistribution(),
+ )
+
class TestCraftRecipeDeleteWithBuilds(TestCaseWithFactory):