← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~cjwatson/launchpad/proposed-as-staging-pocket into lp:launchpad

 

Colin Watson has proposed merging lp:~cjwatson/launchpad/proposed-as-staging-pocket into lp:launchpad.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)
Related bugs:
  Bug #930217 in Launchpad itself: "Make proposed pocket useful for staging uploads"
  https://bugs.launchpad.net/launchpad/+bug/930217

For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/proposed-as-staging-pocket/+merge/99911

== Summary ==

As bug 930217 reports, we would like to use the PROPOSED pocket as a general staging area to reduce problems with inter-architecture build skew, permit installability and other automatic testing before promotion to the RELEASE pocket, and suchlike, in pursuit of a general goal to make the development release of Ubuntu more continuously usable.  This is hampered by the fact that it is not currently possible to upload to the PROPOSED pocket pre-release, except when the distroseries is frozen.

== Proposed fix ==

Make it possible to upload to the PROPOSED pocket pre-release, and auto-approve such uploads in the same situations that we would auto-approve uploads to the RELEASE pocket.

== Implementation details ==

The meat of the implementation is in DistroSeries.canUploadToPocket (to enable uploads at all) and InsecureUploadPolicy.autoApprove and BasicCopyPolicy.autoApprove (to deal with auto-approval).  The only real nit here is that we need an extra distroseries.isUnstable() check in the autoApprove methods, since the addition of auto-approval for PROPOSED means that it's no longer enough to rely upon canUploadToPocket denying RELEASE pocket uploads post-release.  The tests in this area were already pretty good, but of course I added a few more.

I faffed around for ages trying to get this branch down to non-net-positive LoC, and did a certain amount of refactoring of tests: particularly, I think lib/lp/soyuz/adapters/tests/test_copypolicy.py is considerably more readable now, and I cleaned up the use of assert* in lib/lp/soyuz/tests/test_archive.py.  Eventually I noticed that there were some test data files in archiveuploader that AFAICS are no longer used, and deleting those was enough to get me comfortably into negative LoC.

== Tests ==

bin/test -vvct archiveuploader -t distroseries -t soyuz

== Demo and Q/A ==

Upload packages to DEVELOPMENT, FROZEN, and CURRENT distroseries on dogfood; they should be respectively auto-approved, held for approval, and held for approval.

== lint ==

I fixed some pre-existing lint.  There are still some long lines in doctests that I didn't feel comfortable cleaning up:

./lib/lp/archiveuploader/tests/nascentupload-announcements.txt
     186: want exceeds 78 characters.
     187: want exceeds 78 characters.
     655: want exceeds 78 characters.
     722: want exceeds 78 characters.
     724: want exceeds 78 characters.
     770: want exceeds 78 characters.
     772: want exceeds 78 characters.
./lib/lp/registry/doc/distroseries.txt
     520: source exceeds 78 characters.
     521: source exceeds 78 characters.
     725: source exceeds 78 characters.
-- 
https://code.launchpad.net/~cjwatson/launchpad/proposed-as-staging-pocket/+merge/99911
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~cjwatson/launchpad/proposed-as-staging-pocket into lp:launchpad.
=== removed file 'lib/lp/archiveuploader/tests/data/badformat-changes'
--- lib/lp/archiveuploader/tests/data/badformat-changes	2007-05-04 17:48:03 +0000
+++ lib/lp/archiveuploader/tests/data/badformat-changes	1970-01-01 00:00:00 +0000
@@ -1,40 +0,0 @@
------BEGIN PGP SIGNED MESSAGE-----
-Hash: SHA1
-
-Format: 1.7
-Date: Sun, 29 Aug 2004 03:47:46 +0200
-Source: abiword
-Binary: abiword-plugins-gnome abiword-gnome abiword-doc xfonts-abi abiword-help abiword abiword-plugins abiword-common
-Architecture: mips
-Version: 2.0.10-1.2
-Distribution: unstable
-Urgency: high
-Maintainer: buildd mips user account <buildd@xxxxxxxxxxxxxxx>
-Changed-By: Jordi Mallach <jordi@xxxxxxxxxx>
-Description: 
- abiword    - WYSIWYG word processor based on GTK2
- abiword-common - WYSIWYG word processor based on GTK2
- abiword-gnome - WYSIWYG word processor based on GTK2/GNOME2
- abiword-plugins - Plugins for AbiWord
- abiword-plugins-gnome - Plugins for AbiWord (with GNOME dependency)
-Closes: 267199
-Changes: 
- abiword (2.0.10-1.2) unstable; urgency=high
- .
-   * Non-Maintainer Upload.
-   * debian/patches/01_relibtoolise.dpatch: damn, I missed the *other*
-     configure script. Abiword's source tarball sucks in many ways...
-     (really closes: #267199).
-Files: 
- 2a0175f42ebcf4c42312ba5837108fc2 1504956 editors optional abiword-common_2.0.10-1.2_mips.deb
- dc5fe0eeb1b233cf656f46665f150488 2014536 editors optional abiword_2.0.10-1.2_mips.deb
- b418b9cc47f3d891a6fecb40cd5524db 2012956 gnome optional abiword-gnome_2.0.10-1.2_mips.deb
- 965cc5212d45154a2bf517f0b081282f 352262 editors optional abiword-plugins_2.0.10-1.2_mips.deb
- 1c1e1380e3b3848745ce9b51f1a29493 33312 gnome optional abiword-plugins-gnome_2.0.10-1.2_mips.deb
------BEGIN PGP SIGNATURE-----
-Version: GnuPG v1.2.4 (GNU/Linux)
-
-iD8DBQFBYnONOtPfPvLSwCgRAq4aAKCXhvAENBbvt4mAlXGPJoIPik5c9QCeIvbs
-EYumagv6d7dl8FfcKRuuQWg=
-=2IzM
------END PGP SIGNATURE-----

=== removed file 'lib/lp/archiveuploader/tests/data/multiple-stanzas'
--- lib/lp/archiveuploader/tests/data/multiple-stanzas	2007-05-04 17:48:03 +0000
+++ lib/lp/archiveuploader/tests/data/multiple-stanzas	1970-01-01 00:00:00 +0000
@@ -1,60 +0,0 @@
-Format: 1.7
-Date: Sun, 29 Aug 2004 03:47:46 +0200
-Source: abiword
-Binary: abiword-plugins-gnome abiword-gnome abiword-doc xfonts-abi abiword-help abiword abiword-plugins abiword-common
-Architecture: mips
-Version: 2.0.10-1.2
-Distribution: unstable
-Urgency: high
-Maintainer: buildd mips user account <buildd@xxxxxxxxxxxxxxx>
-Changed-By: Jordi Mallach <jordi@xxxxxxxxxx>
-Description: 
- abiword    - WYSIWYG word processor based on GTK2
- abiword-common - WYSIWYG word processor based on GTK2
- abiword-gnome - WYSIWYG word processor based on GTK2/GNOME2
- abiword-plugins - Plugins for AbiWord
- abiword-plugins-gnome - Plugins for AbiWord (with GNOME dependency)
-Closes: 267199
-Changes: 
- abiword (2.0.10-1.2) unstable; urgency=high
- .
-   * Non-Maintainer Upload.
-   * debian/patches/01_relibtoolise.dpatch: damn, I missed the *other*
-     configure script. Abiword's source tarball sucks in many ways...
-     (really closes: #267199).
-Files: 
- 2a0175f42ebcf4c42312ba5837108fc2 1504956 editors optional abiword-common_2.0.10-1.2_mips.deb
- dc5fe0eeb1b233cf656f46665f150488 2014536 editors optional abiword_2.0.10-1.2_mips.deb
- b418b9cc47f3d891a6fecb40cd5524db 2012956 gnome optional abiword-gnome_2.0.10-1.2_mips.deb
- 965cc5212d45154a2bf517f0b081282f 352262 editors optional abiword-plugins_2.0.10-1.2_mips.deb
- 1c1e1380e3b3848745ce9b51f1a29493 33312 gnome optional abiword-plugins-gnome_2.0.10-1.2_mips.deb
-
-Format: 1.7
-Date: Sun, 29 Aug 2004 03:47:46 +0200
-Source: abiword
-Binary: abiword-plugins-gnome abiword-gnome abiword-doc xfonts-abi abiword-help abiword abiword-plugins abiword-common
-Architecture: mips
-Version: 2.0.10-1.3
-Distribution: unstable
-Urgency: high
-Maintainer: Daniel Silverstone <dsilvers@xxxxxxxxxx>
-Changed-By: Jordi Mallach <jordi@xxxxxxxxxx>
-Description: 
- abiword    - WYSIWYG word processor based on GTK2
- abiword-common - WYSIWYG word processor based on GTK2
- abiword-gnome - WYSIWYG word processor based on GTK2/GNOME2
- abiword-plugins - Plugins for AbiWord
- abiword-plugins-gnome - Plugins for AbiWord (with GNOME dependency)
-Closes: 267199
-Changes: 
- abiword (2.0.10-1.3) unstable; urgency=high
- .
-   * Non-Maintainer Upload.
-   * Ate some cake
-Files: 
- 2a0175f42ebcf4c42312ba5837108fc2 1504956 editors optional abiword-common_2.0.10-1.2_mips.deb
- dc5fe0eeb1b233cf656f46665f150488 2014536 editors optional abiword_2.0.10-1.2_mips.deb
- b418b9cc47f3d891a6fecb40cd5524db 2012956 gnome optional abiword-gnome_2.0.10-1.2_mips.deb
- 965cc5212d45154a2bf517f0b081282f 352262 editors optional abiword-plugins_2.0.10-1.2_mips.deb
- 1c1e1380e3b3848745ce9b51f1a29493 33312 gnome optional abiword-plugins-gnome_2.0.10-1.2_mips.deb
-

=== removed file 'lib/lp/archiveuploader/tests/data/singular-stanza'
--- lib/lp/archiveuploader/tests/data/singular-stanza	2007-05-04 17:48:03 +0000
+++ lib/lp/archiveuploader/tests/data/singular-stanza	1970-01-01 00:00:00 +0000
@@ -1,31 +0,0 @@
-Format: 1.7
-Date: Sun, 29 Aug 2004 03:47:46 +0200
-Source: abiword
-Binary: abiword-plugins-gnome abiword-gnome abiword-doc xfonts-abi abiword-help abiword abiword-plugins abiword-common
-Architecture: mips
-Version: 2.0.10-1.2
-Distribution: unstable
-Urgency: high
-Maintainer: buildd mips user account <buildd@xxxxxxxxxxxxxxx>
-Changed-By: Jordi Mallach <jordi@xxxxxxxxxx>
-Description: 
- abiword    - WYSIWYG word processor based on GTK2
- abiword-common - WYSIWYG word processor based on GTK2
- abiword-gnome - WYSIWYG word processor based on GTK2/GNOME2
- abiword-plugins - Plugins for AbiWord
- abiword-plugins-gnome - Plugins for AbiWord (with GNOME dependency)
-Closes: 267199
-Changes: 
- abiword (2.0.10-1.2) unstable; urgency=high
- .
-   * Non-Maintainer Upload.
-   * debian/patches/01_relibtoolise.dpatch: damn, I missed the *other*
-     configure script. Abiword's source tarball sucks in many ways...
-     (really closes: #267199).
-Files: 
- 2a0175f42ebcf4c42312ba5837108fc2 1504956 editors optional abiword-common_2.0.10-1.2_mips.deb
- dc5fe0eeb1b233cf656f46665f150488 2014536 editors optional abiword_2.0.10-1.2_mips.deb
- b418b9cc47f3d891a6fecb40cd5524db 2012956 gnome optional abiword-gnome_2.0.10-1.2_mips.deb
- 965cc5212d45154a2bf517f0b081282f 352262 editors optional abiword-plugins_2.0.10-1.2_mips.deb
- 1c1e1380e3b3848745ce9b51f1a29493 33312 gnome optional abiword-plugins-gnome_2.0.10-1.2_mips.deb
-

=== modified file 'lib/lp/archiveuploader/tests/nascentupload-announcements.txt'
--- lib/lp/archiveuploader/tests/nascentupload-announcements.txt	2012-01-20 15:42:44 +0000
+++ lib/lp/archiveuploader/tests/nascentupload-announcements.txt	2012-03-29 11:22:21 +0000
@@ -4,16 +4,16 @@
 NascentUpload announces uploads according its final status (NEW,
 AUTO-APPROVED, UNAPPROVED) and its destination pocket:
 
- * NEW to RELEASE (via insecure): submitter set (changes signer,
+ * NEW to RELEASE/PROPOSED (via insecure): submitter set (changes signer,
    Changed-by and maintainer) receives an 'new' warning message.
 
  * UNAPPROVED to frozen-RELEASE/UPDATES/BACKPORTS/PROPOSED (via insecure):
    submitter set receives an 'unapproved' warning (announcement is
    sent after the upload gets reviewed by archive-admin at queue time).
 
- * AUTO-APPROVED to RELEASE (via insecure): submitter set receives an
-   'acceptance' warning and the target distroseries changeslist
-   address receives an 'announcement' message.
+ * AUTO-APPROVED to RELEASE/PROPOSED (via insecure): submitter set receives
+   an 'acceptance' warning and the target distroseries changeslist address
+   receives an 'announcement' message.
 
  * AUTO-APPROVED to BACKPORTS (via sync): submitter set receives an
    'acceptance' warning ('announcement' is skipped).
@@ -255,7 +255,6 @@
 
 A PPA upload will contain the X-Launchpad-PPA header.
 
-    >>> from lp.registry.interfaces.distribution import IDistributionSet
     >>> from lp.registry.interfaces.person import IPersonSet
     >>> from lp.soyuz.enums import ArchivePurpose
     >>> from lp.soyuz.interfaces.archive import IArchiveSet
@@ -265,8 +264,7 @@
     >>> name16_ppa = getUtility(IArchiveSet).new(
     ...     owner=name16, distribution=ubuntu, purpose=ArchivePurpose.PPA)
 
-    >>> ppa_policy = getPolicy(
-    ...     name='insecure', distro='ubuntu', distroseries=None)
+    >>> ppa_policy = getPolicy(name='insecure', distro='ubuntu')
     >>> ppa_policy.archive = name16_ppa
     >>> ppa_policy.setDistroSeriesAndPocket('hoary')
 
@@ -397,8 +395,7 @@
 
 UNAPPROVED source uploads for 'translations' section via insecure:
 
-    >>> insecure_policy = getPolicy(
-    ...     name='insecure', distro='ubuntu', distroseries=None)
+    >>> insecure_policy = getPolicy(name='insecure', distro='ubuntu')
     >>> insecure_policy.setDistroSeriesAndPocket('hoary-updates')
 
     >>> lang_pack = NascentUpload.from_changesfile_path(
@@ -487,8 +484,7 @@
 
 AUTO-APPROVED upload to BACKPORTS pocket via 'sync' policy:
 
-    >>> modified_sync_policy = getPolicy(
-    ...     name='sync', distro='ubuntu', distroseries=None)
+    >>> modified_sync_policy = getPolicy(name='sync', distro='ubuntu')
     >>> modified_sync_policy.setDistroSeriesAndPocket('hoary-backports')
 
     >>> bar_src = NascentUpload.from_changesfile_path(
@@ -805,7 +801,7 @@
 
 We first create a misnamed copy of the changes file.
 
-    >>> import os, shutil
+    >>> import shutil
     >>> originalp = datadir('suite/bar_1.0-1/bar_1.0-1_source.changes')
     >>> copyp = datadir('suite/bar_1.0-1/z-z_0.4.12-2~ppa2.changes')
     >>> shutil.copyfile(originalp, copyp)

=== modified file 'lib/lp/archiveuploader/tests/uploadpolicy.txt'
--- lib/lp/archiveuploader/tests/uploadpolicy.txt	2011-12-30 06:14:56 +0000
+++ lib/lp/archiveuploader/tests/uploadpolicy.txt	2012-03-29 11:22:21 +0000
@@ -1,4 +1,5 @@
-== The uploader policies ==
+The uploader policies
+---------------------
 
 When the uploader is invoked, it is given a policy to work in. This governs
 such things as what tests get run at what stages of the upload, and whether or
@@ -10,122 +11,139 @@
 means a call to getUtility(IArchiveUploadPolicy, name) will return the class
 itself rather than an instance of it.
 
-  >>> from lp.archiveuploader.uploadpolicy import (
-  ...     IArchiveUploadPolicy, findPolicyByName)
-  >>> policy_cls = getUtility(IArchiveUploadPolicy, 'insecure')
-  >>> policy_cls
-  <class '...InsecureUploadPolicy'>
+    >>> from lp.archiveuploader.uploadpolicy import (
+    ...     IArchiveUploadPolicy, findPolicyByName)
+    >>> policy_cls = getUtility(IArchiveUploadPolicy, 'insecure')
+    >>> policy_cls
+    <class '...InsecureUploadPolicy'>
 
 There's a helper function which returns an instance of the policy with the
 given name, though, and it's preferred over using getUtility() directly.
 
-  >>> insecure_policy = findPolicyByName('insecure')
-  >>> insecure_policy
-  <lp...InsecureUploadPolicy object...
+    >>> insecure_policy = findPolicyByName('insecure')
+    >>> insecure_policy
+    <lp...InsecureUploadPolicy object...
 
 Two of the policies defined so far are the insecure and buildd policies.
 
-  >>> insecure_policy.name
-  'insecure'
-  >>> buildd_policy = findPolicyByName('buildd')
-  >>> buildd_policy.name
-  'buildd'
-  >>> abstract_policy = findPolicyByName('abstract')
-  Traceback (most recent call last):
-  ...
-  ComponentLookupError:...
+    >>> insecure_policy.name
+    'insecure'
+    >>> buildd_policy = findPolicyByName('buildd')
+    >>> buildd_policy.name
+    'buildd'
+    >>> abstract_policy = findPolicyByName('abstract')
+    Traceback (most recent call last):
+    ...
+    ComponentLookupError:...
 
 There is a bunch of attributes which we expect to have and which can vary
 from policy to policy.
 
-  >>> insecure_policy.unsigned_changes_ok
-  False
-  >>> buildd_policy.unsigned_changes_ok
-  True
-  >>> insecure_policy.unsigned_dsc_ok
-  False
-  >>> buildd_policy.unsigned_dsc_ok
-  True
+    >>> insecure_policy.unsigned_changes_ok
+    False
+    >>> buildd_policy.unsigned_changes_ok
+    True
+    >>> insecure_policy.unsigned_dsc_ok
+    False
+    >>> buildd_policy.unsigned_dsc_ok
+    True
 
 The policies require certain values to be present in the options at times...
 
-  >>> class MockAbstractOptions:
-  ...     distro = 'ubuntu'
-  ...     distroseries = None
-  >>> class MockOptions(MockAbstractOptions):
-  ...     builds = True
-
-  >>> ab_opts = MockAbstractOptions()
-  >>> bd_opts = MockOptions()
-
-  >>> insecure_policy.setOptions(ab_opts)
-  >>> insecure_policy.distro.name
-  u'ubuntu'
-  >>> buildd_policy.setOptions(ab_opts)
-  >>> buildd_policy.setOptions(bd_opts)
-  >>> buildd_policy.distro.name
-  u'ubuntu'
+    >>> class MockAbstractOptions:
+    ...     distro = 'ubuntu'
+    ...     distroseries = None
+    >>> class MockOptions(MockAbstractOptions):
+    ...     builds = True
+
+    >>> ab_opts = MockAbstractOptions()
+    >>> bd_opts = MockOptions()
+
+    >>> insecure_policy.setOptions(ab_opts)
+    >>> insecure_policy.distro.name
+    u'ubuntu'
+    >>> buildd_policy.setOptions(ab_opts)
+    >>> buildd_policy.setOptions(bd_opts)
+    >>> buildd_policy.distro.name
+    u'ubuntu'
 
 Policies can think about distroseriess...
 
-  >>> buildd_policy.setDistroSeriesAndPocket("hoary")
-  >>> print buildd_policy.distroseries.name
-  hoary
+    >>> buildd_policy.setDistroSeriesAndPocket("hoary")
+    >>> print buildd_policy.distroseries.name
+    hoary
 
 Policies can make decisions based on whether or not they want to
 approve an upload automatically (I.E. move it straight to ACCEPTED
 instead of UNAPPROVED)
 
-  >>> from lp.registry.interfaces.distribution import IDistributionSet
-  >>> from lp.registry.interfaces.series import SeriesStatus
-  >>> ubuntu = getUtility(IDistributionSet)['ubuntu']
-  >>> hoary = ubuntu['hoary']
-
-  >>> class FakeUpload:
-  ...   def __init__(self, ppa=False):
-  ...       self.is_ppa = ppa
-
-  >>> print hoary.status.name
-  DEVELOPMENT
+    >>> from lp.registry.interfaces.distribution import IDistributionSet
+    >>> from lp.registry.interfaces.series import SeriesStatus
+    >>> ubuntu = getUtility(IDistributionSet)['ubuntu']
+    >>> hoary = ubuntu['hoary']
+
+    >>> class FakeUpload:
+    ...   def __init__(self, ppa=False):
+    ...       self.is_ppa = ppa
+
+    >>> print hoary.status.name
+    DEVELOPMENT
 
 Uploads to the RELEASE pocket of not FROZEN distroseries are approved
 by the insecure policy:
 
-  >>> insecure_policy.setDistroSeriesAndPocket('hoary')
-  >>> insecure_policy.autoApprove(FakeUpload())
-  True
-
-  >>> insecure_policy.autoApprove(FakeUpload(ppa=True))
-  True
+    >>> insecure_policy.setDistroSeriesAndPocket('hoary')
+    >>> insecure_policy.autoApprove(FakeUpload())
+    True
+
+    >>> insecure_policy.autoApprove(FakeUpload(ppa=True))
+    True
+
+So are uploads to the PROPOSED pocket:
+
+    >>> insecure_policy.setDistroSeriesAndPocket('hoary-proposed')
+    >>> insecure_policy.autoApprove(FakeUpload())
+    True
 
 When the distroseries is FROZEN the uploads should wait in UNAPPROVED queue:
 
-  >>> login('foo.bar@xxxxxxxxxxxxx')
-  >>> hoary.status = SeriesStatus.FROZEN
-  >>> from lp.services.database.sqlbase import flush_database_updates
-  >>> flush_database_updates()
+    >>> login('foo.bar@xxxxxxxxxxxxx')
+    >>> hoary.status = SeriesStatus.FROZEN
+    >>> from lp.services.database.sqlbase import flush_database_updates
+    >>> flush_database_updates()
 
-  >>> insecure_policy.autoApprove(FakeUpload())
-  False
+    >>> insecure_policy.autoApprove(FakeUpload())
+    False
 
 PPA uploads continue to be auto-approved:
 
-  >>> insecure_policy.autoApprove(FakeUpload(ppa=True))
-  True
+    >>> insecure_policy.autoApprove(FakeUpload(ppa=True))
+    True
 
 Reset the policy so that we can try again...
 
-  >>> insecure_policy.policy = None
-  >>> insecure_policy.distroseries = None
+    >>> insecure_policy.policy = None
+    >>> insecure_policy.distroseries = None
 
 Uploads to the UPDATES pocket are not auto-approved by the insecure policy
 
-  >>> insecure_policy.setDistroSeriesAndPocket('hoary-updates')
-  >>> insecure_policy.autoApprove(FakeUpload())
-  False
+    >>> insecure_policy.setDistroSeriesAndPocket('hoary-updates')
+    >>> insecure_policy.autoApprove(FakeUpload())
+    False
 
 Despite of not being allowed yet (see UploadPolicy.checkUpload) PPA
 uploads to post-release pockets would also be auto-approved:
 
-  >>> insecure_policy.autoApprove(FakeUpload(ppa=True))
-  True
+    >>> insecure_policy.autoApprove(FakeUpload(ppa=True))
+    True
+
+Uploads to the PROPOSED pocket are also not auto-approved by the insecure
+policy, either before or after release:
+
+    >>> insecure_policy.setDistroSeriesAndPocket('hoary-proposed')
+    >>> insecure_policy.autoApprove(FakeUpload())
+    False
+    >>> hoary.status = SeriesStatus.CURRENT
+    >>> flush_database_updates()
+    >>> insecure_policy.autoApprove(FakeUpload())
+    False

=== modified file 'lib/lp/archiveuploader/uploadpolicy.py'
--- lib/lp/archiveuploader/uploadpolicy.py	2011-06-01 05:45:53 +0000
+++ lib/lp/archiveuploader/uploadpolicy.py	2012-03-29 11:22:21 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2012 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Policy management for the upload handler."""
@@ -29,7 +29,6 @@
     Interface,
     )
 
-from lp.app.interfaces.launchpad import ILaunchpadCelebrities
 from lp.registry.interfaces.distribution import IDistributionSet
 from lp.registry.interfaces.pocket import PackagePublishingPocket
 from lp.registry.interfaces.series import SeriesStatus
@@ -76,7 +75,7 @@
 
     name = 'abstract'
     options = None
-    accepted_type = None # Must be defined in subclasses.
+    accepted_type = None  # Must be defined in subclasses.
 
     def __init__(self):
         """Prepare a policy..."""
@@ -259,19 +258,26 @@
                     "This upload queue does not permit SECURITY uploads.")
 
     def autoApprove(self, upload):
-        """The insecure policy only auto-approves RELEASE pocket stuff.
+        """The insecure policy auto-approves RELEASE/PROPOSED pocket stuff.
 
         PPA uploads are always auto-approved.
-        Other uploads (to main archives) are only auto-approved if the
-        distroseries is not FROZEN (note that we already performed the
-        IDistroSeries.canUploadToPocket check in the checkUpload base method).
+        RELEASE and PROPOSED pocket uploads (to main archives) are only
+        auto-approved if the distroseries is in a non-FROZEN state
+        pre-release.  (We already performed the
+        IDistroSeries.canUploadToPocket check in the checkUpload base
+        method, which will deny RELEASE uploads post-release, but it doesn't
+        hurt to repeat this for that case.)
         """
         if upload.is_ppa:
             return True
 
-        if self.pocket == PackagePublishingPocket.RELEASE:
-            if (self.distroseries.status !=
-                SeriesStatus.FROZEN):
+        auto_approve_pockets = (
+            PackagePublishingPocket.RELEASE,
+            PackagePublishingPocket.PROPOSED,
+            )
+        if self.pocket in auto_approve_pockets:
+            if (self.distroseries.isUnstable() and
+                self.distroseries.status != SeriesStatus.FROZEN):
                 return True
         return False
 

=== modified file 'lib/lp/registry/doc/distroseries.txt'
--- lib/lp/registry/doc/distroseries.txt	2012-02-21 22:46:28 +0000
+++ lib/lp/registry/doc/distroseries.txt	2012-03-29 11:22:21 +0000
@@ -169,7 +169,6 @@
 canUploadToPocket method helps us to decide if an upload is allowed or
 not, according to the distroseries status and the upload target pocket.
 
-    >>> from lp.registry.interfaces.distribution import IDistributionSet
     >>> ubuntu = getUtility(IDistributionSet)['ubuntu']
     >>> breezy_autotest = ubuntu['breezy-autotest']
     >>> hoary = ubuntu['hoary']
@@ -204,6 +203,16 @@
     >>> hoary.canUploadToPocket(PackagePublishingPocket.SECURITY)
     True
 
+The PROPOSED pocket is also special.  Pre-release, it may be used for
+staging uploads on their way into the RELEASE pocket; post-release, it may
+be used for staging uploads on their way into the UPDATES pocket.
+
+    >>> warty.canUploadToPocket(PackagePublishingPocket.PROPOSED)
+    True
+    >>> breezy_autotest.canUploadToPocket(PackagePublishingPocket.PROPOSED)
+    True
+    >>> hoary.canUploadToPocket(PackagePublishingPocket.PROPOSED)
+    True
 
 Package searching
 -----------------

=== modified file 'lib/lp/registry/model/distroseries.py'
--- lib/lp/registry/model/distroseries.py	2012-02-14 23:50:16 +0000
+++ lib/lp/registry/model/distroseries.py	2012-03-29 11:22:21 +0000
@@ -724,8 +724,12 @@
             self.status in stable_states):
             return False
 
-        # Deny uploads for post-release pockets in unstable states.
-        if (pocket != PackagePublishingPocket.RELEASE and
+        # Deny uploads for post-release-only pockets in unstable states.
+        pre_release_pockets = (
+            PackagePublishingPocket.RELEASE,
+            PackagePublishingPocket.PROPOSED,
+            )
+        if (pocket not in pre_release_pockets and
             self.status not in stable_states):
             return False
 

=== modified file 'lib/lp/soyuz/adapters/copypolicy.py'
--- lib/lp/soyuz/adapters/copypolicy.py	2011-06-10 11:17:39 +0000
+++ lib/lp/soyuz/adapters/copypolicy.py	2012-03-29 11:22:21 +0000
@@ -1,4 +1,4 @@
-# Copyright 2011 Canonical Ltd.  This software is licensed under the
+# Copyright 2011-2012 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Copy Policy Classes.
@@ -42,12 +42,17 @@
         if archive.is_ppa:
             return True
 
-        # If the pocket is RELEASE and we're not frozen then you can
-        # upload to it.  Any other states mean the upload is unapproved.
+        # If the pocket is RELEASE or PROPOSED and we're not frozen then you
+        # can upload to it.  Any other states mean the upload is unapproved.
         #
         # This check is orthogonal to the
         # IDistroSeries.canUploadToPocket check.
-        if (pocket == PackagePublishingPocket.RELEASE and
+        auto_approve_pockets = (
+            PackagePublishingPocket.RELEASE,
+            PackagePublishingPocket.PROPOSED,
+            )
+        if (pocket in auto_approve_pockets and
+            distroseries.isUnstable() and
             distroseries.status != SeriesStatus.FROZEN):
             return True
 
@@ -63,7 +68,7 @@
     def send_email(self, archive):
         if archive.is_ppa:
             return False
-            
+
         return True
 
 

=== modified file 'lib/lp/soyuz/adapters/tests/test_copypolicy.py'
--- lib/lp/soyuz/adapters/tests/test_copypolicy.py	2012-01-01 02:58:52 +0000
+++ lib/lp/soyuz/adapters/tests/test_copypolicy.py	2012-03-29 11:22:21 +0000
@@ -1,4 +1,4 @@
-# Copyright 2011 Canonical Ltd.  This software is licensed under the
+# Copyright 2011-2012 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 from lp.registry.interfaces.pocket import PackagePublishingPocket
@@ -21,23 +21,26 @@
 
     layer = ZopelessDatabaseLayer
 
-    def _getUploadCriteria(self, archive_purpose):
+    def _getUploadCriteria(self, archive_purpose, status=None, pocket=None):
         archive = self.factory.makeArchive(purpose=archive_purpose)
         distroseries = self.factory.makeDistroSeries()
-        pocket = self.factory.getAnyPocket()
+        if status is not None:
+            distroseries.status = status
+        if pocket is None:
+            pocket = self.factory.getAnyPocket()
         return archive, distroseries, pocket
 
-    def assertApproved(self, archive_purpose, method):
+    def assertApproved(self, archive_purpose, method,
+                       status=None, pocket=None):
         archive, distroseries, pocket = self._getUploadCriteria(
-            archive_purpose)
-        approved = method(archive, distroseries, pocket)
-        self.assertTrue(approved)
+            archive_purpose, status=status, pocket=pocket)
+        self.assertTrue(method(archive, distroseries, pocket))
 
-    def assertUnapproved(self, archive_purpose, method):
+    def assertUnapproved(self, archive_purpose, method,
+                         status=None, pocket=None):
         archive, distroseries, pocket = self._getUploadCriteria(
-            archive_purpose)
-        approved = method(archive, distroseries, pocket)
-        self.assertFalse(approved)
+            archive_purpose, status=status, pocket=pocket)
+        self.assertFalse(method(archive, distroseries, pocket))
 
     def test_insecure_holds_new_distro_package(self):
         cp = InsecureCopyPolicy()
@@ -48,30 +51,41 @@
         self.assertApproved(ArchivePurpose.PPA, cp.autoApproveNew)
 
     def test_insecure_approves_known_distro_package_to_unfrozen_release(self):
-        archive = self.factory.makeArchive(purpose=ArchivePurpose.PRIMARY)
-        distroseries = self.factory.makeDistroSeries()
-        pocket = PackagePublishingPocket.RELEASE
         cp = InsecureCopyPolicy()
-        approve = cp.autoApprove(archive, distroseries, pocket)
-        self.assertTrue(approve)
+        self.assertApproved(
+            ArchivePurpose.PRIMARY, cp.autoApprove,
+            pocket=PackagePublishingPocket.RELEASE)
 
     def test_insecure_holds_copy_to_updates_pocket_in_frozen_series(self):
-        archive = self.factory.makeArchive(purpose=ArchivePurpose.PRIMARY)
-        distroseries = self.factory.makeDistroSeries()
-        distroseries.status = SeriesStatus.FROZEN
-        pocket = PackagePublishingPocket.UPDATES
         cp = InsecureCopyPolicy()
-        approve = cp.autoApprove(archive, distroseries, pocket)
-        self.assertFalse(approve)
+        self.assertUnapproved(
+            ArchivePurpose.PRIMARY, cp.autoApprove, status=SeriesStatus.FROZEN,
+            pocket=PackagePublishingPocket.UPDATES)
 
     def test_insecure_holds_copy_to_release_pocket_in_frozen_series(self):
-        archive = self.factory.makeArchive(purpose=ArchivePurpose.PRIMARY)
-        distroseries = self.factory.makeDistroSeries()
-        pocket = PackagePublishingPocket.RELEASE
-        distroseries.status = SeriesStatus.FROZEN
-        cp = InsecureCopyPolicy()
-        approve = cp.autoApprove(archive, distroseries, pocket)
-        self.assertFalse(approve)
+        cp = InsecureCopyPolicy()
+        self.assertUnapproved(
+            ArchivePurpose.PRIMARY, cp.autoApprove, status=SeriesStatus.FROZEN,
+            pocket=PackagePublishingPocket.RELEASE)
+
+    def test_insecure_approves_copy_to_proposed_in_unfrozen_series(self):
+        cp = InsecureCopyPolicy()
+        self.assertApproved(
+            ArchivePurpose.PRIMARY, cp.autoApprove,
+            pocket=PackagePublishingPocket.PROPOSED)
+
+    def test_insecure_holds_copy_to_proposed_in_frozen_series(self):
+        cp = InsecureCopyPolicy()
+        self.assertUnapproved(
+            ArchivePurpose.PRIMARY, cp.autoApprove, status=SeriesStatus.FROZEN,
+            pocket=PackagePublishingPocket.PROPOSED)
+
+    def test_insecure_holds_copy_to_proposed_in_current_series(self):
+        cp = InsecureCopyPolicy()
+        self.assertUnapproved(
+            ArchivePurpose.PRIMARY, cp.autoApprove,
+            status=SeriesStatus.CURRENT,
+            pocket=PackagePublishingPocket.PROPOSED)
 
     def test_insecure_approves_existing_ppa_package(self):
         cp = InsecureCopyPolicy()

=== modified file 'lib/lp/soyuz/tests/test_archive.py'
--- lib/lp/soyuz/tests/test_archive.py	2012-02-28 11:14:44 +0000
+++ lib/lp/soyuz/tests/test_archive.py	2012-03-29 11:22:21 +0000
@@ -128,8 +128,7 @@
         archives, sourcepackagename = self.makeArchivesWithPublications()
         results = self.getPublications(
             sourcepackagename, archives, archives[0].distribution)
-        num_results = results.count()
-        self.assertEquals(3, num_results)
+        self.assertEqual(3, results.count())
 
     def test_getPublications_empty_list_of_archives(self):
         # Passing an empty list of archives will result in an empty
@@ -137,12 +136,12 @@
         archives, sourcepackagename = self.makeArchivesWithPublications()
         results = self.getPublications(
             sourcepackagename, [], archives[0].distribution)
-        self.assertEquals([], list(results))
+        self.assertEqual([], list(results))
 
     def assertPublicationsFromArchives(self, publications, archives):
-        self.assertEquals(len(archives), publications.count())
+        self.assertEqual(len(archives), publications.count())
         for publication, archive in zip(publications, archives):
-            self.assertEquals(archive, publication.archive)
+            self.assertEqual(archive, publication.archive)
 
     def test_getPublications_returns_only_for_given_archives(self):
         # Returns only publications for the specified archives
@@ -160,7 +159,7 @@
             status=PackagePublishingStatus.PENDING)
         results = self.getPublications(
             sourcepackagename, [archive], archive.distribution)
-        self.assertEquals([], list(results))
+        self.assertEqual([], list(results))
 
     def publishSourceInNewArchive(self, sourcepackagename):
         distribution = self.factory.makeDistribution()
@@ -192,12 +191,12 @@
     def test_empty_ppa_has_zero_binaries_size(self):
         # An empty PPA has no binaries so has zero binaries_size.
         ppa = self.factory.makeArchive(purpose=ArchivePurpose.PPA)
-        self.assertEquals(0, ppa.binaries_size)
+        self.assertEqual(0, ppa.binaries_size)
 
     def test_sources_size_on_empty_archive(self):
         # Zero is returned for an archive without sources.
         archive = self.factory.makeArchive()
-        self.assertEquals(0, archive.sources_size)
+        self.assertEqual(0, archive.sources_size)
 
     def publishSourceFile(self, archive, library_file):
         """Publish a source package with the given content to the archive.
@@ -220,12 +219,10 @@
         archive = self.factory.makeArchive()
         library_file = self.factory.makeLibraryFileAlias()
         self.publishSourceFile(archive, library_file)
-        self.assertEquals(
-            library_file.content.filesize, archive.sources_size)
+        self.assertEqual(library_file.content.filesize, archive.sources_size)
 
         self.publishSourceFile(archive, library_file)
-        self.assertEquals(
-            library_file.content.filesize, archive.sources_size)
+        self.assertEqual(library_file.content.filesize, archive.sources_size)
 
 
 class TestSeriesWithSources(TestCaseWithFactory):
@@ -326,23 +323,23 @@
             distribution=distribution, purpose=ArchivePurpose.PRIMARY)
         debug = self.factory.makeArchive(
             distribution=distribution, purpose=ArchivePurpose.DEBUG)
-        self.assertEquals(primary.debug_archive, debug)
+        self.assertEqual(primary.debug_archive, debug)
 
     def testPartnerDebugArchiveIsSelf(self):
         partner = self.factory.makeArchive(purpose=ArchivePurpose.PARTNER)
-        self.assertEquals(partner.debug_archive, partner)
+        self.assertEqual(partner.debug_archive, partner)
 
     def testCopyDebugArchiveIsSelf(self):
         copy = self.factory.makeArchive(purpose=ArchivePurpose.COPY)
-        self.assertEquals(copy.debug_archive, copy)
+        self.assertEqual(copy.debug_archive, copy)
 
     def testDebugDebugArchiveIsSelf(self):
         debug = self.factory.makeArchive(purpose=ArchivePurpose.DEBUG)
-        self.assertEquals(debug.debug_archive, debug)
+        self.assertEqual(debug.debug_archive, debug)
 
     def testPPADebugArchiveIsSelf(self):
         ppa = self.factory.makeArchive(purpose=ArchivePurpose.PPA)
-        self.assertEquals(ppa.debug_archive, ppa)
+        self.assertEqual(ppa.debug_archive, ppa)
 
     def testMissingPrimaryDebugArchiveIsNone(self):
         primary = self.factory.makeArchive(purpose=ArchivePurpose.PRIMARY)
@@ -419,8 +416,7 @@
         # Disabling an already disabled Archive should raise an
         # AssertionError.
         archive = self.factory.makeArchive(enabled=False)
-        self.assertRaises(
-            AssertionError, removeSecurityProxy(archive).disable)
+        self.assertRaises(AssertionError, removeSecurityProxy(archive).disable)
 
 
 class TestCollectLatestPublishedSources(TestCaseWithFactory):
@@ -497,10 +493,9 @@
         # Uploading to a PPA should be allowed for a user that is the owner
         owner = self.factory.makePerson(name="somebody")
         archive = self.factory.makeArchive(owner=owner)
-        self.assertEquals(True, archive.checkArchivePermission(owner))
+        self.assertTrue(archive.checkArchivePermission(owner))
         someone_unrelated = self.factory.makePerson(name="somebody-unrelated")
-        self.assertEquals(False,
-            archive.checkArchivePermission(someone_unrelated))
+        self.assertFalse(archive.checkArchivePermission(someone_unrelated))
 
     def test_checkArchivePermission_distro_archive(self):
         # Regular users can not upload to ubuntu
@@ -511,13 +506,12 @@
         main = getUtility(IComponentSet)["main"]
         # A regular user doesn't have access
         somebody = self.factory.makePerson()
-        self.assertEquals(False,
-            archive.checkArchivePermission(somebody, main))
+        self.assertFalse(archive.checkArchivePermission(somebody, main))
         # An ubuntu core developer does have access
         coredev = self.factory.makePerson()
         with person_logged_in(archive.distribution.owner):
             archive.newComponentUploader(coredev, main.name)
-        self.assertEquals(True, archive.checkArchivePermission(coredev, main))
+        self.assertTrue(archive.checkArchivePermission(coredev, main))
 
     def test_checkArchivePermission_ppa(self):
         owner = self.factory.makePerson()
@@ -525,13 +519,11 @@
                                            owner=owner)
         somebody = self.factory.makePerson()
         # The owner has access
-        self.assertEquals(True, archive.checkArchivePermission(owner))
+        self.assertTrue(archive.checkArchivePermission(owner))
         # Somebody unrelated does not
-        self.assertEquals(False, archive.checkArchivePermission(somebody))
+        self.assertFalse(archive.checkArchivePermission(somebody))
 
-    def makeArchiveAndActiveDistroSeries(self, purpose=None):
-        if purpose is None:
-            purpose = ArchivePurpose.PRIMARY
+    def makeArchiveAndActiveDistroSeries(self, purpose=ArchivePurpose.PRIMARY):
         archive = self.factory.makeArchive(purpose=purpose)
         distroseries = self.factory.makeDistroSeries(
             distribution=archive.distribution,
@@ -562,8 +554,7 @@
                         distroseries=None, component=None,
                         pocket=None, strict_component=False):
         """Assert an upload to 'archive' will be accepted."""
-        self.assertIs(
-            None,
+        self.assertIsNone(
             self.checkUpload(
                 archive, person, sourcepackagename,
                 distroseries=distroseries, component=component,
@@ -610,6 +601,20 @@
         self.assertCannotUpload(
             CannotUploadToPocket, archive,
             self.factory.makePerson(), self.factory.makeSourcePackageName(),
+            pocket=PackagePublishingPocket.UPDATES,
+            distroseries=distroseries)
+
+    def test_checkUpload_primary_proposed_development(self):
+        # It should be possible to upload to the PROPOSED pocket while the
+        # distroseries is in the DEVELOPMENT status.
+        archive, distroseries = self.makeArchiveAndActiveDistroSeries(
+            purpose=ArchivePurpose.PRIMARY)
+        sourcepackagename = self.factory.makeSourcePackageName()
+        person = self.factory.makePerson()
+        removeSecurityProxy(archive).newPackageUploader(
+            person, sourcepackagename)
+        self.assertCanUpload(
+            archive, person, sourcepackagename,
             pocket=PackagePublishingPocket.PROPOSED,
             distroseries=distroseries)
 
@@ -665,8 +670,7 @@
         spn = self.factory.makeSourcePackageName()
         distroseries = self.factory.makeDistroSeries(
             status=SeriesStatus.CURRENT)
-        self.assertCanUpload(
-            archive, person, spn, distroseries=distroseries)
+        self.assertCanUpload(archive, person, spn, distroseries=distroseries)
 
     def test_checkUpload_copy_archive_no_permission(self):
         archive, distroseries = self.makeArchiveAndActiveDistroSeries(
@@ -688,8 +692,7 @@
         distroseries = self.factory.makeDistroSeries(
             distribution=archive.distribution,
             status=SeriesStatus.CURRENT)
-        self.assertIs(
-            None,
+        self.assertIsNone(
             archive.checkUploadToPocket(
                 distroseries, PackagePublishingPocket.RELEASE))
 
@@ -812,8 +815,7 @@
             purpose=ArchivePurpose.PPA, owner=person)
         suitesourcepackage = self.factory.makeSuiteSourcePackage(
             pocket=PackagePublishingPocket.PROPOSED)
-        self.assertEqual(
-            False,
+        self.assertFalse(
             archive.canUploadSuiteSourcePackage(person, suitesourcepackage))
 
     def test_canUploadSuiteSourcePackage_no_permission(self):
@@ -823,8 +825,7 @@
         suitesourcepackage = self.factory.makeSuiteSourcePackage(
             pocket=PackagePublishingPocket.RELEASE)
         person = self.factory.makePerson()
-        self.assertEqual(
-            False,
+        self.assertFalse(
             archive.canUploadSuiteSourcePackage(person, suitesourcepackage))
 
     def test_canUploadSuiteSourcePackage_package_permission(self):
@@ -835,8 +836,7 @@
         person = self.factory.makePerson()
         removeSecurityProxy(archive).newPackageUploader(
             person, suitesourcepackage.sourcepackagename)
-        self.assertEqual(
-            True,
+        self.assertTrue(
             archive.canUploadSuiteSourcePackage(person, suitesourcepackage))
 
     def test_canUploadSuiteSourcePackage_component_permission(self):
@@ -846,8 +846,7 @@
         suitesourcepackage = self.makePackageToUpload(distroseries)
         person = self.factory.makePerson()
         removeSecurityProxy(archive).newComponentUploader(person, "universe")
-        self.assertEqual(
-            True,
+        self.assertTrue(
             archive.canUploadSuiteSourcePackage(person, suitesourcepackage))
 
     def test_canUploadSuiteSourcePackage_strict_component(self):
@@ -867,8 +866,7 @@
         # This time the user can't upload as there has been a
         # publication and they don't have permission for the component
         # the package is published in.
-        self.assertEqual(
-            False,
+        self.assertFalse(
             archive.canUploadSuiteSourcePackage(person, suitesourcepackage))
 
     def test_hasAnyPermission(self):
@@ -922,7 +920,7 @@
         # country will create a new BinaryPackageReleaseDownloadCount
         # entry.
         day = date(2010, 2, 20)
-        self.assertIs(None, self.store.find(
+        self.assertIsNone(self.store.find(
             BinaryPackageReleaseDownloadCount,
             archive=self.archive, binary_package_release=self.bpr_1,
             day=day, country=self.australia).one())
@@ -1035,7 +1033,7 @@
 
     def test_default(self):
         """By default, ARM builds are not allowed as ARM is restricted."""
-        self.assertEquals(0,
+        self.assertEqual(0,
             self.archive_arch_set.getByArchive(
                 self.archive, self.arm).count())
         self.assertContentEqual([], self.archive.enabled_restricted_families)
@@ -1045,7 +1043,7 @@
         enable enabled_restricted_families for arm for that archive."""
         self.assertContentEqual([], self.archive.enabled_restricted_families)
         self.archive_arch_set.new(self.archive, self.arm)
-        self.assertEquals([self.arm],
+        self.assertEqual([self.arm],
                 list(self.archive.enabled_restricted_families))
 
     def test_get_returns_restricted_only(self):
@@ -1062,13 +1060,12 @@
         self.archive.enabled_restricted_families = [self.arm]
         allowed_restricted_families = self.archive_arch_set.getByArchive(
             self.archive, self.arm)
-        self.assertEquals(1, allowed_restricted_families.count())
-        self.assertEquals(self.arm,
-            allowed_restricted_families[0].processorfamily)
-        self.assertEquals(
-            [self.arm], self.archive.enabled_restricted_families)
+        self.assertEqual(1, allowed_restricted_families.count())
+        self.assertEqual(
+            self.arm, allowed_restricted_families[0].processorfamily)
+        self.assertEqual([self.arm], self.archive.enabled_restricted_families)
         self.archive.enabled_restricted_families = []
-        self.assertEquals(0,
+        self.assertEqual(0,
             self.archive_arch_set.getByArchive(
                 self.archive, self.arm).count())
         self.assertContentEqual([], self.archive.enabled_restricted_families)
@@ -1152,8 +1149,7 @@
 
     def test_returns_none_for_nonexistent_binary(self):
         # Non-existent files return None.
-        self.assertIs(
-            None,
+        self.assertIsNone(
             self.archive.getBinaryPackageRelease(
                 self.bpns['cdrkit'], '1.2.3-4', 'i386'))
 
@@ -1168,15 +1164,13 @@
             status=PackagePublishingStatus.PUBLISHED,
             architecturespecific=True)
 
-        self.assertIs(
-            None,
+        self.assertIsNone(
             self.archive.getBinaryPackageRelease(
                 self.bpns['foo-bin'], '1.2.3-4', 'i386'))
 
     def test_returns_none_from_another_archive(self):
         # Cross-archive searches are not performed.
-        self.assertIs(
-            None,
+        self.assertIsNone(
             self.factory.makeArchive().getBinaryPackageRelease(
                 self.bpns['foo-bin'], '1.2.3-4', 'i386'))
 
@@ -1229,15 +1223,13 @@
 
     def test_returns_none_for_source_file(self):
         # None is returned if the file is a source component instead.
-        self.assertIs(
-            None,
+        self.assertIsNone(
             self.archive.getBinaryPackageReleaseByFileName(
                 "foo_1.2.3-4.dsc"))
 
     def test_returns_none_for_nonexistent_file(self):
         # Non-existent files return None.
-        self.assertIs(
-            None,
+        self.assertIsNone(
             self.archive.getBinaryPackageReleaseByFileName(
                 "this-is-not-real_1.2.3-4_all.deb"))
 
@@ -1252,15 +1244,14 @@
             status=PackagePublishingStatus.PUBLISHED,
             architecturespecific=True)
 
-        self.assertEquals(
+        self.assertEqual(
             new_pubs[0].binarypackagerelease,
             self.archive.getBinaryPackageReleaseByFileName(
                 "foo-bin_1.2.3-4_i386.deb"))
 
     def test_returns_none_from_another_archive(self):
         # Cross-archive searches are not performed.
-        self.assertIs(
-            None,
+        self.assertIsNone(
             self.factory.makeArchive().getBinaryPackageReleaseByFileName(
                 "foo-bin_1.2.3-4_i386.deb"))
 
@@ -1282,13 +1273,13 @@
     def test_delete(self):
         # Sanity check for the unit-test.
         self.archive.delete(deleted_by=self.archive.owner)
-        self.failUnlessEqual(ArchiveStatus.DELETING, self.archive.status)
+        self.assertEqual(ArchiveStatus.DELETING, self.archive.status)
 
     def test_delete_when_disabled(self):
         # A disabled archive can also be deleted (bug 574246).
         self.archive.disable()
         self.archive.delete(deleted_by=self.archive.owner)
-        self.failUnlessEqual(ArchiveStatus.DELETING, self.archive.status)
+        self.assertEqual(ArchiveStatus.DELETING, self.archive.status)
 
 
 class TestCommercialArchive(TestCaseWithFactory):
@@ -1311,8 +1302,7 @@
         self.assertFalse(self.archive.commercial)
 
         # The archive owner can't change the value.
-        self.assertRaises(
-            Unauthorized, self.setCommercial, self.archive, True)
+        self.assertRaises(Unauthorized, self.setCommercial, self.archive, True)
 
         # Commercial admins can change it.
         login(COMMERCIAL_ADMIN_EMAIL)
@@ -1384,8 +1374,7 @@
         with person_logged_in(archive.owner):
             archive_dependency = archive.addArchiveDependency(dependency,
                 PackagePublishingPocket.RELEASE)
-            self.assertContentEqual(
-                archive.dependencies, [archive_dependency])
+            self.assertContentEqual(archive.dependencies, [archive_dependency])
 
 
 class TestArchiveDependencies(TestCaseWithFactory):
@@ -1443,12 +1432,11 @@
         if archive is None:
             archive = self.archive
 
-        self.assertEquals(
-            list(
-                archive.findDepCandidates(
-                    self.publisher.distroseries[arch_tag], pocket, component,
-                    source_package_name, name)),
-            expected)
+        self.assertEqual(
+            expected,
+            list(archive.findDepCandidates(
+                self.publisher.distroseries[arch_tag], pocket, component,
+                source_package_name, name)))
 
     def test_finds_candidate_in_same_archive(self):
         # A published candidate in the same archive should be found.
@@ -1466,7 +1454,7 @@
 
     def test_ppa_searches_primary_archive(self):
         # PPA searches implicitly look in the primary archive too.
-        self.assertEquals(self.archive.purpose, ArchivePurpose.PPA)
+        self.assertEqual(self.archive.purpose, ArchivePurpose.PPA)
         self.assertDep('i386', 'foo', [])
 
         bins = self.publisher.getPubBinaries(
@@ -1690,15 +1678,13 @@
         with celebrity_logged_in('admin'):
             comm = getUtility(ILaunchpadCelebrities).commercial_admin
             comm.addMember(ppa_owner, comm.teamowner)
-        self.assertIs(
-            None,
+        self.assertIsNone(
             Archive.validatePPA(ppa_owner, self.factory.getUniqueString(),
                                 private=True))
 
     def test_private_ppa_admin(self):
         ppa_owner = self.factory.makeAdministrator()
-        self.assertIs(
-            None,
+        self.assertIsNone(
             Archive.validatePPA(ppa_owner, self.factory.getUniqueString(),
                                 private=True))
 
@@ -1716,7 +1702,7 @@
 
     def test_valid_ppa(self):
         ppa_owner = self.factory.makePerson()
-        self.assertEqual(None, Archive.validatePPA(ppa_owner, None))
+        self.assertIsNone(Archive.validatePPA(ppa_owner, None))
 
 
 class TestGetComponentsForSeries(TestCaseWithFactory):
@@ -1733,14 +1719,13 @@
     def test_series_components_for_primary_archive(self):
         # The primary archive uses the series' defined components.
         archive = self.factory.makeArchive(purpose=ArchivePurpose.PRIMARY)
-        self.assertEquals(
-            0, len(archive.getComponentsForSeries(self.series)))
+        self.assertEqual(0, len(archive.getComponentsForSeries(self.series)))
 
         ComponentSelection(distroseries=self.series, component=self.comp1)
         ComponentSelection(distroseries=self.series, component=self.comp2)
         clear_property_cache(self.series)
 
-        self.assertEquals(
+        self.assertEqual(
             set((self.comp1, self.comp2)),
             set(archive.getComponentsForSeries(self.series)))
 
@@ -1749,7 +1734,7 @@
         archive = self.factory.makeArchive(purpose=ArchivePurpose.PARTNER)
         ComponentSelection(distroseries=self.series, component=self.comp1)
         partner_comp = getUtility(IComponentSet)['partner']
-        self.assertEquals(
+        self.assertEqual(
             [partner_comp],
             list(archive.getComponentsForSeries(self.series)))
 
@@ -1758,7 +1743,7 @@
         archive = self.factory.makeArchive(purpose=ArchivePurpose.PPA)
         ComponentSelection(distroseries=self.series, component=self.comp1)
         main_comp = getUtility(IComponentSet)['main']
-        self.assertEquals(
+        self.assertEqual(
             [main_comp], list(archive.getComponentsForSeries(self.series)))
 
 
@@ -1769,16 +1754,16 @@
 
     def test_default_component_for_other_archives(self):
         archive = self.factory.makeArchive(purpose=ArchivePurpose.PRIMARY)
-        self.assertIs(None, archive.default_component)
+        self.assertIsNone(archive.default_component)
 
     def test_default_component_for_partner(self):
         archive = self.factory.makeArchive(purpose=ArchivePurpose.PARTNER)
-        self.assertEquals(
+        self.assertEqual(
             getUtility(IComponentSet)['partner'], archive.default_component)
 
     def test_default_component_for_ppas(self):
         archive = self.factory.makeArchive(purpose=ArchivePurpose.PPA)
-        self.assertEquals(
+        self.assertEqual(
             getUtility(IComponentSet)['main'], archive.default_component)
 
 
@@ -1818,7 +1803,7 @@
         self.assertRaises(
             NotFoundError, self.archive.getFileByName, dsc.filename)
         pub.sourcepackagerelease.addFile(dsc)
-        self.assertEquals(dsc, self.archive.getFileByName(dsc.filename))
+        self.assertEqual(dsc, self.archive.getFileByName(dsc.filename))
 
     def test_nonexistent_source_file_is_not_found(self):
         # Something that looks like a source file but isn't is not
@@ -1834,7 +1819,7 @@
         self.assertRaises(
             NotFoundError, self.archive.getFileByName, deb.filename)
         pub.binarypackagerelease.addFile(deb)
-        self.assertEquals(deb, self.archive.getFileByName(deb.filename))
+        self.assertEqual(deb, self.archive.getFileByName(deb.filename))
 
     def test_nonexistent_binary_file_is_not_found(self):
         # Something that looks like a binary file but isn't is not
@@ -1850,10 +1835,9 @@
             changes_filename='foo_1.0_source.changes')
         pu.setDone()
         self.assertRaises(
-            NotFoundError, self.archive.getFileByName,
-            pu.changesfile.filename)
+            NotFoundError, self.archive.getFileByName, pu.changesfile.filename)
         pu.addSource(pub.sourcepackagerelease)
-        self.assertEquals(
+        self.assertEqual(
             pu.changesfile,
             self.archive.getFileByName(pu.changesfile.filename))
 
@@ -1871,7 +1855,7 @@
         diff = self.factory.makePackageDiff(
             to_source=pub.sourcepackagerelease,
             diff_filename='foo_1.0.diff.gz')
-        self.assertEquals(
+        self.assertEqual(
             diff.diff_content,
             self.archive.getFileByName(diff.diff_content.filename))
 
@@ -1883,7 +1867,7 @@
         pub.sourcepackagerelease.addFile(dsc)
 
         # The file is initially found without trouble.
-        self.assertEquals(dsc, self.archive.getFileByName(dsc.filename))
+        self.assertEqual(dsc, self.archive.getFileByName(dsc.filename))
 
         # But after expiry it is not.
         removeSecurityProxy(dsc).content = None
@@ -1893,7 +1877,7 @@
         # It reappears if we create a new one.
         new_dsc = self.factory.makeLibraryFileAlias(filename=dsc.filename)
         pub.sourcepackagerelease.addFile(new_dsc)
-        self.assertEquals(new_dsc, self.archive.getFileByName(dsc.filename))
+        self.assertEqual(new_dsc, self.archive.getFileByName(dsc.filename))
 
 
 class TestGetPublishedSources(TestCaseWithFactory):
@@ -1992,11 +1976,8 @@
             name=['package1', 'package2'])
 
         self.assertEqual(
-            3,
-            distroseries.main_archive.getPublishedSources().count())
-        self.assertEqual(
-            2,
-            filtered_sources.count())
+            3, distroseries.main_archive.getPublishedSources().count())
+        self.assertEqual(2, filtered_sources.count())
         self.assertContentEqual(
             ['package1', 'package2'],
             [filtered_source.sourcepackagerelease.name for filtered_source in
@@ -2155,7 +2136,7 @@
         # The source should not be published yet in the target_archive.
         published = target_archive.getPublishedSources(
             name=source.source_package_name).any()
-        self.assertIs(None, published)
+        self.assertIsNone(published)
 
         # There should be one copy job.
         job_source = getUtility(IPlainPackageCopyJobSource)
@@ -2241,7 +2222,7 @@
         # The source should not be published yet in the target_archive.
         published = target_archive.getPublishedSources(
             name=source.source_package_name).any()
-        self.assertIs(None, published)
+        self.assertIsNone(published)
 
         # There should be one copy job.
         job_source = getUtility(IPlainPackageCopyJobSource)
@@ -2475,11 +2456,11 @@
 
         source = getUtility(IPlainPackageCopyJobSource)
         found_jobs = source.getIncompleteJobsForArchive(archive2)
-        self.assertEqual(None, found_jobs.any())
+        self.assertIsNone(found_jobs.any())
 
     def test_removeCopyNotification_raises_for_not_failed(self):
         distroseries, archive1, archive2, requester, job = self.makeJob()
-        
+
         self.assertNotEqual(JobStatus.FAILED, job.status)
         with person_logged_in(archive2.owner):
             self.assertRaises(