Merge lp:~cjwatson/launchpad/snap-source-deletion into lp:launchpad

Proposed by Colin Watson
Status: Merged
Merged at revision: 17675
Proposed branch: lp:~cjwatson/launchpad/snap-source-deletion
Merge into: lp:launchpad
Diff against target: 366 lines (+180/-16)
7 files modified
lib/lp/code/model/branch.py (+9/-2)
lib/lp/code/model/gitrepository.py (+9/-2)
lib/lp/code/model/tests/test_branch.py (+21/-2)
lib/lp/code/model/tests/test_gitrepository.py (+28/-0)
lib/lp/snappy/interfaces/snap.py (+20/-0)
lib/lp/snappy/model/snap.py (+18/-0)
lib/lp/snappy/tests/test_snap.py (+75/-10)
To merge this branch: bzr merge lp:~cjwatson/launchpad/snap-source-deletion
Reviewer Review Type Date Requested Status
William Grant code Approve
Review via email: mp+266866@code.launchpad.net

Commit message

Detach snap packages from their source branch/repository when it is deleted.

Description of the change

Detach snap packages from their source branch/repository when it is deleted.

This will make it feasible to add the tests requested in a review comment on https://code.launchpad.net/~cjwatson/launchpad/snap-build-behaviour/+merge/266736.

To post a comment you must log in.
Revision history for this message
William Grant (wgrant) :
review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/code/model/branch.py'
2--- lib/lp/code/model/branch.py 2015-07-08 16:05:11 +0000
3+++ lib/lp/code/model/branch.py 2015-08-05 10:53:23 +0000
4@@ -179,6 +179,7 @@
5 from lp.services.propertycache import cachedproperty
6 from lp.services.webapp import urlappend
7 from lp.services.webapp.authorization import check_permission
8+from lp.snappy.interfaces.snap import ISnapSet
9
10
11 @implementer(IBranch, IPrivacy, IInformationType)
12@@ -814,6 +815,10 @@
13 deletion_operations.extend(
14 DeletionCallable.forSourcePackageRecipe(recipe)
15 for recipe in self.recipes)
16+ if not getUtility(ISnapSet).findByBranch(self).is_empty():
17+ alteration_operations.append(DeletionCallable(
18+ None, _('Some snap packages build from this branch.'),
19+ getUtility(ISnapSet).detachFromBranch, self))
20 return (alteration_operations, deletion_operations)
21
22 def deletionRequirements(self):
23@@ -1436,12 +1441,14 @@
24 class DeletionCallable(DeletionOperation):
25 """Deletion operation that invokes a callable."""
26
27- def __init__(self, affected_object, rationale, func):
28+ def __init__(self, affected_object, rationale, func, *args, **kwargs):
29 DeletionOperation.__init__(self, affected_object, rationale)
30 self.func = func
31+ self.args = args
32+ self.kwargs = kwargs
33
34 def __call__(self):
35- self.func()
36+ self.func(*self.args, **self.kwargs)
37
38 @classmethod
39 def forSourcePackageRecipe(cls, recipe):
40
41=== modified file 'lib/lp/code/model/gitrepository.py'
42--- lib/lp/code/model/gitrepository.py 2015-07-17 07:34:21 +0000
43+++ lib/lp/code/model/gitrepository.py 2015-08-05 10:53:23 +0000
44@@ -148,6 +148,7 @@
45 from lp.services.webapp.authorization import available_with_permission
46 from lp.services.webhooks.interfaces import IWebhookSource
47 from lp.services.webhooks.model import WebhookTargetMixin
48+from lp.snappy.interfaces.snap import ISnapSet
49
50
51 object_type_map = {
52@@ -1002,6 +1003,10 @@
53 prerequisite_git_repository=self):
54 alteration_operations.append(
55 ClearPrerequisiteRepository(merge_proposal))
56+ if not getUtility(ISnapSet).findByGitRepository(self).is_empty():
57+ alteration_operations.append(DeletionCallable(
58+ None, msg("Some snap packages build from this repository."),
59+ getUtility(ISnapSet).detachFromGitRepository, self))
60
61 return (alteration_operations, deletion_operations)
62
63@@ -1103,12 +1108,14 @@
64 class DeletionCallable(DeletionOperation):
65 """Deletion operation that invokes a callable."""
66
67- def __init__(self, affected_object, rationale, func):
68+ def __init__(self, affected_object, rationale, func, *args, **kwargs):
69 super(DeletionCallable, self).__init__(affected_object, rationale)
70 self.func = func
71+ self.args = args
72+ self.kwargs = kwargs
73
74 def __call__(self):
75- self.func()
76+ self.func(*self.args, **self.kwargs)
77
78
79 class ClearPrerequisiteRepository(DeletionOperation):
80
81=== modified file 'lib/lp/code/model/tests/test_branch.py'
82--- lib/lp/code/model/tests/test_branch.py 2015-07-24 09:16:50 +0000
83+++ lib/lp/code/model/tests/test_branch.py 2015-08-05 10:53:23 +0000
84@@ -1,4 +1,4 @@
85-# Copyright 2009-2013 Canonical Ltd. This software is licensed under the
86+# Copyright 2009-2015 Canonical Ltd. This software is licensed under the
87 # GNU Affero General Public License version 3 (see the file LICENSE).
88
89 """Tests for Branches."""
90@@ -72,7 +72,6 @@
91 from lp.code.interfaces.branchjob import (
92 IBranchScanJobSource,
93 IBranchUpgradeJobSource,
94- IReclaimBranchSpaceJobSource,
95 )
96 from lp.code.interfaces.branchlookup import IBranchLookup
97 from lp.code.interfaces.branchmergeproposal import (
98@@ -143,6 +142,7 @@
99 IOpenLaunchBag,
100 OAuthPermission,
101 )
102+from lp.snappy.interfaces.snap import SNAP_FEATURE_FLAG
103 from lp.testing import (
104 admin_logged_in,
105 ANONYMOUS,
106@@ -1672,6 +1672,25 @@
107 self.factory.makePerson())
108 merge_proposal.target_branch.destroySelf(break_references=True)
109
110+ def test_snap_requirements(self):
111+ # If a branch is used by a snap package, the deletion requirements
112+ # indicate this.
113+ self.useFixture(FeatureFixture({SNAP_FEATURE_FLAG: u"on"}))
114+ self.factory.makeSnap(branch=self.branch)
115+ self.assertEqual(
116+ {None: ('alter', _('Some snap packages build from this branch.'))},
117+ self.branch.deletionRequirements())
118+
119+ def test_snap_deletion(self):
120+ # break_references allows deleting a branch used by a snap package.
121+ self.useFixture(FeatureFixture({SNAP_FEATURE_FLAG: u"on"}))
122+ snap1 = self.factory.makeSnap(branch=self.branch)
123+ snap2 = self.factory.makeSnap(branch=self.branch)
124+ self.branch.destroySelf(break_references=True)
125+ transaction.commit()
126+ self.assertIsNone(snap1.branch)
127+ self.assertIsNone(snap2.branch)
128+
129 def test_ClearDependentBranch(self):
130 """ClearDependent.__call__ must clear the prerequisite branch."""
131 merge_proposal = removeSecurityProxy(self.makeMergeProposals()[0])
132
133=== modified file 'lib/lp/code/model/tests/test_gitrepository.py'
134--- lib/lp/code/model/tests/test_gitrepository.py 2015-07-17 07:34:21 +0000
135+++ lib/lp/code/model/tests/test_gitrepository.py 2015-08-05 10:53:23 +0000
136@@ -104,12 +104,14 @@
137 from lp.services.config import config
138 from lp.services.database.constants import UTC_NOW
139 from lp.services.database.interfaces import IStore
140+from lp.services.features.testing import FeatureFixture
141 from lp.services.job.interfaces.job import JobStatus
142 from lp.services.job.model.job import Job
143 from lp.services.job.runner import JobRunner
144 from lp.services.mail import stub
145 from lp.services.webapp.authorization import check_permission
146 from lp.services.webapp.interfaces import OAuthPermission
147+from lp.snappy.interfaces.snap import SNAP_FEATURE_FLAG
148 from lp.testing import (
149 admin_logged_in,
150 ANONYMOUS,
151@@ -620,6 +622,32 @@
152 self.factory.makePerson(), self.factory.makePerson())
153 merge_proposal.target_git_repository.destroySelf(break_references=True)
154
155+ def test_snap_requirements(self):
156+ # If a repository is used by a snap package, the deletion
157+ # requirements indicate this.
158+ self.useFixture(FeatureFixture({SNAP_FEATURE_FLAG: u"on"}))
159+ [ref] = self.factory.makeGitRefs()
160+ self.factory.makeSnap(git_ref=ref)
161+ self.assertEqual(
162+ {None:
163+ ("alter", _("Some snap packages build from this repository."))},
164+ ref.repository.getDeletionRequirements())
165+
166+ def test_snap_deletion(self):
167+ # break_references allows deleting a repository used by a snap package.
168+ self.useFixture(FeatureFixture({SNAP_FEATURE_FLAG: u"on"}))
169+ repository = self.factory.makeGitRepository()
170+ [ref1, ref2] = self.factory.makeGitRefs(
171+ repository=repository, paths=[u"refs/heads/1", u"refs/heads/2"])
172+ snap1 = self.factory.makeSnap(git_ref=ref1)
173+ snap2 = self.factory.makeSnap(git_ref=ref2)
174+ repository.destroySelf(break_references=True)
175+ transaction.commit()
176+ self.assertIsNone(snap1.git_repository)
177+ self.assertIsNone(snap1.git_path)
178+ self.assertIsNone(snap2.git_repository)
179+ self.assertIsNone(snap2.git_path)
180+
181 def test_ClearPrerequisiteRepository(self):
182 # ClearPrerequisiteRepository.__call__ must clear the prerequisite
183 # repository.
184
185=== modified file 'lib/lp/snappy/interfaces/snap.py'
186--- lib/lp/snappy/interfaces/snap.py 2015-08-03 13:16:48 +0000
187+++ lib/lp/snappy/interfaces/snap.py 2015-08-05 10:53:23 +0000
188@@ -347,6 +347,26 @@
189 def findByPerson(owner):
190 """Return all snap packages with the given `owner`."""
191
192+ def findByBranch(branch):
193+ """Return all snap packages for the given Bazaar branch."""
194+
195+ def findByGitRepository(repository):
196+ """Return all snap packages for the given Git repository."""
197+
198+ def detachFromBranch(branch):
199+ """Detach all snap packages from the given Bazaar branch.
200+
201+ After this, any snap packages that previously used this branch will
202+ have no source and so cannot dispatch new builds.
203+ """
204+
205+ def detachFromGitRepository(repository):
206+ """Detach all snap packages from the given Git repository.
207+
208+ After this, any snap packages that previously used this repository
209+ will have no source and so cannot dispatch new builds.
210+ """
211+
212 @collection_default_content()
213 def empty_list():
214 """Return an empty collection of snap packages.
215
216=== modified file 'lib/lp/snappy/model/snap.py'
217--- lib/lp/snappy/model/snap.py 2015-08-03 13:16:48 +0000
218+++ lib/lp/snappy/model/snap.py 2015-08-05 10:53:23 +0000
219@@ -334,6 +334,24 @@
220 """See `ISnapSet`."""
221 return IStore(Snap).find(Snap, Snap.owner == owner)
222
223+ def findByBranch(self, branch):
224+ """See `ISnapSet`."""
225+ return IStore(Snap).find(Snap, Snap.branch == branch)
226+
227+ def findByGitRepository(self, repository):
228+ """See `ISnapSet`."""
229+ return IStore(Snap).find(Snap, Snap.git_repository == repository)
230+
231+ def detachFromBranch(self, branch):
232+ """See `ISnapSet`."""
233+ self.findByBranch(branch).set(
234+ branch_id=None, date_last_modified=UTC_NOW)
235+
236+ def detachFromGitRepository(self, repository):
237+ """See `ISnapSet`."""
238+ self.findByGitRepository(repository).set(
239+ git_repository_id=None, git_path=None, date_last_modified=UTC_NOW)
240+
241 def empty_list(self):
242 """See `ISnapSet`."""
243 return []
244
245=== modified file 'lib/lp/snappy/tests/test_snap.py'
246--- lib/lp/snappy/tests/test_snap.py 2015-08-03 13:16:48 +0000
247+++ lib/lp/snappy/tests/test_snap.py 2015-08-05 10:53:23 +0000
248@@ -5,13 +5,9 @@
249
250 __metaclass__ = type
251
252-from datetime import (
253- datetime,
254- timedelta,
255- )
256+from datetime import timedelta
257
258 from lazr.lifecycle.event import ObjectModifiedEvent
259-import pytz
260 from storm.locals import Store
261 from testtools.matchers import Equals
262 import transaction
263@@ -28,7 +24,10 @@
264 from lp.buildmaster.model.buildqueue import BuildQueue
265 from lp.registry.interfaces.distribution import IDistributionSet
266 from lp.registry.interfaces.pocket import PackagePublishingPocket
267-from lp.services.database.constants import UTC_NOW
268+from lp.services.database.constants import (
269+ ONE_DAY_AGO,
270+ UTC_NOW,
271+ )
272 from lp.services.features.testing import FeatureFixture
273 from lp.services.webapp.interfaces import OAuthPermission
274 from lp.snappy.interfaces.snap import (
275@@ -98,15 +97,13 @@
276
277 def test_initial_date_last_modified(self):
278 # The initial value of date_last_modified is date_created.
279- snap = self.factory.makeSnap(
280- date_created=datetime(2014, 04, 25, 10, 38, 0, tzinfo=pytz.UTC))
281+ snap = self.factory.makeSnap(date_created=ONE_DAY_AGO)
282 self.assertEqual(snap.date_created, snap.date_last_modified)
283
284 def test_modifiedevent_sets_date_last_modified(self):
285 # When a Snap receives an object modified event, the last modified
286 # date is set to UTC_NOW.
287- snap = self.factory.makeSnap(
288- date_created=datetime(2014, 04, 25, 10, 38, 0, tzinfo=pytz.UTC))
289+ snap = self.factory.makeSnap(date_created=ONE_DAY_AGO)
290 notify(ObjectModifiedEvent(
291 removeSecurityProxy(snap), snap, [ISnap["name"]]))
292 self.assertSqlAttributeEqualsDate(snap, "date_last_modified", UTC_NOW)
293@@ -451,6 +448,74 @@
294 self.assertContentEqual(
295 snaps[2:], getUtility(ISnapSet).findByPerson(owners[1]))
296
297+ def test_findByBranch(self):
298+ # ISnapSet.findByBranch returns all Snaps with the given Bazaar branch.
299+ branches = [self.factory.makeAnyBranch() for i in range(2)]
300+ snaps = []
301+ for branch in branches:
302+ for i in range(2):
303+ snaps.append(self.factory.makeSnap(branch=branch))
304+ self.assertContentEqual(
305+ snaps[:2], getUtility(ISnapSet).findByBranch(branches[0]))
306+ self.assertContentEqual(
307+ snaps[2:], getUtility(ISnapSet).findByBranch(branches[1]))
308+
309+ def test_findByGitRepository(self):
310+ # ISnapSet.findByGitRepository returns all Snaps with the given Git
311+ # repository.
312+ repositories = [self.factory.makeGitRepository() for i in range(2)]
313+ snaps = []
314+ for repository in repositories:
315+ for i in range(2):
316+ [ref] = self.factory.makeGitRefs(repository=repository)
317+ snaps.append(self.factory.makeSnap(git_ref=ref))
318+ self.assertContentEqual(
319+ snaps[:2],
320+ getUtility(ISnapSet).findByGitRepository(repositories[0]))
321+ self.assertContentEqual(
322+ snaps[2:],
323+ getUtility(ISnapSet).findByGitRepository(repositories[1]))
324+
325+ def test_detachFromBranch(self):
326+ # ISnapSet.detachFromBranch clears the given Bazaar branch from all
327+ # Snaps.
328+ branches = [self.factory.makeAnyBranch() for i in range(2)]
329+ snaps = []
330+ for branch in branches:
331+ for i in range(2):
332+ snaps.append(self.factory.makeSnap(
333+ branch=branch, date_created=ONE_DAY_AGO))
334+ getUtility(ISnapSet).detachFromBranch(branches[0])
335+ self.assertEqual(
336+ [None, None, branches[1], branches[1]],
337+ [snap.branch for snap in snaps])
338+ for snap in snaps[:2]:
339+ self.assertSqlAttributeEqualsDate(
340+ snap, "date_last_modified", UTC_NOW)
341+
342+ def test_detachFromGitRepository(self):
343+ # ISnapSet.detachFromGitRepository clears the given Git repository
344+ # from all Snaps.
345+ repositories = [self.factory.makeGitRepository() for i in range(2)]
346+ snaps = []
347+ paths = []
348+ for repository in repositories:
349+ for i in range(2):
350+ [ref] = self.factory.makeGitRefs(repository=repository)
351+ paths.append(ref.path)
352+ snaps.append(self.factory.makeSnap(
353+ git_ref=ref, date_created=ONE_DAY_AGO))
354+ getUtility(ISnapSet).detachFromGitRepository(repositories[0])
355+ self.assertEqual(
356+ [None, None, repositories[1], repositories[1]],
357+ [snap.git_repository for snap in snaps])
358+ self.assertEqual(
359+ [None, None, paths[2], paths[3]],
360+ [snap.git_path for snap in snaps])
361+ for snap in snaps[:2]:
362+ self.assertSqlAttributeEqualsDate(
363+ snap, "date_last_modified", UTC_NOW)
364+
365
366 class TestSnapProcessors(TestCaseWithFactory):
367