Merge lp:~cjwatson/launchpad/snap-source-deletion into lp:launchpad
- snap-source-deletion
- Merge into devel
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 | ||||
Related bugs: |
|
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:/
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 |