Merge lp:~benji/launchpad/bug-994694 into lp:launchpad

Proposed by Benji York
Status: Merged
Approved by: Benji York
Approved revision: no longer in the source branch.
Merged at revision: 15215
Proposed branch: lp:~benji/launchpad/bug-994694
Merge into: lp:launchpad
Diff against target: 97 lines (+44/-2)
2 files modified
lib/lp/bugs/scripts/bugnotification.py (+1/-1)
lib/lp/bugs/scripts/tests/test_bugnotification.py (+43/-1)
To merge this branch: bzr merge lp:~benji/launchpad/bug-994694
Reviewer Review Type Date Requested Status
Richard Harding (community) code Approve
Review via email: mp+105090@code.launchpad.net

Commit message

Make a generator react correctly to being closed before all of its results have been yielded.

Description of the change

Bug 994694 describes some odd output resulting from a generator not
reacting correctly to being closed before yielding all of their values.
This branch fixes that bug.

The fix was simple, add GeneratorExit to the list of exceptions reraised
by get_email_notifications. The bulk of the change is dedicated to
testing the fix. The comments in the test describe it sufficiently (I
hope), so I won't address that here.

LOC Rationale: The entirety of the LOC increase is in tests.

Tests: The new test can be run with this command:

    bin/test -c -t test_early_exit

Removing the GeneratorExit from the try/except in
get_email_notifications will cause the test to fail.

The "make lint" report is clean.

To post a comment you must log in.
Revision history for this message
Richard Harding (rharding) wrote :

Thanks, didn't even know there was a GeneratorExit exception.

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/lp/bugs/scripts/bugnotification.py'
--- lib/lp/bugs/scripts/bugnotification.py 2012-03-13 00:45:33 +0000
+++ lib/lp/bugs/scripts/bugnotification.py 2012-05-08 17:15:23 +0000
@@ -294,7 +294,7 @@
294 # being sent, so catch and log all exceptions.294 # being sent, so catch and log all exceptions.
295 try:295 try:
296 yield construct_email_notifications(batch)296 yield construct_email_notifications(batch)
297 except (KeyboardInterrupt, SystemExit):297 except (KeyboardInterrupt, SystemExit, GeneratorExit):
298 raise298 raise
299 except:299 except:
300 log.exception("Error while building email notifications.")300 log.exception("Error while building email notifications.")
301301
=== modified file 'lib/lp/bugs/scripts/tests/test_bugnotification.py'
--- lib/lp/bugs/scripts/tests/test_bugnotification.py 2012-05-03 00:08:11 +0000
+++ lib/lp/bugs/scripts/tests/test_bugnotification.py 2012-05-08 17:15:23 +0000
@@ -4,10 +4,12 @@
44
5__metaclass__ = type5__metaclass__ = type
66
7import StringIO
7from datetime import (8from datetime import (
8 datetime,9 datetime,
9 timedelta,10 timedelta,
10 )11 )
12import logging
11import re13import re
12import unittest14import unittest
1315
@@ -240,12 +242,13 @@
240 'some bug task identifier:status')242 'some bug task identifier:status')
241243
242244
243class TestGetEmailNotifications(unittest.TestCase):245class TestGetEmailNotifications(TestCase):
244 """Tests for the exception handling in get_email_notifications()."""246 """Tests for the exception handling in get_email_notifications()."""
245 layer = LaunchpadZopelessLayer247 layer = LaunchpadZopelessLayer
246248
247 def setUp(self):249 def setUp(self):
248 """Set up some mock bug notifications to use."""250 """Set up some mock bug notifications to use."""
251 super(TestGetEmailNotifications, self).setUp()
249 switch_dbuser(config.malone.bugnotification_dbuser)252 switch_dbuser(config.malone.bugnotification_dbuser)
250 sample_person = getUtility(IPersonSet).getByEmail(253 sample_person = getUtility(IPersonSet).getByEmail(
251 'test@canonical.com')254 'test@canonical.com')
@@ -290,6 +293,7 @@
290 sm.registerUtility(self._fake_utility)293 sm.registerUtility(self._fake_utility)
291294
292 def tearDown(self):295 def tearDown(self):
296 super(TestGetEmailNotifications, self).tearDown()
293 sm = getSiteManager()297 sm = getSiteManager()
294 sm.unregisterUtility(self._fake_utility)298 sm.unregisterUtility(self._fake_utility)
295 sm.registerUtility(self._original_utility)299 sm.registerUtility(self._original_utility)
@@ -383,6 +387,44 @@
383 bug_four = getUtility(IBugSet).get(4)387 bug_four = getUtility(IBugSet).get(4)
384 self.assertEqual(bug_four.id, 4)388 self.assertEqual(bug_four.id, 4)
385389
390 def test_early_exit(self):
391 # When not-yet-exhausted generators need to be deallocated Python
392 # raises a GeneratorExit exception at the point of their last yield.
393 # The get_email_notifications generator was catching that exception in
394 # a try/except and logging it, leading to bug 994694. This test
395 # verifies that the fix for that bug (re-raising the exception) stays
396 # in place.
397
398 # Set up logging so we can later assert that no exceptions are logged.
399 log_output = StringIO.StringIO()
400 logger = logging.getLogger()
401 log_handler = logging.StreamHandler(log_output)
402 logger.addHandler(logging.StreamHandler(log_output))
403 self.addCleanup(logger.removeHandler, log_handler)
404
405 # Make some data to feed to get_email_notifications.
406 person = getUtility(IPersonSet).getByEmail('test@canonical.com')
407 msg = getUtility(IMessageSet).fromText('', '', owner=person)
408 bug = MockBug(1, person)
409 # We need more than one notification because we want the generator to
410 # stay around after being started. Consuming the first starts it but
411 # since the second exists, the generator stays active.
412 notifications = [
413 MockBugNotification(
414 message=msg, bug=bug, is_comment=True, date_emailed=None),
415 MockBugNotification(
416 message=msg, bug=bug, is_comment=True, date_emailed=None),
417 ]
418
419 # Now we create the generator, start it, and then close it, triggering
420 # a GeneratorExit exception inside the generator.
421 email_notifications = get_email_notifications(notifications)
422 email_notifications.next()
423 email_notifications.close()
424
425 # Verify that no "Error while building email notifications." is logged.
426 self.assertEqual('', log_output.getvalue())
427
386428
387class TestNotificationCommentBatches(unittest.TestCase):429class TestNotificationCommentBatches(unittest.TestCase):
388 """Tests of `notification_comment_batches`."""430 """Tests of `notification_comment_batches`."""