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
1=== modified file 'lib/lp/bugs/scripts/bugnotification.py'
2--- lib/lp/bugs/scripts/bugnotification.py 2012-03-13 00:45:33 +0000
3+++ lib/lp/bugs/scripts/bugnotification.py 2012-05-08 17:15:23 +0000
4@@ -294,7 +294,7 @@
5 # being sent, so catch and log all exceptions.
6 try:
7 yield construct_email_notifications(batch)
8- except (KeyboardInterrupt, SystemExit):
9+ except (KeyboardInterrupt, SystemExit, GeneratorExit):
10 raise
11 except:
12 log.exception("Error while building email notifications.")
13
14=== modified file 'lib/lp/bugs/scripts/tests/test_bugnotification.py'
15--- lib/lp/bugs/scripts/tests/test_bugnotification.py 2012-05-03 00:08:11 +0000
16+++ lib/lp/bugs/scripts/tests/test_bugnotification.py 2012-05-08 17:15:23 +0000
17@@ -4,10 +4,12 @@
18
19 __metaclass__ = type
20
21+import StringIO
22 from datetime import (
23 datetime,
24 timedelta,
25 )
26+import logging
27 import re
28 import unittest
29
30@@ -240,12 +242,13 @@
31 'some bug task identifier:status')
32
33
34-class TestGetEmailNotifications(unittest.TestCase):
35+class TestGetEmailNotifications(TestCase):
36 """Tests for the exception handling in get_email_notifications()."""
37 layer = LaunchpadZopelessLayer
38
39 def setUp(self):
40 """Set up some mock bug notifications to use."""
41+ super(TestGetEmailNotifications, self).setUp()
42 switch_dbuser(config.malone.bugnotification_dbuser)
43 sample_person = getUtility(IPersonSet).getByEmail(
44 'test@canonical.com')
45@@ -290,6 +293,7 @@
46 sm.registerUtility(self._fake_utility)
47
48 def tearDown(self):
49+ super(TestGetEmailNotifications, self).tearDown()
50 sm = getSiteManager()
51 sm.unregisterUtility(self._fake_utility)
52 sm.registerUtility(self._original_utility)
53@@ -383,6 +387,44 @@
54 bug_four = getUtility(IBugSet).get(4)
55 self.assertEqual(bug_four.id, 4)
56
57+ def test_early_exit(self):
58+ # When not-yet-exhausted generators need to be deallocated Python
59+ # raises a GeneratorExit exception at the point of their last yield.
60+ # The get_email_notifications generator was catching that exception in
61+ # a try/except and logging it, leading to bug 994694. This test
62+ # verifies that the fix for that bug (re-raising the exception) stays
63+ # in place.
64+
65+ # Set up logging so we can later assert that no exceptions are logged.
66+ log_output = StringIO.StringIO()
67+ logger = logging.getLogger()
68+ log_handler = logging.StreamHandler(log_output)
69+ logger.addHandler(logging.StreamHandler(log_output))
70+ self.addCleanup(logger.removeHandler, log_handler)
71+
72+ # Make some data to feed to get_email_notifications.
73+ person = getUtility(IPersonSet).getByEmail('test@canonical.com')
74+ msg = getUtility(IMessageSet).fromText('', '', owner=person)
75+ bug = MockBug(1, person)
76+ # We need more than one notification because we want the generator to
77+ # stay around after being started. Consuming the first starts it but
78+ # since the second exists, the generator stays active.
79+ notifications = [
80+ MockBugNotification(
81+ message=msg, bug=bug, is_comment=True, date_emailed=None),
82+ MockBugNotification(
83+ message=msg, bug=bug, is_comment=True, date_emailed=None),
84+ ]
85+
86+ # Now we create the generator, start it, and then close it, triggering
87+ # a GeneratorExit exception inside the generator.
88+ email_notifications = get_email_notifications(notifications)
89+ email_notifications.next()
90+ email_notifications.close()
91+
92+ # Verify that no "Error while building email notifications." is logged.
93+ self.assertEqual('', log_output.getvalue())
94+
95
96 class TestNotificationCommentBatches(unittest.TestCase):
97 """Tests of `notification_comment_batches`."""