Merge lp:~gmb/launchpad/jobbifiy-bug-heat-calculations-509193 into lp:launchpad/db-devel
- jobbifiy-bug-heat-calculations-509193
- Merge into db-devel
Status: | Merged | ||||
---|---|---|---|---|---|
Merged at revision: | not available | ||||
Proposed branch: | lp:~gmb/launchpad/jobbifiy-bug-heat-calculations-509193 | ||||
Merge into: | lp:launchpad/db-devel | ||||
Diff against target: |
570 lines (+483/-1) 11 files modified
cronscripts/calculate-bug-heat.py (+33/-0) database/schema/security.cfg (+8/-0) lib/canonical/config/schema-lazr.conf (+9/-0) lib/lp/bugs/configure.zcml (+11/-0) lib/lp/bugs/interfaces/bugjob.py (+66/-0) lib/lp/bugs/model/bugheat.py (+54/-0) lib/lp/bugs/model/bugjob.py (+148/-0) lib/lp/bugs/scripts/bugheat.py (+3/-1) lib/lp/bugs/tests/test_bugheat.py (+95/-0) lib/lp/bugs/tests/test_bugjob.py (+55/-0) lib/lp/services/job/interfaces/job.py (+1/-0) |
||||
To merge this branch: | bzr merge lp:~gmb/launchpad/jobbifiy-bug-heat-calculations-509193 | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Paul Hummer (community) | code | Approve | |
Review via email: mp+17831@code.launchpad.net |
Commit message
Add infrastructure to allow the Bug Tracker to use the Jobs system for task. Also add specific infrastructure for using the Jobs system to update bug heat.
Description of the change
Graham Binns (gmb) wrote : | # |
Paul Hummer (rockstar) wrote : | # |
Hi Graham-
This branch looks good. I have a few comments.
=== added file 'cronscripts/
--- cronscripts/
+++ cronscripts/
@@ -0,0 +1,33 @@
+#!/usr/
+#
+# Copyright 2009 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+# pylint: disable-msg=W0403
+
It's 2010 now. :)
=== added file 'lib/lp/
--- lib/lp/
+++ lib/lp/
@@ -0,0 +1,158 @@
+# Copyright 2009 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Job classes related to BugJobs are in here."""
+
+__metaclass__ = type
+__all__ = [
+ 'BugJob',
+ ]
+
+import simplejson
+
+from sqlobject import SQLObjectNotFound
+from storm.base import Storm
+from storm.expr import And
+from storm.locals import Int, Reference, Unicode
+
+from zope.component import getUtility
+from zope.interface import implements
+from zope.security.proxy import removeSecurityProxy
+
+from canonical.
+from canonical.
+ DEFAULT_FLAVOR, IStoreSelector, MAIN_STORE, MASTER_FLAVOR)
+
+from lazr.delegates import delegates
+
+from lp.bugs.
+from lp.bugs.model.bug import Bug
+from lp.services.
+from lp.services.
+
+
+class BugJob(Storm):
+ """Base class for jobs related to Bugs."""
+
+ implements(IBugJob)
+
+ __storm_table__ = 'BugJob'
+
+ id = Int(primary=True)
+
+ job_id = Int(name='job')
+ job = Reference(job_id, Job.id)
+
+ bug_id = Int(name='bug')
+ bug = Reference(bug_id, Bug.id)
+
+ job_type = EnumCol(
+
+ _json_data = Unicode(
+
+ @property
+ def metadata(self):
+ return simplejson.
+
+ def __init__(self, bug, job_type, metadata):
+ """Constructor.
+
+ :param bug: The proposal this job relates to.
+ :param job_type: The BugJobType of this job.
+ :param metadata: The type-specific variables, as a JSON-compatible
+ dict.
+ """
+ Storm._
+ json_data = simplejson.
+ self.job = Job()
+ self.bug = bug
+ self.job_type = job_type
+ # XXX AaronBentley 2009-01-29 bug=322819: This should be a bytestring,
+ # but the DB representation is unicode.
+ self._json_data = json_data.
+
+ @classmethod
+ def get(cls, key):
+ """Return the instance of this class whose key is supplied."""
+ store = getUtility(
+ instance = store.get(cls, key)
+ if instance is None:
+ raise SQLObjectNotFound(
+ 'No occurrence of %s has key %s' % (cls.__name__, key))
+ return instance
+
+
+class BugJobDerived(
+ """Inte...
Graham Binns (gmb) wrote : | # |
On Thu, Jan 21, 2010 at 07:17:01PM -0000, Paul Hummer wrote:
> Review: Needs Information code
> Hi Graham-
>
> This branch looks good. I have a few comments.
>
> === added file 'cronscripts/
> --- cronscripts/
> +++ cronscripts/
> @@ -0,0 +1,33 @@
> +#!/usr/
> +#
> +# Copyright 2009 Canonical Ltd. This software is licensed under the
> +# GNU Affero General Public License version 3 (see the file LICENSE).
> +
> +# pylint: disable-msg=W0403
> +
>
> It's 2010 now. :)
>
Erk. Fixed.
> === added file 'lib/lp/
> --- lib/lp/
> +++ lib/lp/
> @@ -0,0 +1,158 @@
> +# Copyright 2009 Canonical Ltd. This software is licensed under the
> +# GNU Affero General Public License version 3 (see the file LICENSE).
> +
> +"""Job classes related to BugJobs are in here."""
> +
> +__metaclass__ = type
> +__all__ = [
> + 'BugJob',
> + ]
> +
> +import simplejson
> +
> +from sqlobject import SQLObjectNotFound
> +from storm.base import Storm
> +from storm.expr import And
> +from storm.locals import Int, Reference, Unicode
> +
> +from zope.component import getUtility
> +from zope.interface import implements
> +from zope.security.proxy import removeSecurityProxy
> +
> +from canonical.
> +from canonical.
> + DEFAULT_FLAVOR, IStoreSelector, MAIN_STORE, MASTER_FLAVOR)
> +
> +from lazr.delegates import delegates
> +
> +from lp.bugs.
> +from lp.bugs.model.bug import Bug
> +from lp.services.
> +from lp.services.
> +
> +
> +class BugJob(Storm):
> + """Base class for jobs related to Bugs."""
> +
> + implements(IBugJob)
> +
> + __storm_table__ = 'BugJob'
> +
> + id = Int(primary=True)
> +
> + job_id = Int(name='job')
> + job = Reference(job_id, Job.id)
> +
> + bug_id = Int(name='bug')
> + bug = Reference(bug_id, Bug.id)
> +
> + job_type = EnumCol(
> +
> + _json_data = Unicode(
> +
> + @property
> + def metadata(self):
> + return simplejson.
> +
> + def __init__(self, bug, job_type, metadata):
> + """Constructor.
> +
> + :param bug: The proposal this job relates to.
> + :param job_type: The BugJobType of this job.
> + :param metadata: The type-specific variables, as a JSON-compatible
> + dict.
> + """
> + Storm._
> + json_data = simplejson.
> + self.job = Job()
> + self.bug = bug
> + self.job_type = job_type
> + # XXX AaronBentley 2009-01-29 bug=322819: This should be a bytestring,
> + # but the DB representation is unicode.
> + self._json_data = json_data.
> +
> + @classmethod
> + def get(cls, key):
> + """Return the instance of this class whose key is supplied."""
> + store = getUtility(ISt...
Paul Hummer (rockstar) : | # |
Preview Diff
1 | === added file 'cronscripts/calculate-bug-heat.py' |
2 | --- cronscripts/calculate-bug-heat.py 1970-01-01 00:00:00 +0000 |
3 | +++ cronscripts/calculate-bug-heat.py 2010-01-21 20:48:17 +0000 |
4 | @@ -0,0 +1,33 @@ |
5 | +#!/usr/bin/python2.5 |
6 | +# |
7 | +# Copyright 2010 Canonical Ltd. This software is licensed under the |
8 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
9 | + |
10 | +# pylint: disable-msg=W0403 |
11 | + |
12 | +"""Calculate bug heat.""" |
13 | + |
14 | +__metaclass__ = type |
15 | + |
16 | +import _pythonpath |
17 | + |
18 | +from canonical.launchpad.webapp import errorlog |
19 | + |
20 | +from lp.services.job.runner import JobCronScript |
21 | +from lp.bugs.interfaces.bugjob import ICalculateBugHeatJobSource |
22 | + |
23 | + |
24 | +class RunCalculateBugHeat(JobCronScript): |
25 | + """Run BranchScanJob jobs.""" |
26 | + |
27 | + config_name = 'calculate_bug_heat' |
28 | + source_interface = ICalculateBugHeatJobSource |
29 | + |
30 | + def main(self): |
31 | + errorlog.globalErrorUtility.configure(self.config_name) |
32 | + return super(RunCalculateBugHeat, self).main() |
33 | + |
34 | + |
35 | +if __name__ == '__main__': |
36 | + script = RunCalculateBugHeat() |
37 | + script.lock_and_run() |
38 | |
39 | === modified file 'database/schema/security.cfg' |
40 | --- database/schema/security.cfg 2010-01-21 00:44:55 +0000 |
41 | +++ database/schema/security.cfg 2010-01-21 20:48:17 +0000 |
42 | @@ -144,6 +144,7 @@ |
43 | public.bugattachment = SELECT, INSERT, UPDATE, DELETE |
44 | public.bugbranch = SELECT, INSERT, UPDATE, DELETE |
45 | public.bugcve = SELECT, INSERT, DELETE |
46 | +public.bugjob = SELECT, INSERT, UPDATE, DELETE |
47 | public.bugnomination = SELECT, UPDATE |
48 | public.bugnotification = SELECT, INSERT, UPDATE, DELETE |
49 | public.bugnotificationattachment = SELECT, INSERT |
50 | @@ -1867,3 +1868,10 @@ |
51 | [modified-branches] |
52 | type=user |
53 | public.branch = SELECT |
54 | + |
55 | +[calculate-bug-heat] |
56 | +type=user |
57 | +groups=script,read |
58 | +public.bug = SELECT, UPDATE |
59 | +public.job = SELECT, UPDATE, DELETE |
60 | +public.bugjob = SELECT, DELETE |
61 | |
62 | === modified file 'lib/canonical/config/schema-lazr.conf' |
63 | --- lib/canonical/config/schema-lazr.conf 2010-01-21 11:07:52 +0000 |
64 | +++ lib/canonical/config/schema-lazr.conf 2010-01-21 20:48:17 +0000 |
65 | @@ -29,6 +29,15 @@ |
66 | base_url: http://ftpmaster.internal/ |
67 | |
68 | |
69 | +[calculate_bug_heat] |
70 | +# The database user which will be used by this process. |
71 | +# datatype: string |
72 | +dbuser: calculate-bug-heat |
73 | +oops_prefix: none |
74 | +error_dir: none |
75 | +copy_to_zlog: false |
76 | + |
77 | + |
78 | [branchscanner] |
79 | # The database user which will be used by this process. |
80 | # datatype: string |
81 | |
82 | === modified file 'lib/lp/bugs/configure.zcml' |
83 | --- lib/lp/bugs/configure.zcml 2010-01-11 12:00:48 +0000 |
84 | +++ lib/lp/bugs/configure.zcml 2010-01-21 20:48:17 +0000 |
85 | @@ -895,4 +895,15 @@ |
86 | for="lp.bugs.interfaces.bugtarget.IBugTarget" |
87 | factory="lp.bugs.browser.bugtarget.BugsVHostBreadcrumb" |
88 | permission="zope.Public"/> |
89 | + |
90 | + <!-- CalculateBugHeatJobs --> |
91 | + <class class="lp.bugs.model.bugheat.CalculateBugHeatJob"> |
92 | + <allow interface="lp.bugs.interfaces.bugjob.ICalculateBugHeatJob"/> |
93 | + </class> |
94 | + <securedutility |
95 | + component="lp.bugs.model.bugheat.CalculateBugHeatJob" |
96 | + provides="lp.bugs.interfaces.bugjob.ICalculateBugHeatJobSource"> |
97 | + <allow |
98 | + interface="lp.bugs.interfaces.bugjob.ICalculateBugHeatJobSource"/> |
99 | + </securedutility> |
100 | </configure> |
101 | |
102 | === added file 'lib/lp/bugs/interfaces/bugjob.py' |
103 | --- lib/lp/bugs/interfaces/bugjob.py 1970-01-01 00:00:00 +0000 |
104 | +++ lib/lp/bugs/interfaces/bugjob.py 2010-01-21 20:48:17 +0000 |
105 | @@ -0,0 +1,66 @@ |
106 | +# Copyright 2010 Canonical Ltd. This software is licensed under the |
107 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
108 | + |
109 | +"""Interfaces for using the Jobs system for Bugs.""" |
110 | + |
111 | +__metaclass__ = type |
112 | +__all__ = [ |
113 | + 'BugJobType', |
114 | + 'IBugJob', |
115 | + 'ICalculateBugHeatJob', |
116 | + 'ICalculateBugHeatJobSource', |
117 | + ] |
118 | + |
119 | +from zope.interface import Attribute, Interface |
120 | +from zope.schema import Int, Object |
121 | + |
122 | +from canonical.launchpad import _ |
123 | + |
124 | +from lazr.enum import DBEnumeratedType, DBItem |
125 | +from lp.bugs.interfaces.bug import IBug |
126 | +from lp.services.job.interfaces.job import IJob, IJobSource, IRunnableJob |
127 | + |
128 | + |
129 | +class BugJobType(DBEnumeratedType): |
130 | + """Values that IBugJob.job_type can take.""" |
131 | + |
132 | + UPDATE_HEAT = DBItem(0, """ |
133 | + Update the heat for a bug. |
134 | + |
135 | + This job recalculates the heat for a Bug. |
136 | + """) |
137 | + |
138 | + |
139 | +class IBugJob(Interface): |
140 | + """A Job related to a Bug.""" |
141 | + |
142 | + id = Int( |
143 | + title=_('DB ID'), required=True, readonly=True, |
144 | + description=_("The tracking number for this job.")) |
145 | + |
146 | + bug = Object( |
147 | + title=_('The Bug this job is about'), |
148 | + schema=IBug, required=True) |
149 | + |
150 | + job = Object(title=_('The common Job attributes'), schema=IJob, |
151 | + required=True) |
152 | + |
153 | + metadata = Attribute('A dict of data about the job.') |
154 | + |
155 | + def destroySelf(): |
156 | + """Destroy this object.""" |
157 | + |
158 | + |
159 | +class IBugJobSource(IJobSource): |
160 | + """An interface for acquiring IBugJobs.""" |
161 | + |
162 | + def create(bug): |
163 | + """Create a new IBugJob for a bug.""" |
164 | + |
165 | + |
166 | +class ICalculateBugHeatJob(IRunnableJob): |
167 | + """A Job to calculate bug heat.""" |
168 | + |
169 | + |
170 | +class ICalculateBugHeatJobSource(IBugJobSource): |
171 | + """Interface for acquiring CalculateBugHeatJobs.""" |
172 | |
173 | === added file 'lib/lp/bugs/model/bugheat.py' |
174 | --- lib/lp/bugs/model/bugheat.py 1970-01-01 00:00:00 +0000 |
175 | +++ lib/lp/bugs/model/bugheat.py 2010-01-21 20:48:17 +0000 |
176 | @@ -0,0 +1,54 @@ |
177 | +# Copyright 2010 Canonical Ltd. This software is licensed under the |
178 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
179 | + |
180 | +"""Job classes related to BugJobs are in here.""" |
181 | + |
182 | +__metaclass__ = type |
183 | +__all__ = [ |
184 | + 'CalculateBugHeatJob', |
185 | + ] |
186 | + |
187 | +from zope.component import getUtility |
188 | +from zope.interface import classProvides, implements |
189 | + |
190 | +from canonical.launchpad.webapp.interfaces import ( |
191 | + DEFAULT_FLAVOR, IStoreSelector, MAIN_STORE) |
192 | + |
193 | +from lp.bugs.interfaces.bugjob import ( |
194 | + BugJobType, ICalculateBugHeatJob, ICalculateBugHeatJobSource) |
195 | +from lp.bugs.model.bugjob import BugJob, BugJobDerived |
196 | +from lp.bugs.scripts.bugheat import BugHeatCalculator |
197 | +from lp.services.job.model.job import Job |
198 | + |
199 | + |
200 | +class CalculateBugHeatJob(BugJobDerived): |
201 | + """A Job to calculate bug heat.""" |
202 | + implements(ICalculateBugHeatJob) |
203 | + |
204 | + class_job_type = BugJobType.UPDATE_HEAT |
205 | + classProvides(ICalculateBugHeatJobSource) |
206 | + |
207 | + def run(self): |
208 | + """See `IRunnableJob`.""" |
209 | + calculator = BugHeatCalculator(self.bug) |
210 | + calculated_heat = calculator.getBugHeat() |
211 | + self.bug.setHeat(calculated_heat) |
212 | + |
213 | + @classmethod |
214 | + def create(cls, bug): |
215 | + """See `ICalculateBugHeatJobSource`.""" |
216 | + # If there's already a job for the bug, don't create a new one. |
217 | + store = getUtility(IStoreSelector).get(MAIN_STORE, DEFAULT_FLAVOR) |
218 | + job_for_bug = store.find( |
219 | + BugJob, |
220 | + BugJob.bug == bug, |
221 | + BugJob.job_type == cls.class_job_type, |
222 | + BugJob.job == Job.id, |
223 | + Job.id.is_in(Job.ready_jobs) |
224 | + ).any() |
225 | + |
226 | + if job_for_bug is not None: |
227 | + return cls(job_for_bug) |
228 | + else: |
229 | + return super(CalculateBugHeatJob, cls).create(bug) |
230 | + |
231 | |
232 | === added file 'lib/lp/bugs/model/bugjob.py' |
233 | --- lib/lp/bugs/model/bugjob.py 1970-01-01 00:00:00 +0000 |
234 | +++ lib/lp/bugs/model/bugjob.py 2010-01-21 20:48:17 +0000 |
235 | @@ -0,0 +1,148 @@ |
236 | +# Copyright 2010 Canonical Ltd. This software is licensed under the |
237 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
238 | + |
239 | +"""Job classes related to BugJobs are in here.""" |
240 | + |
241 | +__metaclass__ = type |
242 | +__all__ = [ |
243 | + 'BugJob', |
244 | + 'BugJobDerived', |
245 | + ] |
246 | + |
247 | +import simplejson |
248 | + |
249 | +from sqlobject import SQLObjectNotFound |
250 | +from storm.base import Storm |
251 | +from storm.expr import And |
252 | +from storm.locals import Int, Reference, Unicode |
253 | + |
254 | +from zope.component import getUtility |
255 | +from zope.interface import classProvides, implements |
256 | +from zope.security.proxy import removeSecurityProxy |
257 | + |
258 | +from canonical.database.enumcol import EnumCol |
259 | +from canonical.launchpad.webapp.interfaces import ( |
260 | + DEFAULT_FLAVOR, IStoreSelector, MAIN_STORE, MASTER_FLAVOR) |
261 | + |
262 | +from lazr.delegates import delegates |
263 | + |
264 | +from lp.bugs.interfaces.bugjob import BugJobType, IBugJob, IBugJobSource |
265 | +from lp.bugs.model.bug import Bug |
266 | +from lp.services.job.model.job import Job |
267 | +from lp.services.job.runner import BaseRunnableJob |
268 | + |
269 | + |
270 | +class BugJob(Storm): |
271 | + """Base class for jobs related to Bugs.""" |
272 | + |
273 | + implements(IBugJob) |
274 | + |
275 | + __storm_table__ = 'BugJob' |
276 | + |
277 | + id = Int(primary=True) |
278 | + |
279 | + job_id = Int(name='job') |
280 | + job = Reference(job_id, Job.id) |
281 | + |
282 | + bug_id = Int(name='bug') |
283 | + bug = Reference(bug_id, Bug.id) |
284 | + |
285 | + job_type = EnumCol(enum=BugJobType, notNull=True) |
286 | + |
287 | + _json_data = Unicode('json_data') |
288 | + |
289 | + @property |
290 | + def metadata(self): |
291 | + return simplejson.loads(self._json_data) |
292 | + |
293 | + def __init__(self, bug, job_type, metadata): |
294 | + """Constructor. |
295 | + |
296 | + :param bug: The proposal this job relates to. |
297 | + :param job_type: The BugJobType of this job. |
298 | + :param metadata: The type-specific variables, as a JSON-compatible |
299 | + dict. |
300 | + """ |
301 | + Storm.__init__(self) |
302 | + json_data = simplejson.dumps(metadata) |
303 | + self.job = Job() |
304 | + self.bug = bug |
305 | + self.job_type = job_type |
306 | + # XXX AaronBentley 2009-01-29 bug=322819: This should be a bytestring, |
307 | + # but the DB representation is unicode. |
308 | + self._json_data = json_data.decode('utf-8') |
309 | + |
310 | + @classmethod |
311 | + def get(cls, key): |
312 | + """Return the instance of this class whose key is supplied.""" |
313 | + store = getUtility(IStoreSelector).get(MAIN_STORE, DEFAULT_FLAVOR) |
314 | + instance = store.get(cls, key) |
315 | + if instance is None: |
316 | + raise SQLObjectNotFound( |
317 | + 'No occurrence of %s has key %s' % (cls.__name__, key)) |
318 | + return instance |
319 | + |
320 | + |
321 | +class BugJobDerived(BaseRunnableJob): |
322 | + """Intermediate class for deriving from BugJob.""" |
323 | + delegates(IBugJob) |
324 | + classProvides(IBugJobSource) |
325 | + |
326 | + def __init__(self, job): |
327 | + self.context = job |
328 | + |
329 | + # We need to define __eq__ and __ne__ here to prevent the security |
330 | + # proxy from mucking up our comparisons in tests and elsewhere. |
331 | + |
332 | + def __eq__(self, job): |
333 | + return ( |
334 | + self.__class__ is removeSecurityProxy(job.__class__) |
335 | + and self.job == job.job) |
336 | + |
337 | + def __ne__(self, job): |
338 | + return not (self == job) |
339 | + |
340 | + @classmethod |
341 | + def create(cls, bug): |
342 | + """See `IBugJob`.""" |
343 | + # If there's already a job for the bug, don't create a new one. |
344 | + job = BugJob(bug, cls.class_job_type, {}) |
345 | + return cls(job) |
346 | + |
347 | + @classmethod |
348 | + def get(cls, job_id): |
349 | + """Get a job by id. |
350 | + |
351 | + :return: the BugJob with the specified id, as the current |
352 | + BugJobDerived subclass. |
353 | + :raises: SQLObjectNotFound if there is no job with the specified id, |
354 | + or its job_type does not match the desired subclass. |
355 | + """ |
356 | + job = BugJob.get(job_id) |
357 | + if job.job_type != cls.class_job_type: |
358 | + raise SQLObjectNotFound( |
359 | + 'No object found with id %d and type %s' % (job_id, |
360 | + cls.class_job_type.title)) |
361 | + return cls(job) |
362 | + |
363 | + @classmethod |
364 | + def iterReady(cls): |
365 | + """Iterate through all ready BugJobs.""" |
366 | + store = getUtility(IStoreSelector).get(MAIN_STORE, MASTER_FLAVOR) |
367 | + jobs = store.find( |
368 | + BugJob, |
369 | + And(BugJob.job_type == cls.class_job_type, |
370 | + BugJob.job == Job.id, |
371 | + Job.id.is_in(Job.ready_jobs), |
372 | + BugJob.bug == Bug.id)) |
373 | + return (cls(job) for job in jobs) |
374 | + |
375 | + def getOopsVars(self): |
376 | + """See `IRunnableJob`.""" |
377 | + vars = BaseRunnableJob.getOopsVars(self) |
378 | + vars.extend([ |
379 | + ('bug_id', self.context.bug.id), |
380 | + ('bug_job_id', self.context.id), |
381 | + ('bug_job_type', self.context.job_type.title), |
382 | + ]) |
383 | + return vars |
384 | |
385 | === modified file 'lib/lp/bugs/scripts/bugheat.py' |
386 | --- lib/lp/bugs/scripts/bugheat.py 2010-01-12 16:41:23 +0000 |
387 | +++ lib/lp/bugs/scripts/bugheat.py 2010-01-21 20:48:17 +0000 |
388 | @@ -4,7 +4,9 @@ |
389 | """The innards of the Bug Heat cronscript.""" |
390 | |
391 | __metaclass__ = type |
392 | -__all__ = [] |
393 | +__all__ = [ |
394 | + 'BugHeatCalculator', |
395 | + ] |
396 | |
397 | |
398 | from zope.component import getUtility |
399 | |
400 | === added file 'lib/lp/bugs/tests/test_bugheat.py' |
401 | --- lib/lp/bugs/tests/test_bugheat.py 1970-01-01 00:00:00 +0000 |
402 | +++ lib/lp/bugs/tests/test_bugheat.py 2010-01-21 20:48:17 +0000 |
403 | @@ -0,0 +1,95 @@ |
404 | +# Copyright 2010 Canonical Ltd. This software is licensed under the |
405 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
406 | + |
407 | +"""Tests for BugJobs.""" |
408 | + |
409 | +__metaclass__ = type |
410 | + |
411 | +import transaction |
412 | +import unittest |
413 | + |
414 | +from zope.component import getUtility |
415 | + |
416 | +from canonical.launchpad.scripts.tests import run_script |
417 | +from canonical.testing import LaunchpadZopelessLayer |
418 | + |
419 | +from lp.bugs.interfaces.bugjob import ICalculateBugHeatJobSource |
420 | +from lp.bugs.model.bugheat import CalculateBugHeatJob |
421 | +from lp.bugs.scripts.bugheat import BugHeatCalculator |
422 | +from lp.testing import TestCaseWithFactory |
423 | + |
424 | + |
425 | +class CalculateBugHeatJobTestCase(TestCaseWithFactory): |
426 | + """Test case for CalculateBugHeatJob.""" |
427 | + |
428 | + layer = LaunchpadZopelessLayer |
429 | + |
430 | + def setUp(self): |
431 | + super(CalculateBugHeatJobTestCase, self).setUp() |
432 | + self.bug = self.factory.makeBug() |
433 | + |
434 | + def test_run(self): |
435 | + # CalculateBugHeatJob.run() sets calculates and sets the heat |
436 | + # for a bug. |
437 | + job = CalculateBugHeatJob.create(self.bug) |
438 | + bug_heat_calculator = BugHeatCalculator(self.bug) |
439 | + |
440 | + job.run() |
441 | + self.assertEqual( |
442 | + bug_heat_calculator.getBugHeat(), self.bug.heat) |
443 | + |
444 | + def test_utility(self): |
445 | + # CalculateBugHeatJobSource is a utility for acquiring |
446 | + # CalculateBugHeatJobs. |
447 | + utility = getUtility(ICalculateBugHeatJobSource) |
448 | + self.assertTrue( |
449 | + ICalculateBugHeatJobSource.providedBy(utility)) |
450 | + |
451 | + def test_create_only_creates_one(self): |
452 | + # If there's already a CalculateBugHeatJob for a bug, |
453 | + # CalculateBugHeatJob.create() won't create a new one. |
454 | + job = CalculateBugHeatJob.create(self.bug) |
455 | + |
456 | + # There will now be one job in the queue. |
457 | + self.assertEqual( |
458 | + 1, len(list(CalculateBugHeatJob.iterReady()))) |
459 | + |
460 | + new_job = CalculateBugHeatJob.create(self.bug) |
461 | + |
462 | + # The two jobs will in fact be the same job. |
463 | + self.assertEqual(job, new_job) |
464 | + |
465 | + # And the queue will still have a length of 1. |
466 | + self.assertEqual( |
467 | + 1, len(list((CalculateBugHeatJob.iterReady())))) |
468 | + |
469 | + def test_cronscript_succeeds(self): |
470 | + # The calculate-bug-heat cronscript will run all pending |
471 | + # CalculateBugHeatJobs. |
472 | + CalculateBugHeatJob.create(self.bug) |
473 | + transaction.commit() |
474 | + |
475 | + retcode, stdout, stderr = run_script( |
476 | + 'cronscripts/calculate-bug-heat.py', [], |
477 | + expect_returncode=0) |
478 | + self.assertEqual('', stdout) |
479 | + self.assertIn( |
480 | + 'INFO Ran 1 ICalculateBugHeatJobSource jobs.\n', stderr) |
481 | + |
482 | + def test_getOopsVars(self): |
483 | + # BugJobDerived.getOopsVars() returns the variables to be used |
484 | + # when logging an OOPS for a bug job. We test this using |
485 | + # CalculateBugHeatJob because BugJobDerived doesn't let us |
486 | + # create() jobs. |
487 | + job = CalculateBugHeatJob.create(self.bug) |
488 | + vars = job.getOopsVars() |
489 | + |
490 | + # The Bug ID, BugJob ID and BugJob type will be returned by |
491 | + # getOopsVars(). |
492 | + self.assertIn(('bug_id', self.bug.id), vars) |
493 | + self.assertIn(('bug_job_id', job.context.id), vars) |
494 | + self.assertIn(('bug_job_type', job.context.job_type.title), vars) |
495 | + |
496 | + |
497 | +def test_suite(): |
498 | + return unittest.TestLoader().loadTestsFromName(__name__) |
499 | |
500 | === added file 'lib/lp/bugs/tests/test_bugjob.py' |
501 | --- lib/lp/bugs/tests/test_bugjob.py 1970-01-01 00:00:00 +0000 |
502 | +++ lib/lp/bugs/tests/test_bugjob.py 2010-01-21 20:48:17 +0000 |
503 | @@ -0,0 +1,55 @@ |
504 | +# Copyright 2010 Canonical Ltd. This software is licensed under the |
505 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
506 | + |
507 | +"""Tests for BugJobs.""" |
508 | + |
509 | +__metaclass__ = type |
510 | + |
511 | +import unittest |
512 | + |
513 | +from canonical.testing import LaunchpadZopelessLayer |
514 | + |
515 | +from lp.bugs.interfaces.bugjob import BugJobType |
516 | +from lp.bugs.model.bugjob import BugJob, BugJobDerived |
517 | +from lp.testing import TestCaseWithFactory |
518 | + |
519 | + |
520 | +class BugJobTestCase(TestCaseWithFactory): |
521 | + """Test case for basic BugJob gubbins.""" |
522 | + |
523 | + layer = LaunchpadZopelessLayer |
524 | + |
525 | + def test_instantiate(self): |
526 | + # BugJob.__init__() instantiates a BugJob instance. |
527 | + bug = self.factory.makeBug() |
528 | + |
529 | + metadata = ('some', 'arbitrary', 'metadata') |
530 | + bug_job = BugJob( |
531 | + bug, BugJobType.UPDATE_HEAT, metadata) |
532 | + |
533 | + self.assertEqual(bug, bug_job.bug) |
534 | + self.assertEqual(BugJobType.UPDATE_HEAT, bug_job.job_type) |
535 | + |
536 | + # When we actually access the BugJob's metadata it gets |
537 | + # unserialized from JSON, so the representation returned by |
538 | + # bug_heat.metadata will be different from what we originally |
539 | + # passed in. |
540 | + metadata_expected = [u'some', u'arbitrary', u'metadata'] |
541 | + self.assertEqual(metadata_expected, bug_job.metadata) |
542 | + |
543 | + |
544 | +class BugJobDerivedTestCase(TestCaseWithFactory): |
545 | + """Test case for the BugJobDerived class.""" |
546 | + |
547 | + layer = LaunchpadZopelessLayer |
548 | + |
549 | + def test_create_explodes(self): |
550 | + # BugJobDerived.create() will blow up because it needs to be |
551 | + # subclassed to work properly. |
552 | + bug = self.factory.makeBug() |
553 | + self.assertRaises( |
554 | + AttributeError, BugJobDerived.create, bug) |
555 | + |
556 | + |
557 | +def test_suite(): |
558 | + return unittest.TestLoader().loadTestsFromName(__name__) |
559 | |
560 | === modified file 'lib/lp/services/job/interfaces/job.py' |
561 | --- lib/lp/services/job/interfaces/job.py 2010-01-14 23:33:30 +0000 |
562 | +++ lib/lp/services/job/interfaces/job.py 2010-01-21 20:48:17 +0000 |
563 | @@ -9,6 +9,7 @@ |
564 | |
565 | __all__ = [ |
566 | 'IJob', |
567 | + 'IJobSource', |
568 | 'IRunnableJob', |
569 | 'JobStatus', |
570 | 'LeaseHeld', |
This branch adds the necessary interfaces and implementations to the bug
tracker to:
a. Allow us to use the Jobs system for Bug-related tasks generally and
b. Allow us to use the Jobs system to update bug heat specifically.
A lot of the work for the Jobs system has been cribbed from how it's
used in Codehosting, but we've pared it down to make sure that we don't
repeat stuff that's defined elsewhere. A full list of the changes we've
made is below.
We've also added a cronscript to run the CalculateBugHea tJobs.
== List of changes ==
=== cronscripts/ calculate- bug-heat. py ===
- We've added a cronscript to run pending CalculateBugHea tJobs.
=== database/ schema/ security. cfg ===
- We've added permissions for the CalculateBugHeatJob runner.
=== lib/canonical/ config/ schema- lazr.conf ===
- We've added config options for the CalculateBugHeatJob runner.
=== lib/lp/ bugs/configure. zcml ===
- We've added ZCML configuration for the CalculateBugHea tJobs.
=== lib/lp/ bugs/interfaces /bugjob. py ===
- We've added the following interfaces: atJob: A base interface for bug heat calculation atJobSource: An interface for acquiring BugHeatJobs.
- IBugJob: A base Job interface for Bugs.
- ICalculateBugHe
job.
- ICalculateBugHe
ICalculate
=== lib/lp/ bugs/model/ bugheat. py ===
- We've added implementation for CalculateBugHea tJob. This uses a lator to calculate the bug heat and then calls
BugHeatCalcu
Bug.setHeat() to set the bug's heat.
=== lib/lp/ bugs/model/ bugjob. py ===
- We've added the following classes (cribbed largely from Codehosting):
- BugJob: Provides a base class for all bug jobs.
- BugJobDerived: Provides an intermediate class for deriving from
BugJob, so that a lot of the job-generic work can be done here
rather than others having to reproduce more boilerplate.
=== lib/lp/ bugs/scripts/ bugheat. py ===
- We've added BugHeatCalculator to the __all__ for this module in order
to fix some lint.
=== lib/lp/ bugs/tests/ test_bugheat. py ===
- We've added unit tests for the CalculateBugHeatJob class.
=== lib/lp/ bugs/tests/ test_bugjob. py ===
- We've added unit tests for the generic BugJob classes.
=== lib/lp/ services/ job/interfaces/ job.py ===
- We've added IJobSource to the __all__ for this module in order to
fix some lint.