Merge lp:~gmb/launchpad/hook-up-stored-proc-boom-bug-585803 into lp:launchpad/db-devel

Proposed by Graham Binns
Status: Merged
Approved by: Graham Binns
Approved revision: no longer in the source branch.
Merged at revision: 9414
Proposed branch: lp:~gmb/launchpad/hook-up-stored-proc-boom-bug-585803
Merge into: lp:launchpad/db-devel
Prerequisite: lp:~gmb/launchpad/stored-proc-for-bug-heat-bug-582195
Diff against target: 532 lines (+155/-198)
5 files modified
database/schema/trusted.sql (+2/-1)
lib/canonical/launchpad/scripts/garbo.py (+22/-31)
lib/lp/bugs/doc/bug-heat.txt (+114/-50)
lib/lp/bugs/model/bug.py (+17/-14)
lib/lp/bugs/tests/test_bugheat.py (+0/-102)
To merge this branch: bzr merge lp:~gmb/launchpad/hook-up-stored-proc-boom-bug-585803
Reviewer Review Type Date Requested Status
Curtis Hovey (community) rc Approve
Abel Deuring (community) code Approve
Review via email: mp+26191@code.launchpad.net

Commit message

Bug heat is now updated directly rather than relying on CalculateBugHeatJob creation.

Description of the change

This branch converts all current sites where CalculateBugHeatJobs are created to use IBug.updateHeat() or to call the calculate_bug_heat() stored procedure directly.

I've removed all CalculateBugHeatJob creation callsites (except in tests, which I'll remove in a subsequent branch along with all the CalculateBugHeatJob infrastructure) and I've updated the BugHeatUpdater garbo job to call the calculate_bug_heat() stored procedure directly rather than looping over out-of-date bugs.

I've also removed the tests that covered the callsites that I've removed.

To post a comment you must log in.
Revision history for this message
Abel Deuring (adeuring) wrote :

Hi Graham,

we discussed that it would be better to update the heat values in chunks, with initial chunk size of 1000. I like your implementation as in http://pastebin.ubuntu.com/440470/

thanks for finally fixing the heat updater!

review: Approve (code)
Revision history for this message
Curtis Hovey (sinzui) wrote :

This is good to land and you have my RC to ensure this landed even if ec2 runs slowly.

review: Approve (rc)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'database/schema/trusted.sql'
--- database/schema/trusted.sql 2010-05-29 16:21:26 +0000
+++ database/schema/trusted.sql 2010-05-29 16:21:27 +0000
@@ -1696,7 +1696,8 @@
1696 days_since_created = (datetime.utcnow() - date_created).days1696 days_since_created = (datetime.utcnow() - date_created).days
1697 max_heat = get_max_heat_for_bug(bug_id)1697 max_heat = get_max_heat_for_bug(bug_id)
1698 if max_heat is not None and days_since_created > 0:1698 if max_heat is not None and days_since_created > 0:
1699 return total_heat + (max_heat * 0.25 / days_since_created)1699 total_heat = (
1700 total_heat + (max_heat * 0.25 / days_since_created))
17001701
1701 return int(total_heat)1702 return int(total_heat)
1702$$;1703$$;
17031704
=== added directory 'lib/canonical/launchpad/apidoc'
=== modified file 'lib/canonical/launchpad/scripts/garbo.py'
--- lib/canonical/launchpad/scripts/garbo.py 2010-04-14 13:14:00 +0000
+++ lib/canonical/launchpad/scripts/garbo.py 2010-05-29 16:21:27 +0000
@@ -13,6 +13,7 @@
13import transaction13import transaction
14from psycopg2 import IntegrityError14from psycopg2 import IntegrityError
15from zope.component import getUtility15from zope.component import getUtility
16from zope.security.proxy import removeSecurityProxy
16from storm.locals import In, SQL, Max, Min17from storm.locals import In, SQL, Max, Min
1718
18from canonical.config import config19from canonical.config import config
@@ -30,6 +31,7 @@
30from canonical.launchpad.webapp.interfaces import (31from canonical.launchpad.webapp.interfaces import (
31 IStoreSelector, MAIN_STORE, MASTER_FLAVOR)32 IStoreSelector, MAIN_STORE, MASTER_FLAVOR)
32from lp.bugs.interfaces.bug import IBugSet33from lp.bugs.interfaces.bug import IBugSet
34from lp.bugs.model.bug import Bug
33from lp.bugs.model.bugattachment import BugAttachment35from lp.bugs.model.bugattachment import BugAttachment
34from lp.bugs.interfaces.bugjob import ICalculateBugHeatJobSource36from lp.bugs.interfaces.bugjob import ICalculateBugHeatJobSource
35from lp.bugs.model.bugnotification import BugNotification37from lp.bugs.model.bugnotification import BugNotification
@@ -536,50 +538,39 @@
536 max_heat_age = config.calculate_bug_heat.max_heat_age538 max_heat_age = config.calculate_bug_heat.max_heat_age
537 self.max_heat_age = max_heat_age539 self.max_heat_age = max_heat_age
538540
541 self.store = IMasterStore(Bug)
542
543 @property
544 def _outdated_bugs(self):
545 outdated_bugs = getUtility(IBugSet).getBugsWithOutdatedHeat(
546 self.max_heat_age)
547 # We remove the security proxy so that we can access the set()
548 # method of the result set.
549 return removeSecurityProxy(outdated_bugs)
550
539 def isDone(self):551 def isDone(self):
540 """See `ITunableLoop`."""552 """See `ITunableLoop`."""
541 # When the main loop has no more Bugs to process it sets553 # When the main loop has no more Bugs to process it sets
542 # offset to None. Until then, it always has a numerical554 # offset to None. Until then, it always has a numerical
543 # value.555 # value.
544 return self.is_done556 return self._outdated_bugs.is_empty()
545557
546 def __call__(self, chunk_size):558 def __call__(self, chunk_size):
547 """Retrieve a batch of Bugs and update their heat.559 """Retrieve a batch of Bugs and update their heat.
548560
549 See `ITunableLoop`.561 See `ITunableLoop`.
550 """562 """
551 # XXX 2010-01-08 gmb bug=198767:563 # We multiply chunk_size by 1000 for the sake of doing updates
552 # We cast chunk_size to an integer to ensure that we're not564 # quickly.
553 # trying to slice using floats or anything similarly565 chunk_size = int(chunk_size * 1000)
554 # foolish. We shouldn't have to do this.
555 chunk_size = int(chunk_size)
556 start = self.offset
557 end = self.offset + chunk_size
558566
559 transaction.begin()567 transaction.begin()
560 bugs = getUtility(IBugSet).getBugsWithOutdatedHeat(568 outdated_bugs = self._outdated_bugs[:chunk_size]
561 self.max_heat_age)[start:end]569 self.log.debug("Updating heat for %s bugs" % outdated_bugs.count())
562570 outdated_bugs.set(
563 bug_count = bugs.count()571 heat=SQL('calculate_bug_heat(Bug.id)'),
564 if bug_count > 0:572 heat_last_updated=datetime.now(pytz.utc))
565 starting_id = bugs.first().id573
566 self.log.debug(
567 "Adding CalculateBugHeatJobs for %i Bugs (starting id: %i)" %
568 (bug_count, starting_id))
569 else:
570 self.is_done = True
571
572 self.offset = None
573 for bug in bugs:
574 # We set the starting point of the next batch to the Bug
575 # id after the one we're looking at now. If there aren't any
576 # bugs this loop will run for 0 iterations and starting_id
577 # will remain set to None.
578 start += 1
579 self.offset = start
580 self.log.debug("Adding CalculateBugHeatJob for bug %s" % bug.id)
581 getUtility(ICalculateBugHeatJobSource).create(bug)
582 self.total_processed += 1
583 transaction.commit()574 transaction.commit()
584575
585576
586577
=== modified file 'lib/lp/bugs/doc/bug-heat.txt'
--- lib/lp/bugs/doc/bug-heat.txt 2010-05-29 16:21:26 +0000
+++ lib/lp/bugs/doc/bug-heat.txt 2010-05-29 16:21:27 +0000
@@ -62,15 +62,11 @@
62 662 6
6363
6464
65Adjusting bug heat in transaction65Events which trigger bug heat updates
66---------------------------------66-------------------------------------
6767
68Sometimes, when a bug changes, we want to see the changes reflected in68There are several events which will cause a bug's heat to be updated.
69the bug's heat value immediately, without waiting for heat to be69First, as stated above, heat will be calculated when the bug is created.
70recalculated. Currently we adjust heat immediately for bug privacy and
71security.
72
73 >>> from canonical.database.sqlbase import flush_database_updates
7470
75 >>> bug = factory.makeBug(owner=bug_owner)71 >>> bug = factory.makeBug(owner=bug_owner)
76 >>> bug.heat72 >>> bug.heat
@@ -80,18 +76,116 @@
80product against which it's filed, whose subscription is converted from76product against which it's filed, whose subscription is converted from
81an indirect to a direct subscription.77an indirect to a direct subscription.
8278
79Marking a bug as private also gives it an extra 150 heat points.
80
83 >>> changed = bug.setPrivate(True, bug_owner)81 >>> changed = bug.setPrivate(True, bug_owner)
84 >>> bug.heat82 >>> bug.heat
85 15883 158
8684
85Setting the bug as security related adds another 250 heat points.
86
87 >>> changed = bug.setSecurityRelated(True)87 >>> changed = bug.setSecurityRelated(True)
88 >>> bug.heat88 >>> bug.heat
89 40889 408
9090
91Marking the bug public removes 150 heat points.
92
91 >>> changed = bug.setPrivate(False, bug_owner)93 >>> changed = bug.setPrivate(False, bug_owner)
92 >>> bug.heat94 >>> bug.heat
93 25895 258
9496
97And marking it not security-related removes 250 points.
98
99 >>> changed = bug.setSecurityRelated(False)
100 >>> bug.heat
101 8
102
103Adding a subscriber to the bug increases its heat by 2 points.
104
105 >>> new_subscriber = factory.makePerson()
106 >>> subscription = bug.subscribe(new_subscriber, new_subscriber)
107 >>> bug.heat
108 10
109
110When a user unsubscribes, the bug loses 2 points of heat.
111
112 >>> bug.unsubscribe(new_subscriber, new_subscriber)
113 >>> bug.heat
114 8
115
116Should a user mark themselves as affected by the bug, it will gain 4
117points of heat.
118
119 >>> bug.markUserAffected(new_subscriber)
120 >>> bug.heat
121 12
122
123If a user who was previously affected marks themself as not affected,
124the bug loses 4 points of heat.
125
126 >>> bug.markUserAffected(new_subscriber, False)
127 >>> bug.heat
128 8
129
130If a user who wasn't affected by the bug marks themselve as explicitly
131unaffected, the bug's heat doesn't change.
132
133 >>> unaffected_person = factory.makePerson()
134 >>> bug.markUserAffected(unaffected_person, False)
135 >>> bug.heat
136 8
137
138Marking the bug as a duplicate will set its heat to zero, whilst also
139adding 10 points of heat to the bug it duplicates, 6 points for the
140duplication and 4 points for the subscribers that the duplicated bug
141inherits.
142
143 >>> duplicated_bug = factory.makeBug()
144 >>> duplicated_bug.heat
145 6
146
147 >>> bug.markAsDuplicate(duplicated_bug)
148 >>> bug.heat
149 0
150
151 >>> duplicated_bug.heat
152 16
153
154Unmarking the bug as a duplicate restores its heat and updates the
155duplicated bug's heat.
156
157 >>> bug.markAsDuplicate(None)
158 >>> bug.heat
159 8
160
161 >>> duplicated_bug.heat
162 6
163
164A number of other changes, handled by the Bug's addChange() method, will
165cause heat to be recalculated, even if the heat itself may not actually
166change.
167
168For example, updating the bug's description calls the addChange() event,
169and will cause the bug's heat to be recalculated.
170
171We'll set the bug's heat to 0 first to demonstrate this.
172
173 >>> bug.setHeat(0)
174 >>> bug.heat
175 0
176
177 >>> from datetime import datetime, timedelta
178 >>> from pytz import timezone
179
180 >>> from lp.bugs.adapters.bugchange import BugDescriptionChange
181 >>> change = BugDescriptionChange(
182 ... when=datetime.now().replace(tzinfo=timezone('UTC')),
183 ... person=bug.owner, what_changed='description',
184 ... old_value=bug.description, new_value='Some text')
185 >>> bug.addChange(change)
186 >>> bug.heat
187 8
188
95189
96Getting bugs whose heat is outdated190Getting bugs whose heat is outdated
97-----------------------------------191-----------------------------------
@@ -122,8 +216,6 @@
122getBugsWithOutdatedHeat() it will appear in the set returned by216getBugsWithOutdatedHeat() it will appear in the set returned by
123getBugsWithOutdatedHeat().217getBugsWithOutdatedHeat().
124218
125 >>> from datetime import datetime, timedelta
126 >>> from pytz import timezone
127 >>> old_heat_bug = factory.makeBug()219 >>> old_heat_bug = factory.makeBug()
128 >>> old_heat_bug.setHeat(220 >>> old_heat_bug.setHeat(
129 ... 0, datetime.now(timezone('UTC')) - timedelta(days=2))221 ... 0, datetime.now(timezone('UTC')) - timedelta(days=2))
@@ -161,52 +253,24 @@
161 >>> from canonical.launchpad.scripts.garbo import BugHeatUpdater253 >>> from canonical.launchpad.scripts.garbo import BugHeatUpdater
162 >>> from canonical.launchpad.scripts import FakeLogger254 >>> from canonical.launchpad.scripts import FakeLogger
163255
256We'll commit the transaction so that the BugHeatUpdater updates the
257right bugs.
258
259 >>> transaction.commit()
164 >>> update_bug_heat = BugHeatUpdater(FakeLogger(), max_heat_age=1)260 >>> update_bug_heat = BugHeatUpdater(FakeLogger(), max_heat_age=1)
165261
166BugHeatUpdater implements ITunableLoop and as such is callable. Calling262BugHeatUpdater implements ITunableLoop and as such is callable. Calling
167it as a method will add jobs to calculate the heat of for all the bugs263it as a method will recalculate the heat for all the out-of-date bugs.
168whose heat is more than seven days old.264
169265There are two bugs with heat more than a day old:
170Before update_bug_heat is called, we'll ensure that there are no waiting
171jobs in the bug heat calculation queue.
172
173 >>> from lp.bugs.interfaces.bugjob import ICalculateBugHeatJobSource
174 >>> for calc_job in getUtility(ICalculateBugHeatJobSource).iterReady():
175 ... calc_job.job.start()
176 ... calc_job.job.complete()
177
178 >>> ready_jobs = list(getUtility(ICalculateBugHeatJobSource).iterReady())
179 >>> len(ready_jobs)
180 0
181
182We need to commit here to ensure that the bugs we've created are
183available to the update_bug_heat script.
184
185 >>> transaction.commit()
186266
187 >>> getUtility(IBugSet).getBugsWithOutdatedHeat(1).count()267 >>> getUtility(IBugSet).getBugsWithOutdatedHeat(1).count()
188 2268 2
189269
190We need to run update_bug_heat() twice to ensure that both the bugs are270Calling our BugHeatUpdater will update the heat of those bugs.
191updated.271
192272 >>> update_bug_heat(chunk_size=1)
193 >>> update_bug_heat(chunk_size=2)273 DEBUG Updating heat for 2 bugs
194 DEBUG Adding CalculateBugHeatJobs for 2 Bugs (starting id: ...)
195 DEBUG Adding CalculateBugHeatJob for bug ...
196 DEBUG Adding CalculateBugHeatJob for bug ...
197
198There will now be two CalculateBugHeatJobs in the queue.
199
200 >>> ready_jobs = list(getUtility(ICalculateBugHeatJobSource).iterReady())
201 >>> len(ready_jobs)
202 2
203
204Running them will update the bugs' heat.
205
206 >>> for calc_job in getUtility(ICalculateBugHeatJobSource).iterReady():
207 ... calc_job.job.start()
208 ... calc_job.run()
209 ... calc_job.job.complete()
210274
211IBugSet.getBugsWithOutdatedHeat() will now return an empty set since all275IBugSet.getBugsWithOutdatedHeat() will now return an empty set since all
212the bugs have been updated.276the bugs have been updated.
213277
=== modified file 'lib/lp/bugs/model/bug.py'
--- lib/lp/bugs/model/bug.py 2010-05-29 16:21:26 +0000
+++ lib/lp/bugs/model/bug.py 2010-05-29 16:21:27 +0000
@@ -73,7 +73,6 @@
73from lp.bugs.interfaces.bugactivity import IBugActivitySet73from lp.bugs.interfaces.bugactivity import IBugActivitySet
74from lp.bugs.interfaces.bugattachment import (74from lp.bugs.interfaces.bugattachment import (
75 BugAttachmentType, IBugAttachmentSet)75 BugAttachmentType, IBugAttachmentSet)
76from lp.bugs.interfaces.bugjob import ICalculateBugHeatJobSource
77from lp.bugs.interfaces.bugmessage import IBugMessageSet76from lp.bugs.interfaces.bugmessage import IBugMessageSet
78from lp.bugs.interfaces.bugnomination import (77from lp.bugs.interfaces.bugnomination import (
79 NominationError, NominationSeriesObsoleteError)78 NominationError, NominationSeriesObsoleteError)
@@ -490,8 +489,6 @@
490 sub = BugSubscription(489 sub = BugSubscription(
491 bug=self, person=person, subscribed_by=subscribed_by)490 bug=self, person=person, subscribed_by=subscribed_by)
492491
493 getUtility(ICalculateBugHeatJobSource).create(self)
494
495 # Ensure that the subscription has been flushed.492 # Ensure that the subscription has been flushed.
496 Store.of(sub).flush()493 Store.of(sub).flush()
497494
@@ -501,6 +498,7 @@
501 if suppress_notify is False:498 if suppress_notify is False:
502 notify(ObjectCreatedEvent(sub, user=subscribed_by))499 notify(ObjectCreatedEvent(sub, user=subscribed_by))
503500
501 self.updateHeat()
504 return sub502 return sub
505503
506 def unsubscribe(self, person, unsubscribed_by):504 def unsubscribe(self, person, unsubscribed_by):
@@ -525,6 +523,7 @@
525 # flushed so that code running with implicit flushes523 # flushed so that code running with implicit flushes
526 # disabled see the change.524 # disabled see the change.
527 store.flush()525 store.flush()
526 self.updateHeat()
528 return527 return
529528
530 def unsubscribeFromDupes(self, person, unsubscribed_by):529 def unsubscribeFromDupes(self, person, unsubscribed_by):
@@ -803,7 +802,7 @@
803 notification_data['text'], change.person, recipients,802 notification_data['text'], change.person, recipients,
804 when)803 when)
805804
806 getUtility(ICalculateBugHeatJobSource).create(self)805 self.updateHeat()
807806
808 def expireNotifications(self):807 def expireNotifications(self):
809 """See `IBug`."""808 """See `IBug`."""
@@ -1459,7 +1458,7 @@
1459 if dupe._getAffectedUser(user) is not None:1458 if dupe._getAffectedUser(user) is not None:
1460 dupe.markUserAffected(user, affected)1459 dupe.markUserAffected(user, affected)
14611460
1462 getUtility(ICalculateBugHeatJobSource).create(self)1461 self.updateHeat()
14631462
1464 @property1463 @property
1465 def readonly_duplicateof(self):1464 def readonly_duplicateof(self):
@@ -1470,6 +1469,7 @@
1470 """See `IBug`."""1469 """See `IBug`."""
1471 field = DuplicateBug()1470 field = DuplicateBug()
1472 field.context = self1471 field.context = self
1472 current_duplicateof = self.duplicateof
1473 try:1473 try:
1474 if duplicate_of is not None:1474 if duplicate_of is not None:
1475 field._validate(duplicate_of)1475 field._validate(duplicate_of)
@@ -1478,15 +1478,17 @@
1478 raise InvalidDuplicateValue(validation_error)1478 raise InvalidDuplicateValue(validation_error)
14791479
1480 if duplicate_of is not None:1480 if duplicate_of is not None:
1481 # Create a job to update the heat of the master bug and set1481 # Update the heat of the master bug and set this bug's heat
1482 # this bug's heat to 0 (since it's a duplicate, it shouldn't1482 # to 0 (since it's a duplicate, it shouldn't have any heat
1483 # have any heat at all).1483 # at all).
1484 getUtility(ICalculateBugHeatJobSource).create(duplicate_of)1484 self.setHeat(0)
1485 duplicate_of.updateHeat()
1486 else:
1487 # Otherwise, recalculate this bug's heat, since it will be 0
1488 # from having been a duplicate. We also update the bug that
1489 # was previously duplicated.
1485 self.updateHeat()1490 self.updateHeat()
1486 else:1491 current_duplicateof.updateHeat()
1487 # Otherwise, create a job to recalculate this bug's heat,
1488 # since it will be 0 from having been a duplicate.
1489 getUtility(ICalculateBugHeatJobSource).create(self)
14901492
1491 def setCommentVisibility(self, user, comment_number, visible):1493 def setCommentVisibility(self, user, comment_number, visible):
1492 """See `IBug`."""1494 """See `IBug`."""
@@ -1863,7 +1865,8 @@
1863 Bug.heat_last_updated < last_updated_cutoff,1865 Bug.heat_last_updated < last_updated_cutoff,
1864 Bug.heat_last_updated == None)1866 Bug.heat_last_updated == None)
18651867
1866 return store.find(Bug, last_updated_clause).order_by('id')1868 return store.find(
1869 Bug, Bug.duplicateof==None, last_updated_clause).order_by('id')
18671870
18681871
1869class BugAffectsPerson(SQLBase):1872class BugAffectsPerson(SQLBase):
18701873
=== modified file 'lib/lp/bugs/tests/test_bugheat.py'
--- lib/lp/bugs/tests/test_bugheat.py 2010-04-01 03:09:43 +0000
+++ lib/lp/bugs/tests/test_bugheat.py 2010-05-29 16:21:27 +0000
@@ -112,108 +112,6 @@
112 self.assertIn(('bug_job_id', job.context.id), vars)112 self.assertIn(('bug_job_id', job.context.id), vars)
113 self.assertIn(('bug_job_type', job.context.job_type.title), vars)113 self.assertIn(('bug_job_type', job.context.job_type.title), vars)
114114
115 def test_bug_changes_adds_job(self):
116 # Calling addChange() on a Bug will add a CalculateBugHeatJob
117 # for that bug to the queue.
118 self.assertEqual(0, self._getJobCount())
119
120 change = BugDescriptionChange(
121 when=datetime.now().replace(tzinfo=pytz.timezone('UTC')),
122 person=self.bug.owner, what_changed='description',
123 old_value=self.bug.description, new_value='Some text')
124 self.bug.addChange(change)
125
126 # There will now be a job in the queue.
127 self.assertEqual(1, self._getJobCount())
128
129 def test_subscribing_adds_job(self):
130 # Calling Bug.subscribe() will add a CalculateBugHeatJob for the
131 # Bug.
132 self.assertEqual(0, self._getJobCount())
133
134 person = self.factory.makePerson()
135 self.bug.subscribe(person, person)
136 transaction.commit()
137
138 # There will now be a job in the queue.
139 self.assertEqual(1, self._getJobCount())
140
141 def test_unsubscribing_adds_job(self):
142 # Calling Bug.unsubscribe() will add a CalculateBugHeatJob for the
143 # Bug.
144 self.assertEqual(0, self._getJobCount())
145
146 self.bug.unsubscribe(self.bug.owner, self.bug.owner)
147 transaction.commit()
148
149 # There will now be a job in the queue.
150 self.assertEqual(1, self._getJobCount())
151
152 def test_marking_affected_adds_job(self):
153 # Marking a user as affected by a bug adds a CalculateBugHeatJob
154 # for the bug.
155 self.assertEqual(0, self._getJobCount())
156
157 person = self.factory.makePerson()
158 self.bug.markUserAffected(person)
159
160 # There will now be a job in the queue.
161 self.assertEqual(1, self._getJobCount())
162
163 def test_marking_unaffected_adds_job(self):
164 # Marking a user as unaffected by a bug adds a CalculateBugHeatJob
165 # for the bug.
166 self.assertEqual(0, self._getJobCount())
167
168 self.bug.markUserAffected(self.bug.owner, False)
169
170 # There will now be a job in the queue.
171 self.assertEqual(1, self._getJobCount())
172
173 def test_bug_creation_creates_job(self):
174 # Creating a bug adds a CalculateBugHeatJob for the new bug.
175 self.assertEqual(0, self._getJobCount())
176
177 new_bug = self.factory.makeBug()
178
179 # There will now be a job in the queue.
180 self.assertEqual(1, self._getJobCount())
181
182 def test_marking_dupe_creates_job(self):
183 # Marking a bug as a duplicate of another bug creates a job to
184 # update the master bug.
185 new_bug = self.factory.makeBug()
186 new_bug.setHeat(42)
187 self._completeJobsAndAssertQueueEmpty()
188
189 new_bug.markAsDuplicate(self.bug)
190
191 # There will now be a job in the queue.
192 self.assertEqual(1, self._getJobCount())
193
194 # And the job will be for the master bug.
195 bug_job = self._getJobs()[0]
196 self.assertEqual(bug_job.bug, self.bug)
197
198 # Also, the duplicate bug's heat will have been set to zero.
199 self.assertEqual(0, new_bug.heat)
200
201 def test_unmarking_dupe_creates_job(self):
202 # Unmarking a bug as a duplicate will create a
203 # CalculateBugHeatJob for the bug, since its heat will be 0 from
204 # having been marked as a duplicate.
205 new_bug = self.factory.makeBug()
206 new_bug.markAsDuplicate(self.bug)
207 self._completeJobsAndAssertQueueEmpty()
208 new_bug.markAsDuplicate(None)
209
210 # There will now be a job in the queue.
211 self.assertEqual(1, self._getJobCount())
212
213 # And the job will be for the master bug.
214 bug_job = self._getJobs()[0]
215 self.assertEqual(bug_job.bug, new_bug)
216
217115
218class MaxHeatByTargetBase:116class MaxHeatByTargetBase:
219 """Base class for testing a bug target's max_bug_heat attribute."""117 """Base class for testing a bug target's max_bug_heat attribute."""

Subscribers

People subscribed via source and target branches

to status/vote changes: