Merge lp:~jtv/launchpad/pre-876594 into lp:launchpad

Proposed by Jeroen T. Vermeulen
Status: Merged
Approved by: Jeroen T. Vermeulen
Approved revision: no longer in the source branch.
Merged at revision: 14202
Proposed branch: lp:~jtv/launchpad/pre-876594
Merge into: lp:launchpad
Diff against target: 646 lines (+203/-174)
7 files modified
lib/lp/archiveuploader/tests/nascentupload-announcements.txt (+4/-4)
lib/lp/soyuz/adapters/notification.py (+82/-78)
lib/lp/soyuz/adapters/tests/test_notification.py (+70/-66)
lib/lp/soyuz/doc/distroseriesqueue-dist-upgrader.txt (+2/-0)
lib/lp/soyuz/doc/distroseriesqueue-notify.txt (+40/-19)
lib/lp/soyuz/model/queue.py (+1/-1)
lib/lp/soyuz/scripts/packagecopier.py (+4/-6)
To merge this branch: bzr merge lp:~jtv/launchpad/pre-876594
Reviewer Review Type Date Requested Status
Robert Collins (community) Approve
Review via email: mp+80537@code.launchpad.net

Commit message

[r=lifeless][no-qa] Cleanups for package upload notification.

Description of the change

= Summary =

We need to make some changes to Soyuz upload notification. Actually this also covers copying, building, and syncing of packages.

Unfortunately the code has become a bit of a mess and some of the tests are painfully brittle.

== Proposed fix ==

Before fixing the bug itself, clean up the code. Make tests independent of arbitrary ordering of email addressee lists; eliminate needless repetition of code; make the name of the function we'll be changing a bit more specific; make complicated code a bit more concise so we can reason about it more effectively.

== Pre-implementation notes ==

Discussed the bigger picture with Julian. We're going to exempt package maintainers and authors from notification for packages that are being accepted into different archives than their releases are in. We'll also want to see if we can remove complexity from notify() that reflects knowledge inherent in the callsite.

== Implementation details ==

The recipients-gathering code no longer checks for various recipients being the same. Instead it just lumps all recipients into a set. Duplicates disappear as a matter of course.

This will affect the logging a bit: the code will now happily say it's adding both the maintainer and the last author to the set of recipients, even if they're the same person. It may actually be clearer; certainly the code becomes easier to manage.

== Tests ==

{{{
./bin/test -vvc lp.archiveuploader
./bin/test -vvc lp.soyuz
}}}

== Demo and Q/A ==

There are no functional changes, so as long as nothing oopses, it should be fine.

= Launchpad lint =

I'll address the lint in distroseriesqueue-notify.txt in a separate lint branch. The other stuff is a bit harder to clean up because it involves whitespace-sensitive tests.

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/soyuz/scripts/packagecopier.py
  lib/lp/soyuz/model/queue.py
  lib/lp/archiveuploader/tests/nascentupload-announcements.txt
  lib/lp/soyuz/doc/distroseriesqueue-dist-upgrader.txt
  lib/lp/soyuz/adapters/notification.py
  lib/lp/soyuz/doc/distroseriesqueue-notify.txt
  lib/lp/soyuz/adapters/tests/test_notification.py

./lib/lp/archiveuploader/tests/nascentupload-announcements.txt
     186: want exceeds 78 characters.
     187: want exceeds 78 characters.
     659: want exceeds 78 characters.
     726: want exceeds 78 characters.
     728: want exceeds 78 characters.
     774: want exceeds 78 characters.
     776: want exceeds 78 characters.
./lib/lp/soyuz/doc/distroseriesqueue-dist-upgrader.txt
     145: want exceeds 78 characters.
     148: want exceeds 78 characters.
./lib/lp/soyuz/doc/distroseriesqueue-notify.txt
       1: narrative uses a moin header.
       6: source has bad indentation.
       9: narrative has trailing whitespace.
      10: narrative has trailing whitespace.
      13: source has bad indentation.
      21: source exceeds 78 characters.
      21: source has bad indentation.
      41: source has bad indentation.
      60: source has bad indentation.
      67: source has bad indentation.
      74: source has bad indentation.
      79: source has bad indentation.
      84: source has bad indentation.
     102: narrative has trailing whitespace.
     103: narrative has trailing whitespace.
     106: source has bad indentation.
     113: source has bad indentation.
     133: source has bad indentation.
     140: source has bad indentation.
     144: source has bad indentation.
     150: source has bad indentation.
     155: source has bad indentation.
     161: source has bad indentation.
     168: source has bad indentation.
     173: source has bad indentation.
     178: source has bad indentation.
     200: source has bad indentation.
     218: source has bad indentation.
     222: source has bad indentation.
     229: source has bad indentation.
     234: source has bad indentation.
     256: source has bad indentation.
     277: source has bad indentation.
     283: source has bad indentation.
./lib/lp/soyuz/adapters/tests/test_notification.py
     217: Line exceeds 78 characters.
     218: Line exceeds 78 characters.
     222: Line exceeds 78 characters.
     217: E501 line too long (85 characters)
     218: E501 line too long (82 characters)
     222: E501 line too long (82 characters)

To post a comment you must log in.
Revision history for this message
Robert Collins (lifeless) wrote :

10 + ... for entry in sorted([addr.strip() for addr in field.split(',')]):
11 + ... print entry

and also in
509 + >>> def extract_addresses(header_field):

might be better as
for entry in sorted(re.split(field, ' \t\r\n,')):

86 + # Some syncs (e.g. from Debian) will involve packages whose
87 + # changed-by person was auto-created in LP and hence does not have a
88 + # preferred email address set.
89 + changedby_person = email_to_person(info['changedby'])

The comment here now looks wonky - there is no special casing happening, so perhaps it should move to email_to_person?

I think this existing comment:
196 # Now filter list of recipients for persons only registered in
197 # Launchpad to avoid spamming the innocent.

means 'Do not email persons that have not configured a preferred email - e.g. folk listed in Launchpad who have not activated their account'. If so, and if my phrasing is clearer, feel free to update it.

For this:
231 + if fullemail is None or fullemail == '':
232 + return None
I would write 'if not fullemail:' - thats what __nonzero__ is for, and its correct here. Our style guide was updated a while ago to remove the admonition that this was bad (its not).

person_to_email looks like something other bits of code (e.g. incoming-mail-processing) will want/need; since you are here you could consider deduping.

Looks fine to me, these are all tweaks.

review: Approve
Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :

Thanks for the fast review.

> 10 + ... for entry in sorted([addr.strip() for addr in
> field.split(',')]):
> 11 + ... print entry
>
> and also in
> 509 + >>> def extract_addresses(header_field):
>
> might be better as
> for entry in sorted(re.split(field, ' \t\r\n,')):

Alas, no. The field may contain a full name, so it generally contains spaces that should not be split.

> 86 + # Some syncs (e.g. from Debian) will involve packages whose
> 87 + # changed-by person was auto-created in LP and hence does not have a
> 88 + # preferred email address set.
> 89 + changedby_person = email_to_person(info['changedby'])
>
> The comment here now looks wonky - there is no special casing happening, so
> perhaps it should move to email_to_person?

Doesn't really make sense to talk about the changed-by author there. Instead, I extended the comment to say that in that situation email_to_person will return None. Puts the rest of the comment in perspective.

> I think this existing comment:
> 196 # Now filter list of recipients for persons only registered in
> 197 # Launchpad to avoid spamming the innocent.
>
> means 'Do not email persons that have not configured a preferred email - e.g.
> folk listed in Launchpad who have not activated their account'. If so, and if
> my phrasing is clearer, feel free to update it.

Did that.

> For this:
> 231 + if fullemail is None or fullemail == '':
> 232 + return None
> I would write 'if not fullemail:' - thats what __nonzero__ is for, and its
> correct here. Our style guide was updated a while ago to remove the admonition
> that this was bad (its not).

Certainly easier. Changed.

> person_to_email looks like something other bits of code (e.g. incoming-mail-
> processing) will want/need; since you are here you could consider deduping.

Probably not worth the trouble for a simple two-liner.

Jeroen

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/archiveuploader/tests/nascentupload-announcements.txt'
2--- lib/lp/archiveuploader/tests/nascentupload-announcements.txt 2011-08-22 04:38:02 +0000
3+++ lib/lp/archiveuploader/tests/nascentupload-announcements.txt 2011-10-27 16:44:31 +0000
4@@ -46,8 +46,8 @@
5 ... return cmp(a[1], b[1])
6
7 >>> def print_addrlist(field):
8- ... for entry in field.split(','):
9- ... print entry.strip()
10+ ... for entry in sorted([addr.strip() for addr in field.split(',')]):
11+ ... print entry
12
13 Import the test keys to use 'insecure' policy.
14
15@@ -443,8 +443,8 @@
16 'Launchpad actually'
17
18 >>> print_addrlist(notification['To'])
19+ Daniel Silverstone <daniel.silverstone@canonical.com>
20 Foo Bar <foo.bar@canonical.com>
21- Daniel Silverstone <daniel.silverstone@canonical.com>
22
23 >>> notification['Subject']
24 '[ubuntu/hoary-updates] bar 1.0-2 (Waiting for approval)'
25@@ -479,8 +479,8 @@
26 'Launchpad actually'
27
28 >>> print_addrlist(notification['To'])
29+ Daniel Silverstone <daniel.silverstone@canonical.com>
30 Foo Bar <foo.bar@canonical.com>
31- Daniel Silverstone <daniel.silverstone@canonical.com>
32
33 >>> notification['Subject']
34 '[ubuntu/hoary-backports] bar 1.0-3 (Waiting for approval)'
35
36=== modified file 'lib/lp/soyuz/adapters/notification.py'
37--- lib/lp/soyuz/adapters/notification.py 2011-08-31 05:06:54 +0000
38+++ lib/lp/soyuz/adapters/notification.py 2011-10-27 16:44:31 +0000
39@@ -6,7 +6,6 @@
40 __metaclass__ = type
41
42 __all__ = [
43- 'get_recipients', # Available for testing only.
44 'notify',
45 ]
46
47@@ -68,7 +67,7 @@
48 }
49 template = get_template(archive, 'rejected')
50 body = template % information
51- to_addrs = get_recipients(
52+ to_addrs = get_upload_notification_recipients(
53 blamer, archive, distroseries, logger, changes=changes)
54 debug(logger, "Sending rejection email.")
55 if not to_addrs:
56@@ -128,7 +127,7 @@
57 summary_text=None, changes=None, changesfile_content=None,
58 changesfile_object=None, action=None, dry_run=False,
59 logger=None, announce_from_person=None, previous_version=None):
60- """Notify about
61+ """Notify about an upload or package copy.
62
63 :param blamer: The `IPerson` who is to blame for this notification.
64 :param spr: The `ISourcePackageRelease` that was created.
65@@ -187,7 +186,7 @@
66 if not files and action != 'rejected':
67 return
68
69- recipients = get_recipients(
70+ recipients = get_upload_notification_recipients(
71 blamer, archive, distroseries, logger, changes=changes, spr=spr,
72 bprs=bprs)
73
74@@ -304,13 +303,12 @@
75 if distroseries.changeslist:
76 information['ANNOUNCE'] = "Announcing to %s" % (
77 distroseries.changeslist)
78- try:
79- changedby_person = email_to_person(info['changedby'])
80- except ParseMaintError:
81- # Some syncs (e.g. from Debian) will involve packages whose
82- # changed-by person was auto-created in LP and hence does not
83- # have a preferred email address set.
84- changedby_person = None
85+
86+ # Some syncs (e.g. from Debian) will involve packages whose
87+ # changed-by person was auto-created in LP and hence does not have a
88+ # preferred email address set. We'll get a None here.
89+ changedby_person = email_to_person(info['changedby'])
90+
91 if blamer is not None and blamer != changedby_person:
92 signer_signature = person_to_email(blamer)
93 if signer_signature != info['changedby']:
94@@ -448,73 +446,69 @@
95 return guess_encoding(s)
96
97
98-def debug(logger, msg):
99+def debug(logger, msg, *args, **kwargs):
100 """Shorthand debug notation for publish() methods."""
101 if logger is not None:
102- logger.debug(msg)
103-
104-
105-def get_recipients(blamer, archive, distroseries, logger, changes=None,
106- spr=None, bprs=None):
107+ logger.debug(msg, *args, **kwargs)
108+
109+
110+def is_valid_uploader(person, distribution):
111+ """Is `person` an uploader for `distribution`?
112+
113+ A `None` person is not an uploader.
114+ """
115+ if person is None:
116+ return None
117+ else:
118+ return person.isUploader(distribution)
119+
120+
121+def get_upload_notification_recipients(blamer, archive, distroseries,
122+ logger=None, changes=None, spr=None,
123+ bprs=None):
124 """Return a list of recipients for notification emails."""
125- candidate_recipients = []
126 debug(logger, "Building recipients list.")
127+ candidate_recipients = [
128+ blamer,
129+ ]
130 info = fetch_information(spr, bprs, changes)
131
132- if info['changedby']:
133- try:
134- changer = email_to_person(info['changedby'])
135- except ParseMaintError:
136- changer = None
137- else:
138- changer = None
139-
140- if info['maintainer']:
141- try:
142- maintainer = email_to_person(info['maintainer'])
143- except ParseMaintError:
144- maintainer = None
145- else:
146- maintainer = None
147-
148- if blamer:
149- # This is a signed upload.
150- candidate_recipients.append(blamer)
151- else:
152- debug(logger,
153- "Changes file is unsigned, adding changer as recipient")
154+ changer = email_to_person(info['changedby'])
155+ maintainer = email_to_person(info['maintainer'])
156+
157+ if blamer is None:
158+ debug(
159+ logger, "Changes file is unsigned; adding changer as recipient.")
160 candidate_recipients.append(changer)
161
162 if archive.is_ppa:
163 # For PPAs, any person or team mentioned explicitly in the
164 # ArchivePermissions as uploaders for the archive will also
165 # get emailed.
166- uploaders = [
167- permission.person for permission in
168- archive.getUploadersForComponent()]
169- candidate_recipients.extend(uploaders)
170-
171- # If this is not a PPA, we also consider maintainer and changed-by.
172- elif blamer is not None:
173- if (maintainer and maintainer != blamer and
174- maintainer.isUploader(distroseries.distribution)):
175- debug(logger, "Adding maintainer to recipients")
176- candidate_recipients.append(maintainer)
177-
178- if (changer and changer != blamer and
179- changer.isUploader(distroseries.distribution)):
180- debug(logger, "Adding changed-by to recipients")
181- candidate_recipients.append(changer)
182-
183- # Now filter list of recipients for persons only registered in
184- # Launchpad to avoid spamming the innocent.
185- recipients = []
186- for person in candidate_recipients:
187- if person is None or person.preferredemail is None:
188- continue
189- recipient = format_address_for_person(person)
190- debug(logger, "Adding recipient: '%s'" % recipient)
191- recipients.append(recipient)
192+ candidate_recipients.extend([
193+ permission.person
194+ for permission in archive.getUploadersForComponent()])
195+ else:
196+ # If this is not a PPA, we also consider maintainer and changed-by.
197+ if blamer is not None:
198+ if is_valid_uploader(maintainer, distroseries.distribution):
199+ debug(logger, "Adding maintainer to recipients")
200+ candidate_recipients.append(maintainer)
201+
202+ if is_valid_uploader(changer, distroseries.distribution):
203+ debug(logger, "Adding changed-by to recipients")
204+ candidate_recipients.append(changer)
205+
206+ # Collect email addresses to notify. Skip persons who do not have a
207+ # preferredemail set, such as people who have not activated their
208+ # Launchpad accounts (and are therefore not expecting this email).
209+ recipients = [
210+ format_address_for_person(person)
211+ for person in filter(None, set(candidate_recipients))
212+ if person.preferredemail is not None]
213+
214+ for recipient in recipients:
215+ debug(logger, "Adding recipient: '%s'", recipient)
216
217 return recipients
218
219@@ -572,18 +566,29 @@
220
221
222 def email_to_person(fullemail):
223- """Return an IPerson given an RFC2047 email address."""
224- # The 2nd arg to s_f_m() doesn't matter as it won't fail since every-
225- # thing will have already parsed at this point.
226- (rfc822, rfc2047, name, email) = safe_fix_maintainer(
227- fullemail, "email")
228- return getUtility(IPersonSet).getByEmail(email)
229+ """Return an `IPerson` given an RFC2047 email address.
230+
231+ :param fullemail: Potential email address.
232+ :return: `IPerson` with the given email address. None if there
233+ isn't one, or if `fullemail` isn't a proper email address.
234+ """
235+ if not fullemail:
236+ return None
237+
238+ try:
239+ # The 2nd arg to s_f_m() doesn't matter as it won't fail since every-
240+ # thing will have already parsed at this point.
241+ (rfc822, rfc2047, name, email) = safe_fix_maintainer(
242+ fullemail, "email")
243+ return getUtility(IPersonSet).getByEmail(email)
244+ except ParseMaintError:
245+ return None
246
247
248 def person_to_email(person):
249 """Return a string of full name <e-mail address> given an IPerson."""
250- # This will use email.Header to encode any unicode.
251 if person and person.preferredemail:
252+ # This will use email.Header to encode any non-ASCII characters.
253 return format_address_for_person(person)
254
255
256@@ -595,12 +600,11 @@
257 user (archive@ubuntu.com).
258 """
259 katie = getUtility(ILaunchpadCelebrities).katie
260- try:
261- changed_by = email_to_person(changed_by_email)
262- except ParseMaintError:
263- return False
264+ changed_by = email_to_person(changed_by_email)
265 return (
266- spr and not bprs and changed_by == katie and
267+ spr and
268+ not bprs and
269+ changed_by == katie and
270 pocket != PackagePublishingPocket.SECURITY)
271
272
273
274=== modified file 'lib/lp/soyuz/adapters/tests/test_notification.py'
275--- lib/lp/soyuz/adapters/tests/test_notification.py 2011-08-31 05:06:54 +0000
276+++ lib/lp/soyuz/adapters/tests/test_notification.py 2011-10-27 16:44:31 +0000
277@@ -6,8 +6,9 @@
278 # GNU Affero General Public License version 3 (see the file LICENSE).
279
280 from email.utils import formataddr
281+from textwrap import dedent
282+
283 from storm.store import Store
284-from textwrap import dedent
285 from zope.component import getUtility
286 from zope.security.proxy import removeSecurityProxy
287
288@@ -18,24 +19,24 @@
289 )
290 from lp.archivepublisher.utils import get_ppa_reference
291 from lp.registry.interfaces.pocket import PackagePublishingPocket
292+from lp.services.log.logger import BufferLogger
293 from lp.services.mail.sendmail import format_address_for_person
294-from lp.services.log.logger import BufferLogger
295 from lp.soyuz.adapters.notification import (
296 assemble_body,
297 calculate_subject,
298- get_recipients,
299 fetch_information,
300+ get_upload_notification_recipients,
301 is_auto_sync_upload,
302+ notify,
303+ person_to_email,
304 reject_changes_file,
305- person_to_email,
306- notify,
307 )
308-from lp.soyuz.interfaces.component import IComponentSet
309-from lp.soyuz.model.component import ComponentSelection
310 from lp.soyuz.enums import (
311 ArchivePurpose,
312 PackageUploadCustomFormat,
313 )
314+from lp.soyuz.interfaces.component import IComponentSet
315+from lp.soyuz.model.component import ComponentSelection
316 from lp.soyuz.model.distroseriessourcepackagerelease import (
317 DistroSeriesSourcePackageRelease,
318 )
319@@ -231,8 +232,8 @@
320 def test_fetch_information_changes(self):
321 changes = {
322 'Date': '2001-01-01',
323- 'Changed-By': 'Foo Bar <foo.bar@canonical.com>',
324- 'Maintainer': 'Foo Bar <foo.bar@canonical.com>',
325+ 'Changed-By': 'Foo Bar <foo.bar@example.com>',
326+ 'Maintainer': 'Foo Bar <foo.bar@example.com>',
327 'Changes': ' * Foo!',
328 }
329 info = fetch_information(
330@@ -246,7 +247,7 @@
331 info['maintainer_displayname'],
332 ]
333 for field in fields:
334- self.assertEqual('Foo Bar <foo.bar@canonical.com>', field)
335+ self.assertEqual('Foo Bar <foo.bar@example.com>', field)
336
337 def test_fetch_information_spr(self):
338 creator = self.factory.makePerson(displayname=u"foø")
339@@ -365,64 +366,67 @@
340 distroseries=distroseries, component=component))
341 archive.newComponentUploader(maintainer, component)
342 archive.newComponentUploader(changer, component)
343- return get_recipients(
344+ return get_upload_notification_recipients(
345 blamer, archive, distroseries, logger=None, changes=changes)
346
347- def test_get_recipients_good_emails(self):
348- # Test get_recipients with good email addresses..
349- blamer = self.factory.makePerson()
350- maintainer = self.factory.makePerson(
351- 'maintainer@canonical.com', displayname='Maintainer')
352- changer = self.factory.makePerson(
353- 'changer@canonical.com', displayname='Changer')
354- changes = {
355- 'Date': '2001-01-01',
356- 'Changed-By': 'Changer <changer@canonical.com>',
357- 'Maintainer': 'Maintainer <maintainer@canonical.com>',
358- 'Changes': ' * Foo!',
359- }
360- recipients = self._run_recipients_test(
361- changes, blamer, maintainer, changer)
362- expected = [format_address_for_person(p)
363- for p in (blamer, maintainer, changer)]
364- self.assertEqual(expected, recipients)
365-
366- def test_get_recipients_bad_maintainer_email(self):
367- blamer = self.factory.makePerson()
368- maintainer = self.factory.makePerson(
369- 'maintainer@canonical.com', displayname='Maintainer')
370- changer = self.factory.makePerson(
371- 'changer@canonical.com', displayname='Changer')
372- changes = {
373- 'Date': '2001-01-01',
374- 'Changed-By': 'Changer <changer@canonical.com>',
375- 'Maintainer': 'Maintainer <maintainer at canonical.com>',
376- 'Changes': ' * Foo!',
377- }
378- recipients = self._run_recipients_test(
379- changes, blamer, maintainer, changer)
380- expected = [format_address_for_person(p)
381- for p in (blamer, changer)]
382- self.assertEqual(expected, recipients)
383-
384- def test_get_recipients_bad_changedby_email(self):
385- # Test get_recipients with invalid changedby email address.
386- blamer = self.factory.makePerson()
387- maintainer = self.factory.makePerson(
388- 'maintainer@canonical.com', displayname='Maintainer')
389- changer = self.factory.makePerson(
390- 'changer@canonical.com', displayname='Changer')
391- changes = {
392- 'Date': '2001-01-01',
393- 'Changed-By': 'Changer <changer at canonical.com>',
394- 'Maintainer': 'Maintainer <maintainer@canonical.com>',
395- 'Changes': ' * Foo!',
396- }
397- recipients = self._run_recipients_test(
398- changes, blamer, maintainer, changer)
399- expected = [format_address_for_person(p)
400- for p in (blamer, maintainer)]
401- self.assertEqual(expected, recipients)
402+ def test_get_upload_notification_recipients_good_emails(self):
403+ # Test get_upload_notification_recipients with good email addresses..
404+ blamer = self.factory.makePerson()
405+ maintainer = self.factory.makePerson(
406+ 'maintainer@example.com', displayname='Maintainer')
407+ changer = self.factory.makePerson(
408+ 'changer@example.com', displayname='Changer')
409+ changes = {
410+ 'Date': '2001-01-01',
411+ 'Changed-By': 'Changer <changer@example.com>',
412+ 'Maintainer': 'Maintainer <maintainer@example.com>',
413+ 'Changes': ' * Foo!',
414+ }
415+ recipients = self._run_recipients_test(
416+ changes, blamer, maintainer, changer)
417+ expected = [
418+ format_address_for_person(person)
419+ for person in (blamer, maintainer, changer)]
420+ self.assertContentEqual(expected, recipients)
421+
422+ def test_get_upload_notification_recipients_bad_maintainer_email(self):
423+ blamer = self.factory.makePerson()
424+ maintainer = self.factory.makePerson(
425+ 'maintainer@example.com', displayname='Maintainer')
426+ changer = self.factory.makePerson(
427+ 'changer@example.com', displayname='Changer')
428+ changes = {
429+ 'Date': '2001-01-01',
430+ 'Changed-By': 'Changer <changer@example.com>',
431+ 'Maintainer': 'Maintainer <maintainer at example.com>',
432+ 'Changes': ' * Foo!',
433+ }
434+ recipients = self._run_recipients_test(
435+ changes, blamer, maintainer, changer)
436+ expected = [
437+ format_address_for_person(person) for person in (blamer, changer)]
438+ self.assertContentEqual(expected, recipients)
439+
440+ def test_get_upload_notification_recipients_bad_changedby_email(self):
441+ # Test get_upload_notification_recipients with invalid changedby
442+ # email address.
443+ blamer = self.factory.makePerson()
444+ maintainer = self.factory.makePerson(
445+ 'maintainer@example.com', displayname='Maintainer')
446+ changer = self.factory.makePerson(
447+ 'changer@example.com', displayname='Changer')
448+ changes = {
449+ 'Date': '2001-01-01',
450+ 'Changed-By': 'Changer <changer at example.com>',
451+ 'Maintainer': 'Maintainer <maintainer@example.com>',
452+ 'Changes': ' * Foo!',
453+ }
454+ recipients = self._run_recipients_test(
455+ changes, blamer, maintainer, changer)
456+ expected = [
457+ format_address_for_person(person)
458+ for person in (blamer, maintainer)]
459+ self.assertContentEqual(expected, recipients)
460
461 def test_assemble_body_handles_no_preferred_email_for_changer(self):
462 # If changer has no preferred email address,
463
464=== modified file 'lib/lp/soyuz/doc/distroseriesqueue-dist-upgrader.txt'
465--- lib/lp/soyuz/doc/distroseriesqueue-dist-upgrader.txt 2011-07-21 23:10:26 +0000
466+++ lib/lp/soyuz/doc/distroseriesqueue-dist-upgrader.txt 2011-10-27 16:44:31 +0000
467@@ -60,6 +60,7 @@
468 DEBUG Creating queue entry
469 DEBUG Setting it to ACCEPTED
470 DEBUG Building recipients list.
471+ ...
472 DEBUG Adding recipient: 'Foo Bar <foo.bar@canonical.com>'
473 DEBUG Sent a mail:
474 ...
475@@ -235,6 +236,7 @@
476 DEBUG Creating queue entry
477 DEBUG Setting it to ACCEPTED
478 DEBUG Building recipients list.
479+ ...
480 DEBUG Adding recipient: 'Foo Bar <foo.bar@canonical.com>'
481 DEBUG Sent a mail:
482 ...
483
484=== modified file 'lib/lp/soyuz/doc/distroseriesqueue-notify.txt'
485--- lib/lp/soyuz/doc/distroseriesqueue-notify.txt 2011-06-01 04:57:27 +0000
486+++ lib/lp/soyuz/doc/distroseriesqueue-notify.txt 2011-10-27 16:44:31 +0000
487@@ -45,11 +45,11 @@
488 >>> netapplet_upload.notify(
489 ... changes_file_object=changes_file, logger=FakeLogger())
490 DEBUG Building recipients list.
491- DEBUG Changes file is unsigned, adding changer as recipient
492+ DEBUG Changes file is unsigned; adding changer as recipient.
493 ...
494 DEBUG Sent a mail:
495 ...
496- DEBUG Recipients: Daniel Silverstone <daniel.silverstone@canonical.com>
497+ DEBUG Recipients: ... Silverstone ...
498 ...
499 DEBUG above if files already exist in other distroseries.
500 ...
501@@ -120,7 +120,7 @@
502 ...
503 DEBUG Sent a mail:
504 ...
505- DEBUG Recipients: Foo Bar <foo.bar@canonical.com>, Daniel Silverstone <daniel.silverstone@canonical.com>
506+ DEBUG Recipients: ... Bar ...
507 ...
508 DEBUG Announcing to autotest_changes@ubuntu.com
509 ...
510@@ -137,22 +137,41 @@
511 The mail 'To:' addresses contain the signer and the changer's email. The
512 announcement email contains the serieses changeslist.
513
514- >>> [msg['To'] for msg in msgs]
515- ['Foo Bar <foo.bar@canonical.com>,\n\tDaniel Silverstone <daniel.silverstone@canonical.com>', 'autotest_changes@ubuntu.com']
516+ >>> def to_lower(address):
517+ ... """Return lower-case version of email address."""
518+ ... return address.lower()
519+
520+ >>> def extract_addresses(header_field):
521+ ... """Extract and sort addresses from an email header field."""
522+ ... return sorted(
523+ ... [addr.strip() for addr in header_field.split(',')],
524+ ... key=to_lower)
525+
526+ >>> for addr in extract_addresses(msgs[0]['To']):
527+ ... print addr
528+ Daniel Silverstone <daniel.silverstone@canonical.com>
529+ Foo Bar <foo.bar@canonical.com>
530+
531+ >>> print msgs[1]['To']
532+ autotest_changes@ubuntu.com
533
534 The mail 'Bcc:' address is the uploader. The announcement has the uploader
535 and the Debian derivatives address for the package uploaded.
536
537- >>> [msg['Bcc'] for msg in msgs]
538- ['Root <root@localhost>', 'Root <root@localhost>, netapplet_derivatives@packages.qa.debian.org']
539+ >>> for msg in msgs:
540+ ... print extract_addresses(msg['Bcc'])
541+ ['Root <root@localhost>']
542+ ['netapplet_derivatives@packages.qa.debian.org', 'Root <root@localhost>']
543
544 The mail 'From:' addresses are the uploader and the changer.
545
546- >>> [msg['From'] for msg in msgs]
547- ['Root <root@localhost>', 'Daniel Silverstone <daniel.silverstone@canonical.com>']
548+ >>> for msg in msgs:
549+ ... print msg['From']
550+ Root <root@localhost>
551+ Daniel Silverstone <daniel.silverstone@canonical.com>
552
553- >>> notification['Subject']
554- '[ubuntu/breezy-autotest] netapplet 0.99.6-1 (New)'
555+ >>> print notification['Subject']
556+ [ubuntu/breezy-autotest] netapplet 0.99.6-1 (New)
557
558 The mail body contains the same list of files again:
559
560@@ -188,7 +207,7 @@
561 ...
562 DEBUG Sent a mail:
563 ...
564- DEBUG Recipients: Foo Bar <foo.bar@canonical.com>, Daniel Silverstone <daniel.silverstone@canonical.com>
565+ DEBUG Recipients: ... Silverstone ...
566 ...
567 DEBUG above if files already exist in other distroseries.
568 ...
569@@ -200,13 +219,15 @@
570
571 The mail headers are the same as before:
572
573- >>> notification['To']
574- 'Foo Bar <foo.bar@canonical.com>,\n\tDaniel Silverstone <daniel.silverstone@canonical.com>'
575- >>> notification['Bcc']
576- 'Root <root@localhost>'
577+ >>> for addr in extract_addresses(notification['To']):
578+ ... print addr
579+ Daniel Silverstone <daniel.silverstone@canonical.com>
580+ Foo Bar <foo.bar@canonical.com>
581+ >>> print notification['Bcc']
582+ Root <root@localhost>
583
584- >>> notification['Subject']
585- '[ubuntu/breezy-autotest] netapplet 0.99.6-1 (New)'
586+ >>> print notification['Subject']
587+ [ubuntu/breezy-autotest] netapplet 0.99.6-1 (New)
588
589 The mail body contains the same list of files again:
590
591@@ -241,7 +262,7 @@
592 ...
593 DEBUG Subject: [ubuntu/breezy-autotest] netapplet 0.99.6-1 (Rejected)
594 DEBUG Sender: Root <root@localhost>
595- DEBUG Recipients: Foo Bar <foo.bar@canonical.com>, Daniel Silverstone <daniel.silverstone@canonical.com>
596+ DEBUG Recipients: ... Bar ...
597 DEBUG Bcc: Root <root@localhost>
598 DEBUG Body:
599 DEBUG Rejected:
600
601=== modified file 'lib/lp/soyuz/model/queue.py'
602--- lib/lp/soyuz/model/queue.py 2011-08-25 08:29:37 +0000
603+++ lib/lp/soyuz/model/queue.py 2011-10-27 16:44:31 +0000
604@@ -868,7 +868,7 @@
605 signer, self.sourcepackagerelease, self.builds, self.customfiles,
606 self.archive, self.distroseries, self.pocket, summary_text,
607 changes, changesfile_content, changes_file_object,
608- status_action[self.status], dry_run, logger)
609+ status_action[self.status], dry_run=dry_run, logger=logger)
610
611 def _isPersonUploader(self, person):
612 """Return True if person is an uploader to the package's distro."""
613
614=== modified file 'lib/lp/soyuz/scripts/packagecopier.py'
615--- lib/lp/soyuz/scripts/packagecopier.py 2011-09-26 06:30:07 +0000
616+++ lib/lp/soyuz/scripts/packagecopier.py 2011-10-27 16:44:31 +0000
617@@ -533,8 +533,8 @@
618 Verifies if each copy can be performed using `CopyChecker` and
619 raises `CannotCopy` if one or more copies could not be performed.
620
621- When `CannotCopy`is raised call sites are in charge to rollback the
622- transaction or performed copies will be commited.
623+ When `CannotCopy` is raised, call sites are responsible for rolling
624+ back the transaction. Otherwise, performed copies will be commited.
625
626 Wrapper for `do_direct_copy`.
627
628@@ -611,8 +611,7 @@
629 # In zopeless mode this email will be sent immediately.
630 notify(
631 person, source.sourcepackagerelease, [], [], archive,
632- series, pocket, summary_text=error_text,
633- action='rejected')
634+ series, pocket, summary_text=error_text, action='rejected')
635 raise CannotCopy(error_text)
636
637 overrides_index = 0
638@@ -648,8 +647,7 @@
639 if send_email:
640 notify(
641 person, source.sourcepackagerelease, [], [], archive,
642- destination_series, pocket, changes=None,
643- action='accepted',
644+ destination_series, pocket, action='accepted',
645 announce_from_person=announce_from_person,
646 previous_version=old_version)
647