Merge lp:~cjwatson/launchpad/process-accepted-bugs-job into lp:launchpad
- process-accepted-bugs-job
- Merge into devel
Status: | Merged |
---|---|
Approved by: | Colin Watson |
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 | Approve | |
Review via email: mp+122420@code.launchpad.net |
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.