launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #19065
[Merge] lp:~cjwatson/launchpad/snap-builds into lp:launchpad
Colin Watson has proposed merging lp:~cjwatson/launchpad/snap-builds into lp:launchpad with lp:~cjwatson/launchpad/snapbuild-basic-model as a prerequisite.
Commit message:
Flesh out Snap.requestBuild, Snap.*builds, and Snap.destroySelf methods.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
Related bugs:
Bug #1476405 in Launchpad itself: "Add support for building snaps"
https://bugs.launchpad.net/launchpad/+bug/1476405
For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/snap-builds/+merge/265698
Flesh out Snap.requestBuild, Snap.*builds, and Snap.destroySelf methods.
Now that both Snap and SnapBuild are modelled, we can link them together.
--
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~cjwatson/launchpad/snap-builds into lp:launchpad.
=== modified file 'lib/lp/snappy/model/snap.py'
--- lib/lp/snappy/model/snap.py 2015-07-23 16:23:50 +0000
+++ lib/lp/snappy/model/snap.py 2015-07-23 16:23:50 +0000
@@ -11,13 +11,18 @@
from storm.locals import (
Bool,
DateTime,
+ Desc,
Int,
+ Not,
Reference,
+ Store,
Storm,
Unicode,
)
+from zope.component import getUtility
from zope.interface import implementer
+from lp.buildmaster.enums import BuildStatus
from lp.registry.interfaces.role import IHasOwner
from lp.services.database.constants import (
DEFAULT,
@@ -27,16 +32,31 @@
IMasterStore,
IStore,
)
+from lp.services.database.stormexpr import (
+ Greatest,
+ NullsLast,
+ )
from lp.services.features import getFeatureFlag
+from lp.services.webapp.interfaces import ILaunchBag
from lp.snappy.interfaces.snap import (
+ CannotDeleteSnap,
DuplicateSnapName,
ISnap,
ISnapSet,
SNAP_FEATURE_FLAG,
+ SnapBuildAlreadyPending,
+ SnapBuildArchiveOwnerMismatch,
SnapFeatureDisabled,
SnapNotOwner,
NoSuchSnap,
)
+from lp.snappy.interfaces.snapbuild import ISnapBuildSet
+from lp.snappy.model.snapbuild import SnapBuild
+from lp.soyuz.interfaces.archive import ArchiveDisabled
+from lp.soyuz.model.archive import (
+ Archive,
+ get_enabled_archive_filter,
+ )
def snap_modified(snap, event):
@@ -106,31 +126,93 @@
def requestBuild(self, requester, archive, distro_arch_series, pocket):
"""See `ISnap`."""
- raise NotImplementedError
+ if not requester.inTeam(self.owner):
+ raise SnapNotOwner(
+ "%s cannot create snap package builds owned by %s." %
+ (requester.displayname, self.owner.displayname))
+ if not archive.enabled:
+ raise ArchiveDisabled(archive.displayname)
+ if archive.private and self.owner != archive.owner:
+ # See rationale in `SnapBuildArchiveOwnerMismatch` docstring.
+ raise SnapBuildArchiveOwnerMismatch()
+
+ pending = IStore(self).find(
+ SnapBuild,
+ SnapBuild.snap_id == self.id,
+ SnapBuild.archive_id == archive.id,
+ SnapBuild.distro_arch_series_id == distro_arch_series.id,
+ SnapBuild.pocket == pocket,
+ SnapBuild.status == BuildStatus.NEEDSBUILD)
+ if pending.any() is not None:
+ raise SnapBuildAlreadyPending
+
+ build = getUtility(ISnapBuildSet).new(
+ requester, self, archive, distro_arch_series, pocket)
+ build.queueBuild()
+ return build
+
+ def _getBuilds(self, filter_term, order_by):
+ """The actual query to get the builds."""
+ query_args = [
+ SnapBuild.snap == self,
+ SnapBuild.archive_id == Archive.id,
+ Archive._enabled == True,
+ get_enabled_archive_filter(
+ getUtility(ILaunchBag).user, include_public=True,
+ include_subscribed=True)
+ ]
+ if filter_term is not None:
+ query_args.append(filter_term)
+ result = Store.of(self).find(SnapBuild, *query_args)
+ result.order_by(order_by)
+ return result
@property
def builds(self):
"""See `ISnap`."""
- return []
+ order_by = (
+ NullsLast(Desc(Greatest(
+ SnapBuild.date_started,
+ SnapBuild.date_finished))),
+ Desc(SnapBuild.date_created),
+ Desc(SnapBuild.id))
+ return self._getBuilds(None, order_by)
@property
def _pending_states(self):
"""All the build states we consider pending (non-final)."""
- raise NotImplementedError
+ return [
+ BuildStatus.NEEDSBUILD,
+ BuildStatus.BUILDING,
+ BuildStatus.UPLOADING,
+ BuildStatus.CANCELLING,
+ ]
@property
def completed_builds(self):
"""See `ISnap`."""
- return []
+ filter_term = (Not(SnapBuild.status.is_in(self._pending_states)))
+ order_by = (
+ NullsLast(Desc(Greatest(
+ SnapBuild.date_started,
+ SnapBuild.date_finished))),
+ Desc(SnapBuild.id))
+ return self._getBuilds(filter_term, order_by)
@property
def pending_builds(self):
"""See `ISnap`."""
- return []
+ filter_term = (SnapBuild.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(SnapBuild.id)
+ return self._getBuilds(filter_term, order_by)
def destroySelf(self):
"""See `ISnap`."""
- raise NotImplementedError
+ if not self.builds.is_empty():
+ raise CannotDeleteSnap("Cannot delete a snap package with builds.")
+ IStore(Snap).remove(self)
@implementer(ISnapSet)
=== modified file 'lib/lp/snappy/tests/test_snap.py'
--- lib/lp/snappy/tests/test_snap.py 2015-07-23 16:23:50 +0000
+++ lib/lp/snappy/tests/test_snap.py 2015-07-23 16:23:50 +0000
@@ -32,6 +32,7 @@
SnapBuildAlreadyPending,
SnapFeatureDisabled,
)
+from lp.snappy.interfaces.snapbuild import ISnapBuild
from lp.testing import (
person_logged_in,
TestCaseWithFactory,
@@ -83,6 +84,183 @@
removeSecurityProxy(snap), snap, [ISnap["name"]]))
self.assertSqlAttributeEqualsDate(snap, "date_last_modified", UTC_NOW)
+ def test_requestBuild(self):
+ # requestBuild creates a new SnapBuild.
+ snap = self.factory.makeSnap()
+ distroarchseries = self.factory.makeDistroArchSeries(
+ distroseries=snap.distro_series)
+ build = snap.requestBuild(
+ snap.owner, snap.distro_series.main_archive, distroarchseries,
+ PackagePublishingPocket.RELEASE)
+ self.assertTrue(ISnapBuild.providedBy(build))
+ self.assertEqual(snap.owner, build.requester)
+ self.assertEqual(snap.distro_series.main_archive, build.archive)
+ self.assertEqual(distroarchseries, build.distro_arch_series)
+ self.assertEqual(PackagePublishingPocket.RELEASE, build.pocket)
+ self.assertEqual(BuildStatus.NEEDSBUILD, build.status)
+ store = Store.of(build)
+ store.flush()
+ build_queue = store.find(
+ BuildQueue,
+ BuildQueue._build_farm_job_id ==
+ removeSecurityProxy(build).build_farm_job_id).one()
+ self.assertProvides(build_queue, IBuildQueue)
+ self.assertEqual(
+ snap.distro_series.main_archive.require_virtualized,
+ build_queue.virtualized)
+ self.assertEqual(BuildQueueStatus.WAITING, build_queue.status)
+
+ def test_requestBuild_score(self):
+ # Build requests have a relatively low queue score (2505).
+ snap = self.factory.makeSnap()
+ distroarchseries = self.factory.makeDistroArchSeries(
+ distroseries=snap.distro_series)
+ build = snap.requestBuild(
+ snap.owner, snap.distro_series.main_archive, distroarchseries,
+ PackagePublishingPocket.RELEASE)
+ queue_record = build.buildqueue_record
+ queue_record.score()
+ self.assertEqual(2505, queue_record.lastscore)
+
+ def test_requestBuild_relative_build_score(self):
+ # Offsets for archives are respected.
+ snap = self.factory.makeSnap()
+ archive = self.factory.makeArchive(owner=snap.owner)
+ removeSecurityProxy(archive).relative_build_score = 100
+ distroarchseries = self.factory.makeDistroArchSeries(
+ distroseries=snap.distro_series)
+ build = snap.requestBuild(
+ snap.owner, archive, distroarchseries,
+ PackagePublishingPocket.RELEASE)
+ queue_record = build.buildqueue_record
+ queue_record.score()
+ self.assertEqual(2605, queue_record.lastscore)
+
+ def test_requestBuild_rejects_repeats(self):
+ # requestBuild refuses if there is already a pending build.
+ snap = self.factory.makeSnap()
+ distroarchseries = self.factory.makeDistroArchSeries(
+ distroseries=snap.distro_series)
+ old_build = snap.requestBuild(
+ snap.owner, snap.distro_series.main_archive, distroarchseries,
+ PackagePublishingPocket.RELEASE)
+ self.assertRaises(
+ SnapBuildAlreadyPending, snap.requestBuild,
+ snap.owner, snap.distro_series.main_archive, distroarchseries,
+ PackagePublishingPocket.RELEASE)
+ # We can build for a different archive.
+ snap.requestBuild(
+ snap.owner, self.factory.makeArchive(owner=snap.owner),
+ distroarchseries, PackagePublishingPocket.RELEASE)
+ # We can build for a different distroarchseries.
+ snap.requestBuild(
+ snap.owner, snap.distro_series.main_archive,
+ self.factory.makeDistroArchSeries(distroseries=snap.distro_series),
+ PackagePublishingPocket.RELEASE)
+ # Changing the status of the old build allows a new build.
+ old_build.updateStatus(BuildStatus.BUILDING)
+ old_build.updateStatus(BuildStatus.FULLYBUILT)
+ snap.requestBuild(
+ snap.owner, snap.distro_series.main_archive, distroarchseries,
+ PackagePublishingPocket.RELEASE)
+
+ def test_requestBuild_virtualization(self):
+ # New builds are virtualized if any of the processor, snap or
+ # archive require it.
+ for proc_nonvirt, snap_virt, archive_virt, build_virt in (
+ (True, False, False, False),
+ (True, False, True, True),
+ (True, True, False, True),
+ (True, True, True, True),
+ (False, False, False, True),
+ (False, False, True, True),
+ (False, True, False, True),
+ (False, True, True, True),
+ ):
+ distroarchseries = self.factory.makeDistroArchSeries(
+ processor=self.factory.makeProcessor(
+ supports_nonvirtualized=proc_nonvirt))
+ snap = self.factory.makeSnap(
+ distroseries=distroarchseries.distroseries,
+ require_virtualized=snap_virt)
+ archive = self.factory.makeArchive(
+ distribution=distroarchseries.distroseries.distribution,
+ owner=snap.owner, virtualized=archive_virt)
+ build = snap.requestBuild(
+ snap.owner, archive, distroarchseries,
+ PackagePublishingPocket.RELEASE)
+ self.assertEqual(build_virt, build.virtualized)
+
+ def test_getBuilds(self):
+ # Test the various getBuilds methods.
+ snap = self.factory.makeSnap()
+ builds = [self.factory.makeSnapBuild(snap=snap) for x in range(3)]
+ # We want the latest builds first.
+ builds.reverse()
+
+ self.assertEqual(builds, list(snap.builds))
+ self.assertEqual([], list(snap.completed_builds))
+ self.assertEqual(builds, list(snap.pending_builds))
+
+ # Change the status of one of the builds and retest.
+ builds[0].updateStatus(BuildStatus.BUILDING)
+ builds[0].updateStatus(BuildStatus.FULLYBUILT)
+ self.assertEqual(builds, list(snap.builds))
+ self.assertEqual(builds[:1], list(snap.completed_builds))
+ self.assertEqual(builds[1:], list(snap.pending_builds))
+
+ def test_getBuilds_cancelled_never_started_last(self):
+ # A cancelled build that was never even started sorts to the end.
+ snap = self.factory.makeSnap()
+ fullybuilt = self.factory.makeSnapBuild(snap=snap)
+ instacancelled = self.factory.makeSnapBuild(snap=snap)
+ fullybuilt.updateStatus(BuildStatus.BUILDING)
+ fullybuilt.updateStatus(BuildStatus.FULLYBUILT)
+ instacancelled.updateStatus(BuildStatus.CANCELLED)
+ self.assertEqual([fullybuilt, instacancelled], list(snap.builds))
+ self.assertEqual(
+ [fullybuilt, instacancelled], list(snap.completed_builds))
+ self.assertEqual([], list(snap.pending_builds))
+
+ def test_getBuilds_privacy(self):
+ # The various getBuilds methods exclude builds against invisible
+ # archives.
+ snap = self.factory.makeSnap()
+ archive = self.factory.makeArchive(
+ distribution=snap.distro_series.distribution, owner=snap.owner,
+ private=True)
+ with person_logged_in(snap.owner):
+ build = self.factory.makeSnapBuild(snap=snap, archive=archive)
+ self.assertEqual([build], list(snap.builds))
+ self.assertEqual([build], list(snap.pending_builds))
+ self.assertEqual([], list(snap.builds))
+ self.assertEqual([], list(snap.pending_builds))
+
+ def test_delete_without_builds(self):
+ # A snap package with no builds can be deleted.
+ owner = self.factory.makePerson()
+ distroseries = self.factory.makeDistroSeries()
+ snap = self.factory.makeSnap(
+ registrant=owner, owner=owner, distroseries=distroseries,
+ name=u"condemned")
+ self.assertTrue(getUtility(ISnapSet).exists(owner, u"condemned"))
+ with person_logged_in(snap.owner):
+ snap.destroySelf()
+ self.assertFalse(getUtility(ISnapSet).exists(owner, u"condemned"))
+
+ def test_delete_with_builds(self):
+ # A snap package with builds cannot be deleted.
+ owner = self.factory.makePerson()
+ distroseries = self.factory.makeDistroSeries()
+ snap = self.factory.makeSnap(
+ registrant=owner, owner=owner, distroseries=distroseries,
+ name=u"condemned")
+ self.factory.makeSnapBuild(snap=snap)
+ self.assertTrue(getUtility(ISnapSet).exists(owner, u"condemned"))
+ with person_logged_in(snap.owner):
+ self.assertRaises(CannotDeleteSnap, snap.destroySelf)
+ self.assertTrue(getUtility(ISnapSet).exists(owner, u"condemned"))
+
class TestSnapSet(TestCaseWithFactory):
Follow ups