Merge lp:~cjwatson/launchpad/branch-delete-job into lp:launchpad

Proposed by Colin Watson
Status: Rejected
Rejected by: Colin Watson
Proposed branch: lp:~cjwatson/launchpad/branch-delete-job
Merge into: lp:launchpad
Prerequisite: lp:~cjwatson/launchpad/code-delete-no-recipe-preload
Diff against target: 948 lines (+377/-48)
11 files modified
database/schema/security.cfg (+48/-0)
lib/lp/code/configure.zcml (+10/-1)
lib/lp/code/enums.py (+12/-1)
lib/lp/code/interfaces/branch.py (+7/-1)
lib/lp/code/interfaces/branchjob.py (+19/-1)
lib/lp/code/model/branch.py (+19/-1)
lib/lp/code/model/branchjob.py (+84/-3)
lib/lp/code/model/tests/test_branch.py (+76/-38)
lib/lp/code/model/tests/test_branchjob.py (+95/-2)
lib/lp/code/stories/webservice/xx-branch.txt (+1/-0)
lib/lp/services/config/schema-lazr.conf (+6/-0)
To merge this branch: bzr merge lp:~cjwatson/launchpad/branch-delete-job
Reviewer Review Type Date Requested Status
Launchpad code reviewers Pending
Review via email: mp+364907@code.launchpad.net

Commit message

Add a branch deletion job.

Description of the change

Nothing creates these jobs yet; that will come in a follow-up branch.

We'll need to get the branch-delete-job DB user created and .pgpass updated on the relevant machines before landing this.

To post a comment you must log in.
Revision history for this message
Colin Watson (cjwatson) wrote :

This seems unnecessary since adding some more indexes has made deletion fast enough, so withdrawing this until we have a clear need.

Unmerged revisions

18912. By Colin Watson

Add a branch deletion job.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'database/schema/security.cfg'
--- database/schema/security.cfg 2019-03-11 14:51:16 +0000
+++ database/schema/security.cfg 2019-03-21 15:55:38 +0000
@@ -2593,3 +2593,51 @@
2593public.teamparticipation = SELECT2593public.teamparticipation = SELECT
2594public.webhook = SELECT2594public.webhook = SELECT
2595public.webhookjob = SELECT, INSERT2595public.webhookjob = SELECT, INSERT
2596
2597[branch-delete-job]
2598type=user
2599groups=script
2600public.accessartifact = SELECT, DELETE
2601public.accessartifactgrant = SELECT, DELETE
2602public.accesspolicyartifact = SELECT, DELETE
2603public.archive = SELECT
2604public.branch = SELECT, UPDATE, DELETE
2605public.branchjob = SELECT, INSERT, DELETE
2606public.branchmergeproposal = SELECT, UPDATE, DELETE
2607public.branchmergeproposaljob = SELECT, DELETE
2608public.branchsubscription = SELECT, DELETE
2609public.bug = SELECT
2610public.bugbranch = SELECT, DELETE
2611public.bugtask = SELECT
2612public.buildfarmjob = SELECT, DELETE
2613public.buildqueue = SELECT, DELETE
2614public.codeimport = SELECT, DELETE
2615public.codeimportjob = SELECT, DELETE
2616public.codereviewinlinecomment = SELECT, DELETE
2617public.codereviewinlinecommentdraft = SELECT, DELETE
2618public.codereviewmessage = SELECT, DELETE
2619public.codereviewvote = SELECT, DELETE
2620public.distribution = SELECT
2621public.distributionsourcepackage = SELECT, DELETE
2622public.distroseries = SELECT
2623public.emailaddress = SELECT
2624public.job = SELECT, INSERT, UPDATE, DELETE
2625public.person = SELECT
2626public.previewdiff = SELECT, DELETE
2627public.product = SELECT
2628public.productseries = SELECT, UPDATE
2629public.seriessourcepackagebranch = SELECT, DELETE
2630public.snap = SELECT, UPDATE
2631public.sourcepackage = SELECT
2632public.sourcepackagename = SELECT
2633public.sourcepackagepublishinghistory = SELECT
2634public.sourcepackagerecipe = SELECT, DELETE
2635public.sourcepackagerecipebuild = SELECT, UPDATE
2636public.sourcepackagerecipedata = SELECT, DELETE
2637public.sourcepackagerecipedatainstruction = SELECT, DELETE
2638public.sourcepackagerecipedistroseries = SELECT, DELETE
2639public.sourcepackagerelease = SELECT
2640public.specificationbranch = SELECT, DELETE
2641public.translationtemplatesbuild = SELECT, DELETE
2642public.webhook = SELECT, DELETE
2643public.webhookjob = SELECT, DELETE
25962644
=== modified file 'lib/lp/code/configure.zcml'
--- lib/lp/code/configure.zcml 2018-10-16 15:52:00 +0000
+++ lib/lp/code/configure.zcml 2019-03-21 15:55:38 +0000
@@ -1,4 +1,4 @@
1<!-- Copyright 2009-2018 Canonical Ltd. This software is licensed under the1<!-- Copyright 2009-2019 Canonical Ltd. This software is licensed under the
2 GNU Affero General Public License version 3 (see the file LICENSE).2 GNU Affero General Public License version 3 (see the file LICENSE).
3-->3-->
44
@@ -602,6 +602,10 @@
602 <allow interface="lp.code.interfaces.branchjob.IBranchModifiedMailJob"/>602 <allow interface="lp.code.interfaces.branchjob.IBranchModifiedMailJob"/>
603 <allow interface="lp.code.interfaces.branchjob.IBranchJob"/>603 <allow interface="lp.code.interfaces.branchjob.IBranchJob"/>
604 </class>604 </class>
605 <class class="lp.code.model.branchjob.BranchDeleteJob">
606 <allow interface="lp.code.interfaces.branchjob.IBranchDeleteJob"/>
607 <allow interface="lp.code.interfaces.branchjob.IBranchJob"/>
608 </class>
605 <securedutility609 <securedutility
606 component="lp.code.model.branchjob.RevisionMailJob"610 component="lp.code.model.branchjob.RevisionMailJob"
607 provides="lp.code.interfaces.branchjob.IRevisionMailJobSource">611 provides="lp.code.interfaces.branchjob.IRevisionMailJobSource">
@@ -627,6 +631,11 @@
627 provides="lp.code.interfaces.branchjob.IBranchModifiedMailJobSource">631 provides="lp.code.interfaces.branchjob.IBranchModifiedMailJobSource">
628 <allow interface="lp.code.interfaces.branchjob.IBranchModifiedMailJobSource"/>632 <allow interface="lp.code.interfaces.branchjob.IBranchModifiedMailJobSource"/>
629 </securedutility>633 </securedutility>
634 <securedutility
635 component="lp.code.model.branchjob.BranchDeleteJob"
636 provides="lp.code.interfaces.branchjob.IBranchDeleteJobSource">
637 <allow interface="lp.code.interfaces.branchjob.IBranchDeleteJobSource"/>
638 </securedutility>
630639
631 <!-- CodeImport -->640 <!-- CodeImport -->
632641
633642
=== modified file 'lib/lp/code/enums.py'
--- lib/lp/code/enums.py 2018-10-19 23:10:41 +0000
+++ lib/lp/code/enums.py 2019-03-21 15:55:38 +0000
@@ -1,10 +1,11 @@
1# Copyright 2009-2018 Canonical Ltd. This software is licensed under the1# Copyright 2009-2019 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Enumerations used in the lp/code modules."""4"""Enumerations used in the lp/code modules."""
55
6__metaclass__ = type6__metaclass__ = type
7__all__ = [7__all__ = [
8 'BranchDeletionStatus',
8 'BranchLifecycleStatus',9 'BranchLifecycleStatus',
9 'BranchLifecycleStatusFilter',10 'BranchLifecycleStatusFilter',
10 'BranchListingSort',11 'BranchListingSort',
@@ -116,6 +117,16 @@
116 """)117 """)
117118
118119
120class BranchDeletionStatus(DBEnumeratedType):
121 """Branch Deletion Status
122
123 The deletion status of a branch is used to track asynchronous deletion.
124 """
125
126 ACTIVE = DBItem(0, "Active")
127 DELETING = DBItem(1, "Deleting")
128
129
119class GitRepositoryType(DBEnumeratedType):130class GitRepositoryType(DBEnumeratedType):
120 """Git Repository Type131 """Git Repository Type
121132
122133
=== modified file 'lib/lp/code/interfaces/branch.py'
--- lib/lp/code/interfaces/branch.py 2019-01-23 17:13:48 +0000
+++ lib/lp/code/interfaces/branch.py 2019-03-21 15:55:38 +0000
@@ -1,4 +1,4 @@
1# Copyright 2009-2018 Canonical Ltd. This software is licensed under the1# Copyright 2009-2019 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Branch interfaces."""4"""Branch interfaces."""
@@ -75,6 +75,7 @@
75 RepositoryFormat,75 RepositoryFormat,
76 )76 )
77from lp.code.enums import (77from lp.code.enums import (
78 BranchDeletionStatus,
78 BranchLifecycleStatus,79 BranchLifecycleStatus,
79 BranchMergeProposalStatus,80 BranchMergeProposalStatus,
80 BranchSubscriptionDiffSize,81 BranchSubscriptionDiffSize,
@@ -293,6 +294,11 @@
293 accepted).294 accepted).
294 """295 """
295296
297 deletion_status = exported(Choice(
298 title=_("Deletion status"), required=True, readonly=True,
299 vocabulary=BranchDeletionStatus,
300 description=_("The deletion status of this branch.")))
301
296 # People attributes302 # People attributes
297 registrant = exported(303 registrant = exported(
298 PublicPersonChoice(304 PublicPersonChoice(
299305
=== modified file 'lib/lp/code/interfaces/branchjob.py'
--- lib/lp/code/interfaces/branchjob.py 2015-09-01 17:10:46 +0000
+++ lib/lp/code/interfaces/branchjob.py 2019-03-21 15:55:38 +0000
@@ -1,4 +1,4 @@
1# Copyright 2009-2011 Canonical Ltd. This software is licensed under the1# Copyright 2009-2019 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""BranchJob interfaces."""4"""BranchJob interfaces."""
@@ -8,6 +8,8 @@
88
99
10__all__ = [10__all__ = [
11 'IBranchDeleteJob',
12 'IBranchDeleteJobSource',
11 'IBranchJob',13 'IBranchJob',
12 'IBranchModifiedMailJob',14 'IBranchModifiedMailJob',
13 'IBranchModifiedMailJobSource',15 'IBranchModifiedMailJobSource',
@@ -201,3 +203,19 @@
201 :param user: The `IPerson` who modified the branch.203 :param user: The `IPerson` who modified the branch.
202 :param branch_delta: An `IBranchDelta` describing the changes.204 :param branch_delta: An `IBranchDelta` describing the changes.
203 """205 """
206
207
208class IBranchDeleteJob(IRunnableJob):
209 """A Job that deletes a branch from the database."""
210
211 branch_id = Int(title=_('The id of the branch to delete.'))
212
213
214class IBranchDeleteJobSource(IJobSource):
215
216 def create(branch, requester):
217 """Delete a branch from the database.
218
219 :param branch: The `IBranch` to delete.
220 :param requester: The `IPerson` who requested the deletion.
221 """
204222
=== modified file 'lib/lp/code/model/branch.py'
--- lib/lp/code/model/branch.py 2019-03-21 15:55:38 +0000
+++ lib/lp/code/model/branch.py 2019-03-21 15:55:38 +0000
@@ -1,4 +1,4 @@
1# Copyright 2009-2018 Canonical Ltd. This software is licensed under the1# Copyright 2009-2019 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4__metaclass__ = type4__metaclass__ = type
@@ -82,6 +82,7 @@
82 RepositoryFormat,82 RepositoryFormat,
83 )83 )
84from lp.code.enums import (84from lp.code.enums import (
85 BranchDeletionStatus,
85 BranchLifecycleStatus,86 BranchLifecycleStatus,
86 BranchMergeProposalStatus,87 BranchMergeProposalStatus,
87 BranchType,88 BranchType,
@@ -304,6 +305,23 @@
304 # such subscriptions.305 # such subscriptions.
305 getUtility(IRemoveArtifactSubscriptionsJobSource).create(who, [self])306 getUtility(IRemoveArtifactSubscriptionsJobSource).create(who, [self])
306307
308 _deletion_status = EnumCol(
309 dbName='deletion_status', enum=BranchDeletionStatus,
310 default=BranchDeletionStatus.ACTIVE)
311
312 @property
313 def deletion_status(self):
314 # XXX cjwatson 2019-03-19: Remove once this column has been
315 # backfilled.
316 if self._deletion_status is None:
317 return BranchDeletionStatus.ACTIVE
318 else:
319 return self._deletion_status
320
321 @deletion_status.setter
322 def deletion_status(self, value):
323 self._deletion_status = value
324
307 registrant = ForeignKey(325 registrant = ForeignKey(
308 dbName='registrant', foreignKey='Person',326 dbName='registrant', foreignKey='Person',
309 storm_validator=validate_public_person, notNull=True)327 storm_validator=validate_public_person, notNull=True)
310328
=== modified file 'lib/lp/code/model/branchjob.py'
--- lib/lp/code/model/branchjob.py 2018-03-30 20:42:14 +0000
+++ lib/lp/code/model/branchjob.py 2019-03-21 15:55:38 +0000
@@ -1,7 +1,8 @@
1# Copyright 2009-2018 Canonical Ltd. This software is licensed under the1# Copyright 2009-2019 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4__all__ = [4__all__ = [
5 'BranchDeleteJob',
5 'BranchJob',6 'BranchJob',
6 'BranchModifiedMailJob',7 'BranchModifiedMailJob',
7 'BranchScanJob',8 'BranchScanJob',
@@ -56,17 +57,22 @@
56 implementer,57 implementer,
57 provider,58 provider,
58 )59 )
60from zope.security.proxy import removeSecurityProxy
5961
60from lp.code.bzr import (62from lp.code.bzr import (
61 branch_revision_history,63 branch_revision_history,
62 get_branch_formats,64 get_branch_formats,
63 )65 )
64from lp.code.enums import (66from lp.code.enums import (
67 BranchDeletionStatus,
65 BranchMergeProposalStatus,68 BranchMergeProposalStatus,
66 BranchSubscriptionDiffSize,69 BranchSubscriptionDiffSize,
67 BranchSubscriptionNotificationLevel,70 BranchSubscriptionNotificationLevel,
68 )71 )
72from lp.code.errors import CannotDeleteBranch
69from lp.code.interfaces.branchjob import (73from lp.code.interfaces.branchjob import (
74 IBranchDeleteJob,
75 IBranchDeleteJobSource,
70 IBranchJob,76 IBranchJob,
71 IBranchModifiedMailJob,77 IBranchModifiedMailJob,
72 IBranchModifiedMailJobSource,78 IBranchModifiedMailJobSource,
@@ -82,6 +88,7 @@
82 IRosettaUploadJob,88 IRosettaUploadJob,
83 IRosettaUploadJobSource,89 IRosettaUploadJobSource,
84 )90 )
91from lp.code.interfaces.branchlookup import IBranchLookup
85from lp.code.mail.branch import BranchMailer92from lp.code.mail.branch import BranchMailer
86from lp.code.model.branch import Branch93from lp.code.model.branch import Branch
87from lp.code.model.branchmergeproposal import BranchMergeProposal94from lp.code.model.branchmergeproposal import BranchMergeProposal
@@ -121,6 +128,7 @@
121 BaseRunnableJobSource,128 BaseRunnableJobSource,
122 )129 )
123from lp.services.mail.sendmail import format_address_for_person130from lp.services.mail.sendmail import format_address_for_person
131from lp.services.scripts import log
124from lp.services.utils import text_delta132from lp.services.utils import text_delta
125from lp.services.webapp import canonical_url133from lp.services.webapp import canonical_url
126from lp.translations.interfaces.translationimportqueue import (134from lp.translations.interfaces.translationimportqueue import (
@@ -195,6 +203,12 @@
195 This job runs against a branch to send emails about modifications.203 This job runs against a branch to send emails about modifications.
196 """)204 """)
197205
206 DELETE_BRANCH = DBItem(9, """
207 Delete branch
208
209 This job deletes a branch from the database.
210 """)
211
198212
199@implementer(IBranchJob)213@implementer(IBranchJob)
200class BranchJob(SQLBase):214class BranchJob(SQLBase):
@@ -295,7 +309,9 @@
295 vars.extend([309 vars.extend([
296 ('branch_job_id', self.context.id),310 ('branch_job_id', self.context.id),
297 ('branch_job_type', self.context.job_type.title)])311 ('branch_job_type', self.context.job_type.title)])
298 if self.context.branch is not None:312 if 'branch_name' in self.metadata:
313 vars.append(('branch_name', self.metadata['branch_name']))
314 elif self.context.branch is not None:
299 vars.append(('branch_name', self.context.branch.unique_name))315 vars.append(('branch_name', self.context.branch.unique_name))
300 return vars316 return vars
301317
@@ -347,7 +363,6 @@
347363
348 def run(self):364 def run(self):
349 """See `IBranchScanJob`."""365 """See `IBranchScanJob`."""
350 from lp.services.scripts import log
351 with server(get_ro_server(), no_replace=True):366 with server(get_ro_server(), no_replace=True):
352 try:367 try:
353 with try_advisory_lock(368 with try_advisory_lock(
@@ -1056,3 +1071,69 @@
1056 def run(self):1071 def run(self):
1057 """See `IBranchModifiedMailJob`."""1072 """See `IBranchModifiedMailJob`."""
1058 self.getMailer().sendAll()1073 self.getMailer().sendAll()
1074
1075
1076@implementer(IBranchDeleteJob)
1077@provider(IBranchDeleteJobSource)
1078class BranchDeleteJob(BranchJobDerived):
1079 """A Job that deletes a branch from the database."""
1080
1081 class_job_type = BranchJobType.DELETE_BRANCH
1082
1083 user_error_types = (CannotDeleteBranch,)
1084
1085 config = config.IBranchDeleteJobSource
1086
1087 def __repr__(self):
1088 return '<DELETE_BRANCH branch job (%(id)s) for %(branch)s>' % {
1089 'id': self.context.id,
1090 'branch': self.branch_name,
1091 }
1092
1093 def getOperationDescription(self):
1094 return 'deleting a branch'
1095
1096 @classmethod
1097 def create(cls, branch, requester):
1098 """See `IBranchDeleteJobSource`."""
1099 metadata = {'branch_id': branch.id, 'branch_name': branch.unique_name}
1100 # The BranchJob has a branch of None, because we don't want to
1101 # delete this job while trying to delete the branch.
1102 branch_job = BranchJob(
1103 None, cls.class_job_type, metadata, requester=requester)
1104 job = cls(branch_job)
1105 job.celeryRunOnCommit()
1106 return job
1107
1108 @property
1109 def branch_id(self):
1110 return self.metadata['branch_id']
1111
1112 @property
1113 def branch_name(self):
1114 return self.metadata['branch_name']
1115
1116 def run(self):
1117 """See `IBranchDeleteJob`."""
1118 branch = getUtility(IBranchLookup).get(self.branch_id)
1119 if branch is None:
1120 log.info(
1121 'Skipping branch %s because it has already been deleted.' %
1122 self.branch_name)
1123 elif branch.deletion_status != BranchDeletionStatus.DELETING:
1124 log.warning(
1125 'Skipping branch %s because its deletion status is not '
1126 'DELETING.' % self.branch_name)
1127 else:
1128 try:
1129 branch.destroySelf(break_references=True)
1130 except CannotDeleteBranch:
1131 # Set the deletion status back to ACTIVE so that it's
1132 # possible to try again. We don't attempt to undo the
1133 # renaming at the moment. Do this in its own transaction
1134 # since the job runner will abort the transaction.
1135 transaction.abort()
1136 removeSecurityProxy(branch).deletion_status = (
1137 BranchDeletionStatus.ACTIVE)
1138 transaction.commit()
1139 raise
10591140
=== modified file 'lib/lp/code/model/tests/test_branch.py'
--- lib/lp/code/model/tests/test_branch.py 2019-01-28 18:09:21 +0000
+++ lib/lp/code/model/tests/test_branch.py 2019-03-21 15:55:38 +0000
@@ -1,4 +1,4 @@
1# Copyright 2009-2018 Canonical Ltd. This software is licensed under the1# Copyright 2009-2019 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Tests for Branches."""4"""Tests for Branches."""
@@ -1247,7 +1247,8 @@
1247 "deleted.")1247 "deleted.")
1248 branch_id = self.branch.id1248 branch_id = self.branch.id
1249 branch_set = getUtility(IBranchLookup)1249 branch_set = getUtility(IBranchLookup)
1250 self.branch.destroySelf()1250 with dbuser(config.IBranchDeleteJobSource.dbuser):
1251 self.branch.destroySelf()
1251 self.assertIsNone(1252 self.assertIsNone(
1252 branch_set.get(branch_id), "The branch has not been deleted.")1253 branch_set.get(branch_id), "The branch has not been deleted.")
12531254
@@ -1280,7 +1281,8 @@
1280 bug.linkBranch(self.branch, self.user)1281 bug.linkBranch(self.branch, self.user)
1281 self.assertEqual(self.branch.canBeDeleted(), False,1282 self.assertEqual(self.branch.canBeDeleted(), False,
1282 "A branch linked to a bug is not deletable.")1283 "A branch linked to a bug is not deletable.")
1283 self.assertRaises(CannotDeleteBranch, self.branch.destroySelf)1284 with dbuser(config.IBranchDeleteJobSource.dbuser):
1285 self.assertRaises(CannotDeleteBranch, self.branch.destroySelf)
12841286
1285 def test_specBranchLinkDisablesDeletion(self):1287 def test_specBranchLinkDisablesDeletion(self):
1286 """A branch linked to a spec cannot be deleted."""1288 """A branch linked to a spec cannot be deleted."""
@@ -1291,7 +1293,8 @@
1291 spec.linkBranch(self.branch, self.user)1293 spec.linkBranch(self.branch, self.user)
1292 self.assertEqual(self.branch.canBeDeleted(), False,1294 self.assertEqual(self.branch.canBeDeleted(), False,
1293 "A branch linked to a spec is not deletable.")1295 "A branch linked to a spec is not deletable.")
1294 self.assertRaises(CannotDeleteBranch, self.branch.destroySelf)1296 with dbuser(config.IBranchDeleteJobSource.dbuser):
1297 self.assertRaises(CannotDeleteBranch, self.branch.destroySelf)
12951298
1296 def test_associatedProductSeriesBranchDisablesDeletion(self):1299 def test_associatedProductSeriesBranchDisablesDeletion(self):
1297 """A branch linked as a branch to a product series cannot be1300 """A branch linked as a branch to a product series cannot be
@@ -1301,14 +1304,16 @@
1301 self.assertEqual(self.branch.canBeDeleted(), False,1304 self.assertEqual(self.branch.canBeDeleted(), False,
1302 "A branch that is a user branch for a product series"1305 "A branch that is a user branch for a product series"
1303 " is not deletable.")1306 " is not deletable.")
1304 self.assertRaises(CannotDeleteBranch, self.branch.destroySelf)1307 with dbuser(config.IBranchDeleteJobSource.dbuser):
1308 self.assertRaises(CannotDeleteBranch, self.branch.destroySelf)
13051309
1306 def test_productSeriesTranslationsBranchDisablesDeletion(self):1310 def test_productSeriesTranslationsBranchDisablesDeletion(self):
1307 self.product.development_focus.translations_branch = self.branch1311 self.product.development_focus.translations_branch = self.branch
1308 self.assertEqual(self.branch.canBeDeleted(), False,1312 self.assertEqual(self.branch.canBeDeleted(), False,
1309 "A branch that is a translations branch for a "1313 "A branch that is a translations branch for a "
1310 "product series is not deletable.")1314 "product series is not deletable.")
1311 self.assertRaises(CannotDeleteBranch, self.branch.destroySelf)1315 with dbuser(config.IBranchDeleteJobSource.dbuser):
1316 self.assertRaises(CannotDeleteBranch, self.branch.destroySelf)
13121317
1313 def test_revisionsDeletable(self):1318 def test_revisionsDeletable(self):
1314 """A branch that has some revisions can be deleted."""1319 """A branch that has some revisions can be deleted."""
@@ -1321,7 +1326,8 @@
1321 self.assertEqual(self.branch.canBeDeleted(), True,1326 self.assertEqual(self.branch.canBeDeleted(), True,
1322 "A branch that has a revision is deletable.")1327 "A branch that has a revision is deletable.")
1323 unique_name = self.branch.unique_name1328 unique_name = self.branch.unique_name
1324 self.branch.destroySelf()1329 with dbuser(config.IBranchDeleteJobSource.dbuser):
1330 self.branch.destroySelf()
1325 # Commit again to trigger the deferred indices.1331 # Commit again to trigger the deferred indices.
1326 transaction.commit()1332 transaction.commit()
1327 branch_lookup = getUtility(IBranchLookup)1333 branch_lookup = getUtility(IBranchLookup)
@@ -1335,7 +1341,8 @@
1335 self.branch.addLandingTarget(self.user, target_branch)1341 self.branch.addLandingTarget(self.user, target_branch)
1336 self.assertEqual(self.branch.canBeDeleted(), False,1342 self.assertEqual(self.branch.canBeDeleted(), False,
1337 "A branch with a landing target is not deletable.")1343 "A branch with a landing target is not deletable.")
1338 self.assertRaises(CannotDeleteBranch, self.branch.destroySelf)1344 with dbuser(config.IBranchDeleteJobSource.dbuser):
1345 self.assertRaises(CannotDeleteBranch, self.branch.destroySelf)
13391346
1340 def test_landingCandidateDisablesDeletion(self):1347 def test_landingCandidateDisablesDeletion(self):
1341 """A branch with a landing candidate cannot be deleted."""1348 """A branch with a landing candidate cannot be deleted."""
@@ -1345,7 +1352,8 @@
1345 self.assertEqual(self.branch.canBeDeleted(), False,1352 self.assertEqual(self.branch.canBeDeleted(), False,
1346 "A branch with a landing candidate is not"1353 "A branch with a landing candidate is not"
1347 " deletable.")1354 " deletable.")
1348 self.assertRaises(CannotDeleteBranch, self.branch.destroySelf)1355 with dbuser(config.IBranchDeleteJobSource.dbuser):
1356 self.assertRaises(CannotDeleteBranch, self.branch.destroySelf)
13491357
1350 def test_prerequisiteBranchDisablesDeletion(self):1358 def test_prerequisiteBranchDisablesDeletion(self):
1351 """A branch that is a prerequisite branch cannot be deleted."""1359 """A branch that is a prerequisite branch cannot be deleted."""
@@ -1357,14 +1365,16 @@
1357 self.assertEqual(self.branch.canBeDeleted(), False,1365 self.assertEqual(self.branch.canBeDeleted(), False,
1358 "A branch with a prerequisite target is not "1366 "A branch with a prerequisite target is not "
1359 "deletable.")1367 "deletable.")
1360 self.assertRaises(CannotDeleteBranch, self.branch.destroySelf)1368 with dbuser(config.IBranchDeleteJobSource.dbuser):
1369 self.assertRaises(CannotDeleteBranch, self.branch.destroySelf)
13611370
1362 def test_relatedBranchJobsDeleted(self):1371 def test_relatedBranchJobsDeleted(self):
1363 # A branch with an associated branch job will delete those jobs.1372 # A branch with an associated branch job will delete those jobs.
1364 branch = self.factory.makeBranch(1373 branch = self.factory.makeBranch(
1365 branch_format=BranchFormat.BZR_BRANCH_6)1374 branch_format=BranchFormat.BZR_BRANCH_6)
1366 removeSecurityProxy(branch).requestUpgrade(branch.owner)1375 removeSecurityProxy(branch).requestUpgrade(branch.owner)
1367 branch.destroySelf()1376 with dbuser(config.IBranchDeleteJobSource.dbuser):
1377 branch.destroySelf()
1368 # Need to commit the transaction to fire off the constraint checks.1378 # Need to commit the transaction to fire off the constraint checks.
1369 transaction.commit()1379 transaction.commit()
13701380
@@ -1373,7 +1383,8 @@
1373 # should be cleared.1383 # should be cleared.
1374 dev_focus = self.branch.product.development_focus1384 dev_focus = self.branch.product.development_focus
1375 dev_focus.translations_branch = self.branch1385 dev_focus.translations_branch = self.branch
1376 self.branch.destroySelf(break_references=True)1386 with dbuser(config.IBranchDeleteJobSource.dbuser):
1387 self.branch.destroySelf(break_references=True)
13771388
1378 def test_related_TranslationTemplatesBuild_cleaned_out(self):1389 def test_related_TranslationTemplatesBuild_cleaned_out(self):
1379 # A TranslationTemplatesBuild may come with a BuildQueue entry.1390 # A TranslationTemplatesBuild may come with a BuildQueue entry.
@@ -1381,7 +1392,8 @@
1381 # remove the TTB.1392 # remove the TTB.
1382 build = self.factory.makeTranslationTemplatesBuild()1393 build = self.factory.makeTranslationTemplatesBuild()
1383 build.queueBuild()1394 build.queueBuild()
1384 build.branch.destroySelf(break_references=True)1395 with dbuser(config.IBranchDeleteJobSource.dbuser):
1396 build.branch.destroySelf(break_references=True)
13851397
1386 def test_unrelated_TranslationTemplatesBuild_intact(self):1398 def test_unrelated_TranslationTemplatesBuild_intact(self):
1387 # No innocent BuildQueue entries are harmed in deleting a1399 # No innocent BuildQueue entries are harmed in deleting a
@@ -1391,7 +1403,8 @@
1391 other_build = self.factory.makeTranslationTemplatesBuild()1403 other_build = self.factory.makeTranslationTemplatesBuild()
1392 other_bq = other_build.queueBuild()1404 other_bq = other_build.queueBuild()
13931405
1394 build.branch.destroySelf(break_references=True)1406 with dbuser(config.IBranchDeleteJobSource.dbuser):
1407 build.branch.destroySelf(break_references=True)
13951408
1396 store = Store.of(build)1409 store = Store.of(build)
1397 # The BuildQueue for the job whose branch we deleted is gone.1410 # The BuildQueue for the job whose branch we deleted is gone.
@@ -1406,7 +1419,8 @@
1406 branch = self.factory.makeAnyBranch()1419 branch = self.factory.makeAnyBranch()
1407 branch_id = branch.id1420 branch_id = branch.id
1408 store = Store.of(branch)1421 store = Store.of(branch)
1409 branch.destroySelf()1422 with dbuser(config.IBranchDeleteJobSource.dbuser):
1423 branch.destroySelf()
1410 jobs = store.find(1424 jobs = store.find(
1411 BranchJob,1425 BranchJob,
1412 BranchJob.job_type == BranchJobType.RECLAIM_BRANCH_SPACE)1426 BranchJob.job_type == BranchJobType.RECLAIM_BRANCH_SPACE)
@@ -1417,7 +1431,8 @@
1417 def test_destroySelf_with_SourcePackageRecipe(self):1431 def test_destroySelf_with_SourcePackageRecipe(self):
1418 """If branch is a base_branch in a recipe, it is deleted."""1432 """If branch is a base_branch in a recipe, it is deleted."""
1419 recipe = self.factory.makeSourcePackageRecipe()1433 recipe = self.factory.makeSourcePackageRecipe()
1420 recipe.base_branch.destroySelf(break_references=True)1434 with dbuser(config.IBranchDeleteJobSource.dbuser):
1435 recipe.base_branch.destroySelf(break_references=True)
14211436
1422 def test_destroySelf_with_SourcePackageRecipe_as_non_base(self):1437 def test_destroySelf_with_SourcePackageRecipe_as_non_base(self):
1423 """If branch is referred to by a recipe, it is deleted."""1438 """If branch is referred to by a recipe, it is deleted."""
@@ -1425,7 +1440,8 @@
1425 branch2 = self.factory.makeAnyBranch()1440 branch2 = self.factory.makeAnyBranch()
1426 self.factory.makeSourcePackageRecipe(1441 self.factory.makeSourcePackageRecipe(
1427 branches=[branch1, branch2])1442 branches=[branch1, branch2])
1428 branch2.destroySelf(break_references=True)1443 with dbuser(config.IBranchDeleteJobSource.dbuser):
1444 branch2.destroySelf(break_references=True)
14291445
1430 def test_destroySelf_with_inline_comments_draft(self):1446 def test_destroySelf_with_inline_comments_draft(self):
1431 # Draft inline comments related to a deleted branch (source1447 # Draft inline comments related to a deleted branch (source
@@ -1439,7 +1455,8 @@
1439 previewdiff_id=preview_diff.id,1455 previewdiff_id=preview_diff.id,
1440 person=self.user,1456 person=self.user,
1441 comments={'1': 'Should vanish.'})1457 comments={'1': 'Should vanish.'})
1442 self.branch.destroySelf(break_references=True)1458 with dbuser(config.IBranchDeleteJobSource.dbuser):
1459 self.branch.destroySelf(break_references=True)
14431460
1444 def test_destroySelf_with_inline_comments_published(self):1461 def test_destroySelf_with_inline_comments_published(self):
1445 # Published inline comments related to a deleted branch (source1462 # Published inline comments related to a deleted branch (source
@@ -1455,12 +1472,14 @@
1455 previewdiff_id=preview_diff.id,1472 previewdiff_id=preview_diff.id,
1456 inline_comments={'1': 'Must disappear.'},1473 inline_comments={'1': 'Must disappear.'},
1457 )1474 )
1458 self.branch.destroySelf(break_references=True)1475 with dbuser(config.IBranchDeleteJobSource.dbuser):
1476 self.branch.destroySelf(break_references=True)
14591477
1460 def test_related_webhooks_deleted(self):1478 def test_related_webhooks_deleted(self):
1461 webhook = self.factory.makeWebhook(target=self.branch)1479 webhook = self.factory.makeWebhook(target=self.branch)
1462 webhook.ping()1480 webhook.ping()
1463 self.branch.destroySelf()1481 with dbuser(config.IBranchDeleteJobSource.dbuser):
1482 self.branch.destroySelf()
1464 transaction.commit()1483 transaction.commit()
1465 self.assertRaises(LostObjectError, getattr, webhook, 'target')1484 self.assertRaises(LostObjectError, getattr, webhook, 'target')
14661485
@@ -1541,7 +1560,8 @@
1541 merge_proposal1, merge_proposal2 = self.makeMergeProposals()1560 merge_proposal1, merge_proposal2 = self.makeMergeProposals()
1542 merge_proposal1_id = merge_proposal1.id1561 merge_proposal1_id = merge_proposal1.id
1543 BranchMergeProposal.get(merge_proposal1_id)1562 BranchMergeProposal.get(merge_proposal1_id)
1544 self.branch.destroySelf(break_references=True)1563 with dbuser(config.IBranchDeleteJobSource.dbuser):
1564 self.branch.destroySelf(break_references=True)
1545 self.assertRaises(SQLObjectNotFound,1565 self.assertRaises(SQLObjectNotFound,
1546 BranchMergeProposal.get, merge_proposal1_id)1566 BranchMergeProposal.get, merge_proposal1_id)
15471567
@@ -1550,14 +1570,17 @@
1550 merge_proposal1, merge_proposal2 = self.makeMergeProposals()1570 merge_proposal1, merge_proposal2 = self.makeMergeProposals()
1551 merge_proposal1_id = merge_proposal1.id1571 merge_proposal1_id = merge_proposal1.id
1552 BranchMergeProposal.get(merge_proposal1_id)1572 BranchMergeProposal.get(merge_proposal1_id)
1553 merge_proposal1.target_branch.destroySelf(break_references=True)1573 with dbuser(config.IBranchDeleteJobSource.dbuser):
1574 merge_proposal1.target_branch.destroySelf(break_references=True)
1554 self.assertRaises(SQLObjectNotFound,1575 self.assertRaises(SQLObjectNotFound,
1555 BranchMergeProposal.get, merge_proposal1_id)1576 BranchMergeProposal.get, merge_proposal1_id)
15561577
1557 def test_deleteMergeProposalDependent(self):1578 def test_deleteMergeProposalDependent(self):
1558 """break_links enables deleting merge proposal dependant branches."""1579 """break_links enables deleting merge proposal dependant branches."""
1559 merge_proposal1, merge_proposal2 = self.makeMergeProposals()1580 merge_proposal1, merge_proposal2 = self.makeMergeProposals()
1560 merge_proposal1.prerequisite_branch.destroySelf(break_references=True)1581 with dbuser(config.IBranchDeleteJobSource.dbuser):
1582 merge_proposal1.prerequisite_branch.destroySelf(
1583 break_references=True)
1561 self.assertEqual(None, merge_proposal1.prerequisite_branch)1584 self.assertEqual(None, merge_proposal1.prerequisite_branch)
15621585
1563 def test_deleteSourceCodeReviewComment(self):1586 def test_deleteSourceCodeReviewComment(self):
@@ -1565,7 +1588,8 @@
1565 comment = self.factory.makeCodeReviewComment()1588 comment = self.factory.makeCodeReviewComment()
1566 comment_id = comment.id1589 comment_id = comment.id
1567 branch = comment.branch_merge_proposal.source_branch1590 branch = comment.branch_merge_proposal.source_branch
1568 branch.destroySelf(break_references=True)1591 with dbuser(config.IBranchDeleteJobSource.dbuser):
1592 branch.destroySelf(break_references=True)
1569 self.assertRaises(1593 self.assertRaises(
1570 SQLObjectNotFound, CodeReviewComment.get, comment_id)1594 SQLObjectNotFound, CodeReviewComment.get, comment_id)
15711595
@@ -1574,7 +1598,8 @@
1574 comment = self.factory.makeCodeReviewComment()1598 comment = self.factory.makeCodeReviewComment()
1575 comment_id = comment.id1599 comment_id = comment.id
1576 branch = comment.branch_merge_proposal.target_branch1600 branch = comment.branch_merge_proposal.target_branch
1577 branch.destroySelf(break_references=True)1601 with dbuser(config.IBranchDeleteJobSource.dbuser):
1602 branch.destroySelf(break_references=True)
1578 self.assertRaises(1603 self.assertRaises(
1579 SQLObjectNotFound, CodeReviewComment.get, comment_id)1604 SQLObjectNotFound, CodeReviewComment.get, comment_id)
15801605
@@ -1592,7 +1617,8 @@
1592 bug1.linkBranch(self.branch, self.branch.owner)1617 bug1.linkBranch(self.branch, self.branch.owner)
1593 bug_branch1 = bug1.linked_bugbranches[0]1618 bug_branch1 = bug1.linked_bugbranches[0]
1594 bug_branch1_id = removeSecurityProxy(bug_branch1).id1619 bug_branch1_id = removeSecurityProxy(bug_branch1).id
1595 self.branch.destroySelf(break_references=True)1620 with dbuser(config.IBranchDeleteJobSource.dbuser):
1621 self.branch.destroySelf(break_references=True)
1596 self.assertRaises(SQLObjectNotFound, BugBranch.get, bug_branch1_id)1622 self.assertRaises(SQLObjectNotFound, BugBranch.get, bug_branch1_id)
15971623
1598 def test_branchWithSpecRequirements(self):1624 def test_branchWithSpecRequirements(self):
@@ -1612,7 +1638,8 @@
1612 spec2 = self.factory.makeSpecification()1638 spec2 = self.factory.makeSpecification()
1613 spec2.linkBranch(self.branch, self.branch.owner)1639 spec2.linkBranch(self.branch, self.branch.owner)
1614 spec2_branch_id = self.branch.spec_links[1].id1640 spec2_branch_id = self.branch.spec_links[1].id
1615 self.branch.destroySelf(break_references=True)1641 with dbuser(config.IBranchDeleteJobSource.dbuser):
1642 self.branch.destroySelf(break_references=True)
1616 self.assertRaises(1643 self.assertRaises(
1617 SQLObjectNotFound, SpecificationBranch.get, spec1_branch_id)1644 SQLObjectNotFound, SpecificationBranch.get, spec1_branch_id)
1618 self.assertRaises(1645 self.assertRaises(
@@ -1630,7 +1657,8 @@
1630 """break_links allows deleting a series' branch."""1657 """break_links allows deleting a series' branch."""
1631 series1 = self.factory.makeProductSeries(branch=self.branch)1658 series1 = self.factory.makeProductSeries(branch=self.branch)
1632 series2 = self.factory.makeProductSeries(branch=self.branch)1659 series2 = self.factory.makeProductSeries(branch=self.branch)
1633 self.branch.destroySelf(break_references=True)1660 with dbuser(config.IBranchDeleteJobSource.dbuser):
1661 self.branch.destroySelf(break_references=True)
1634 self.assertEqual(None, series1.branch)1662 self.assertEqual(None, series1.branch)
1635 self.assertEqual(None, series2.branch)1663 self.assertEqual(None, series2.branch)
16361664
@@ -1661,7 +1689,8 @@
1661 package.development_version.setBranch,1689 package.development_version.setBranch,
1662 pocket, branch, package.distribution.owner)1690 pocket, branch, package.distribution.owner)
1663 self.assertEqual(False, branch.canBeDeleted())1691 self.assertEqual(False, branch.canBeDeleted())
1664 branch.destroySelf(break_references=True)1692 with dbuser(config.IBranchDeleteJobSource.dbuser):
1693 branch.destroySelf(break_references=True)
1665 self.assertIs(None, package.getBranch(pocket))1694 self.assertIs(None, package.getBranch(pocket))
16661695
1667 def test_branchWithCodeImportRequirements(self):1696 def test_branchWithCodeImportRequirements(self):
@@ -1676,7 +1705,8 @@
1676 """break_references allows deleting a code import branch."""1705 """break_references allows deleting a code import branch."""
1677 code_import = self.factory.makeCodeImport()1706 code_import = self.factory.makeCodeImport()
1678 code_import_id = code_import.id1707 code_import_id = code_import.id
1679 code_import.branch.destroySelf(break_references=True)1708 with dbuser(config.IBranchDeleteJobSource.dbuser):
1709 code_import.branch.destroySelf(break_references=True)
1680 self.assertRaises(1710 self.assertRaises(
1681 NotFoundError, getUtility(ICodeImportSet).get, code_import_id)1711 NotFoundError, getUtility(ICodeImportSet).get, code_import_id)
16821712
@@ -1685,14 +1715,16 @@
1685 merge_proposal = self.factory.makeBranchMergeProposal()1715 merge_proposal = self.factory.makeBranchMergeProposal()
1686 merge_proposal.nominateReviewer(self.factory.makePerson(),1716 merge_proposal.nominateReviewer(self.factory.makePerson(),
1687 self.factory.makePerson())1717 self.factory.makePerson())
1688 merge_proposal.source_branch.destroySelf(break_references=True)1718 with dbuser(config.IBranchDeleteJobSource.dbuser):
1719 merge_proposal.source_branch.destroySelf(break_references=True)
16891720
1690 def test_targetBranchWithCodeReviewVoteReference(self):1721 def test_targetBranchWithCodeReviewVoteReference(self):
1691 """Break_references handles CodeReviewVoteReference target branch."""1722 """Break_references handles CodeReviewVoteReference target branch."""
1692 merge_proposal = self.factory.makeBranchMergeProposal()1723 merge_proposal = self.factory.makeBranchMergeProposal()
1693 merge_proposal.nominateReviewer(self.factory.makePerson(),1724 merge_proposal.nominateReviewer(self.factory.makePerson(),
1694 self.factory.makePerson())1725 self.factory.makePerson())
1695 merge_proposal.target_branch.destroySelf(break_references=True)1726 with dbuser(config.IBranchDeleteJobSource.dbuser):
1727 merge_proposal.target_branch.destroySelf(break_references=True)
16961728
1697 def test_snap_requirements(self):1729 def test_snap_requirements(self):
1698 # If a branch is used by a snap package, the deletion requirements1730 # If a branch is used by a snap package, the deletion requirements
@@ -1706,7 +1738,8 @@
1706 # break_references allows deleting a branch used by a snap package.1738 # break_references allows deleting a branch used by a snap package.
1707 snap1 = self.factory.makeSnap(branch=self.branch)1739 snap1 = self.factory.makeSnap(branch=self.branch)
1708 snap2 = self.factory.makeSnap(branch=self.branch)1740 snap2 = self.factory.makeSnap(branch=self.branch)
1709 self.branch.destroySelf(break_references=True)1741 with dbuser(config.IBranchDeleteJobSource.dbuser):
1742 self.branch.destroySelf(break_references=True)
1710 transaction.commit()1743 transaction.commit()
1711 self.assertIsNone(snap1.branch)1744 self.assertIsNone(snap1.branch)
1712 self.assertIsNone(snap2.branch)1745 self.assertIsNone(snap2.branch)
@@ -1715,7 +1748,8 @@
1715 """ClearDependent.__call__ must clear the prerequisite branch."""1748 """ClearDependent.__call__ must clear the prerequisite branch."""
1716 merge_proposal = removeSecurityProxy(self.makeMergeProposals()[0])1749 merge_proposal = removeSecurityProxy(self.makeMergeProposals()[0])
1717 with person_logged_in(merge_proposal.prerequisite_branch.owner):1750 with person_logged_in(merge_proposal.prerequisite_branch.owner):
1718 ClearDependentBranch(merge_proposal)()1751 with dbuser(config.IBranchDeleteJobSource.dbuser):
1752 ClearDependentBranch(merge_proposal)()
1719 self.assertEqual(None, merge_proposal.prerequisite_branch)1753 self.assertEqual(None, merge_proposal.prerequisite_branch)
17201754
1721 def test_ClearOfficialPackageBranch(self):1755 def test_ClearOfficialPackageBranch(self):
@@ -1730,14 +1764,16 @@
1730 pocket, branch, package.distribution.owner)1764 pocket, branch, package.distribution.owner)
1731 series_set = getUtility(IFindOfficialBranchLinks)1765 series_set = getUtility(IFindOfficialBranchLinks)
1732 [link] = list(series_set.findForBranch(branch))1766 [link] = list(series_set.findForBranch(branch))
1733 ClearOfficialPackageBranch(link)()1767 with dbuser(config.IBranchDeleteJobSource.dbuser):
1768 ClearOfficialPackageBranch(link)()
1734 self.assertIs(None, package.getBranch(pocket))1769 self.assertIs(None, package.getBranch(pocket))
17351770
1736 def test_ClearSeriesBranch(self):1771 def test_ClearSeriesBranch(self):
1737 """ClearSeriesBranch.__call__ must clear the user branch."""1772 """ClearSeriesBranch.__call__ must clear the user branch."""
1738 series = removeSecurityProxy(self.factory.makeProductSeries(1773 series = removeSecurityProxy(self.factory.makeProductSeries(
1739 branch=self.branch))1774 branch=self.branch))
1740 ClearSeriesBranch(series, self.branch)()1775 with dbuser(config.IBranchDeleteJobSource.dbuser):
1776 ClearSeriesBranch(series, self.branch)()
1741 self.assertEqual(None, series.branch)1777 self.assertEqual(None, series.branch)
17421778
1743 def test_DeletionOperation(self):1779 def test_DeletionOperation(self):
@@ -1749,7 +1785,8 @@
1749 spec = self.factory.makeSpecification()1785 spec = self.factory.makeSpecification()
1750 spec_link = spec.linkBranch(self.branch, self.branch.owner)1786 spec_link = spec.linkBranch(self.branch, self.branch.owner)
1751 spec_link_id = spec_link.id1787 spec_link_id = spec_link.id
1752 DeletionCallable(spec, 'blah', spec_link.destroySelf)()1788 with dbuser(config.IBranchDeleteJobSource.dbuser):
1789 DeletionCallable(spec, 'blah', spec_link.destroySelf)()
1753 self.assertRaises(SQLObjectNotFound, SpecificationBranch.get,1790 self.assertRaises(SQLObjectNotFound, SpecificationBranch.get,
1754 spec_link_id)1791 spec_link_id)
17551792
@@ -1757,7 +1794,8 @@
1757 """DeleteCodeImport.__call__ must delete the CodeImport."""1794 """DeleteCodeImport.__call__ must delete the CodeImport."""
1758 code_import = self.factory.makeCodeImport()1795 code_import = self.factory.makeCodeImport()
1759 code_import_id = code_import.id1796 code_import_id = code_import.id
1760 DeleteCodeImport(code_import)()1797 with dbuser(config.IBranchDeleteJobSource.dbuser):
1798 DeleteCodeImport(code_import)()
1761 self.assertRaises(1799 self.assertRaises(
1762 NotFoundError, getUtility(ICodeImportSet).get, code_import_id)1800 NotFoundError, getUtility(ICodeImportSet).get, code_import_id)
17631801
17641802
=== modified file 'lib/lp/code/model/tests/test_branchjob.py'
--- lib/lp/code/model/tests/test_branchjob.py 2018-03-30 20:42:14 +0000
+++ lib/lp/code/model/tests/test_branchjob.py 2019-03-21 15:55:38 +0000
@@ -1,4 +1,4 @@
1# Copyright 2009-2018 Canonical Ltd. This software is licensed under the1# Copyright 2009-2019 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Tests for BranchJobs."""4"""Tests for BranchJobs."""
@@ -16,7 +16,10 @@
16from bzrlib.bzrdir import BzrDir16from bzrlib.bzrdir import BzrDir
17from bzrlib.revision import NULL_REVISION17from bzrlib.revision import NULL_REVISION
18from bzrlib.transport import get_transport18from bzrlib.transport import get_transport
19from fixtures import MockPatch19from fixtures import (
20 FakeLogger,
21 MockPatch,
22 )
20import pytz23import pytz
21from sqlobject import SQLObjectNotFound24from sqlobject import SQLObjectNotFound
22from storm.locals import Store25from storm.locals import Store
@@ -31,6 +34,7 @@
31 RepositoryFormat,34 RepositoryFormat,
32 )35 )
33from lp.code.enums import (36from lp.code.enums import (
37 BranchDeletionStatus,
34 BranchMergeProposalStatus,38 BranchMergeProposalStatus,
35 BranchSubscriptionDiffSize,39 BranchSubscriptionDiffSize,
36 BranchSubscriptionNotificationLevel,40 BranchSubscriptionNotificationLevel,
@@ -38,6 +42,8 @@
38 )42 )
39from lp.code.errors import AlreadyLatestFormat43from lp.code.errors import AlreadyLatestFormat
40from lp.code.interfaces.branchjob import (44from lp.code.interfaces.branchjob import (
45 IBranchDeleteJob,
46 IBranchDeleteJobSource,
41 IBranchJob,47 IBranchJob,
42 IBranchScanJob,48 IBranchScanJob,
43 IBranchUpgradeJob,49 IBranchUpgradeJob,
@@ -46,6 +52,7 @@
46 IRevisionMailJob,52 IRevisionMailJob,
47 IRosettaUploadJob,53 IRosettaUploadJob,
48 )54 )
55from lp.code.interfaces.branchlookup import IBranchLookup
49from lp.code.model.branchjob import (56from lp.code.model.branchjob import (
50 BranchJob,57 BranchJob,
51 BranchJobDerived,58 BranchJobDerived,
@@ -72,6 +79,7 @@
72from lp.services.job.model.job import Job79from lp.services.job.model.job import Job
73from lp.services.job.runner import JobRunner80from lp.services.job.runner import JobRunner
74from lp.services.job.tests import block_on_job81from lp.services.job.tests import block_on_job
82from lp.services.mail.sendmail import format_address_for_person
75from lp.services.osutils import override_environ83from lp.services.osutils import override_environ
76from lp.services.webapp import canonical_url84from lp.services.webapp import canonical_url
77from lp.testing import (85from lp.testing import (
@@ -86,6 +94,7 @@
86 CeleryBzrsyncdJobLayer,94 CeleryBzrsyncdJobLayer,
87 DatabaseFunctionalLayer,95 DatabaseFunctionalLayer,
88 LaunchpadZopelessLayer,96 LaunchpadZopelessLayer,
97 ZopelessDatabaseLayer,
89 )98 )
90from lp.testing.librarianhelpers import get_newest_librarian_file99from lp.testing.librarianhelpers import get_newest_librarian_file
91from lp.testing.mail_helpers import pop_notifications100from lp.testing.mail_helpers import pop_notifications
@@ -1390,3 +1399,87 @@
1390 os.makedirs(branch_path)1399 os.makedirs(branch_path)
1391 self.runReadyJobs()1400 self.runReadyJobs()
1392 self.assertFalse(os.path.exists(branch_path))1401 self.assertFalse(os.path.exists(branch_path))
1402
1403
1404class TestBranchDeleteJob(TestCaseWithFactory):
1405
1406 layer = ZopelessDatabaseLayer
1407
1408 def test_providesInterface(self):
1409 branch = self.factory.makeAnyBranch()
1410 requester = branch.registrant
1411 job = getUtility(IBranchDeleteJobSource).create(branch, requester)
1412 self.assertProvides(job, IBranchDeleteJob)
1413
1414 def test_run(self):
1415 branch = self.factory.makeAnyBranch()
1416 branch_id = branch.id
1417 requester = branch.registrant
1418 job = getUtility(IBranchDeleteJobSource).create(branch, requester)
1419 removeSecurityProxy(branch).deletion_status = (
1420 BranchDeletionStatus.DELETING)
1421 logger = self.useFixture(FakeLogger())
1422 with dbuser(config.IBranchDeleteJobSource.dbuser):
1423 job.run()
1424 self.assertEqual('', logger.output)
1425 self.assertIsNone(getUtility(IBranchLookup).get(branch_id))
1426
1427 def test_already_deleted(self):
1428 branch = self.factory.makeAnyBranch()
1429 branch_name = branch.unique_name
1430 requester = branch.registrant
1431 job = getUtility(IBranchDeleteJobSource).create(branch, requester)
1432 branch.destroySelf()
1433 logger = self.useFixture(FakeLogger())
1434 with dbuser(config.IBranchDeleteJobSource.dbuser):
1435 job.run()
1436 self.assertEqual(
1437 'Skipping branch %s because it has already been '
1438 'deleted.\n' % branch_name,
1439 logger.output)
1440
1441 def test_not_deleting(self):
1442 # The job skips branches that aren't DELETING. This shouldn't be
1443 # possible in practice, but is a guard against accidents.
1444 branch = self.factory.makeAnyBranch()
1445 branch_id = branch.id
1446 branch_name = branch.unique_name
1447 self.assertNotEqual(
1448 BranchDeletionStatus.DELETING, branch.deletion_status)
1449 requester = branch.registrant
1450 job = getUtility(IBranchDeleteJobSource).create(branch, requester)
1451 logger = self.useFixture(FakeLogger())
1452 with dbuser(config.IBranchDeleteJobSource.dbuser):
1453 job.run()
1454 self.assertEqual(
1455 'Skipping branch %s because its deletion status is not '
1456 'DELETING.\n' % branch_name,
1457 logger.output)
1458 self.assertEqual(branch, getUtility(IBranchLookup).get(branch_id))
1459
1460 def test_error(self):
1461 # If deleting the branch fails, an error message is sent to the
1462 # requester and the deletion status is set back to ACTIVE. Most
1463 # cases where this can happen are caught earlier, but this can still
1464 # happen if a branch stacked on the to-be-deleted branch is created
1465 # between the deletion job being created and being run.
1466 branch = self.factory.makeAnyBranch()
1467 branch_id = branch.id
1468 requester = branch.registrant
1469 job = getUtility(IBranchDeleteJobSource).create(branch, requester)
1470 removeSecurityProxy(branch).deletion_status = (
1471 BranchDeletionStatus.DELETING)
1472 self.factory.makeAnyBranch(stacked_on=branch)
1473 logger = self.useFixture(FakeLogger())
1474 with dbuser(config.IBranchDeleteJobSource.dbuser):
1475 JobRunner([job]).runJobHandleError(job)
1476 self.assertIn(
1477 'failed with user error CannotDeleteBranch', logger.output)
1478 self.assertEqual(branch, getUtility(IBranchLookup).get(branch_id))
1479 self.assertEqual(BranchDeletionStatus.ACTIVE, branch.deletion_status)
1480 self.assertEqual([], self.oopses)
1481 [mail] = pop_notifications()
1482 self.assertEqual(format_address_for_person(requester), mail['to'])
1483 self.assertEqual(
1484 'Launchpad error while deleting a branch', mail['subject'])
1485 self.assertIn('Cannot delete branch', mail.get_payload(decode=True))
13931486
=== modified file 'lib/lp/code/stories/webservice/xx-branch.txt'
--- lib/lp/code/stories/webservice/xx-branch.txt 2018-05-13 10:35:52 +0000
+++ lib/lp/code/stories/webservice/xx-branch.txt 2019-03-21 15:55:38 +0000
@@ -112,6 +112,7 @@
112 control_format: None112 control_format: None
113 date_created: u'2009-01-01T00:00:00+00:00'113 date_created: u'2009-01-01T00:00:00+00:00'
114 date_last_modified: u'2009-01-01T00:00:00+00:00'114 date_last_modified: u'2009-01-01T00:00:00+00:00'
115 deletion_status: u'Active'
115 dependent_branches_collection_link: u'.../~eric/fooix/trunk/dependent_branches'116 dependent_branches_collection_link: u'.../~eric/fooix/trunk/dependent_branches'
116 description: None117 description: None
117 display_name: u'lp://dev/~eric/fooix/trunk'118 display_name: u'lp://dev/~eric/fooix/trunk'
118119
=== modified file 'lib/lp/services/config/schema-lazr.conf'
--- lib/lp/services/config/schema-lazr.conf 2019-03-18 12:22:55 +0000
+++ lib/lp/services/config/schema-lazr.conf 2019-03-21 15:55:38 +0000
@@ -1867,6 +1867,7 @@
1867# dbuser, the crontab_group, and the module that the job source class1867# dbuser, the crontab_group, and the module that the job source class
1868# can be loaded from.1868# can be loaded from.
1869job_sources:1869job_sources:
1870 IBranchDeleteJobSource,
1870 IBranchModifiedMailJobSource,1871 IBranchModifiedMailJobSource,
1871 ICommercialExpiredJobSource,1872 ICommercialExpiredJobSource,
1872 IExpiringMembershipNotificationJobSource,1873 IExpiringMembershipNotificationJobSource,
@@ -1888,6 +1889,11 @@
1888 ITeamJoinNotificationJobSource,1889 ITeamJoinNotificationJobSource,
1889 IThirtyDayCommercialExpirationJobSource1890 IThirtyDayCommercialExpirationJobSource
18901891
1892[IBranchDeleteJobSource]
1893module: lp.code.interfaces.branchjob
1894dbuser: branch-delete-job
1895crontab_group: MAIN
1896
1891[IBranchMergeProposalJobSource]1897[IBranchMergeProposalJobSource]
1892module: lp.code.interfaces.branchmergeproposal1898module: lp.code.interfaces.branchmergeproposal
1893dbuser: merge-proposal-jobs1899dbuser: merge-proposal-jobs