Merge lp:~salgado/launchpad/remove-security-upload-policy into lp:launchpad
- remove-security-upload-policy
- Merge into devel
Status: | Merged |
---|---|
Approved by: | Michael Nelson |
Approved revision: | no longer in the source branch. |
Merged at revision: | 11450 |
Proposed branch: | lp:~salgado/launchpad/remove-security-upload-policy |
Merge into: | lp:launchpad |
Diff against target: |
979 lines (+144/-722) 6 files modified
lib/lp/archiveuploader/tests/nascentupload-announcements.txt (+8/-269) lib/lp/archiveuploader/tests/nascentupload-security-uploads.txt (+0/-144) lib/lp/archiveuploader/tests/test_buildduploads.py (+130/-4) lib/lp/archiveuploader/tests/test_securityuploads.py (+0/-263) lib/lp/archiveuploader/uploadpolicy.py (+1/-24) lib/lp/soyuz/doc/soyuz-set-of-uploads.txt (+5/-18) |
To merge this branch: | bzr merge lp:~salgado/launchpad/remove-security-upload-policy |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Michael Nelson (community) | code | Approve | |
Jeroen T. Vermeulen (community) | Approve | ||
Review via email: mp+33581@code.launchpad.net |
Commit message
Description of the change
Remove SecurityUploadP
Guilherme Salgado (salgado) wrote : | # |
The change from an email do Daniel to one from Celso is in a test that
gets a random (in fact, the last one) upload and prints some details
about it. It changed because I removed the code that did an upload
right before that test.
I got back from ec2 test this morning and soyuz-set-
failing because it uses the security policy in some parts, so I'm having
yet more fun with doctests. I'll let you know once I figure out a way to
fix it -- using one of the test-specific policies that accept anything
is not working.
Guilherme Salgado (salgado) wrote : | # |
I've managed to get that test passing again, as well as fixing another one that I broke because it was importing its base class from a file that had been removed.
In the latter case I just had to copy the class from the removed file back.
For the former I removed a chunk of the test but to make sure we don't lose coverage I'll write a unit test on my vostok-
Michael Nelson (michael.nelson) wrote : | # |
Approving changes in r11428.
14:40 < salgado> noodles775, and I've added a new unit test (in the other MP) to exercise the same code that was exercised by the doctest chunk I've removed
14:40 < noodles775> salgado: The doctest snippet that you removed in r11428 (reject instead of fail for non-existent pocket), you mention that you'll cover it with a unit test in the following branch...
14:40 < noodles775> lol
14:40 < noodles775> but
14:40 < salgado> :)
14:41 < salgado> http://
14:41 < noodles775> as far as I can see, the following branch tests that an assertion is raised, but not that a rejection is sent
14:41 * noodles775 double checks, guessing that he missed it.
14:41 < salgado> that's correct
14:42 < noodles775> Is there a test elsewhere that ensures when the assertion is raised an email is sent?
14:42 < salgado> I thought just checking for the exception was good enough, as we have plenty of tests showing a rejection is sent when there's an exception
Preview Diff
1 | === modified file 'lib/lp/archiveuploader/tests/nascentupload-announcements.txt' |
2 | --- lib/lp/archiveuploader/tests/nascentupload-announcements.txt 2010-08-20 12:11:30 +0000 |
3 | +++ lib/lp/archiveuploader/tests/nascentupload-announcements.txt 2010-08-25 19:32:53 +0000 |
4 | @@ -17,13 +17,6 @@ |
5 | * AUTO-APPROVED to BACKPORTS (via sync): submitter set receives an |
6 | 'acceptance' warning ('announcement' is skipped). |
7 | |
8 | - * AUTO-APPROVED sources to SECURITY (via security): submitter set |
9 | - receives an 'acceptance' warning and the target distroseries |
10 | - changeslist address receives an 'announcement' message. |
11 | - |
12 | - * AUTO-APPROVED binaries to SECURITY (via security): submitter set |
13 | - receives only an 'acceptance' warning ('announcement' is skipped). |
14 | - |
15 | * NEW, AUTO-APPROVED or UNAPPROVED source uploads targeted to section |
16 | 'translations' (all policies, all pockets) do not generate any |
17 | messages. Remembering that NEW and UNAPPROVED messages are also |
18 | @@ -530,196 +523,6 @@ |
19 | >>> import os |
20 | >>> os.remove(os.path.join(datadir('suite/bar_1.0-4'), 'bar_1.0.orig.tar.gz')) |
21 | |
22 | -AUTO-APPROVED source upload to SECURITY pocket via 'security' policy: |
23 | - |
24 | - >>> security_policy = getPolicy( |
25 | - ... name='security', distro='ubuntu', distroseries=None) |
26 | - >>> security_policy.setDistroSeriesAndPocket('hoary-security') |
27 | - |
28 | - >>> bar_src = NascentUpload.from_changesfile_path( |
29 | - ... datadir('suite/bar_1.0-2/bar_1.0-2_source.changes'), |
30 | - ... security_policy, mock_logger_quiet) |
31 | - >>> bar_src.process() |
32 | - |
33 | - >>> bar_src.logger = mock_logger |
34 | - >>> result = bar_src.do_accept() |
35 | - DEBUG: Creating queue entry |
36 | - ... |
37 | - DEBUG: Sent a mail: |
38 | - DEBUG: Subject: [ubuntu/hoary-security] bar 1.0-2 (Accepted) |
39 | - DEBUG: Recipients: Daniel Silverstone <daniel.silverstone@canonical.com> |
40 | - DEBUG: Body: |
41 | - DEBUG: bar (1.0-2) breezy; urgency=low |
42 | - DEBUG: |
43 | - DEBUG: * A second upload to ensure that binary overrides of _all work |
44 | - DEBUG: |
45 | - DEBUG: * Also closes Launchpad bug #6 |
46 | - DEBUG: |
47 | - DEBUG: |
48 | - DEBUG: Date: Thu, 30 Mar 2006 01:36:14 +0100 |
49 | - DEBUG: Changed-By: Daniel Silverstone <daniel.silverstone@canonical.com> |
50 | - DEBUG: Maintainer: Launchpad team <launchpad@lists.canonical.com> |
51 | - DEBUG: http://launchpad.dev/ubuntu/hoary/+source/bar/1.0-2 |
52 | - DEBUG: |
53 | - DEBUG: == |
54 | - DEBUG: |
55 | - DEBUG: Announcing to hoary-announce@lists.ubuntu.com |
56 | - DEBUG: |
57 | - DEBUG: Thank you for your contribution to Ubuntu Linux. |
58 | - DEBUG: |
59 | - DEBUG: -- |
60 | - DEBUG: You are receiving this email because you are the uploader, maintainer or |
61 | - DEBUG: signer of the above package. |
62 | - DEBUG: Sent a mail: |
63 | - DEBUG: Subject: [ubuntu/hoary-security] bar 1.0-2 (Accepted) |
64 | - DEBUG: Recipients: hoary-announce@lists.ubuntu.com |
65 | - DEBUG: Body: |
66 | - DEBUG: bar (1.0-2) breezy; urgency=low |
67 | - DEBUG: |
68 | - DEBUG: * A second upload to ensure that binary overrides of _all work |
69 | - DEBUG: |
70 | - DEBUG: * Also closes Launchpad bug #6 |
71 | - DEBUG: |
72 | - DEBUG: |
73 | - DEBUG: Date: Thu, 30 Mar 2006 01:36:14 +0100 |
74 | - DEBUG: Changed-By: Daniel Silverstone <daniel.silverstone@canonical.com> |
75 | - DEBUG: Maintainer: Launchpad team <launchpad@lists.canonical.com> |
76 | - DEBUG: http://launchpad.dev/ubuntu/hoary/+source/bar/1.0-2 |
77 | - |
78 | - >>> import operator |
79 | - >>> msgs = pop_notifications(sort_key=operator.itemgetter('To')) |
80 | - >>> len(msgs) |
81 | - 2 |
82 | - |
83 | - >>> [message['To'] for message in msgs] |
84 | - ['Daniel Silverstone <daniel.silverstone@canonical.com>', |
85 | - 'hoary-announce@lists.ubuntu.com'] |
86 | - |
87 | - >>> [message['Subject'] for message in msgs] |
88 | - ['[ubuntu/hoary-security] bar 1.0-2 (Accepted)', |
89 | - '[ubuntu/hoary-security] bar 1.0-2 (Accepted)'] |
90 | - |
91 | - >>> [message['X-Katie'] for message in msgs] |
92 | - ['Launchpad actually', 'Launchpad actually'] |
93 | - |
94 | -The upload notification message has an attachment with the original changes |
95 | -file. |
96 | - |
97 | - >>> announcement = msgs[0] |
98 | - >>> attachment = announcement.get_payload()[1] |
99 | - >>> attachment['Content-Disposition'] |
100 | - 'attachment; filename="changesfile"' |
101 | - |
102 | -Here it is: |
103 | - |
104 | - >>> print attachment.as_string() # doctest: -NORMALIZE_WHITESPACE |
105 | - Content-Type: text/plain; charset="utf-8" |
106 | - MIME-Version: 1.0 |
107 | - Content-Transfer-Encoding: quoted-printable |
108 | - Content-Disposition: attachment; filename="changesfile" |
109 | - <BLANKLINE> |
110 | - -----BEGIN PGP SIGNED MESSAGE----- |
111 | - Hash: SHA1 |
112 | - <BLANKLINE> |
113 | - Format: 1.7 |
114 | - Date: Thu, 30 Mar 2006 01:36:14 +0100 |
115 | - Source: bar |
116 | - Binary: bar |
117 | - Architecture: source |
118 | - Version: 1.0-2 |
119 | - Distribution: breezy |
120 | - Urgency: low |
121 | - Maintainer: Launchpad team <launchpad@lists.canonical.com> |
122 | - Changed-By: Daniel Silverstone <daniel.silverstone@canonical.com> |
123 | - Launchpad-bugs-fixed: 6 |
124 | - Description: = |
125 | - <BLANKLINE> |
126 | - bar - Stuff for testing |
127 | - Changes: = |
128 | - <BLANKLINE> |
129 | - bar (1.0-2) breezy; urgency=3Dlow |
130 | - . |
131 | - * A second upload to ensure that binary overrides of _all work |
132 | - . = |
133 | - <BLANKLINE> |
134 | - * Also closes Launchpad bug #6 |
135 | - . |
136 | - Files: = |
137 | - <BLANKLINE> |
138 | - bbaf6fbf41cdbbdd422b0382076f615a 512 devel optional bar_1.0-2.dsc |
139 | - ac6b4efe44e31f47ec9f0d0fac6935f4 622 devel optional bar_1.0-2.diff.gz |
140 | - <BLANKLINE> |
141 | - -----BEGIN PGP SIGNATURE----- |
142 | - Version: GnuPG v1.4.6 (GNU/Linux) |
143 | - <BLANKLINE> |
144 | - iD8DBQFGe+Yjjn63CGxkqMURAuPGAJ9ub5UHjrzKnEGmUK1oCoRuOrdligCePKxt |
145 | - QRCBMda2V9lNtxldkGRtc88=3D |
146 | - =3DgtPz |
147 | - -----END PGP SIGNATURE----- |
148 | - <BLANKLINE> |
149 | - |
150 | -The upload announcement email also has the attachment with the original |
151 | -changes file. |
152 | - |
153 | - >>> announcement = msgs[1] |
154 | - >>> attachment = announcement.get_payload()[1] |
155 | - >>> attachment['Content-Disposition'] |
156 | - 'attachment; filename="changesfile"' |
157 | - |
158 | -It has the same contents as the attachment of the upload notification |
159 | -email. |
160 | - |
161 | - >>> print attachment.as_string() # doctest: -NORMALIZE_WHITESPACE |
162 | - Content-Type: text/plain; charset="utf-8" |
163 | - MIME-Version: 1.0 |
164 | - Content-Transfer-Encoding: quoted-printable |
165 | - Content-Disposition: attachment; filename="changesfile" |
166 | - <BLANKLINE> |
167 | - -----BEGIN PGP SIGNED MESSAGE----- |
168 | - Hash: SHA1 |
169 | - <BLANKLINE> |
170 | - Format: 1.7 |
171 | - Date: Thu, 30 Mar 2006 01:36:14 +0100 |
172 | - Source: bar |
173 | - Binary: bar |
174 | - Architecture: source |
175 | - Version: 1.0-2 |
176 | - Distribution: breezy |
177 | - Urgency: low |
178 | - Maintainer: Launchpad team <launchpad@lists.canonical.com> |
179 | - Changed-By: Daniel Silverstone <daniel.silverstone@canonical.com> |
180 | - Launchpad-bugs-fixed: 6 |
181 | - Description: = |
182 | - <BLANKLINE> |
183 | - bar - Stuff for testing |
184 | - Changes: = |
185 | - <BLANKLINE> |
186 | - bar (1.0-2) breezy; urgency=3Dlow |
187 | - . |
188 | - * A second upload to ensure that binary overrides of _all work |
189 | - . = |
190 | - <BLANKLINE> |
191 | - * Also closes Launchpad bug #6 |
192 | - . |
193 | - Files: = |
194 | - <BLANKLINE> |
195 | - bbaf6fbf41cdbbdd422b0382076f615a 512 devel optional bar_1.0-2.dsc |
196 | - ac6b4efe44e31f47ec9f0d0fac6935f4 622 devel optional bar_1.0-2.diff.gz |
197 | - <BLANKLINE> |
198 | - -----BEGIN PGP SIGNATURE----- |
199 | - Version: GnuPG v1.4.6 (GNU/Linux) |
200 | - <BLANKLINE> |
201 | - iD8DBQFGe+Yjjn63CGxkqMURAuPGAJ9ub5UHjrzKnEGmUK1oCoRuOrdligCePKxt |
202 | - QRCBMda2V9lNtxldkGRtc88=3D |
203 | - =3DgtPz |
204 | - -----END PGP SIGNATURE----- |
205 | - <BLANKLINE> |
206 | - |
207 | -Remove orig.tar.gz pumped from librarian to disk during the upload |
208 | -checks: |
209 | - |
210 | - >>> os.remove(os.path.join(datadir('suite/bar_1.0-2'), 'bar_1.0.orig.tar.gz')) |
211 | - |
212 | DEBIAN SYNC upload of a source via the 'sync' policy. |
213 | These uploads do not generate any announcement emails for auto-accepted |
214 | packages, just the upload notification. |
215 | @@ -762,6 +565,7 @@ |
216 | |
217 | Two emails generated: |
218 | |
219 | + >>> import operator |
220 | >>> msgs = pop_notifications(sort_key=operator.itemgetter('To')) |
221 | >>> len(msgs) |
222 | 2 |
223 | @@ -782,58 +586,6 @@ |
224 | >>> os.remove(os.path.join(datadir('suite/bar_1.0-6'), |
225 | ... 'bar_1.0.orig.tar.gz')) |
226 | |
227 | - |
228 | -AUTO-APPROVED binary upload to SECURITY pocket via 'security' policy: |
229 | - |
230 | - >>> security_policy = getPolicy( |
231 | - ... name='security', distro='ubuntu', distroseries=None) |
232 | - >>> security_policy.setDistroSeriesAndPocket('hoary-security') |
233 | - |
234 | - >>> bar_bin = NascentUpload.from_changesfile_path( |
235 | - ... datadir('suite/bar_1.0-2_binary/bar_1.0-2_i386.changes'), |
236 | - ... security_policy, mock_logger_quiet) |
237 | - >>> bar_bin.process() |
238 | - |
239 | - >>> bar_bin.logger = mock_logger |
240 | - >>> result = bar_bin.do_accept() |
241 | - DEBUG: Creating queue entry |
242 | - ... |
243 | - DEBUG: Sent a mail: |
244 | - DEBUG: Subject: [ubuntu/hoary-security] bar 1.0-2 (Accepted) |
245 | - DEBUG: Recipients: Daniel Silverstone <daniel.silverstone@canonical.com> |
246 | - DEBUG: Body: |
247 | - DEBUG: bar (1.0-2) breezy; urgency=low |
248 | - DEBUG: |
249 | - DEBUG: * A second upload to ensure that binary overrides of _all work |
250 | - DEBUG: |
251 | - DEBUG: Date: Thu, 30 Mar 2006 01:36:14 +0100 |
252 | - DEBUG: Changed-By: Daniel Silverstone <daniel.silverstone@canonical.com> |
253 | - DEBUG: Maintainer: Launchpad team <launchpad@lists.canonical.com> |
254 | - DEBUG: http://launchpad.dev/ubuntu/hoary/+source/bar/1.0-2 |
255 | - DEBUG: |
256 | - DEBUG: == |
257 | - DEBUG: |
258 | - DEBUG: Announcing to hoary-announce@lists.ubuntu.com |
259 | - DEBUG: |
260 | - DEBUG: Thank you for your contribution to Ubuntu Linux. |
261 | - DEBUG: |
262 | - DEBUG: -- |
263 | - DEBUG: You are receiving this email because you are the uploader, maintainer or |
264 | - DEBUG: signer of the above package. |
265 | - |
266 | -One email generated: |
267 | - |
268 | - >>> [notification] = pop_notifications() |
269 | - |
270 | - >>> notification['X-Katie'] |
271 | - 'Launchpad actually' |
272 | - |
273 | - >>> notification['To'] |
274 | - 'Daniel Silverstone <daniel.silverstone@canonical.com>' |
275 | - |
276 | - >>> notification['Subject'] |
277 | - '[ubuntu/hoary-security] bar 1.0-2 (Accepted)' |
278 | - |
279 | Dry run uploads should not generate any emails. Call do_accept with |
280 | notify=False: |
281 | |
282 | @@ -873,27 +625,14 @@ |
283 | DEBUG: Building recipients list. |
284 | ... |
285 | INFO: Would have sent a mail: |
286 | - INFO: Subject: [ubuntu/hoary-security] bar 1.0-2 (Accepted) |
287 | - INFO: Sender: Root <root@localhost> |
288 | - INFO: Recipients: Daniel Silverstone <daniel.silverstone@canonical.com> |
289 | - INFO: Bcc: Root <root@localhost> |
290 | - INFO: Body: |
291 | - INFO: bar (1.0-2) breezy; urgency=low |
292 | - INFO: |
293 | - INFO: * A second upload to ensure that binary overrides of _all work |
294 | - INFO: |
295 | - INFO: Date: Thu, 30 Mar 2006 01:36:14 +0100 |
296 | - INFO: Changed-By: Daniel Silverstone <daniel.silverstone@canonical.com> |
297 | - INFO: Maintainer: Launchpad team <launchpad@lists.canonical.com> |
298 | - INFO: http://launchpad.dev/ubuntu/hoary/+source/bar/1.0-2 |
299 | - INFO: |
300 | - INFO: == |
301 | - INFO: |
302 | + INFO: Subject: [ubuntu/hoary] bar 1.0-6 (Accepted) |
303 | + ... |
304 | + INFO: Recipients: Celso Providelo <celso.providelo@canonical.com> |
305 | + ... |
306 | + INFO: bar (1.0-6) breezy; urgency=low |
307 | + ... |
308 | INFO: No announcement sent |
309 | - INFO: |
310 | - INFO: Thank you for your contribution to Ubuntu Linux. |
311 | - INFO: |
312 | - INFO: -- |
313 | + ... |
314 | INFO: You are receiving this email because you are the uploader, maintainer or |
315 | INFO: signer of the above package. |
316 | |
317 | |
318 | === removed file 'lib/lp/archiveuploader/tests/nascentupload-security-uploads.txt' |
319 | --- lib/lp/archiveuploader/tests/nascentupload-security-uploads.txt 2010-08-04 00:16:44 +0000 |
320 | +++ lib/lp/archiveuploader/tests/nascentupload-security-uploads.txt 1970-01-01 00:00:00 +0000 |
321 | @@ -1,144 +0,0 @@ |
322 | -= Fully Transactional Security Uploads = |
323 | - |
324 | -In order to allow security uploads to be transactional, i.e., either |
325 | -get published entirely, source and binaries, or get fully rejected; we |
326 | -have to allow the security uploads to produce a changesfile that |
327 | -includes the source and all binaries for the task in question. |
328 | - |
329 | -This is an extension of the already implemented mixed_mode upload, |
330 | -which previously allow the user to upload the source and one |
331 | -respective binary. |
332 | - |
333 | -While we don't have Security-in-Soyuz (s-i-s) implemented, this will |
334 | -be a more secure way to perform security uploads of binaires built in |
335 | -dak, since it guarantee that the interactions will be atomic. |
336 | - |
337 | -We need to be logged into the security framework in order to get any further |
338 | - |
339 | - >>> login('foo.bar@canonical.com') |
340 | - |
341 | -A NascentUpload is a collection of files in a directory. They |
342 | -represent what may turn out to be an acceptable upload to a launchpad |
343 | -managed archive. |
344 | - |
345 | - >>> from lp.archiveuploader.nascentupload import NascentUpload |
346 | - >>> from lp.archiveuploader.tests import ( |
347 | - ... datadir, getPolicy, mock_logger, mock_logger_quiet) |
348 | - |
349 | - >>> security_policy = getPolicy(name='security', distro='ubuntu') |
350 | - |
351 | -We are going to use 'warty/powerpc' distroarchseries and its |
352 | -respective processorfamily and processor. Let's create them |
353 | -on-the-fly: |
354 | - |
355 | - >>> from canonical.testing.layers import LaunchpadZopelessLayer |
356 | - >>> LaunchpadZopelessLayer.switchDbUser('launchpad') |
357 | - >>> from canonical.launchpad.interfaces import IDistributionSet |
358 | - >>> from canonical.launchpad.database import ( |
359 | - ... Processor, ProcessorFamily) |
360 | - |
361 | - >>> powerpc_family = ProcessorFamily.selectOneBy(name='powerpc') |
362 | - >>> powerpc_proc = Processor( |
363 | - ... family=powerpc_family, name='G4', title='foo', description='nahh') |
364 | - |
365 | - >>> ubuntu = getUtility(IDistributionSet)['ubuntu'] |
366 | - >>> warty = ubuntu['warty'] |
367 | - >>> warty_powerpc = warty.newArch('powerpc', powerpc_family, True, warty.owner) |
368 | - |
369 | - >>> import transaction |
370 | - >>> transaction.commit() |
371 | - >>> LaunchpadZopelessLayer.switchDbUser('uploader') |
372 | - |
373 | - |
374 | -== Mixed Security Upload == |
375 | - |
376 | -The upload in question contains a source and its 2 builds for i386 and |
377 | -powerpc: |
378 | - |
379 | - >>> foo_mixed_upload = NascentUpload.from_changesfile_path( |
380 | - ... datadir('suite/foo_1.0-1_multi_binary/foo_1.0-1_multi.changes'), |
381 | - ... security_policy, mock_logger_quiet) |
382 | - >>> foo_mixed_upload.process() |
383 | - |
384 | -Inspecting the files processed: |
385 | - |
386 | - >>> for file in foo_mixed_upload.changes.files: |
387 | - ... print file.filename |
388 | - foo_1.0-1.dsc |
389 | - foo_1.0.orig.tar.gz |
390 | - foo_1.0-1.diff.gz |
391 | - foo_1.0-1_i386.deb |
392 | - foo_1.0-1_powerpc.deb |
393 | - |
394 | -Perform acceptance, creating the respective PackageUpload item and children. |
395 | -Please note how the package name appears only one time in the subject line |
396 | -(although the upload has one source and two builds associated with it). |
397 | - |
398 | - >>> foo_mixed_upload.logger = mock_logger |
399 | - >>> success = foo_mixed_upload.do_accept() |
400 | - DEBUG: ... |
401 | - DEBUG: Subject: [ubuntu/warty-security] foo 1.0-1 (New) |
402 | - ... |
403 | - |
404 | - >>> foo_mixed_queue = foo_mixed_upload.queue_root |
405 | - >>> foo_mixed_queue.status.name |
406 | - 'NEW' |
407 | - |
408 | -Ensure we have only one source attaches to the PackageUpload record: |
409 | - |
410 | - >>> foo_mixed_queue.sources.count() |
411 | - 1 |
412 | - |
413 | -And it is the right one: |
414 | - |
415 | - >>> for source in foo_mixed_queue.sources: |
416 | - ... source.sourcepackagerelease.name |
417 | - u'foo' |
418 | - |
419 | -Ensure we have the two expected builds attached to the PackageUpload record: |
420 | - |
421 | - >>> foo_mixed_queue.builds.count() |
422 | - 2 |
423 | - |
424 | -And they are the correct ones: |
425 | - |
426 | - >>> for build in foo_mixed_queue.builds: |
427 | - ... build.build.title |
428 | - u'i386 build of foo 1.0-1 in ubuntu warty SECURITY' |
429 | - u'powerpc build of foo 1.0-1 in ubuntu warty SECURITY' |
430 | - |
431 | -Including the uploaded binaries |
432 | - |
433 | - >>> for build in foo_mixed_queue.builds: |
434 | - ... [(bin.name, bin.version) for bin in build.build.binarypackages] |
435 | - [(u'foo', u'1.0-1')] |
436 | - [(u'foo', u'1.0-1')] |
437 | - |
438 | - |
439 | -== Detecting Inconsistencies == |
440 | - |
441 | -NascentUpload code will be able to detect inconsistencies in a |
442 | -security upload, for example, detecting that the source and the |
443 | -binaries sent do not match. |
444 | - |
445 | - >>> bar_mixed_upload = NascentUpload.from_changesfile_path( |
446 | - ... datadir('suite/foo_1.0-1_broken_binary/bar_1.0-1_multi.changes'), |
447 | - ... security_policy, mock_logger_quiet) |
448 | - >>> bar_mixed_upload.process() |
449 | - |
450 | -Inspecting the files processed: |
451 | - |
452 | - >>> for file in bar_mixed_upload.changes.files: |
453 | - ... print file.filename |
454 | - bar_1.0-1.dsc |
455 | - bar_1.0.orig.tar.gz |
456 | - bar_1.0-1.diff.gz |
457 | - foo_1.0-1_i386.deb |
458 | - foo_1.0-1_powerpc.deb |
459 | - |
460 | - >>> bar_mixed_upload.is_rejected |
461 | - True |
462 | - |
463 | - >>> print bar_mixed_upload.rejection_message |
464 | - foo_1.0-1_i386.deb: control file lists name as 'foo', which isn't in changes file. |
465 | - foo_1.0-1_powerpc.deb: control file lists name as 'foo', which isn't in changes file. |
466 | |
467 | === modified file 'lib/lp/archiveuploader/tests/test_buildduploads.py' |
468 | --- lib/lp/archiveuploader/tests/test_buildduploads.py 2010-08-20 20:31:18 +0000 |
469 | +++ lib/lp/archiveuploader/tests/test_buildduploads.py 2010-08-25 19:32:53 +0000 |
470 | @@ -5,13 +5,139 @@ |
471 | |
472 | __metaclass__ = type |
473 | |
474 | +import os |
475 | + |
476 | +from zope.component import getUtility |
477 | + |
478 | from canonical.database.constants import UTC_NOW |
479 | from canonical.launchpad.ftests import import_public_test_keys |
480 | -from canonical.launchpad.interfaces import PackagePublishingStatus |
481 | -from lp.archiveuploader.tests.test_securityuploads import ( |
482 | - TestStagedBinaryUploadBase, |
483 | - ) |
484 | +from canonical.launchpad.interfaces import ( |
485 | + PackagePublishingStatus, |
486 | + PackageUploadStatus, |
487 | + ) |
488 | + |
489 | +from lp.archiveuploader.tests.test_uploadprocessor import ( |
490 | + TestUploadProcessorBase, |
491 | + ) |
492 | +from lp.registry.interfaces.distribution import IDistributionSet |
493 | from lp.registry.interfaces.pocket import PackagePublishingPocket |
494 | +from lp.soyuz.model.binarypackagebuild import BinaryPackageBuild |
495 | + |
496 | + |
497 | +class TestStagedBinaryUploadBase(TestUploadProcessorBase): |
498 | + name = 'baz' |
499 | + version = '1.0-1' |
500 | + distribution_name = None |
501 | + distroseries_name = None |
502 | + pocket = None |
503 | + policy = 'buildd' |
504 | + no_mails = True |
505 | + |
506 | + @property |
507 | + def distribution(self): |
508 | + return getUtility(IDistributionSet)[self.distribution_name] |
509 | + |
510 | + @property |
511 | + def distroseries(self): |
512 | + return self.distribution[self.distroseries_name] |
513 | + |
514 | + @property |
515 | + def package_name(self): |
516 | + return "%s_%s" % (self.name, self.version) |
517 | + |
518 | + @property |
519 | + def source_dir(self): |
520 | + return self.package_name |
521 | + |
522 | + @property |
523 | + def source_changesfile(self): |
524 | + return "%s_source.changes" % self.package_name |
525 | + |
526 | + @property |
527 | + def binary_dir(self): |
528 | + return "%s_binary" % self.package_name |
529 | + |
530 | + def getBinaryChangesfileFor(self, archtag): |
531 | + return "%s_%s.changes" % (self.package_name, archtag) |
532 | + |
533 | + def setUp(self): |
534 | + """Setup environment for staged binaries upload via security policy. |
535 | + |
536 | + 1. Setup queue directory and other basic attributes |
537 | + 2. Override policy options to get security policy and not send emails |
538 | + 3. Setup a common UploadProcessor with the overridden options |
539 | + 4. Store number of build present before issuing any upload |
540 | + 5. Upload the source package via security policy |
541 | + 6. Clean log messages. |
542 | + 7. Commit transaction, so the upload source can be seen. |
543 | + """ |
544 | + super(TestStagedBinaryUploadBase, self).setUp() |
545 | + self.options.context = self.policy |
546 | + self.options.nomails = self.no_mails |
547 | + # Set up the uploadprocessor with appropriate options and logger |
548 | + self.uploadprocessor = self.getUploadProcessor(self.layer.txn) |
549 | + self.builds_before_upload = BinaryPackageBuild.select().count() |
550 | + self.source_queue = None |
551 | + self._uploadSource() |
552 | + self.log.lines = [] |
553 | + self.layer.txn.commit() |
554 | + |
555 | + def assertBuildsCreated(self, amount): |
556 | + """Assert that a given 'amount' of build records was created.""" |
557 | + builds_count = BinaryPackageBuild.select().count() |
558 | + self.assertEqual( |
559 | + self.builds_before_upload + amount, builds_count) |
560 | + |
561 | + def _prepareUpload(self, upload_dir): |
562 | + """Place a copy of the upload directory into incoming queue.""" |
563 | + os.system("cp -a %s %s" % |
564 | + (os.path.join(self.test_files_dir, upload_dir), |
565 | + os.path.join(self.queue_folder, "incoming"))) |
566 | + |
567 | + def _uploadSource(self): |
568 | + """Upload and Accept (if necessary) the base source.""" |
569 | + self._prepareUpload(self.source_dir) |
570 | + self.uploadprocessor.processChangesFile( |
571 | + os.path.join(self.queue_folder, "incoming", self.source_dir), |
572 | + self.source_changesfile) |
573 | + queue_item = self.uploadprocessor.last_processed_upload.queue_root |
574 | + self.assertTrue( |
575 | + queue_item is not None, |
576 | + "Source Upload Failed\nGot: %s" % "\n".join(self.log.lines)) |
577 | + acceptable_statuses = [ |
578 | + PackageUploadStatus.NEW, |
579 | + PackageUploadStatus.UNAPPROVED, |
580 | + ] |
581 | + if queue_item.status in acceptable_statuses: |
582 | + queue_item.setAccepted() |
583 | + # Store source queue item for future use. |
584 | + self.source_queue = queue_item |
585 | + |
586 | + def _uploadBinary(self, archtag): |
587 | + """Upload the base binary. |
588 | + |
589 | + Ensure it got processed and has a respective queue record. |
590 | + Return the IBuild attached to upload. |
591 | + """ |
592 | + self._prepareUpload(self.binary_dir) |
593 | + self.uploadprocessor.processChangesFile( |
594 | + os.path.join(self.queue_folder, "incoming", self.binary_dir), |
595 | + self.getBinaryChangesfileFor(archtag)) |
596 | + queue_item = self.uploadprocessor.last_processed_upload.queue_root |
597 | + self.assertTrue( |
598 | + queue_item is not None, |
599 | + "Binary Upload Failed\nGot: %s" % "\n".join(self.log.lines)) |
600 | + self.assertEqual(queue_item.builds.count(), 1) |
601 | + return queue_item.builds[0].build |
602 | + |
603 | + def _createBuild(self, archtag): |
604 | + """Create a build record attached to the base source.""" |
605 | + spr = self.source_queue.sources[0].sourcepackagerelease |
606 | + build = spr.createBuild( |
607 | + distro_arch_series=self.distroseries[archtag], |
608 | + pocket=self.pocket, archive=self.distroseries.main_archive) |
609 | + self.layer.txn.commit() |
610 | + return build |
611 | |
612 | |
613 | class TestBuilddUploads(TestStagedBinaryUploadBase): |
614 | |
615 | === removed file 'lib/lp/archiveuploader/tests/test_securityuploads.py' |
616 | --- lib/lp/archiveuploader/tests/test_securityuploads.py 2010-08-20 20:31:18 +0000 |
617 | +++ lib/lp/archiveuploader/tests/test_securityuploads.py 1970-01-01 00:00:00 +0000 |
618 | @@ -1,263 +0,0 @@ |
619 | -# Copyright 2009-2010 Canonical Ltd. This software is licensed under the |
620 | -# GNU Affero General Public License version 3 (see the file LICENSE). |
621 | - |
622 | -"""Test security uploads use-cases.""" |
623 | - |
624 | -__metaclass__ = type |
625 | - |
626 | -import os |
627 | - |
628 | -from zope.component import getUtility |
629 | - |
630 | -from canonical.launchpad.interfaces import ( |
631 | - IDistributionSet, |
632 | - PackageUploadStatus, |
633 | - ) |
634 | -from lp.archiveuploader.tests.test_uploadprocessor import ( |
635 | - TestUploadProcessorBase, |
636 | - ) |
637 | -from lp.registry.interfaces.pocket import PackagePublishingPocket |
638 | -from lp.soyuz.model.binarypackagebuild import BinaryPackageBuild |
639 | -from lp.soyuz.model.processor import ProcessorFamily |
640 | - |
641 | - |
642 | -class TestStagedBinaryUploadBase(TestUploadProcessorBase): |
643 | - name = 'baz' |
644 | - version = '1.0-1' |
645 | - distribution_name = None |
646 | - distroseries_name = None |
647 | - pocket = None |
648 | - policy = 'buildd' |
649 | - no_mails = True |
650 | - |
651 | - @property |
652 | - def distribution(self): |
653 | - return getUtility(IDistributionSet)[self.distribution_name] |
654 | - |
655 | - @property |
656 | - def distroseries(self): |
657 | - return self.distribution[self.distroseries_name] |
658 | - |
659 | - @property |
660 | - def package_name(self): |
661 | - return "%s_%s" % (self.name, self.version) |
662 | - |
663 | - @property |
664 | - def source_dir(self): |
665 | - return self.package_name |
666 | - |
667 | - @property |
668 | - def source_changesfile(self): |
669 | - return "%s_source.changes" % self.package_name |
670 | - |
671 | - @property |
672 | - def binary_dir(self): |
673 | - return "%s_binary" % self.package_name |
674 | - |
675 | - def getBinaryChangesfileFor(self, archtag): |
676 | - return "%s_%s.changes" % (self.package_name, archtag) |
677 | - |
678 | - def setUp(self): |
679 | - """Setup environment for staged binaries upload via security policy. |
680 | - |
681 | - 1. Setup queue directory and other basic attributes |
682 | - 2. Override policy options to get security policy and not send emails |
683 | - 3. Setup a common UploadProcessor with the overridden options |
684 | - 4. Store number of build present before issuing any upload |
685 | - 5. Upload the source package via security policy |
686 | - 6. Clean log messages. |
687 | - 7. Commit transaction, so the upload source can be seen. |
688 | - """ |
689 | - super(TestStagedBinaryUploadBase, self).setUp() |
690 | - self.options.context = self.policy |
691 | - self.options.nomails = self.no_mails |
692 | - # Set up the uploadprocessor with appropriate options and logger |
693 | - self.uploadprocessor = self.getUploadProcessor(self.layer.txn) |
694 | - self.builds_before_upload = BinaryPackageBuild.select().count() |
695 | - self.source_queue = None |
696 | - self._uploadSource() |
697 | - self.log.lines = [] |
698 | - self.layer.txn.commit() |
699 | - |
700 | - def assertBuildsCreated(self, amount): |
701 | - """Assert that a given 'amount' of build records was created.""" |
702 | - builds_count = BinaryPackageBuild.select().count() |
703 | - self.assertEqual( |
704 | - self.builds_before_upload + amount, builds_count) |
705 | - |
706 | - def _prepareUpload(self, upload_dir): |
707 | - """Place a copy of the upload directory into incoming queue.""" |
708 | - os.system("cp -a %s %s" % |
709 | - (os.path.join(self.test_files_dir, upload_dir), |
710 | - os.path.join(self.queue_folder, "incoming"))) |
711 | - |
712 | - def _uploadSource(self): |
713 | - """Upload and Accept (if necessary) the base source.""" |
714 | - self._prepareUpload(self.source_dir) |
715 | - self.uploadprocessor.processChangesFile( |
716 | - os.path.join(self.queue_folder, "incoming", self.source_dir), |
717 | - self.source_changesfile) |
718 | - queue_item = self.uploadprocessor.last_processed_upload.queue_root |
719 | - self.assertTrue( |
720 | - queue_item is not None, |
721 | - "Source Upload Failed\nGot: %s" % "\n".join(self.log.lines)) |
722 | - acceptable_statuses = [ |
723 | - PackageUploadStatus.NEW, |
724 | - PackageUploadStatus.UNAPPROVED, |
725 | - ] |
726 | - if queue_item.status in acceptable_statuses: |
727 | - queue_item.setAccepted() |
728 | - # Store source queue item for future use. |
729 | - self.source_queue = queue_item |
730 | - |
731 | - def _uploadBinary(self, archtag): |
732 | - """Upload the base binary. |
733 | - |
734 | - Ensure it got processed and has a respective queue record. |
735 | - Return the IBuild attached to upload. |
736 | - """ |
737 | - self._prepareUpload(self.binary_dir) |
738 | - self.uploadprocessor.processChangesFile( |
739 | - os.path.join(self.queue_folder, "incoming", self.binary_dir), |
740 | - self.getBinaryChangesfileFor(archtag)) |
741 | - queue_item = self.uploadprocessor.last_processed_upload.queue_root |
742 | - self.assertTrue( |
743 | - queue_item is not None, |
744 | - "Binary Upload Failed\nGot: %s" % "\n".join(self.log.lines)) |
745 | - self.assertEqual(queue_item.builds.count(), 1) |
746 | - return queue_item.builds[0].build |
747 | - |
748 | - def _createBuild(self, archtag): |
749 | - """Create a build record attached to the base source.""" |
750 | - spr = self.source_queue.sources[0].sourcepackagerelease |
751 | - build = spr.createBuild( |
752 | - distro_arch_series=self.distroseries[archtag], |
753 | - pocket=self.pocket, archive=self.distroseries.main_archive) |
754 | - self.layer.txn.commit() |
755 | - return build |
756 | - |
757 | - |
758 | -class TestStagedSecurityUploads(TestStagedBinaryUploadBase): |
759 | - """Test how security uploads behave inside Soyuz. |
760 | - |
761 | - Security uploads still coming from dak system, we have special upload |
762 | - policy which allows source and binary uploads. |
763 | - |
764 | - An upload of a source and its binaries does not necessary need |
765 | - to happen in the same batch, and Soyuz is prepared to cope with it. |
766 | - |
767 | - The only mandatory condition is to process the sources first. |
768 | - |
769 | - This class will start to tests all known/possible cases using a test |
770 | - (empty) upload and its binary. |
771 | - |
772 | - * 'lib/lp/archivepublisher/tests/data/suite/baz_1.0-1/' |
773 | - * 'lib/lp/archivepublisher/tests/data/suite/baz_1.0-1_binary/' |
774 | - """ |
775 | - name = 'baz' |
776 | - version = '1.0-1' |
777 | - distribution_name = 'ubuntu' |
778 | - distroseries_name = 'warty' |
779 | - pocket = PackagePublishingPocket.SECURITY |
780 | - policy = 'security' |
781 | - no_mails = True |
782 | - |
783 | - def setUp(self): |
784 | - """Setup base class and create the required new distroarchseries.""" |
785 | - super(TestStagedSecurityUploads, self).setUp() |
786 | - distribution = getUtility(IDistributionSet).getByName( |
787 | - self.distribution_name) |
788 | - distroseries = distribution[self.distroseries.name] |
789 | - proc_family = ProcessorFamily.selectOneBy(name='amd64') |
790 | - distroseries.newArch( |
791 | - 'amd64', proc_family, True, distribution.owner) |
792 | - |
793 | - def testBuildCreation(self): |
794 | - """Check if the builds get created for a binary security uploads. |
795 | - |
796 | - That is the usual case, security binary uploads come after the |
797 | - not published (accepted) source but in the same batch. |
798 | - |
799 | - NascentUpload should create appropriate builds attached to the |
800 | - correct source for the incoming binaries. |
801 | - """ |
802 | - build_used = self._uploadBinary('i386') |
803 | - |
804 | - self.assertBuildsCreated(1) |
805 | - self.assertEqual( |
806 | - u'i386 build of baz 1.0-1 in ubuntu warty SECURITY', |
807 | - build_used.title) |
808 | - self.assertEqual('FULLYBUILT', build_used.status.name) |
809 | - |
810 | - build_used = self._uploadBinary('amd64') |
811 | - |
812 | - self.assertBuildsCreated(2) |
813 | - self.assertEqual( |
814 | - u'amd64 build of baz 1.0-1 in ubuntu warty SECURITY', |
815 | - build_used.title) |
816 | - |
817 | - self.assertEqual('FULLYBUILT', build_used.status.name) |
818 | - |
819 | - def testBuildLookup(self): |
820 | - """Check if an available build gets used when it is appropriate. |
821 | - |
822 | - It happens when the security source upload got already published |
823 | - when the binary uploads arrive. |
824 | - The queue-build has already created build records for it and |
825 | - NascentUpload should identify this condition and used them instead |
826 | - of creating new ones. |
827 | - Also verify that builds for another architecture does not got |
828 | - erroneously attached. |
829 | - """ |
830 | - build_right_candidate = self._createBuild('i386') |
831 | - build_wrong_candidate = self._createBuild('hppa') |
832 | - build_used = self._uploadBinary('i386') |
833 | - |
834 | - self.assertEqual(build_right_candidate.id, build_used.id) |
835 | - self.assertNotEqual(build_wrong_candidate.id, build_used.id) |
836 | - self.assertBuildsCreated(2) |
837 | - self.assertEqual( |
838 | - u'i386 build of baz 1.0-1 in ubuntu warty SECURITY', |
839 | - build_used.title) |
840 | - self.assertEqual('FULLYBUILT', build_used.status.name) |
841 | - |
842 | - def testCorrectBuildPassedViaCommandLine(self): |
843 | - """Check if command-line build argument gets attached correctly. |
844 | - |
845 | - It's also possible to pass an specific buildid via the command-line |
846 | - to be attached to the current upload. |
847 | - |
848 | - This is only used in 'buildd' policy and does not produce very useful |
849 | - results in 'security', however we want to check if it, at least, |
850 | - does not 'break the system' entirely. |
851 | - """ |
852 | - build_candidate = self._createBuild('i386') |
853 | - self.options.buildid = str(build_candidate.id) |
854 | - self.uploadprocessor = self.getUploadProcessor(self.layer.txn) |
855 | - |
856 | - build_used = self._uploadBinary('i386') |
857 | - |
858 | - self.assertEqual(build_candidate.id, build_used.id) |
859 | - self.assertBuildsCreated(1) |
860 | - self.assertEqual( |
861 | - u'i386 build of baz 1.0-1 in ubuntu warty SECURITY', |
862 | - build_used.title) |
863 | - |
864 | - self.assertEqual('FULLYBUILT', build_used.status.name) |
865 | - |
866 | - def testWrongBuildPassedViaCommandLine(self): |
867 | - """Check if a misapplied passed buildid is correctly identified. |
868 | - |
869 | - When we identify misapplied build, either by getting it from command |
870 | - line or by a failure in lookup methods the upload is rejected before |
871 | - anything wrong gets into the DB. |
872 | - """ |
873 | - build_candidate = self._createBuild('hppa') |
874 | - self.options.buildid = str(build_candidate.id) |
875 | - self.uploadprocessor = self.getUploadProcessor(self.layer.txn) |
876 | - |
877 | - self.assertRaises(AssertionError, self._uploadBinary, 'i386') |
878 | - |
879 | - self.assertLogContains( |
880 | - "UploadError: Attempt to upload binaries specifying build %d, " |
881 | - "where they don't fit.\n" % (build_candidate.id, )) |
882 | |
883 | === modified file 'lib/lp/archiveuploader/uploadpolicy.py' |
884 | --- lib/lp/archiveuploader/uploadpolicy.py 2010-08-20 20:31:18 +0000 |
885 | +++ lib/lp/archiveuploader/uploadpolicy.py 2010-08-25 19:32:53 +0000 |
886 | @@ -331,28 +331,6 @@ |
887 | pass |
888 | |
889 | |
890 | -class SecurityUploadPolicy(AbstractUploadPolicy): |
891 | - """The security-upload policy. |
892 | - |
893 | - It allows unsigned changes and binary uploads. |
894 | - """ |
895 | - |
896 | - name = 'security' |
897 | - |
898 | - def __init__(self): |
899 | - AbstractUploadPolicy.__init__(self) |
900 | - self.unsigned_dsc_ok = True |
901 | - self.unsigned_changes_ok = True |
902 | - self.can_upload_mixed = True |
903 | - self.can_upload_binaries = True |
904 | - |
905 | - def policySpecificChecks(self, upload): |
906 | - """Deny uploads to any pocket other than the security pocket.""" |
907 | - if self.pocket != PackagePublishingPocket.SECURITY: |
908 | - upload.reject( |
909 | - "Not permitted to do security upload to non SECURITY pocket") |
910 | - |
911 | - |
912 | def findPolicyByName(policy_name): |
913 | """Return a new policy instance for the given policy name.""" |
914 | return getUtility(IArchiveUploadPolicy, policy_name)() |
915 | @@ -362,8 +340,7 @@ |
916 | policies = [ |
917 | BuildDaemonUploadPolicy, |
918 | InsecureUploadPolicy, |
919 | - SyncUploadPolicy, |
920 | - SecurityUploadPolicy] |
921 | + SyncUploadPolicy] |
922 | sm = getGlobalSiteManager() |
923 | for policy in policies: |
924 | sm.registerUtility( |
925 | |
926 | === modified file 'lib/lp/soyuz/doc/soyuz-set-of-uploads.txt' |
927 | --- lib/lp/soyuz/doc/soyuz-set-of-uploads.txt 2010-08-06 14:29:36 +0000 |
928 | +++ lib/lp/soyuz/doc/soyuz-set-of-uploads.txt 2010-08-25 19:32:53 +0000 |
929 | @@ -319,22 +319,6 @@ |
930 | Subject: bar_1.0-3_source.changes rejected |
931 | ... |
932 | |
933 | -Check the rejection (instead of failure) of an upload to a series or |
934 | -pocket which does not exist. We use the security upload policy for easier |
935 | -testing, as unsigned uploads are allowed. |
936 | - |
937 | - >>> simulate_upload( |
938 | - ... 'unstable_1.0-1', upload_policy="security", loglevel=logging.ERROR) |
939 | - Rejected uploads: ['unstable_1.0-1'] |
940 | - |
941 | - >>> read_email() |
942 | - To: Celso Providelo <celso.providelo@canonical.com> |
943 | - Subject: unstable_1.0-1_source.changes rejected |
944 | - ... |
945 | - Rejected: |
946 | - Unable to find distroseries: unstable |
947 | - ... |
948 | - |
949 | Force weird behavior with rfc2047 sentences containing '.' on |
950 | bar_1.0-4, which caused bug # 41102. |
951 | |
952 | @@ -605,6 +589,9 @@ |
953 | |
954 | == Staged Security Uploads == |
955 | |
956 | +XXX: Salgado, 2010-08-25, bug=624078: This entire section should be removed |
957 | +but if you do so publish-distro.py (called further down) will hang. |
958 | + |
959 | Perform a staged (source-first) security upload, simulating a run of |
960 | the buildd-queuebuilder inbetween, to verify fix for bug 53437. To be |
961 | allowed a security upload, we need to use a released distroseries, |
962 | @@ -617,7 +604,7 @@ |
963 | >>> ss = SectionSelection(distroseries=warty, section=devel) |
964 | |
965 | >>> simulate_upload( |
966 | - ... 'baz_1.0-1', is_new=True, upload_policy="security", |
967 | + ... 'baz_1.0-1', is_new=True, upload_policy="anything", |
968 | ... series="warty-security", distro="ubuntu") |
969 | |
970 | Check there's a SourcePackageRelease with no build. |
971 | @@ -650,7 +637,7 @@ |
972 | Upload the i386 binary: |
973 | |
974 | >>> simulate_upload( |
975 | - ... 'baz_1.0-1_single_binary', is_new=True, upload_policy="security", |
976 | + ... 'baz_1.0-1_single_binary', is_new=True, upload_policy="anything", |
977 | ... distro="ubuntu", series="warty-security") |
978 | |
979 | Should still just have one build, and it should now be FULLYBUILT. |
Wow, that's a very red diff. Death to doctests!
Not much here to review, really. I'll have to trust that you're not bluffing on how an email to Daniel Silverstone turned into one to Celso Providelo in lines 286—307 of the diff. But then again, doctests have a tendency to invite bluffing and I can't help feeling it's their own fault.