Merge lp:~cjwatson/launchpad/more-pop-notifications into lp:launchpad

Proposed by Colin Watson
Status: Merged
Merged at revision: 17722
Proposed branch: lp:~cjwatson/launchpad/more-pop-notifications
Merge into: lp:launchpad
Diff against target: 1092 lines (+179/-243)
8 files modified
lib/lp/archivepublisher/tests/test_generate_ppa_htaccess.py (+4/-15)
lib/lp/archiveuploader/tests/test_ppauploadprocessor.py (+17/-26)
lib/lp/archiveuploader/tests/test_uploadprocessor.py (+84/-108)
lib/lp/bugs/doc/externalbugtracker-debbugs.txt (+9/-9)
lib/lp/bugs/mail/tests/test_handler.py (+16/-45)
lib/lp/services/mail/tests/incomingmail.txt (+17/-20)
lib/lp/soyuz/tests/test_packageupload.py (+17/-19)
lib/lp/testing/__init__.py (+15/-1)
To merge this branch: bzr merge lp:~cjwatson/launchpad/more-pop-notifications
Reviewer Review Type Date Requested Status
William Grant code Approve
Review via email: mp+270371@code.launchpad.net

Commit message

Use pop_notifications in more places to avoid fragile patterns with stub.test_emails, and introduce an assertEmailQueueLength helper.

Description of the change

This is kind of yak-shaving. I've been making some progress on https://bugs.launchpad.net/launchpad/+bug/29744, but in order to turn off immediate mail delivery I need to make sure that doing so isn't going to hide test problems. A number of tests do something like "code_under_test(); self.assertEqual([], stub.test_emails);", and not all of them make sure to commit beforehand. With immediate mail delivery disabled, this could hide bugs because test_emails could be empty until the transaction is committed. The pop_notifications interface is in most situations better (it commits the transaction first by default, and it saves you having to clear test_emails yourself).

I've gone through the cases where test_emails was used directly and converted most of them to pop_notifications or something based on it. This branch contains only changes to files that contained unsafe uses of test_emails; the rest can come later.

Checking the length of the mail queue is a common pattern in tests, so I introduced an assertEmailQueueLength helper for this.

To post a comment you must log in.
Revision history for this message
William Grant (wgrant) wrote :

I'd consider making assertEmailQueueLength include the actual set of emails in the mismatch, but it's no worse than it was before.

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/archivepublisher/tests/test_generate_ppa_htaccess.py'
2--- lib/lp/archivepublisher/tests/test_generate_ppa_htaccess.py 2014-10-29 06:04:09 +0000
3+++ lib/lp/archivepublisher/tests/test_generate_ppa_htaccess.py 2015-09-10 16:19:01 +0000
4@@ -1,4 +1,4 @@
5-# Copyright 2009-2010 Canonical Ltd. This software is licensed under the
6+# Copyright 2009-2015 Canonical Ltd. This software is licensed under the
7 # GNU Affero General Public License version 3 (see the file LICENSE).
8
9 """Test the generate_ppa_htaccess.py script. """
10@@ -33,7 +33,6 @@
11 from lp.registry.interfaces.teammembership import TeamMembershipStatus
12 from lp.services.config import config
13 from lp.services.log.logger import BufferLogger
14-from lp.services.mail import stub
15 from lp.services.osutils import (
16 ensure_directory_exists,
17 remove_if_exists,
18@@ -281,9 +280,7 @@
19 self.assertNotDeactivated(tokens[person])
20
21 # Ensure that a cancellation email was sent.
22- num_emails = len(stub.test_emails)
23- self.assertEqual(
24- num_emails, 1, "Expected 1 email, got %s" % num_emails)
25+ self.assertEmailQueueLength(1)
26
27 # Promiscuous_person now leaves team1, but does not lose his
28 # token because he's also in team2. No other tokens are
29@@ -298,9 +295,7 @@
30 self.assertNotDeactivated(tokens[person])
31
32 # Ensure that a cancellation email was not sent.
33- num_emails = len(stub.test_emails)
34- self.assertEqual(
35- num_emails, 0, "Expected no emails, got %s" % num_emails)
36+ self.assertEmailQueueLength(0)
37
38 # Team 2 now leaves parent_team, and all its members lose their
39 # tokens.
40@@ -486,10 +481,6 @@
41
42 script.sendCancellationEmail(tokens[0])
43
44- num_emails = len(stub.test_emails)
45- self.assertEqual(
46- num_emails, 1, "Expected 1 email, got %s" % num_emails)
47-
48 [email] = pop_notifications()
49 self.assertEqual(
50 email['Subject'],
51@@ -537,9 +528,7 @@
52
53 script.sendCancellationEmail(token)
54
55- num_emails = len(stub.test_emails)
56- self.assertEqual(
57- num_emails, 0, "Expected 0 emails, got %s" % num_emails)
58+ self.assertEmailQueueLength(0)
59
60 def test_getTimeToSyncFrom(self):
61 # Sync from 1s before previous start to catch anything made during the
62
63=== modified file 'lib/lp/archiveuploader/tests/test_ppauploadprocessor.py'
64--- lib/lp/archiveuploader/tests/test_ppauploadprocessor.py 2015-08-26 13:41:21 +0000
65+++ lib/lp/archiveuploader/tests/test_ppauploadprocessor.py 2015-09-10 16:19:01 +0000
66@@ -9,7 +9,7 @@
67
68 __metaclass__ = type
69
70-from email import message_from_string
71+from operator import itemgetter
72 import os
73 import shutil
74
75@@ -27,7 +27,6 @@
76 from lp.services.config import config
77 from lp.services.database.constants import UTC_NOW
78 from lp.services.librarian.interfaces import ILibraryFileAliasSet
79-from lp.services.mail import stub
80 from lp.soyuz.enums import (
81 PackagePublishingStatus,
82 PackageUploadStatus,
83@@ -43,6 +42,7 @@
84 from lp.soyuz.tests.fakepackager import FakePackager
85 from lp.soyuz.tests.test_publishing import SoyuzTestPublisher
86 from lp.testing.dbuser import switch_dbuser
87+from lp.testing.mail_helpers import pop_notifications
88
89
90 class TestPPAUploadProcessorBase(TestUploadProcessorBase):
91@@ -87,20 +87,19 @@
92 "X-Launchpad-PPA" header; it defaults to "name16" and should be
93 explicitly set to None for non-PPA or rejection notifications.
94 """
95- for item in expected:
96+ notifications = self.assertEmailQueueLength(
97+ len(expected), sort_key=itemgetter('X-Envelope-To'))
98+
99+ for item, msg in zip(expected, notifications):
100 recipient = item.get("recipient", self.name16_recipient)
101 contents = item.get("contents", [])
102 ppa_header = item.get("ppa_header", "name16")
103
104- from_addr, to_addrs, raw_msg = stub.test_emails.pop()
105- msg = message_from_string(raw_msg)
106-
107 # This is now a non-multipart message.
108 self.assertFalse(msg.is_multipart())
109 body = msg.get_payload(decode=True)
110
111- clean_recipients = [r.strip() for r in to_addrs]
112- self.assertContentEqual([recipient], clean_recipients)
113+ self.assertEqual(recipient, msg['X-Envelope-To'])
114
115 subject = "Subject: %s\n" % msg['Subject']
116 body = subject + body
117@@ -112,10 +111,6 @@
118 self.assertIn('X-Launchpad-PPA', msg.keys())
119 self.assertEqual(msg['X-Launchpad-PPA'], ppa_header)
120
121- self.assertEqual(
122- [], stub.test_emails,
123- "%d emails left over" % len(stub.test_emails))
124-
125 def checkFilesRestrictedInLibrarian(self, queue_item, condition):
126 """Check the libraryfilealias restricted flag.
127
128@@ -300,8 +295,8 @@
129 self.assertEqual(
130 self.uploadprocessor.last_processed_upload.queue_root.status,
131 PackageUploadStatus.DONE)
132- # Consume the test email so the assertion futher down does not fail.
133- _from_addr, _to_addrs, _raw_msg = stub.test_emails.pop()
134+ # Consume the test email so the assertion further down does not fail.
135+ pop_notifications()
136
137 # The SourcePackageRelease still has a component of universe:
138 pub_foo = self.name16.archive.getPublishedSources(name=u"bar").one()
139@@ -320,8 +315,7 @@
140 self.build_uploadprocessor, upload_dir, build=build)
141
142 # No mails are sent for successful binary uploads.
143- self.assertEqual(len(stub.test_emails), 0,
144- "Unexpected email generated on binary upload.")
145+ self.assertEmailQueueLength(0)
146
147 # Publish the binary.
148 [queue_item] = self.breezy.getPackageUploads(
149@@ -477,7 +471,7 @@
150 name12.preferredemail.email)
151 self.assertEmails([
152 {"ppa_header": "cprov", "recipient": expected_recipient}
153- for expected_recipient in reversed(sorted(expected_recipients))])
154+ for expected_recipient in sorted(expected_recipients)])
155
156 def testPPADistroSeriesOverrides(self):
157 """It's possible to override target distroseries of PPA uploads.
158@@ -850,9 +844,7 @@
159 # Please note: this upload goes to the Ubuntu main archive.
160 upload_dir = self.queueUpload("bar_1.0-10")
161 self.processUpload(self.uploadprocessor, upload_dir)
162- # Discard the announcement email and check the acceptance message
163- # content.
164- stub.test_emails.pop()
165+ pop_notifications()
166
167 self.assertEqual(
168 self.uploadprocessor.last_processed_upload.queue_root.status,
169@@ -991,9 +983,9 @@
170 u'Files specified in DSC are broken or missing, skipping package '
171 u'unpack verification.')
172
173- # Also, the email generated should be sane.
174- from_addr, to_addrs, raw_msg = stub.test_emails.pop()
175- msg = message_from_string(raw_msg)
176+ # Also, the email generated should be sane. Any of the multiple
177+ # notifications will do.
178+ msg = pop_notifications()[-1]
179 body = msg.get_payload(decode=True)
180
181 self.assertTrue(
182@@ -1015,8 +1007,7 @@
183 self.processUpload(self.uploadprocessor, upload_dir)
184
185 # The email generated should be sane.
186- from_addr, to_addrs, raw_msg = stub.test_emails.pop()
187- msg = message_from_string(raw_msg)
188+ [msg] = pop_notifications()
189 body = msg.get_payload(decode=True)
190
191 self.assertTrue(
192@@ -1148,7 +1139,7 @@
193 queue_item.setAccepted()
194 queue_item.realiseUpload()
195 transaction.commit()
196- stub.test_emails.pop()
197+ pop_notifications()
198
199 # Now upload a 3.0 (quilt) source with missing orig*.tar.* to a
200 # PPA. All of the missing files will be retrieved from the
201
202=== modified file 'lib/lp/archiveuploader/tests/test_uploadprocessor.py'
203--- lib/lp/archiveuploader/tests/test_uploadprocessor.py 2015-08-25 14:05:24 +0000
204+++ lib/lp/archiveuploader/tests/test_uploadprocessor.py 2015-09-10 16:19:01 +0000
205@@ -9,7 +9,6 @@
206 "TestUploadProcessorBase",
207 ]
208
209-from email import message_from_string
210 import os
211 import shutil
212 from StringIO import StringIO
213@@ -65,7 +64,6 @@
214 BufferLogger,
215 DevNullLogger,
216 )
217-from lp.services.mail import stub
218 from lp.soyuz.enums import (
219 ArchivePermissionType,
220 ArchivePurpose,
221@@ -357,12 +355,16 @@
222 over after checking the ones in expected.
223 """
224
225- for item in expected:
226+ if allow_leftover:
227+ notifications = pop_notifications()
228+ self.assertTrue(len(notifications) >= len(expected))
229+ else:
230+ notifications = self.assertEmailQueueLength(len(expected))
231+
232+ for item, msg in zip(expected, notifications):
233 recipient = item.get("recipient", self.name16_recipient)
234 contents = item.get("contents", [])
235
236- from_addr, to_addrs, raw_msg = stub.test_emails.pop()
237- msg = message_from_string(raw_msg)
238 # This is now a MIMEMultipart message.
239 body = msg.get_payload(0)
240 body = body.get_payload(decode=True)
241@@ -370,8 +372,7 @@
242 # Only check the recipient if the caller didn't explicitly pass
243 # "recipient": None.
244 if recipient is not None:
245- clean_recipients = [r.strip() for r in to_addrs]
246- self.assertContentEqual([recipient], clean_recipients)
247+ self.assertEqual(recipient, msg['X-Envelope-To'])
248
249 subject = "Subject: %s\n" % msg['Subject']
250 body = subject + body
251@@ -381,11 +382,6 @@
252 content in body,
253 "Expect: '%s'\nGot:\n%s" % (content, body))
254
255- if not allow_leftover:
256- self.assertEqual(
257- [], stub.test_emails,
258- "%d emails left over" % len(stub.test_emails))
259-
260 def PGPSignatureNotPreserved(self, archive=None):
261 """PGP signatures should be removed from .changes files.
262
263@@ -423,13 +419,11 @@
264
265 def _checkPartnerUploadEmailSuccess(self):
266 """Ensure partner uploads generate the right email."""
267- from_addr, to_addrs, raw_msg = stub.test_emails.pop()
268- foo_bar = "foo.bar@canonical.com"
269- self.assertEqual([e.strip() for e in to_addrs], [foo_bar])
270+ [msg] = pop_notifications()
271+ self.assertEqual("foo.bar@canonical.com", msg["X-Envelope-To"])
272 self.assertTrue(
273- "rejected" not in raw_msg,
274- "Expected acceptance email not rejection. Actually Got:\n%s"
275- % raw_msg)
276+ "rejected" not in str(msg),
277+ "Expected acceptance email not rejection. Actually Got:\n%s" % msg)
278
279 def testInstantiate(self):
280 """UploadProcessor should instantiate"""
281@@ -563,13 +557,13 @@
282 self.processUpload(uploadprocessor, upload_dir)
283
284 # Check the mailer stub has a rejection email for Daniel
285- from_addr, to_addrs, raw_msg = stub.test_emails.pop()
286+ [msg] = pop_notifications()
287 # This is now a MIMEMultipart message.
288- msg = message_from_string(raw_msg)
289 body = msg.get_payload(0)
290 body = body.get_payload(decode=True)
291
292- self.assertEqual(["daniel.silverstone@canonical.com"], to_addrs)
293+ self.assertEqual(
294+ "daniel.silverstone@canonical.com", msg["X-Envelope-To"])
295 self.assertTrue("Unhandled exception processing upload: Exception "
296 "raised by BrokenUploadPolicy for testing."
297 in body)
298@@ -599,15 +593,14 @@
299 self.processUpload(uploadprocessor, upload_dir)
300
301 # Check it went ok to the NEW queue and all is going well so far.
302- foo_bar = "foo.bar@canonical.com"
303 daniel = "daniel.silverstone@canonical.com"
304- for expected_to_addr in foo_bar, daniel:
305- from_addr, to_addrs, raw_msg = stub.test_emails.pop()
306- to_addrs = [e.strip() for e in to_addrs]
307- self.assertContentEqual(to_addrs, [expected_to_addr])
308+ foo_bar = "foo.bar@canonical.com"
309+ notifications = self.assertEmailQueueLength(2)
310+ for expected_to_addr, msg in zip((daniel, foo_bar), notifications):
311+ self.assertEqual(expected_to_addr, msg["X-Envelope-To"])
312 self.assertTrue(
313- "NEW" in raw_msg, "Expected email containing 'NEW', got:\n%s"
314- % raw_msg)
315+ "NEW" in str(msg),
316+ "Expected email containing 'NEW', got:\n%s" % msg)
317
318 # Accept and publish the upload.
319 # This is required so that the next upload of a later version of
320@@ -635,13 +628,12 @@
321 self.processUpload(uploadprocessor, upload_dir)
322
323 # Verify we get an email talking about awaiting approval.
324- for expected_to_addr in foo_bar, daniel:
325- from_addr, to_addrs, raw_msg = stub.test_emails.pop()
326- to_addrs = [e.strip() for e in to_addrs]
327- self.assertContentEqual(to_addrs, [expected_to_addr])
328- self.assertTrue("Waiting for approval" in raw_msg,
329- "Expected an 'upload awaits approval' email.\n"
330- "Got:\n%s" % raw_msg)
331+ notifications = self.assertEmailQueueLength(2)
332+ for expected_to_addr, msg in zip((daniel, foo_bar), notifications):
333+ self.assertEqual(expected_to_addr, msg["X-Envelope-To"])
334+ self.assertTrue(
335+ "Waiting for approval" in str(msg),
336+ "Expected an 'upload awaits approval' email.\nGot:\n%s" % msg)
337
338 # And verify that the queue item is in the unapproved state.
339 queue_items = self.breezy.getPackageUploads(
340@@ -918,10 +910,10 @@
341 self.processUpload(uploadprocessor, upload_dir)
342
343 # Check that it was rejected appropriately.
344- from_addr, to_addrs, raw_msg = stub.test_emails.pop()
345- self.assertTrue(
346- "Partner archive for distro '%s' not found" % self.ubuntu.name
347- in raw_msg)
348+ [msg] = pop_notifications()
349+ self.assertIn(
350+ "Partner archive for distro '%s' not found" % self.ubuntu.name,
351+ str(msg))
352
353 def testMixedPartnerUploadFails(self):
354 """Uploads with partner and non-partner files are rejected.
355@@ -937,13 +929,12 @@
356 self.processUpload(uploadprocessor, upload_dir)
357
358 # Check that it was rejected.
359- from_addr, to_addrs, raw_msg = stub.test_emails.pop()
360- foo_bar = "foo.bar@canonical.com"
361- self.assertEqual([e.strip() for e in to_addrs], [foo_bar])
362+ [msg] = pop_notifications()
363+ self.assertEqual("foo.bar@canonical.com", msg["X-Envelope-To"])
364 self.assertTrue(
365- "Cannot mix partner files with non-partner." in raw_msg,
366+ "Cannot mix partner files with non-partner." in str(msg),
367 "Expected email containing 'Cannot mix partner files with "
368- "non-partner.', got:\n%s" % raw_msg)
369+ "non-partner.', got:\n%s" % msg)
370
371 def testPartnerReusingOrigFromPartner(self):
372 """Partner uploads reuse 'orig.tar.gz' from the partner archive."""
373@@ -977,8 +968,7 @@
374 self.processUpload(uploadprocessor, upload_dir)
375 # Discard the announcement email and check the acceptance message
376 # content.
377- from_addr, to_addrs, raw_msg = stub.test_emails.pop()
378- msg = message_from_string(raw_msg)
379+ msg = pop_notifications()[-1]
380 # This is now a MIMEMultipart message.
381 body = msg.get_payload(0)
382 body = body.get_payload(decode=True)
383@@ -1095,11 +1085,10 @@
384 self.processUpload(uploadprocessor, upload_dir)
385
386 # Check it went ok to the NEW queue and all is going well so far.
387- from_addr, to_addrs, raw_msg = stub.test_emails.pop()
388+ [msg] = pop_notifications()
389 self.assertTrue(
390- "NEW" in raw_msg,
391- "Expected email containing 'NEW', got:\n%s"
392- % raw_msg)
393+ "NEW" in str(msg),
394+ "Expected email containing 'NEW', got:\n%s" % msg)
395
396 # Accept and publish the upload.
397 partner_archive = getUtility(IArchiveSet).getByDistroPurpose(
398@@ -1198,10 +1187,10 @@
399 # Check it is rejected.
400 expect_msg = ("Partner uploads must be for the RELEASE or "
401 "PROPOSED pocket.")
402- from_addr, to_addrs, raw_msg = stub.test_emails.pop()
403+ [msg] = pop_notifications()
404 self.assertTrue(
405- expect_msg in raw_msg,
406- "Expected email with %s, got:\n%s" % (expect_msg, raw_msg))
407+ expect_msg in str(msg),
408+ "Expected email with %s, got:\n%s" % (expect_msg, msg))
409
410 # And an oops should be filed for the error.
411 error_report = self.oopses[new_index]
412@@ -1428,10 +1417,9 @@
413 upload_dir = self.queueUpload("bar_1.0-1_lzma")
414 self.processUpload(uploadprocessor, upload_dir)
415 # Make sure it went ok:
416- from_addr, to_addrs, raw_msg = stub.test_emails.pop()
417- self.assertTrue(
418- "rejected" not in raw_msg,
419- "Failed to upload bar source:\n%s" % raw_msg)
420+ [msg] = pop_notifications()
421+ self.assertFalse(
422+ "rejected" in str(msg), "Failed to upload bar source:\n%s" % msg)
423 self.publishPackage("bar", "1.0-1")
424 # Clear out emails generated during upload.
425 pop_notifications()
426@@ -1441,11 +1429,7 @@
427 self.processUpload(uploadprocessor, upload_dir)
428
429 # Successful binary uploads won't generate any email.
430- if len(stub.test_emails) != 0:
431- from_addr, to_addrs, raw_msg = stub.test_emails.pop()
432- self.assertEqual(
433- len(stub.test_emails), 0,
434- "Expected no emails! Actually got:\n%s" % raw_msg)
435+ self.assertEmailQueueLength(0)
436
437 # Check in the queue to see if it really made it:
438 queue_items = self.breezy.getPackageUploads(
439@@ -1472,10 +1456,9 @@
440 upload_dir = self.queueUpload("bar_1.0-1_xz")
441 self.processUpload(uploadprocessor, upload_dir)
442 # Make sure it went ok:
443- from_addr, to_addrs, raw_msg = stub.test_emails.pop()
444- self.assertTrue(
445- "rejected" not in raw_msg,
446- "Failed to upload bar source:\n%s" % raw_msg)
447+ [msg] = pop_notifications()
448+ self.assertFalse(
449+ "rejected" in str(msg), "Failed to upload bar source:\n%s" % msg)
450 self.publishPackage("bar", "1.0-1")
451 # Clear out emails generated during upload.
452 pop_notifications()
453@@ -1485,11 +1468,7 @@
454 self.processUpload(uploadprocessor, upload_dir)
455
456 # Successful binary uploads won't generate any email.
457- if len(stub.test_emails) != 0:
458- from_addr, to_addrs, raw_msg = stub.test_emails.pop()
459- self.assertEqual(
460- len(stub.test_emails), 0,
461- "Expected no emails! Actually got:\n%s" % raw_msg)
462+ self.assertEmailQueueLength(0)
463
464 # Check in the queue to see if it really made it:
465 queue_items = self.breezy.getPackageUploads(
466@@ -1683,8 +1662,8 @@
467 self.processUpload(uploadprocessor, upload_dir)
468
469 # Check that it failed (permissions were granted for wrong series).
470- from_addr, to_addrs, raw_msg = stub.test_emails.pop()
471- msg = message_from_string(raw_msg)
472+ # Any of the multiple notifications will do.
473+ msg = pop_notifications()[-1]
474 self.assertEqual(
475 msg['Subject'], '[ubuntu] bar_1.0-2_source.changes (Rejected)')
476
477@@ -1747,16 +1726,16 @@
478 expected = []
479 expected.append({
480 "contents": base_contents + [
481- "You are receiving this email because you made this upload."],
482- "recipient": "foo.bar@canonical.com",
483- })
484- expected.append({
485- "contents": base_contents + [
486 "You are receiving this email because you are the most "
487 "recent person",
488 "listed in this package's changelog."],
489 "recipient": "daniel.silverstone@canonical.com",
490 })
491+ expected.append({
492+ "contents": base_contents + [
493+ "You are receiving this email because you made this upload."],
494+ "recipient": "foo.bar@canonical.com",
495+ })
496 self.assertEmails(expected)
497
498 def test30QuiltUploadToUnsupportingSeriesIsRejected(self):
499@@ -1774,11 +1753,11 @@
500 upload_dir = self.queueUpload("bar_1.0-1_3.0-quilt")
501 self.processUpload(uploadprocessor, upload_dir)
502 # Make sure it was rejected.
503- from_addr, to_addrs, raw_msg = stub.test_emails.pop()
504+ [msg] = pop_notifications()
505 self.assertTrue(
506 "bar_1.0-1.dsc: format '3.0 (quilt)' is not permitted in "
507- "breezy." in raw_msg,
508- "Source was not rejected properly:\n%s" % raw_msg)
509+ "breezy." in str(msg),
510+ "Source was not rejected properly:\n%s" % msg)
511
512 def test30QuiltUpload(self):
513 """Ensure that 3.0 (quilt) uploads work properly. """
514@@ -1792,10 +1771,9 @@
515 upload_dir = self.queueUpload("bar_1.0-1_3.0-quilt")
516 self.processUpload(uploadprocessor, upload_dir)
517 # Make sure it went ok:
518- from_addr, to_addrs, raw_msg = stub.test_emails.pop()
519- self.assertTrue(
520- "rejected" not in raw_msg,
521- "Failed to upload bar source:\n%s" % raw_msg)
522+ [msg] = pop_notifications()
523+ self.assertFalse(
524+ "rejected" in str(msg), "Failed to upload bar source:\n%s" % msg)
525 spph = self.publishPackage("bar", "1.0-1")
526
527 self.assertEquals(
528@@ -1827,10 +1805,9 @@
529 upload_dir = self.queueUpload("bar_1.0-1_3.0-quilt")
530 self.processUpload(uploadprocessor, upload_dir)
531 # Make sure it went ok:
532- from_addr, to_addrs, raw_msg = stub.test_emails.pop()
533- self.assertTrue(
534- "rejected" not in raw_msg,
535- "Failed to upload bar source:\n%s" % raw_msg)
536+ [msg] = pop_notifications()
537+ self.assertFalse(
538+ "rejected" in str(msg), "Failed to upload bar source:\n%s" % msg)
539 self.publishPackage("bar", "1.0-1")
540
541 # Upload another source sharing the same (component) orig.
542@@ -1865,10 +1842,9 @@
543 upload_dir = self.queueUpload("bar_1.0_3.0-native")
544 self.processUpload(uploadprocessor, upload_dir)
545 # Make sure it went ok:
546- from_addr, to_addrs, raw_msg = stub.test_emails.pop()
547- self.assertTrue(
548- "rejected" not in raw_msg,
549- "Failed to upload bar source:\n%s" % raw_msg)
550+ [msg] = pop_notifications()
551+ self.assertFalse(
552+ "rejected" in str(msg), "Failed to upload bar source:\n%s" % msg)
553 spph = self.publishPackage("bar", "1.0")
554
555 self.assertEquals(
556@@ -1890,11 +1866,11 @@
557 upload_dir = self.queueUpload("bar_1.0-1_1.0-bzip2")
558 self.processUpload(uploadprocessor, upload_dir)
559 # Make sure it was rejected.
560- from_addr, to_addrs, raw_msg = stub.test_emails.pop()
561+ [msg] = pop_notifications()
562 self.assertTrue(
563 "bar_1.0-1.dsc: is format 1.0 but uses bzip2 compression."
564- in raw_msg,
565- "Source was not rejected properly:\n%s" % raw_msg)
566+ in str(msg),
567+ "Source was not rejected properly:\n%s" % msg)
568
569 def testUploadToWrongPocketIsRejected(self):
570 # Uploads to the wrong pocket are rejected.
571@@ -1922,16 +1898,16 @@
572 expected = []
573 expected.append({
574 "contents": base_contents + [
575- "You are receiving this email because you made this upload."],
576- "recipient": "foo.bar@canonical.com",
577- })
578- expected.append({
579- "contents": base_contents + [
580 "You are receiving this email because you are the most "
581 "recent person",
582 "listed in this package's changelog."],
583 "recipient": "daniel.silverstone@canonical.com",
584 })
585+ expected.append({
586+ "contents": base_contents + [
587+ "You are receiving this email because you made this upload."],
588+ "recipient": "foo.bar@canonical.com",
589+ })
590 self.assertEmails(expected)
591
592 def testPGPSignatureNotPreserved(self):
593@@ -1969,7 +1945,7 @@
594 self.assertEqual(UploadStatusEnum.REJECTED, result)
595 self.assertLogContains(
596 "INFO Failed to parse changes file")
597- self.assertEqual(len(stub.test_emails), 0)
598+ self.assertEmailQueueLength(0)
599 self.assertEqual([], self.oopses)
600
601 def test_ddeb_upload_overrides(self):
602@@ -2137,13 +2113,13 @@
603 queue_entry=leaf_name)
604 self.options.context = 'buildd'
605 self.options.builds = True
606- last_stub_mail_count = len(stub.test_emails)
607+ pop_notifications()
608 BuildUploadHandler(self.uploadprocessor, self.incoming_folder,
609 leaf_name).process()
610 self.layer.txn.commit()
611 # No emails are sent on success
612- self.assertEquals(len(stub.test_emails), last_stub_mail_count)
613- self.assertEquals(BuildStatus.FULLYBUILT, build.status)
614+ self.assertEmailQueueLength(0)
615+ self.assertEqual(BuildStatus.FULLYBUILT, build.status)
616 # Upon full build the upload log is unset.
617 self.assertIs(None, build.upload_log)
618
619@@ -2192,7 +2168,7 @@
620 See bug 778437.
621 """
622 self.doSuccessRecipeBuild()
623- self.assertEquals(len(pop_notifications()), 0, pop_notifications)
624+ self.assertEmailQueueLength(0)
625
626 def doFailureRecipeBuild(self):
627 # A source package recipe build will fail if no files are present.
628@@ -2284,7 +2260,7 @@
629 status=PackageUploadStatus.ACCEPTED,
630 version=u"1.0-1", name=u"bar")
631 queue_item.setDone()
632- stub.test_emails = []
633+ pop_notifications()
634
635 build.buildqueue_record.markAsBuilding(self.factory.makeBuilder())
636 build.updateStatus(status)
637@@ -2300,7 +2276,7 @@
638 "bar_1.0-1_binary", queue_entry=leaf_name)
639 self.options.context = 'buildd'
640 self.options.builds = True
641- self.assertEqual([], stub.test_emails)
642+ self.assertEmailQueueLength(0)
643 BuildUploadHandler(self.uploadprocessor, self.incoming_folder,
644 leaf_name).process()
645 self.layer.txn.commit()
646
647=== modified file 'lib/lp/bugs/doc/externalbugtracker-debbugs.txt'
648--- lib/lp/bugs/doc/externalbugtracker-debbugs.txt 2015-03-13 19:05:50 +0000
649+++ lib/lp/bugs/doc/externalbugtracker-debbugs.txt 2015-09-10 16:19:01 +0000
650@@ -596,15 +596,15 @@
651 ... '<123456@launchpad.net>')
652 '<123456@launchpad.net>'
653
654-We can look in the test_emails list for the mail that would have been
655-sent.
656-
657- >>> from lp.services.mail import stub
658- >>> recipent, to_addrs, comment_email = stub.test_emails.pop()
659- >>> print to_addrs
660- ['1234@example.com']
661-
662- >>> print comment_email
663+We can look for the mail that would have been sent.
664+
665+ >>> from lp.testing.mail_helpers import pop_notifications
666+ >>> [msg] = pop_notifications()
667+ >>> print msg['X-Envelope-To']
668+ 1234@example.com
669+
670+ >>> print str(msg)
671+ From ...
672 Content-Type...
673 Message-Id: <123456@launchpad.net>
674 To: 1234@example.com
675
676=== modified file 'lib/lp/bugs/mail/tests/test_handler.py'
677--- lib/lp/bugs/mail/tests/test_handler.py 2012-09-17 16:13:40 +0000
678+++ lib/lp/bugs/mail/tests/test_handler.py 2015-09-10 16:19:01 +0000
679@@ -1,11 +1,10 @@
680-# Copyright 2010-2012 Canonical Ltd. This software is licensed under the
681+# Copyright 2010-2015 Canonical Ltd. This software is licensed under the
682 # GNU Affero General Public License version 3 (see the file LICENSE).
683
684 """Test MaloneHandler."""
685
686 __metaclass__ = type
687
688-import email
689 import time
690
691 import transaction
692@@ -32,7 +31,6 @@
693 from lp.registry.enums import BugSharingPolicy
694 from lp.services.config import config
695 from lp.services.identity.interfaces.emailaddress import EmailAddressStatus
696-from lp.services.mail import stub
697 from lp.services.webapp.authorization import LaunchpadSecurityPolicy
698 from lp.testing import (
699 celebrity_logged_in,
700@@ -124,12 +122,11 @@
701 response = handler.extractAndAuthenticateCommands(
702 message, 'help@bugs.launchpad.net')
703 mail_handled, add_comment_to_bug, commands = response
704- self.assertEquals(mail_handled, True)
705- emails = self.getSentMail()
706- self.assertEquals(1, len(emails))
707- self.assertEquals(['non@eg.dom'], emails[0][1])
708- self.assertTrue(
709- 'Subject: Launchpad Bug Tracker Email Interface' in emails[0][2])
710+ self.assertTrue(mail_handled)
711+ emails = self.assertEmailQueueLength(1)
712+ self.assertEqual('non@eg.dom', emails[0]['X-Envelope-To'])
713+ self.assertEqual(
714+ 'Launchpad Bug Tracker Email Interface Help', emails[0]['Subject'])
715
716 def test_mailToHelpFromUnknownUser(self):
717 """Mail from people of no account to help@ is simply dropped.
718@@ -140,8 +137,8 @@
719 mail_handled, add_comment_to_bug, commands = \
720 handler.extractAndAuthenticateCommands(message,
721 'help@bugs.launchpad.net')
722- self.assertEquals(mail_handled, True)
723- self.assertEquals(self.getSentMail(), [])
724+ self.assertTrue(mail_handled)
725+ self.assertEmailQueueLength(0)
726
727 def test_mailToHelp(self):
728 """Mail to help@ generates a help command."""
729@@ -152,19 +149,11 @@
730 mail_handled, add_comment_to_bug, commands = \
731 handler.extractAndAuthenticateCommands(message,
732 'help@bugs.launchpad.net')
733- self.assertEquals(mail_handled, True)
734- emails = self.getSentMail()
735- self.assertEquals(1, len(emails))
736- self.assertEquals([message['From']], emails[0][1])
737- self.assertTrue(
738- 'Subject: Launchpad Bug Tracker Email Interface' in emails[0][2])
739-
740- def getSentMail(self):
741- # Sending mail is (unfortunately) a side effect of parsing the
742- # commands, and unfortunately you must commit the transaction to get
743- # them sent.
744- transaction.commit()
745- return stub.test_emails[:]
746+ self.assertEqual(mail_handled, True)
747+ emails = self.assertEmailQueueLength(1)
748+ self.assertEqual(message['From'], emails[0]['X-Envelope-To'])
749+ self.assertEqual(
750+ 'Launchpad Bug Tracker Email Interface Help', emails[0]['Subject'])
751
752 def getFailureForMessage(self, to_address, from_address=None, body=None):
753 mail = self.factory.makeSignedMessage(
754@@ -683,22 +672,6 @@
755 self.timestamp = timestamp
756
757
758-def get_last_email():
759- from_addr, to_addrs, raw_message = stub.test_emails[-1]
760- sent_msg = email.message_from_string(raw_message)
761- error_mail, original_mail = sent_msg.get_payload()
762- # clear the emails so we don't accidentally get one from a previous test
763- return dict(
764- subject=sent_msg['Subject'],
765- body=error_mail.get_payload(decode=True))
766-
767-
768-BAD_SIGNATURE_TIMESTAMP_MESSAGE = (
769- 'The message you sent included commands to modify the bug '
770- 'report, but the\nsignature was (apparently) generated too far '
771- 'in the past or future.')
772-
773-
774 class TestSignatureTimestampValidation(TestCaseWithFactory):
775 """GPG signature timestamps are checked for emails containing commands."""
776
777@@ -715,10 +688,9 @@
778 handler = MaloneHandler()
779 with person_logged_in(self.factory.makePerson()):
780 handler.process(msg, msg['To'])
781- transaction.commit()
782 # Since there were no commands in the poorly-timestamped message, no
783 # error emails were generated.
784- self.assertEqual(stub.test_emails, [])
785+ self.assertEmailQueueLength(0)
786
787 def test_bad_timestamp_but_no_commands(self):
788 # If an email message's GPG signature's timestamp is too far in the
789@@ -732,10 +704,9 @@
790 msg.signature = FakeSignature(timestamp=now + one_week)
791 handler = MaloneHandler()
792 # Clear old emails before potentially generating more.
793- del stub.test_emails[:]
794+ pop_notifications()
795 with person_logged_in(self.factory.makePerson()):
796 handler.process(msg, msg['To'])
797- transaction.commit()
798 # Since there were no commands in the poorly-timestamped message, no
799 # error emails were generated.
800- self.assertEqual(stub.test_emails, [])
801+ self.assertEmailQueueLength(0)
802
803=== modified file 'lib/lp/services/mail/tests/incomingmail.txt'
804--- lib/lp/services/mail/tests/incomingmail.txt 2015-07-08 16:05:11 +0000
805+++ lib/lp/services/mail/tests/incomingmail.txt 2015-09-10 16:19:01 +0000
806@@ -121,8 +121,9 @@
807 wasn't a handler registered for that domain, an OOPS was recorded with
808 a link to the original message.
809
810- >>> from lp.services.mail import stub
811- >>> print stub.test_emails[-1][2]
812+ >>> from lp.testing.mail_helpers import pop_notifications
813+ >>> print str(pop_notifications()[-1])
814+ From ...
815 Content-Type: multipart/mixed...
816 ...
817 To: Sample Person <test@canonical.com>
818@@ -140,8 +141,6 @@
819 Subject: Signed Email
820 ...
821
822- >>> stub.test_emails = []
823-
824 ---------------------------------------------
825 Mail from Persons not registered in Launchpad
826 ---------------------------------------------
827@@ -156,7 +155,7 @@
828 >>> msgid not in foo_handler.handledMails
829 True
830
831- >>> stub.test_emails = []
832+ >>> _ = pop_notifications()
833
834 However, bar_handler specifies that it can handle such emails:
835
836@@ -171,7 +170,7 @@
837 >>> msgid in bar_handler.handledMails
838 True
839
840- >>> stub.test_emails = []
841+ >>> _ = pop_notifications()
842
843 ---------------------------------------------------------
844 Mail from Persons with with an inactive Launchpad account
845@@ -260,7 +259,8 @@
846 An exception is raised, an OOPS is recorded, and an email is sent back
847 to the user, citing the OOPS ID, with the original message attached.
848
849- >>> print stub.test_emails[-1][2]
850+ >>> print str(pop_notifications()[-1])
851+ From ...
852 Content-Type: multipart/mixed...
853 ...
854 To: Foo Bar <foo.bar@canonical.com>
855@@ -279,8 +279,6 @@
856 Subject: doesn't matter
857 ...
858
859- >>> stub.test_emails = []
860-
861 Unauthorized exceptions, which are ignored for the purpose of OOPS
862 reporting in the web interface, are not ignored in the email interface.
863
864@@ -305,7 +303,8 @@
865 ...
866 Unauthorized
867
868- >>> print stub.test_emails[-1][2]
869+ >>> print str(pop_notifications()[-1])
870+ From ...
871 Content-Type: multipart/mixed...
872 ...
873 To: Foo Bar <foo.bar@canonical.com>
874@@ -324,8 +323,6 @@
875 Subject: doesn't matter
876 ...
877
878- >>> stub.test_emails = []
879-
880 -------------
881 DB exceptions
882 -------------
883@@ -382,6 +379,7 @@
884 There is only one mail left in the mail box - the one sent back to
885 the user reporting the error:
886
887+ >>> from lp.services.mail import stub
888 >>> len(stub.test_emails)
889 1
890
891@@ -412,7 +410,7 @@
892 2
893
894 >>> LibrarianLayer.reveal()
895- >>> stub.test_emails = []
896+ >>> _ = pop_notifications()
897
898 ----------------
899 Handling bounces
900@@ -425,7 +423,6 @@
901
902 Emails with an empty Return-Path header should be dropped:
903
904- >>> stub.test_emails = []
905 >>> msg = read_test_message('signed_detached.txt')
906 >>> msg.replace_header('To', '123@foo.com')
907 >>> msg['Return-Path'] = '<>'
908@@ -437,8 +434,8 @@
909 Since this happens way too often, as we seem to get more spam than
910 legitimate email, an email is not sent about it to the errors-list.
911
912- >>> len(stub.test_emails)
913- 0
914+ >>> pop_notifications()
915+ []
916
917 If the content type is multipart/report, it's most likely a DSN
918 (RFC 3464), so those get dropped as well. Normally a DSN should have an
919@@ -455,8 +452,8 @@
920 >>> msgid in foo_handler.handledMails
921 False
922
923- >>> len(stub.test_emails)
924- 0
925+ >>> pop_notifications()
926+ []
927
928 Email with the Precedence header are probably from an auto-responder or
929 another robot. We also drop those.
930@@ -470,8 +467,8 @@
931 >>> msgid in foo_handler.handledMails
932 False
933
934- >>> len(stub.test_emails)
935- 0
936+ >>> pop_notifications()
937+ []
938
939
940 .. Doctest cleanup
941
942=== modified file 'lib/lp/soyuz/tests/test_packageupload.py'
943--- lib/lp/soyuz/tests/test_packageupload.py 2015-09-04 12:19:07 +0000
944+++ lib/lp/soyuz/tests/test_packageupload.py 2015-09-10 16:19:01 +0000
945@@ -29,7 +29,6 @@
946 from lp.services.job.interfaces.job import JobStatus
947 from lp.services.job.runner import JobRunner
948 from lp.services.librarian.browser import ProxiedLibraryFileAlias
949-from lp.services.mail import stub
950 from lp.soyuz.adapters.overrides import SourceOverride
951 from lp.soyuz.enums import (
952 PackagePublishingStatus,
953@@ -192,10 +191,11 @@
954 with dbuser(config.IPackageUploadNotificationJobSource.dbuser):
955 JobRunner([job]).runAll()
956
957- def assertEmail(self, expected_to_addrs):
958- """Pop an email from the stub queue and check its recipients."""
959- _, to_addrs, _ = stub.test_emails.pop()
960- self.assertEqual(expected_to_addrs, to_addrs)
961+ def assertEmails(self, expected_to_addrs):
962+ """Pop emails from the stub queue and check their recipients."""
963+ notifications = self.assertEmailQueueLength(len(expected_to_addrs))
964+ for expected_to_addr, msg in zip(expected_to_addrs, notifications):
965+ self.assertEqual(expected_to_addr, msg["X-Envelope-To"])
966
967 def test_acceptFromQueue_source_sends_email(self):
968 # Accepting a source package sends emails to the announcement list
969@@ -204,10 +204,11 @@
970 upload, uploader = self.makeSourcePackageUpload()
971 upload.acceptFromQueue()
972 self.runPackageUploadNotificationJob()
973- self.assertEqual(2, len(stub.test_emails))
974 # Emails sent are the uploader's notification and the announcement:
975- self.assertEmail([uploader.preferredemail.email])
976- self.assertEmail(["autotest_changes@ubuntu.com"])
977+ self.assertEmails([
978+ uploader.preferredemail.email,
979+ "autotest_changes@ubuntu.com",
980+ ])
981
982 def test_acceptFromQueue_source_backports_sends_no_announcement(self):
983 # Accepting a source package into BACKPORTS does not send an
984@@ -219,10 +220,9 @@
985 pocket=PackagePublishingPocket.BACKPORTS)
986 upload.acceptFromQueue()
987 self.runPackageUploadNotificationJob()
988- self.assertEqual(1, len(stub.test_emails))
989 # Only one email is sent, to the person in the changed-by field. No
990 # announcement email is sent.
991- self.assertEmail([uploader.preferredemail.email])
992+ self.assertEmails([uploader.preferredemail.email])
993
994 def test_acceptFromQueue_source_translations_sends_no_email(self):
995 # Accepting source packages in the "translations" section (i.e.
996@@ -235,7 +235,7 @@
997 upload.acceptFromQueue()
998 self.runPackageUploadNotificationJob()
999 self.assertEqual("DONE", upload.status.name)
1000- self.assertEqual(0, len(stub.test_emails))
1001+ self.assertEmailQueueLength(0)
1002
1003 def test_acceptFromQueue_source_creates_builds(self):
1004 # Accepting a source package creates build records.
1005@@ -281,7 +281,7 @@
1006 upload, _ = self.makeBuildPackageUpload()
1007 upload.acceptFromQueue()
1008 self.runPackageUploadNotificationJob()
1009- self.assertEqual(0, len(stub.test_emails))
1010+ self.assertEmailQueueLength(0)
1011
1012 def test_acceptFromQueue_handles_duplicates(self):
1013 # Duplicate queue entries are handled sensibly.
1014@@ -329,8 +329,7 @@
1015 upload, uploader = self.makeSourcePackageUpload()
1016 upload.rejectFromQueue(self.factory.makePerson())
1017 self.runPackageUploadNotificationJob()
1018- self.assertEqual(1, len(stub.test_emails))
1019- self.assertEmail([uploader.preferredemail.email])
1020+ self.assertEmails([uploader.preferredemail.email])
1021
1022 def test_rejectFromQueue_binary_sends_email(self):
1023 # Rejecting a binary package sends an email to the uploader.
1024@@ -338,8 +337,7 @@
1025 upload, uploader = self.makeBuildPackageUpload()
1026 upload.rejectFromQueue(self.factory.makePerson())
1027 self.runPackageUploadNotificationJob()
1028- self.assertEqual(1, len(stub.test_emails))
1029- self.assertEmail([uploader.preferredemail.email])
1030+ self.assertEmails([uploader.preferredemail.email])
1031
1032 def test_rejectFromQueue_source_translations_sends_no_email(self):
1033 # Rejecting a language pack sends no email.
1034@@ -350,7 +348,7 @@
1035 section_name="translations")
1036 upload.rejectFromQueue(self.factory.makePerson())
1037 self.runPackageUploadNotificationJob()
1038- self.assertEqual(0, len(stub.test_emails))
1039+ self.assertEmailQueueLength(0)
1040
1041 def test_rejectFromQueue_source_with_reason(self):
1042 # Rejecting a source package with a reason includes it in the email to
1043@@ -360,10 +358,10 @@
1044 person = self.factory.makePerson()
1045 upload.rejectFromQueue(user=person, comment='Because.')
1046 self.runPackageUploadNotificationJob()
1047- self.assertEqual(1, len(stub.test_emails))
1048+ [msg] = self.assertEmailQueueLength(1)
1049 self.assertIn(
1050 'Rejected:\nRejected by %s: Because.' % person.displayname,
1051- stub.test_emails[0][-1])
1052+ str(msg))
1053
1054
1055 class TestPackageUploadSecurity(TestCaseWithFactory):
1056
1057=== modified file 'lib/lp/testing/__init__.py'
1058--- lib/lp/testing/__init__.py 2015-08-03 12:59:18 +0000
1059+++ lib/lp/testing/__init__.py 2015-09-10 16:19:01 +0000
1060@@ -1,4 +1,4 @@
1061-# Copyright 2009-2011 Canonical Ltd. This software is licensed under the
1062+# Copyright 2009-2015 Canonical Ltd. This software is licensed under the
1063 # GNU Affero General Public License version 3 (see the file LICENSE).
1064
1065 from __future__ import absolute_import
1066@@ -184,6 +184,7 @@
1067 from lp.testing.dbuser import switch_dbuser
1068 from lp.testing.fixture import CaptureOops
1069 from lp.testing.karma import KarmaRecorder
1070+from lp.testing.mail_helpers import pop_notifications
1071
1072 # The following names have been imported for the purpose of being
1073 # exported. They are referred to here to silence lint warnings.
1074@@ -744,6 +745,19 @@
1075 sorted(
1076 expected_permissions[permission] - attribute_names)))
1077
1078+ def assertEmailQueueLength(self, length, sort_key=None):
1079+ """Pop the email queue, assert its length, and return it.
1080+
1081+ This commits the transaction as part of pop_notifications.
1082+ """
1083+ notifications = pop_notifications(sort_key=sort_key)
1084+ self.assertEqual(
1085+ length, len(notifications),
1086+ "Expected %d emails, got %d:\n\n%s" % (
1087+ length, len(notifications),
1088+ "\n\n".join(str(n) for n in notifications)))
1089+ return notifications
1090+
1091
1092 class TestCaseWithFactory(TestCase):
1093