Merge lp:~cjwatson/launchpad/process-accepted-bugs-job into lp:launchpad
- process-accepted-bugs-job
- Merge into devel
| Status: | Merged |
|---|---|
| Approved by: | Colin Watson on 2012-09-19 |
| Approved revision: | no longer in the source branch. |
| Merged at revision: | 15988 |
| Proposed branch: | lp:~cjwatson/launchpad/process-accepted-bugs-job |
| Merge into: | lp:launchpad |
| Diff against target: |
975 lines (+598/-119) 10 files modified
lib/lp/services/config/schema-lazr.conf (+7/-0) lib/lp/services/features/flags.py (+6/-0) lib/lp/soyuz/configure.zcml (+26/-14) lib/lp/soyuz/interfaces/processacceptedbugsjob.py (+57/-0) lib/lp/soyuz/model/processacceptedbugsjob.py (+174/-0) lib/lp/soyuz/scripts/processaccepted.py (+47/-80) lib/lp/soyuz/scripts/tests/test_processaccepted.py (+3/-3) lib/lp/soyuz/scripts/tests/test_queue.py (+34/-6) lib/lp/soyuz/tests/test_processaccepted.py (+17/-16) lib/lp/soyuz/tests/test_processacceptedbugsjob.py (+227/-0) |
| To merge this branch: | bzr merge lp:~cjwatson/launchpad/process-accepted-bugs-job |
| Related bugs: |
| Reviewer | Review Type | Date Requested | Status |
|---|---|---|---|
| Steve Kowalik (community) | code | 2012-09-02 | Approve on 2012-09-05 |
|
Review via email:
|
|||
Commit Message
When accepting packages, create a job to close bugs rather than closing them directly.
Description of the Change
== Summary ==
Bug 745799: Accepting an upload may need to close an arbitrarily large number of bugs. Closing bugs involves a large number of queries due to structural subscriptions. Uploads are sometimes accepted in contexts involving a timeout (notably DistroSeries:+queue and the PackageUpload.
== Proposed fix ==
I understand that notifications may be made asynchronous at some point. I'm not exactly sure what shape that work will take as I haven't seen anything concrete. Pending that, I think a reasonable solution is simply to create a dedicated job for this, which can be run frequently; this can always be refactored in terms of something more general later if it turns out to be appropriate.
== Pre-implementation notes ==
http://
Also, when this is deployed, we'll need to change the production crontabs to run this job. We could also pre-emptively change ackee-launchpad to run 'cronscripts/
https:/
Comments welcome.
== LOC Rationale ==
+445. This is the last blocker for https:/
== Tests ==
bin/test -vvct lp.soyuz.
== Demo and Q/A ==
Set a distroseries to FROZEN on dogfood; upload a package to it, closing some open bug on that package; accept the package from the queue using 'queue accept' (lp:ubuntu-archive-tools) or the web UI; check that the bug is not closed immediately; run 'cronscripts/
== Lint ==
Pre-existing lint, not straightforwardly fixable:
./lib/lp/
450: Line exceeds 80 characters.
1032: Line exceeds 80 characters.
1039: Line exceeds 80 characters.
1577: Line exceeds 80 characters.
| Colin Watson (cjwatson) wrote : | # |
Thanks, I've applied something similar to your suggestion for get_bug_
The job tests do run under the process_accepted dbuser; they just spell it "config.
Preview Diff
| 1 | === modified file 'lib/lp/services/config/schema-lazr.conf' |
| 2 | --- lib/lp/services/config/schema-lazr.conf 2012-08-09 04:44:13 +0000 |
| 3 | +++ lib/lp/services/config/schema-lazr.conf 2012-09-19 23:41:25 +0000 |
| 4 | @@ -1747,6 +1747,7 @@ |
| 5 | IMembershipNotificationJobSource, |
| 6 | IPersonMergeJobSource, |
| 7 | IPlainPackageCopyJobSource, |
| 8 | + IProcessAcceptedBugsJobSource, |
| 9 | IQuestionEmailJobSource, |
| 10 | IRemoveArtifactSubscriptionsJobSource, |
| 11 | ISevenDayCommercialExpirationJobSource, |
| 12 | @@ -1775,6 +1776,12 @@ |
| 13 | dbuser: copy_packages |
| 14 | crontab_group: FREQUENT |
| 15 | |
| 16 | +[IProcessAcceptedBugsJobSource] |
| 17 | +# This section is used by cronscripts/process-job-source.py. |
| 18 | +module: lp.soyuz.interfaces.processacceptedbugsjob |
| 19 | +dbuser: process_accepted |
| 20 | +crontab_group: FREQUENT |
| 21 | + |
| 22 | [IQuestionEmailJobSource] |
| 23 | # This section is used by cronscripts/process-job-source.py. |
| 24 | module: lp.answers.interfaces.questionjob |
| 25 | |
| 26 | === modified file 'lib/lp/services/features/flags.py' |
| 27 | --- lib/lp/services/features/flags.py 2012-08-31 14:35:33 +0000 |
| 28 | +++ lib/lp/services/features/flags.py 2012-09-19 23:41:25 +0000 |
| 29 | @@ -264,6 +264,12 @@ |
| 30 | 'disabled', |
| 31 | '', |
| 32 | 'https://dev.launchpad.net/LEP/PrivateProjects'), |
| 33 | + ('soyuz.processacceptedbugsjob.enabled', |
| 34 | + 'boolean', |
| 35 | + 'If true, use a job to close bugs when accepting uploads from a queue.', |
| 36 | + 'disabled', |
| 37 | + '', |
| 38 | + ''), |
| 39 | |
| 40 | ]) |
| 41 | |
| 42 | |
| 43 | === modified file 'lib/lp/soyuz/configure.zcml' |
| 44 | --- lib/lp/soyuz/configure.zcml 2012-07-11 13:27:35 +0000 |
| 45 | +++ lib/lp/soyuz/configure.zcml 2012-09-19 23:41:25 +0000 |
| 46 | @@ -988,19 +988,31 @@ |
| 47 | <allow interface="lp.soyuz.interfaces.packagerelationship.IPackageRelationshipSet"/> |
| 48 | </class> |
| 49 | |
| 50 | - <!-- Overrides --> |
| 51 | - <class class="lp.soyuz.adapters.overrides.SourceOverride"> |
| 52 | - <allow interface="lp.soyuz.adapters.overrides.ISourceOverride" /> |
| 53 | - </class> |
| 54 | - <class class="lp.soyuz.adapters.overrides.BinaryOverride"> |
| 55 | - <allow interface="lp.soyuz.adapters.overrides.IBinaryOverride" /> |
| 56 | - </class> |
| 57 | - |
| 58 | - <!-- OverridePolicy --> |
| 59 | - <class class="lp.soyuz.adapters.overrides.UbuntuOverridePolicy"> |
| 60 | - <allow interface="lp.soyuz.adapters.overrides.IOverridePolicy" /> |
| 61 | - </class> |
| 62 | - |
| 63 | - <webservice:register module="lp.soyuz.interfaces.webservice" /> |
| 64 | + <!-- Overrides --> |
| 65 | + <class class="lp.soyuz.adapters.overrides.SourceOverride"> |
| 66 | + <allow interface="lp.soyuz.adapters.overrides.ISourceOverride" /> |
| 67 | + </class> |
| 68 | + <class class="lp.soyuz.adapters.overrides.BinaryOverride"> |
| 69 | + <allow interface="lp.soyuz.adapters.overrides.IBinaryOverride" /> |
| 70 | + </class> |
| 71 | + |
| 72 | + <!-- OverridePolicy --> |
| 73 | + <class class="lp.soyuz.adapters.overrides.UbuntuOverridePolicy"> |
| 74 | + <allow interface="lp.soyuz.adapters.overrides.IOverridePolicy" /> |
| 75 | + </class> |
| 76 | + |
| 77 | + <!-- ProcessAcceptedBugsJobSource --> |
| 78 | + <securedutility |
| 79 | + component=".model.processacceptedbugsjob.ProcessAcceptedBugsJob" |
| 80 | + provides=".interfaces.processacceptedbugsjob.IProcessAcceptedBugsJobSource"> |
| 81 | + <allow interface=".interfaces.processacceptedbugsjob.IProcessAcceptedBugsJobSource" /> |
| 82 | + </securedutility> |
| 83 | + |
| 84 | + <!-- ProcessAcceptedBugsJob --> |
| 85 | + <class class=".model.processacceptedbugsjob.ProcessAcceptedBugsJob"> |
| 86 | + <allow interface=".interfaces.processacceptedbugsjob.IProcessAcceptedBugsJob" /> |
| 87 | + </class> |
| 88 | + |
| 89 | + <webservice:register module="lp.soyuz.interfaces.webservice" /> |
| 90 | |
| 91 | </configure> |
| 92 | |
| 93 | === added file 'lib/lp/soyuz/interfaces/processacceptedbugsjob.py' |
| 94 | --- lib/lp/soyuz/interfaces/processacceptedbugsjob.py 1970-01-01 00:00:00 +0000 |
| 95 | +++ lib/lp/soyuz/interfaces/processacceptedbugsjob.py 2012-09-19 23:41:25 +0000 |
| 96 | @@ -0,0 +1,57 @@ |
| 97 | +# Copyright 2012 Canonical Ltd. This software is licensed under the |
| 98 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
| 99 | + |
| 100 | +__metaclass__ = type |
| 101 | + |
| 102 | +__all__ = [ |
| 103 | + "IProcessAcceptedBugsJob", |
| 104 | + "IProcessAcceptedBugsJobSource", |
| 105 | + ] |
| 106 | + |
| 107 | +from lazr.restful.fields import Reference |
| 108 | +from zope.interface import Attribute |
| 109 | + |
| 110 | +from lp import _ |
| 111 | +from lp.registry.interfaces.distroseries import IDistroSeries |
| 112 | +from lp.services.job.interfaces.job import ( |
| 113 | + IJob, |
| 114 | + IJobSource, |
| 115 | + IRunnableJob, |
| 116 | + ) |
| 117 | +from lp.soyuz.interfaces.sourcepackagerelease import ISourcePackageRelease |
| 118 | + |
| 119 | + |
| 120 | +class IProcessAcceptedBugsJob(IRunnableJob): |
| 121 | + """An `IJob` to close bugs for accepted package uploads.""" |
| 122 | + |
| 123 | + job = Reference( |
| 124 | + schema=IJob, title=_("The common Job attributes"), |
| 125 | + required=True, readonly=True) |
| 126 | + |
| 127 | + distroseries = Reference( |
| 128 | + schema=IDistroSeries, title=_("Context distroseries"), |
| 129 | + required=True, readonly=True) |
| 130 | + |
| 131 | + sourcepackagerelease = Reference( |
| 132 | + schema=ISourcePackageRelease, title=_("Context sourcepackagerelease"), |
| 133 | + required=True, readonly=True) |
| 134 | + |
| 135 | + metadata = Attribute(_("A dict of data about the job.")) |
| 136 | + |
| 137 | + bug_ids = Attribute(_("A list of bug IDs.")) |
| 138 | + |
| 139 | + def getOperationDescription(): |
| 140 | + """Return a description of the bug-closing operation.""" |
| 141 | + |
| 142 | + |
| 143 | +class IProcessAcceptedBugsJobSource(IJobSource): |
| 144 | + """A source for jobs to close bugs for accepted package uploads.""" |
| 145 | + |
| 146 | + def create(distroseries, sourcepackagerelease, bug_ids): |
| 147 | + """Create a new `IProcessAcceptedBugsJob`. |
| 148 | + |
| 149 | + :param distroseries: A `IDistroSeries` for which to close bugs. |
| 150 | + :param sourcepackagerelease: An `ISourcePackageRelease` for which to |
| 151 | + close bugs. |
| 152 | + :param bug_ids: An iterable of bug IDs to close. |
| 153 | + """ |
| 154 | |
| 155 | === added file 'lib/lp/soyuz/model/processacceptedbugsjob.py' |
| 156 | --- lib/lp/soyuz/model/processacceptedbugsjob.py 1970-01-01 00:00:00 +0000 |
| 157 | +++ lib/lp/soyuz/model/processacceptedbugsjob.py 2012-09-19 23:41:25 +0000 |
| 158 | @@ -0,0 +1,174 @@ |
| 159 | +# Copyright 2012 Canonical Ltd. This software is licensed under the |
| 160 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
| 161 | + |
| 162 | +__metaclass__ = type |
| 163 | + |
| 164 | +__all__ = [ |
| 165 | + "close_bug_ids_for_sourcepackagerelease", |
| 166 | + "ProcessAcceptedBugsJob", |
| 167 | + ] |
| 168 | + |
| 169 | +import logging |
| 170 | + |
| 171 | +from storm.locals import ( |
| 172 | + And, |
| 173 | + Int, |
| 174 | + JSON, |
| 175 | + Reference, |
| 176 | + ) |
| 177 | +from zope.component import getUtility |
| 178 | +from zope.interface import ( |
| 179 | + classProvides, |
| 180 | + implements, |
| 181 | + ) |
| 182 | +from zope.security.proxy import removeSecurityProxy |
| 183 | + |
| 184 | +from lp.app.interfaces.launchpad import ILaunchpadCelebrities |
| 185 | +from lp.bugs.interfaces.bug import IBugSet |
| 186 | +from lp.bugs.interfaces.bugtask import BugTaskStatus |
| 187 | +from lp.registry.model.distroseries import DistroSeries |
| 188 | +from lp.services.config import config |
| 189 | +from lp.services.database.lpstorm import IMasterStore |
| 190 | +from lp.services.database.stormbase import StormBase |
| 191 | +from lp.services.job.model.job import Job |
| 192 | +from lp.services.job.runner import BaseRunnableJob |
| 193 | +from lp.services.webapp.interfaces import ( |
| 194 | + DEFAULT_FLAVOR, |
| 195 | + IStoreSelector, |
| 196 | + MAIN_STORE, |
| 197 | + ) |
| 198 | +from lp.soyuz.interfaces.processacceptedbugsjob import ( |
| 199 | + IProcessAcceptedBugsJob, |
| 200 | + IProcessAcceptedBugsJobSource, |
| 201 | + ) |
| 202 | +from lp.soyuz.model.sourcepackagerelease import SourcePackageRelease |
| 203 | + |
| 204 | + |
| 205 | +def close_bug_ids_for_sourcepackagerelease(distroseries, spr, bug_ids): |
| 206 | + bugs = list(getUtility(IBugSet).getByNumbers(bug_ids)) |
| 207 | + janitor = getUtility(ILaunchpadCelebrities).janitor |
| 208 | + target = distroseries.getSourcePackage(spr.sourcepackagename) |
| 209 | + assert spr.changelog_entry is not None, ( |
| 210 | + "New source uploads should have a changelog.") |
| 211 | + content = ( |
| 212 | + "This bug was fixed in the package %s" |
| 213 | + "\n\n---------------\n%s" % (spr.title, spr.changelog_entry)) |
| 214 | + |
| 215 | + for bug in bugs: |
| 216 | + # We need to remove the security proxy here because the bug might be |
| 217 | + # private and if this code is called via someone using the +queue |
| 218 | + # page they will get an OOPS. BE CAREFUL with the unproxied bug |
| 219 | + # object and look at what you're doing with it that might violate |
| 220 | + # security. |
| 221 | + # XXX cjwatson 2012-09-19: This can be removed once the |
| 222 | + # soyuz.processacceptedbugsjob.enabled feature flag is switched on |
| 223 | + # everywhere and the code to support disabling it is removed. |
| 224 | + bug = removeSecurityProxy(bug) |
| 225 | + edited_task = bug.setStatus( |
| 226 | + target=target, status=BugTaskStatus.FIXRELEASED, user=janitor) |
| 227 | + if edited_task is not None: |
| 228 | + bug.newMessage( |
| 229 | + owner=janitor, |
| 230 | + subject=bug.followup_subject(), |
| 231 | + content=content) |
| 232 | + |
| 233 | + |
| 234 | +class ProcessAcceptedBugsJob(StormBase, BaseRunnableJob): |
| 235 | + """Base class for jobs to close bugs for accepted package uploads.""" |
| 236 | + |
| 237 | + __storm_table__ = "ProcessAcceptedBugsJob" |
| 238 | + |
| 239 | + config = config.IProcessAcceptedBugsJobSource |
| 240 | + |
| 241 | + implements(IProcessAcceptedBugsJob) |
| 242 | + |
| 243 | + # Oddly, BaseRunnableJob inherits from BaseRunnableJobSource so this class |
| 244 | + # is both the factory for jobs (the "implements", above) and the source |
| 245 | + # for runnable jobs (not the constructor of the job source, the class |
| 246 | + # provides the IJobSource interface itself). |
| 247 | + classProvides(IProcessAcceptedBugsJobSource) |
| 248 | + |
| 249 | + # The Job table contains core job details. |
| 250 | + job_id = Int("job", primary=True) |
| 251 | + job = Reference(job_id, Job.id) |
| 252 | + |
| 253 | + distroseries_id = Int(name="distroseries") |
| 254 | + distroseries = Reference(distroseries_id, DistroSeries.id) |
| 255 | + |
| 256 | + sourcepackagerelease_id = Int(name="sourcepackagerelease") |
| 257 | + sourcepackagerelease = Reference( |
| 258 | + sourcepackagerelease_id, SourcePackageRelease.id) |
| 259 | + |
| 260 | + metadata = JSON('json_data') |
| 261 | + |
| 262 | + def __init__(self, distroseries, sourcepackagerelease, bug_ids): |
| 263 | + self.job = Job() |
| 264 | + self.distroseries = distroseries |
| 265 | + self.sourcepackagerelease = sourcepackagerelease |
| 266 | + self.metadata = {"bug_ids": list(bug_ids)} |
| 267 | + super(ProcessAcceptedBugsJob, self).__init__() |
| 268 | + |
| 269 | + @property |
| 270 | + def bug_ids(self): |
| 271 | + return self.metadata["bug_ids"] |
| 272 | + |
| 273 | + @classmethod |
| 274 | + def create(cls, distroseries, sourcepackagerelease, bug_ids): |
| 275 | + """See `IProcessAcceptedBugsJobSource`.""" |
| 276 | + assert distroseries is not None, "No distroseries specified." |
| 277 | + assert sourcepackagerelease is not None, ( |
| 278 | + "No sourcepackagerelease specified.") |
| 279 | + assert sourcepackagerelease.changelog_entry is not None, ( |
| 280 | + "New source uploads should have a changelog.") |
| 281 | + assert bug_ids, "No bug IDs specified." |
| 282 | + job = ProcessAcceptedBugsJob( |
| 283 | + distroseries, sourcepackagerelease, bug_ids) |
| 284 | + IMasterStore(ProcessAcceptedBugsJob).add(job) |
| 285 | + job.celeryRunOnCommit() |
| 286 | + return job |
| 287 | + |
| 288 | + def getOperationDescription(self): |
| 289 | + """See `IRunnableJob`.""" |
| 290 | + return "closing bugs for accepted package upload" |
| 291 | + |
| 292 | + def run(self): |
| 293 | + """See `IRunnableJob`.""" |
| 294 | + logger = logging.getLogger() |
| 295 | + spr = self.sourcepackagerelease |
| 296 | + logger.info( |
| 297 | + "Closing bugs for %s/%s (%s)" % |
| 298 | + (spr.name, spr.version, self.distroseries)) |
| 299 | + close_bug_ids_for_sourcepackagerelease( |
| 300 | + self.distroseries, spr, self.metadata["bug_ids"]) |
| 301 | + |
| 302 | + def __repr__(self): |
| 303 | + """Returns an informative representation of the job.""" |
| 304 | + parts = ["%s to close bugs [" % self.__class__.__name__] |
| 305 | + parts.append(", ".join(str(bug_id) for bug_id in self.bug_ids)) |
| 306 | + spr = self.sourcepackagerelease |
| 307 | + parts.append( |
| 308 | + "] for %s/%s (%s)" % (spr.name, spr.version, self.distroseries)) |
| 309 | + return "<%s>" % "".join(parts) |
| 310 | + |
| 311 | + @staticmethod |
| 312 | + def iterReady(): |
| 313 | + """See `IJobSource`.""" |
| 314 | + store = getUtility(IStoreSelector).get(MAIN_STORE, DEFAULT_FLAVOR) |
| 315 | + return store.find((ProcessAcceptedBugsJob), |
| 316 | + And(ProcessAcceptedBugsJob.job == Job.id, |
| 317 | + Job.id.is_in(Job.ready_jobs))) |
| 318 | + |
| 319 | + def makeDerived(self): |
| 320 | + """Support UniversalJobSource. |
| 321 | + |
| 322 | + (Most Job ORM classes are generic, because their database table is |
| 323 | + used for several related job types. Therefore, they have derived |
| 324 | + classes to implement the specific Job. |
| 325 | + |
| 326 | + ProcessAcceptedBugsJob implements the specific job, so its |
| 327 | + makeDerived returns itself.) |
| 328 | + """ |
| 329 | + return self |
| 330 | + |
| 331 | + def getDBClass(self): |
| 332 | + return self.__class__ |
| 333 | |
| 334 | === modified file 'lib/lp/soyuz/scripts/processaccepted.py' |
| 335 | --- lib/lp/soyuz/scripts/processaccepted.py 2012-07-05 09:43:58 +0000 |
| 336 | +++ lib/lp/soyuz/scripts/processaccepted.py 2012-09-19 23:41:25 +0000 |
| 337 | @@ -1,4 +1,4 @@ |
| 338 | -# Copyright 2009-2011 Canonical Ltd. This software is licensed under the |
| 339 | +# Copyright 2009-2012 Canonical Ltd. This software is licensed under the |
| 340 | # GNU Affero General Public License version 3 (see the file LICENSE). |
| 341 | |
| 342 | """Helper functions for the process-accepted.py script.""" |
| 343 | @@ -8,7 +8,7 @@ |
| 344 | 'close_bugs_for_queue_item', |
| 345 | 'close_bugs_for_sourcepackagerelease', |
| 346 | 'close_bugs_for_sourcepublication', |
| 347 | - 'get_bugs_from_changes_file', |
| 348 | + 'get_bug_ids_from_changes_file', |
| 349 | 'ProcessAccepted', |
| 350 | ] |
| 351 | |
| 352 | @@ -17,20 +17,18 @@ |
| 353 | |
| 354 | from debian.deb822 import Deb822Dict |
| 355 | from zope.component import getUtility |
| 356 | -from zope.security.proxy import removeSecurityProxy |
| 357 | +from zope.security.management import getSecurityPolicy |
| 358 | |
| 359 | -from lp.app.errors import NotFoundError |
| 360 | -from lp.app.interfaces.launchpad import ILaunchpadCelebrities |
| 361 | from lp.archivepublisher.publishing import GLOBAL_PUBLISHER_LOCK |
| 362 | from lp.archiveuploader.tagfiles import parse_tagfile_content |
| 363 | -from lp.bugs.interfaces.bug import IBugSet |
| 364 | -from lp.bugs.interfaces.bugtask import BugTaskStatus |
| 365 | from lp.registry.interfaces.distribution import IDistributionSet |
| 366 | from lp.registry.interfaces.pocket import PackagePublishingPocket |
| 367 | +from lp.services.features import getFeatureFlag |
| 368 | from lp.services.scripts.base import ( |
| 369 | LaunchpadCronScript, |
| 370 | LaunchpadScriptFailure, |
| 371 | ) |
| 372 | +from lp.services.webapp.authorization import LaunchpadPermissiveSecurityPolicy |
| 373 | from lp.services.webapp.errorlog import ( |
| 374 | ErrorReportingUtility, |
| 375 | ScriptRequest, |
| 376 | @@ -43,34 +41,29 @@ |
| 377 | re_lp_closes, |
| 378 | ) |
| 379 | from lp.soyuz.interfaces.archive import IArchiveSet |
| 380 | +from lp.soyuz.interfaces.processacceptedbugsjob import ( |
| 381 | + IProcessAcceptedBugsJobSource, |
| 382 | + ) |
| 383 | from lp.soyuz.interfaces.queue import IPackageUploadSet |
| 384 | - |
| 385 | - |
| 386 | -def get_bugs_from_changes_file(changes_file): |
| 387 | - """Parse the changes file and return a list of bugs referenced by it. |
| 388 | +from lp.soyuz.model.processacceptedbugsjob import ( |
| 389 | + close_bug_ids_for_sourcepackagerelease, |
| 390 | + ) |
| 391 | + |
| 392 | + |
| 393 | +def get_bug_ids_from_changes_file(changes_file): |
| 394 | + """Parse the changes file and return a list of bug IDs referenced by it. |
| 395 | |
| 396 | The bugs is specified in the Launchpad-bugs-fixed header, and are |
| 397 | separated by a space character. Nonexistent bug ids are ignored. |
| 398 | """ |
| 399 | tags = Deb822Dict(parse_tagfile_content(changes_file.read())) |
| 400 | - bugs_fixed_line = tags.get('Launchpad-bugs-fixed', '') |
| 401 | - bugs = [] |
| 402 | - for bug_id in bugs_fixed_line.split(): |
| 403 | - if not bug_id.isdigit(): |
| 404 | - continue |
| 405 | - bug_id = int(bug_id) |
| 406 | - try: |
| 407 | - bug = getUtility(IBugSet).get(bug_id) |
| 408 | - except NotFoundError: |
| 409 | - continue |
| 410 | - else: |
| 411 | - bugs.append(bug) |
| 412 | - return bugs |
| 413 | - |
| 414 | - |
| 415 | -def get_bugs_from_changelog_entry(sourcepackagerelease, since_version): |
| 416 | + bugs_fixed = tags.get('Launchpad-bugs-fixed', '').split() |
| 417 | + return [int(bug_id) for bug_id in bugs_fixed if bug_id.isdigit()] |
| 418 | + |
| 419 | + |
| 420 | +def get_bug_ids_from_changelog_entry(sourcepackagerelease, since_version): |
| 421 | """Parse the changelog_entry in the sourcepackagerelease and return a |
| 422 | - list of `IBug`s referenced by it. |
| 423 | + list of bug IDs referenced by it. |
| 424 | """ |
| 425 | changelog = sourcepackagerelease.aggregate_changelog(since_version) |
| 426 | closes = [] |
| 427 | @@ -84,17 +77,7 @@ |
| 428 | for match in regex: |
| 429 | bug_match = re_bug_numbers.findall(match.group(0)) |
| 430 | closes += map(int, bug_match) |
| 431 | - |
| 432 | - bugs = [] |
| 433 | - for bug_id in closes: |
| 434 | - try: |
| 435 | - bug = getUtility(IBugSet).get(bug_id) |
| 436 | - except NotFoundError: |
| 437 | - continue |
| 438 | - else: |
| 439 | - bugs.append(bug) |
| 440 | - |
| 441 | - return bugs |
| 442 | + return closes |
| 443 | |
| 444 | |
| 445 | def can_close_bugs(target): |
| 446 | @@ -148,7 +131,8 @@ |
| 447 | |
| 448 | for source_queue_item in queue_item.sources: |
| 449 | close_bugs_for_sourcepackagerelease( |
| 450 | - source_queue_item.sourcepackagerelease, changesfile_object) |
| 451 | + queue_item.distroseries, source_queue_item.sourcepackagerelease, |
| 452 | + changesfile_object) |
| 453 | |
| 454 | |
| 455 | def close_bugs_for_sourcepublication(source_publication, since_version=None): |
| 456 | @@ -164,17 +148,18 @@ |
| 457 | changesfile_object = sourcepackagerelease.upload_changesfile |
| 458 | |
| 459 | close_bugs_for_sourcepackagerelease( |
| 460 | - sourcepackagerelease, changesfile_object, since_version, |
| 461 | - upload_distroseries=source_publication.distroseries) |
| 462 | - |
| 463 | - |
| 464 | -def close_bugs_for_sourcepackagerelease(source_release, changesfile_object, |
| 465 | - since_version=None, |
| 466 | - upload_distroseries=None): |
| 467 | + source_publication.distroseries, sourcepackagerelease, |
| 468 | + changesfile_object, since_version) |
| 469 | + |
| 470 | + |
| 471 | +def close_bugs_for_sourcepackagerelease(distroseries, source_release, |
| 472 | + changesfile_object, |
| 473 | + since_version=None): |
| 474 | """Close bugs for a given source. |
| 475 | |
| 476 | - Given a `ISourcePackageRelease` and a corresponding changesfile object, |
| 477 | - close bugs mentioned in the changesfile in the context of the source. |
| 478 | + Given an `IDistroSeries`, an `ISourcePackageRelease`, and a |
| 479 | + corresponding changesfile object, close bugs mentioned in the |
| 480 | + changesfile in the context of the source. |
| 481 | |
| 482 | If changesfile_object is None and since_version is supplied, |
| 483 | close all the bugs in changelog entries made after that version and up |
| 484 | @@ -184,45 +169,27 @@ |
| 485 | requirement to do so right now. |
| 486 | """ |
| 487 | if since_version and source_release.changelog: |
| 488 | - bugs_to_close = get_bugs_from_changelog_entry( |
| 489 | + bug_ids_to_close = get_bug_ids_from_changelog_entry( |
| 490 | source_release, since_version=since_version) |
| 491 | elif changesfile_object: |
| 492 | - bugs_to_close = get_bugs_from_changes_file(changesfile_object) |
| 493 | + bug_ids_to_close = get_bug_ids_from_changes_file(changesfile_object) |
| 494 | else: |
| 495 | return |
| 496 | |
| 497 | # No bugs to be closed by this upload, move on. |
| 498 | - if not bugs_to_close: |
| 499 | + if not bug_ids_to_close: |
| 500 | return |
| 501 | |
| 502 | - janitor = getUtility(ILaunchpadCelebrities).janitor |
| 503 | - for bug in bugs_to_close: |
| 504 | - # We need to remove the security proxy here because the bug |
| 505 | - # might be private and if this code is called via someone using |
| 506 | - # the +queue page they will get an OOPS. Ideally, we should |
| 507 | - # migrate this code to the Job system though, but that's a lot |
| 508 | - # of work. If you don't do that and you're changing stuff in |
| 509 | - # here, BE CAREFUL with the unproxied bug object and look at |
| 510 | - # what you're doing with it that might violate security. |
| 511 | - bug = removeSecurityProxy(bug) |
| 512 | - if upload_distroseries is not None: |
| 513 | - target = upload_distroseries.getSourcePackage( |
| 514 | - source_release.sourcepackagename) |
| 515 | - else: |
| 516 | - target = source_release.sourcepackage |
| 517 | - edited_task = bug.setStatus( |
| 518 | - target=target, status=BugTaskStatus.FIXRELEASED, user=janitor) |
| 519 | - if edited_task is not None: |
| 520 | - assert source_release.changelog_entry is not None, ( |
| 521 | - "New source uploads should have a changelog.") |
| 522 | - content = ( |
| 523 | - "This bug was fixed in the package %s" |
| 524 | - "\n\n---------------\n%s" % ( |
| 525 | - source_release.title, source_release.changelog_entry)) |
| 526 | - bug.newMessage( |
| 527 | - owner=janitor, |
| 528 | - subject=bug.followup_subject(), |
| 529 | - content=content) |
| 530 | + if (getSecurityPolicy() == LaunchpadPermissiveSecurityPolicy or |
| 531 | + not getFeatureFlag("soyuz.processacceptedbugsjob.enabled")): |
| 532 | + # We're already running in a script (or the feature flag to allow |
| 533 | + # use of the job is disabled), so we can just close the bugs |
| 534 | + # directly. |
| 535 | + close_bug_ids_for_sourcepackagerelease( |
| 536 | + distroseries, source_release, bug_ids_to_close) |
| 537 | + else: |
| 538 | + job_source = getUtility(IProcessAcceptedBugsJobSource) |
| 539 | + job_source.create(distroseries, source_release, bug_ids_to_close) |
| 540 | |
| 541 | |
| 542 | class TargetPolicy: |
| 543 | |
| 544 | === modified file 'lib/lp/soyuz/scripts/tests/test_processaccepted.py' |
| 545 | --- lib/lp/soyuz/scripts/tests/test_processaccepted.py 2012-01-01 02:58:52 +0000 |
| 546 | +++ lib/lp/soyuz/scripts/tests/test_processaccepted.py 2012-09-19 23:41:25 +0000 |
| 547 | @@ -1,4 +1,4 @@ |
| 548 | -# Copyright 2009-2011 Canonical Ltd. This software is licensed under the |
| 549 | +# Copyright 2009-2012 Canonical Ltd. This software is licensed under the |
| 550 | # GNU Affero General Public License version 3 (see the file LICENSE). |
| 551 | |
| 552 | __metaclass__ = type |
| 553 | @@ -94,8 +94,8 @@ |
| 554 | bugs = self.makeChangelogWithBugs(spr) |
| 555 | |
| 556 | # Call the method and test it's closed the bugs. |
| 557 | - close_bugs_for_sourcepackagerelease(spr, changesfile_object=None, |
| 558 | - since_version="1.0-1") |
| 559 | + close_bugs_for_sourcepackagerelease( |
| 560 | + spr.upload_distroseries, spr, None, since_version="1.0-1") |
| 561 | for bug, bugtask in bugs: |
| 562 | if bug.id != bugs[5][0].id: |
| 563 | self.assertEqual(BugTaskStatus.FIXRELEASED, bugtask.status) |
| 564 | |
| 565 | === modified file 'lib/lp/soyuz/scripts/tests/test_queue.py' |
| 566 | --- lib/lp/soyuz/scripts/tests/test_queue.py 2012-08-08 11:48:29 +0000 |
| 567 | +++ lib/lp/soyuz/scripts/tests/test_queue.py 2012-09-19 23:41:25 +0000 |
| 568 | @@ -1,4 +1,4 @@ |
| 569 | -# Copyright 2009-2011 Canonical Ltd. This software is licensed under the |
| 570 | +# Copyright 2009-2012 Canonical Ltd. This software is licensed under the |
| 571 | # GNU Affero General Public License version 3 (see the file LICENSE). |
| 572 | |
| 573 | """queue tool base class tests.""" |
| 574 | @@ -35,6 +35,7 @@ |
| 575 | from lp.registry.interfaces.series import SeriesStatus |
| 576 | from lp.services.config import config |
| 577 | from lp.services.database.lpstorm import IStore |
| 578 | +from lp.services.features.testing import FeatureFixture |
| 579 | from lp.services.librarian.interfaces import ILibraryFileAliasSet |
| 580 | from lp.services.librarian.model import LibraryFileAlias |
| 581 | from lp.services.librarian.utils import filechunks |
| 582 | @@ -47,6 +48,9 @@ |
| 583 | PackageUploadStatus, |
| 584 | ) |
| 585 | from lp.soyuz.interfaces.archive import IArchiveSet |
| 586 | +from lp.soyuz.interfaces.processacceptedbugsjob import ( |
| 587 | + IProcessAcceptedBugsJobSource, |
| 588 | + ) |
| 589 | from lp.soyuz.interfaces.queue import IPackageUploadSet |
| 590 | from lp.soyuz.model.queue import PackageUploadBuild |
| 591 | from lp.soyuz.scripts.processaccepted import ( |
| 592 | @@ -1076,6 +1080,11 @@ |
| 593 | |
| 594 | layer = DatabaseFunctionalLayer |
| 595 | |
| 596 | + def assertBugChanges(self, series, spr, bug): |
| 597 | + with celebrity_logged_in("admin"): |
| 598 | + self.assertEqual( |
| 599 | + BugTaskStatus.FIXRELEASED, bug.default_bugtask.status) |
| 600 | + |
| 601 | def test_close_bugs_for_sourcepackagerelease_with_private_bug(self): |
| 602 | # lp.soyuz.scripts.processaccepted.close_bugs_for_sourcepackagerelease |
| 603 | # should work with private bugs where the person using the queue |
| 604 | @@ -1085,8 +1094,8 @@ |
| 605 | # we're testing. |
| 606 | spr = self.factory.makeSourcePackageRelease(changelog_entry="blah") |
| 607 | archive_admin = self.factory.makePerson() |
| 608 | - dsp = spr.upload_distroseries.distribution.getSourcePackage( |
| 609 | - spr.sourcepackagename) |
| 610 | + series = spr.upload_distroseries |
| 611 | + dsp = series.distribution.getSourcePackage(spr.sourcepackagename) |
| 612 | bug = self.factory.makeBug( |
| 613 | target=dsp, information_type=InformationType.USERDATA) |
| 614 | changes = StringIO(changes_file_template % bug.id) |
| 615 | @@ -1095,12 +1104,31 @@ |
| 616 | # The archive admin user can't normally see this bug. |
| 617 | self.assertRaises(ForbiddenAttribute, bug, 'status') |
| 618 | # But the bug closure should work. |
| 619 | - close_bugs_for_sourcepackagerelease(spr, changes) |
| 620 | + close_bugs_for_sourcepackagerelease(series, spr, changes) |
| 621 | |
| 622 | # Verify it was closed. |
| 623 | + self.assertBugChanges(series, spr, bug) |
| 624 | + |
| 625 | + |
| 626 | +class TestQueuePageClosingBugsJob(TestQueuePageClosingBugs): |
| 627 | + # Repeat TestQueuePageClosingBugs, but with the feature flag set to |
| 628 | + # cause close_bugs_for_sourcepackagerelease to create a job rather than |
| 629 | + # closing bugs immediately. |
| 630 | + |
| 631 | + def setUp(self): |
| 632 | + super(TestQueuePageClosingBugsJob, self).setUp() |
| 633 | + self.useFixture(FeatureFixture( |
| 634 | + {"soyuz.processacceptedbugsjob.enabled": "on"}, |
| 635 | + )) |
| 636 | + |
| 637 | + def assertBugChanges(self, series, spr, bug): |
| 638 | with celebrity_logged_in("admin"): |
| 639 | - self.assertEqual( |
| 640 | - bug.default_bugtask.status, BugTaskStatus.FIXRELEASED) |
| 641 | + self.assertEqual(BugTaskStatus.NEW, bug.default_bugtask.status) |
| 642 | + job_source = getUtility(IProcessAcceptedBugsJobSource) |
| 643 | + [job] = list(job_source.iterReady()) |
| 644 | + self.assertEqual(series, job.distroseries) |
| 645 | + self.assertEqual(spr, job.sourcepackagerelease) |
| 646 | + self.assertEqual([bug.id], job.bug_ids) |
| 647 | |
| 648 | |
| 649 | class TestQueueToolInJail(TestQueueBase, TestCase): |
| 650 | |
| 651 | === modified file 'lib/lp/soyuz/tests/test_processaccepted.py' |
| 652 | --- lib/lp/soyuz/tests/test_processaccepted.py 2012-01-20 15:42:44 +0000 |
| 653 | +++ lib/lp/soyuz/tests/test_processaccepted.py 2012-09-19 23:41:25 +0000 |
| 654 | @@ -1,4 +1,4 @@ |
| 655 | -# Copyright 2010-2011 Canonical Ltd. This software is licensed under the |
| 656 | +# Copyright 2010-2012 Canonical Ltd. This software is licensed under the |
| 657 | # GNU Affero General Public License version 3 (see the file LICENSE). |
| 658 | |
| 659 | """Test process-accepted.py""" |
| 660 | @@ -22,7 +22,7 @@ |
| 661 | ) |
| 662 | from lp.soyuz.model.queue import PackageUpload |
| 663 | from lp.soyuz.scripts.processaccepted import ( |
| 664 | - get_bugs_from_changes_file, |
| 665 | + get_bug_ids_from_changes_file, |
| 666 | ProcessAccepted, |
| 667 | ) |
| 668 | from lp.soyuz.tests.test_publishing import SoyuzTestPublisher |
| 669 | @@ -221,54 +221,55 @@ |
| 670 | dsp.derived_series.distribution, script.findTargetDistros()) |
| 671 | |
| 672 | |
| 673 | -class TestBugsFromChangesFile(TestCaseWithFactory): |
| 674 | - """Test get_bugs_from_changes_file.""" |
| 675 | +class TestBugIDsFromChangesFile(TestCaseWithFactory): |
| 676 | + """Test get_bug_ids_from_changes_file.""" |
| 677 | |
| 678 | layer = LaunchpadZopelessLayer |
| 679 | dbuser = config.uploadqueue.dbuser |
| 680 | |
| 681 | def setUp(self): |
| 682 | - super(TestBugsFromChangesFile, self).setUp() |
| 683 | + super(TestBugIDsFromChangesFile, self).setUp() |
| 684 | self.changes = Changes({ |
| 685 | 'Format': '1.8', |
| 686 | 'Source': 'swat', |
| 687 | }) |
| 688 | |
| 689 | - def getBugs(self): |
| 690 | - """Serialize self.changes and use get_bugs_from_changes_file to |
| 691 | - extract bugs from it. |
| 692 | + def getBugIDs(self): |
| 693 | + """Serialize self.changes and use get_bug_ids_from_changes_file to |
| 694 | + extract bug IDs from it. |
| 695 | """ |
| 696 | stream = StringIO() |
| 697 | self.changes.dump(stream) |
| 698 | stream.seek(0) |
| 699 | - return get_bugs_from_changes_file(stream) |
| 700 | + return get_bug_ids_from_changes_file(stream) |
| 701 | |
| 702 | def test_no_bugs(self): |
| 703 | # An empty list is returned if there are no bugs |
| 704 | # mentioned. |
| 705 | - self.assertEquals([], self.getBugs()) |
| 706 | + self.assertEqual([], self.getBugIDs()) |
| 707 | |
| 708 | def test_invalid_bug_id(self): |
| 709 | # Invalid bug ids (i.e. containing non-digit characters) are ignored. |
| 710 | self.changes["Launchpad-Bugs-Fixed"] = "bla" |
| 711 | - self.assertEquals([], self.getBugs()) |
| 712 | + self.assertEqual([], self.getBugIDs()) |
| 713 | |
| 714 | def test_unknown_bug_id(self): |
| 715 | - # Unknown bug ids are ignored. |
| 716 | + # Unknown bug ids are passed through; they will be ignored later, by |
| 717 | + # close_bug_ids_for_sourcepackagerelease. |
| 718 | self.changes["Launchpad-Bugs-Fixed"] = "45120" |
| 719 | - self.assertEquals([], self.getBugs()) |
| 720 | + self.assertEqual([45120], self.getBugIDs()) |
| 721 | |
| 722 | def test_valid_bug(self): |
| 723 | # For valid bug ids the bug object is returned. |
| 724 | bug = self.factory.makeBug() |
| 725 | self.changes["Launchpad-Bugs-Fixed"] = "%d" % bug.id |
| 726 | - self.assertEquals([bug], self.getBugs()) |
| 727 | + self.assertEqual([bug.id], self.getBugIDs()) |
| 728 | |
| 729 | def test_case_sensitivity(self): |
| 730 | # The spelling of Launchpad-Bugs-Fixed is case-insensitive. |
| 731 | bug = self.factory.makeBug() |
| 732 | self.changes["LaUnchpad-Bugs-fixed"] = "%d" % bug.id |
| 733 | - self.assertEquals([bug], self.getBugs()) |
| 734 | + self.assertEqual([bug.id], self.getBugIDs()) |
| 735 | |
| 736 | def test_multiple_bugs(self): |
| 737 | # Multiple bug ids can be specified, separated by spaces. |
| 738 | @@ -276,4 +277,4 @@ |
| 739 | bug2 = self.factory.makeBug() |
| 740 | self.changes["Launchpad-Bugs-Fixed"] = "%d invalid %d" % ( |
| 741 | bug1.id, bug2.id) |
| 742 | - self.assertEquals([bug1, bug2], self.getBugs()) |
| 743 | + self.assertEqual([bug1.id, bug2.id], self.getBugIDs()) |
| 744 | |
| 745 | === added file 'lib/lp/soyuz/tests/test_processacceptedbugsjob.py' |
| 746 | --- lib/lp/soyuz/tests/test_processacceptedbugsjob.py 1970-01-01 00:00:00 +0000 |
| 747 | +++ lib/lp/soyuz/tests/test_processacceptedbugsjob.py 2012-09-19 23:41:25 +0000 |
| 748 | @@ -0,0 +1,227 @@ |
| 749 | +# Copyright 2012 Canonical Ltd. This software is licensed under the |
| 750 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
| 751 | + |
| 752 | +"""Tests for jobs to close bugs for accepted package uploads.""" |
| 753 | + |
| 754 | +from itertools import product |
| 755 | + |
| 756 | +from testtools.content import text_content |
| 757 | +import transaction |
| 758 | +from zope.component import getUtility |
| 759 | +from zope.security.proxy import removeSecurityProxy |
| 760 | + |
| 761 | +from lp.bugs.interfaces.bugtask import BugTaskStatus |
| 762 | +from lp.registry.enums import InformationType |
| 763 | +from lp.registry.interfaces.series import SeriesStatus |
| 764 | +from lp.services.config import config |
| 765 | +from lp.services.features.testing import FeatureFixture |
| 766 | +from lp.services.job.interfaces.job import JobStatus |
| 767 | +from lp.services.job.runner import JobRunner |
| 768 | +from lp.services.job.tests import block_on_job |
| 769 | +from lp.services.webapp.testing import verifyObject |
| 770 | +from lp.soyuz.interfaces.processacceptedbugsjob import ( |
| 771 | + IProcessAcceptedBugsJob, |
| 772 | + IProcessAcceptedBugsJobSource, |
| 773 | + ) |
| 774 | +from lp.soyuz.model.processacceptedbugsjob import ( |
| 775 | + close_bug_ids_for_sourcepackagerelease, |
| 776 | + ) |
| 777 | +from lp.soyuz.tests.test_publishing import SoyuzTestPublisher |
| 778 | +from lp.testing import ( |
| 779 | + run_script, |
| 780 | + TestCaseWithFactory, |
| 781 | + ) |
| 782 | +from lp.testing.fakemethod import FakeMethod |
| 783 | +from lp.testing.layers import ( |
| 784 | + CeleryJobLayer, |
| 785 | + LaunchpadZopelessLayer, |
| 786 | + ) |
| 787 | + |
| 788 | + |
| 789 | +class TestCloseBugIDsForSourcePackageRelease(TestCaseWithFactory): |
| 790 | + |
| 791 | + layer = LaunchpadZopelessLayer |
| 792 | + dbuser = config.IProcessAcceptedBugsJobSource.dbuser |
| 793 | + |
| 794 | + def setUp(self): |
| 795 | + super(TestCloseBugIDsForSourcePackageRelease, self).setUp() |
| 796 | + # Create a distribution with two series, two source package names, |
| 797 | + # and an SPR and a bug task for all combinations of those. |
| 798 | + self.distro = self.factory.makeDistribution() |
| 799 | + self.series = [ |
| 800 | + self.factory.makeDistroSeries( |
| 801 | + distribution=self.distro, status=status) |
| 802 | + for status in (SeriesStatus.CURRENT, SeriesStatus.DEVELOPMENT)] |
| 803 | + self.spns = [self.factory.makeSourcePackageName() for _ in range(2)] |
| 804 | + self.bug = self.factory.makeBug() |
| 805 | + self.sprs = [ |
| 806 | + self.factory.makeSourcePackageRelease( |
| 807 | + sourcepackagename=spn, distroseries=series, |
| 808 | + changelog_entry="changelog") |
| 809 | + for spn, series in product(self.spns, self.series)] |
| 810 | + self.bugtasks = [ |
| 811 | + self.factory.makeBugTask( |
| 812 | + target=spr.upload_distroseries.getSourcePackage( |
| 813 | + spr.sourcepackagename), |
| 814 | + bug=self.bug) |
| 815 | + for spr in self.sprs] |
| 816 | + |
| 817 | + def test_correct_tasks_with_distroseries(self): |
| 818 | + # Close the task for the correct source package name and the given |
| 819 | + # series. |
| 820 | + close_bug_ids_for_sourcepackagerelease( |
| 821 | + self.series[0], self.sprs[0], [self.bug.id]) |
| 822 | + self.assertEqual(BugTaskStatus.FIXRELEASED, self.bugtasks[0].status) |
| 823 | + for i in (1, 2, 3): |
| 824 | + self.assertEqual(BugTaskStatus.NEW, self.bugtasks[i].status) |
| 825 | + |
| 826 | + def test_correct_message(self): |
| 827 | + # When closing a bug, a reasonable message is added. |
| 828 | + close_bug_ids_for_sourcepackagerelease( |
| 829 | + self.series[0], self.sprs[0], [self.bug.id]) |
| 830 | + self.assertEqual(2, self.bug.messages.count()) |
| 831 | + self.assertEqual( |
| 832 | + "This bug was fixed in the package %s" |
| 833 | + "\n\n---------------\nchangelog" % self.sprs[0].title, |
| 834 | + self.bug.messages[1].text_contents) |
| 835 | + |
| 836 | + def test_ignore_unknown_bug_ids(self): |
| 837 | + # Unknown bug IDs are ignored, and no message is added. |
| 838 | + close_bug_ids_for_sourcepackagerelease( |
| 839 | + self.series[0], self.sprs[0], [self.bug.id + 1]) |
| 840 | + for bugtask in self.bugtasks: |
| 841 | + self.assertEqual(BugTaskStatus.NEW, bugtask.status) |
| 842 | + self.assertEqual(1, self.bug.messages.count()) |
| 843 | + |
| 844 | + def test_private_bug(self): |
| 845 | + # Closing private bugs is not a problem. |
| 846 | + self.bug.transitionToInformationType( |
| 847 | + InformationType.USERDATA, self.distro.owner) |
| 848 | + close_bug_ids_for_sourcepackagerelease( |
| 849 | + self.series[0], self.sprs[0], [self.bug.id]) |
| 850 | + self.assertEqual(BugTaskStatus.FIXRELEASED, self.bugtasks[0].status) |
| 851 | + |
| 852 | + |
| 853 | +class TestProcessAcceptedBugsJob(TestCaseWithFactory): |
| 854 | + |
| 855 | + layer = LaunchpadZopelessLayer |
| 856 | + dbuser = config.IProcessAcceptedBugsJobSource.dbuser |
| 857 | + |
| 858 | + def setUp(self): |
| 859 | + super(TestProcessAcceptedBugsJob, self).setUp() |
| 860 | + self.publisher = SoyuzTestPublisher() |
| 861 | + self.publisher.prepareBreezyAutotest() |
| 862 | + self.distroseries = self.publisher.breezy_autotest |
| 863 | + |
| 864 | + def makeJob(self, distroseries=None, spr=None, bug_ids=[1]): |
| 865 | + """Create a `ProcessAcceptedBugsJob`.""" |
| 866 | + if distroseries is None: |
| 867 | + distroseries = self.distroseries |
| 868 | + if spr is None: |
| 869 | + spr = self.factory.makeSourcePackageRelease( |
| 870 | + distroseries=distroseries, changelog_entry="changelog") |
| 871 | + return getUtility(IProcessAcceptedBugsJobSource).create( |
| 872 | + distroseries, spr, bug_ids) |
| 873 | + |
| 874 | + def test_job_implements_IProcessAcceptedBugsJob(self): |
| 875 | + job = self.makeJob() |
| 876 | + self.assertTrue(verifyObject(IProcessAcceptedBugsJob, job)) |
| 877 | + |
| 878 | + def test_job_source_implements_IProcessAcceptedBugsJobSource(self): |
| 879 | + job_source = getUtility(IProcessAcceptedBugsJobSource) |
| 880 | + self.assertTrue( |
| 881 | + verifyObject(IProcessAcceptedBugsJobSource, job_source)) |
| 882 | + |
| 883 | + def test_create(self): |
| 884 | + # A ProcessAcceptedBugsJob can be created and stores its arguments. |
| 885 | + spr = self.factory.makeSourcePackageRelease( |
| 886 | + distroseries=self.distroseries, changelog_entry="changelog") |
| 887 | + bug_ids = [1, 2] |
| 888 | + job = self.makeJob(spr=spr, bug_ids=bug_ids) |
| 889 | + self.assertProvides(job, IProcessAcceptedBugsJob) |
| 890 | + self.assertEqual(self.distroseries, job.distroseries) |
| 891 | + self.assertEqual(spr, job.sourcepackagerelease) |
| 892 | + self.assertEqual(bug_ids, job.bug_ids) |
| 893 | + |
| 894 | + def test_run_raises_errors(self): |
| 895 | + # A job reports unexpected errors as exceptions. |
| 896 | + class Boom(Exception): |
| 897 | + pass |
| 898 | + |
| 899 | + distroseries = self.factory.makeDistroSeries() |
| 900 | + removeSecurityProxy(distroseries).getSourcePackage = FakeMethod( |
| 901 | + failure=Boom()) |
| 902 | + job = self.makeJob(distroseries=distroseries) |
| 903 | + self.assertRaises(Boom, job.run) |
| 904 | + |
| 905 | + def test___repr__(self): |
| 906 | + spr = self.factory.makeSourcePackageRelease( |
| 907 | + distroseries=self.distroseries, changelog_entry="changelog") |
| 908 | + bug_ids = [1, 2] |
| 909 | + job = self.makeJob(spr=spr, bug_ids=bug_ids) |
| 910 | + self.assertEqual( |
| 911 | + ("<ProcessAcceptedBugsJob to close bugs [1, 2] for " |
| 912 | + "{spr.name}/{spr.version} ({distroseries.distribution.name} " |
| 913 | + "{distroseries.name})>").format( |
| 914 | + distroseries=self.distroseries, spr=spr), |
| 915 | + repr(job)) |
| 916 | + |
| 917 | + def test_run(self): |
| 918 | + # A proper test run closes bugs. |
| 919 | + spr = self.factory.makeSourcePackageRelease( |
| 920 | + distroseries=self.distroseries, changelog_entry="changelog") |
| 921 | + bug = self.factory.makeBug() |
| 922 | + bugtask = self.factory.makeBugTask(target=spr.sourcepackage, bug=bug) |
| 923 | + self.assertEqual(BugTaskStatus.NEW, bugtask.status) |
| 924 | + job = self.makeJob(spr=spr, bug_ids=[bug.id]) |
| 925 | + JobRunner([job]).runAll() |
| 926 | + self.assertEqual(BugTaskStatus.FIXRELEASED, bugtask.status) |
| 927 | + |
| 928 | + def test_smoke(self): |
| 929 | + spr = self.factory.makeSourcePackageRelease( |
| 930 | + distroseries=self.distroseries, changelog_entry="changelog") |
| 931 | + bug = self.factory.makeBug() |
| 932 | + bugtask = self.factory.makeBugTask(target=spr.sourcepackage, bug=bug) |
| 933 | + self.assertEqual(BugTaskStatus.NEW, bugtask.status) |
| 934 | + self.makeJob(spr=spr, bug_ids=[bug.id]) |
| 935 | + transaction.commit() |
| 936 | + |
| 937 | + out, err, exit_code = run_script( |
| 938 | + "LP_DEBUG_SQL=1 cronscripts/process-job-source.py -vv %s" % ( |
| 939 | + IProcessAcceptedBugsJobSource.getName())) |
| 940 | + |
| 941 | + self.addDetail("stdout", text_content(out)) |
| 942 | + self.addDetail("stderr", text_content(err)) |
| 943 | + |
| 944 | + self.assertEqual(0, exit_code) |
| 945 | + self.assertEqual(BugTaskStatus.FIXRELEASED, bugtask.status) |
| 946 | + |
| 947 | + |
| 948 | +class TestViaCelery(TestCaseWithFactory): |
| 949 | + """ProcessAcceptedBugsJob runs under Celery.""" |
| 950 | + |
| 951 | + layer = CeleryJobLayer |
| 952 | + |
| 953 | + def test_run(self): |
| 954 | + # A proper test run closes bugs. |
| 955 | + self.useFixture(FeatureFixture({ |
| 956 | + "jobs.celery.enabled_classes": "ProcessAcceptedBugsJob", |
| 957 | + })) |
| 958 | + |
| 959 | + distroseries = self.factory.makeDistroSeries() |
| 960 | + spr = self.factory.makeSourcePackageRelease( |
| 961 | + distroseries=distroseries, changelog_entry="changelog") |
| 962 | + bug = self.factory.makeBug() |
| 963 | + bugtask = self.factory.makeBugTask(target=spr.sourcepackage, bug=bug) |
| 964 | + self.assertEqual(BugTaskStatus.NEW, bugtask.status) |
| 965 | + job = getUtility(IProcessAcceptedBugsJobSource).create( |
| 966 | + distroseries, spr, [bug.id]) |
| 967 | + self.assertEqual(distroseries, job.distroseries) |
| 968 | + self.assertEqual(spr, job.sourcepackagerelease) |
| 969 | + self.assertEqual([bug.id], job.bug_ids) |
| 970 | + |
| 971 | + with block_on_job(self): |
| 972 | + transaction.commit() |
| 973 | + |
| 974 | + self.assertEqual(JobStatus.COMPLETED, job.status) |
| 975 | + self.assertEqual(BugTaskStatus.FIXRELEASED, bugtask.status) |

374 + bug_ids = [] line.split( ): append( int(bug_ id))
375 for bug_id in bugs_fixed_
376 if not bug_id.isdigit():
377 continue
389 + bug_ids.
390 + return bug_ids
Excuse the terrible indentation, but what about return [int(bug_id) for bug_id in bugs_fixed_ line.split( ) if bug_id.isdigits()] instead of that block?
I'd also prefer to see the tests running under the process_accepted dbuser so we can pick up any missing permissions -- on say, public.job.