Merge lp:~gmb/launchpad/jobbifiy-bug-heat-calculations-509193 into lp:launchpad/db-devel

Proposed by Graham Binns
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
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.

To post a comment you must log in.
Revision history for this message
Graham Binns (gmb) wrote :

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 CalculateBugHeatJobs.

== List of changes ==

=== cronscripts/calculate-bug-heat.py ===

 - We've added a cronscript to run pending CalculateBugHeatJobs.

=== 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 CalculateBugHeatJobs.

=== lib/lp/bugs/interfaces/bugjob.py ===

 - We've added the following interfaces:
   - IBugJob: A base Job interface for Bugs.
   - ICalculateBugHeatJob: A base interface for bug heat calculation
     job.
   - ICalculateBugHeatJobSource: An interface for acquiring
     ICalculateBugHeatJobs.

=== lib/lp/bugs/model/bugheat.py ===

 - We've added implementation for CalculateBugHeatJob. This uses a
   BugHeatCalculator to calculate the bug heat and then calls
   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.

Revision history for this message
Paul Hummer (rockstar) wrote :
Download full text (4.5 KiB)

Hi Graham-

  This branch looks good. I have a few comments.

=== added file 'cronscripts/calculate-bug-heat.py'
--- cronscripts/calculate-bug-heat.py 1970-01-01 00:00:00 +0000
+++ cronscripts/calculate-bug-heat.py 2010-01-21 18:28:18 +0000
@@ -0,0 +1,33 @@
+#!/usr/bin/python2.5
+#
+# 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/bugs/model/bugjob.py'
--- lib/lp/bugs/model/bugjob.py 1970-01-01 00:00:00 +0000
+++ lib/lp/bugs/model/bugjob.py 2010-01-21 18:28:18 +0000
@@ -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.database.enumcol import EnumCol
+from canonical.launchpad.webapp.interfaces import (
+ DEFAULT_FLAVOR, IStoreSelector, MAIN_STORE, MASTER_FLAVOR)
+
+from lazr.delegates import delegates
+
+from lp.bugs.interfaces.bugjob import BugJobType, IBugJob
+from lp.bugs.model.bug import Bug
+from lp.services.job.model.job import Job
+from lp.services.job.runner import BaseRunnableJob
+
+
+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(enum=BugJobType, notNull=True)
+
+ _json_data = Unicode('json_data')
+
+ @property
+ def metadata(self):
+ return simplejson.loads(self._json_data)
+
+ 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.__init__(self)
+ json_data = simplejson.dumps(metadata)
+ 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.decode('utf-8')
+
+ @classmethod
+ def get(cls, key):
+ """Return the instance of this class whose key is supplied."""
+ store = getUtility(IStoreSelector).get(MAIN_STORE, DEFAULT_FLAVOR)
+ 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(BaseRunnableJob):
+ """Inte...

Read more...

review: Needs Information (code)
Revision history for this message
Graham Binns (gmb) wrote :
Download full text (5.5 KiB)

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/calculate-bug-heat.py'
> --- cronscripts/calculate-bug-heat.py 1970-01-01 00:00:00 +0000
> +++ cronscripts/calculate-bug-heat.py 2010-01-21 18:28:18 +0000
> @@ -0,0 +1,33 @@
> +#!/usr/bin/python2.5
> +#
> +# 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/bugs/model/bugjob.py'
> --- lib/lp/bugs/model/bugjob.py 1970-01-01 00:00:00 +0000
> +++ lib/lp/bugs/model/bugjob.py 2010-01-21 18:28:18 +0000
> @@ -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.database.enumcol import EnumCol
> +from canonical.launchpad.webapp.interfaces import (
> + DEFAULT_FLAVOR, IStoreSelector, MAIN_STORE, MASTER_FLAVOR)
> +
> +from lazr.delegates import delegates
> +
> +from lp.bugs.interfaces.bugjob import BugJobType, IBugJob
> +from lp.bugs.model.bug import Bug
> +from lp.services.job.model.job import Job
> +from lp.services.job.runner import BaseRunnableJob
> +
> +
> +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(enum=BugJobType, notNull=True)
> +
> + _json_data = Unicode('json_data')
> +
> + @property
> + def metadata(self):
> + return simplejson.loads(self._json_data)
> +
> + 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.__init__(self)
> + json_data = simplejson.dumps(metadata)
> + 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.decode('utf-8')
> +
> + @classmethod
> + def get(cls, key):
> + """Return the instance of this class whose key is supplied."""
> + store = getUtility(ISt...

Read more...

Revision history for this message
Paul Hummer (rockstar) :
review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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',

Subscribers

People subscribed via source and target branches

to status/vote changes: