← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~jtv/launchpad/pre-788078 into lp:launchpad

 

Jeroen T. Vermeulen has proposed merging lp:~jtv/launchpad/pre-788078 into lp:launchpad.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)
Related bugs:
  Bug #189866 in Launchpad itself: "publish-distro should be a LaunchpadCronScript"
  https://bugs.launchpad.net/launchpad/+bug/189866

For more details, see:
https://code.launchpad.net/~jtv/launchpad/pre-788078/+merge/68374

= Summary =

The publish-distro script needs to be cleaned up before we can sensibly extend it further.  Bug 189866 says that it should also be turned into a proper LaunchpadScript; that alone saves a lot of boilerplate.  The bug has been open for well over 3 years, and meanwhile we've merely continued to grow the hairball of code.


== Proposed fix ==

This turns the script into a proper LaunchpadCronScript, and adds fine-grained tests.


== Implementation details ==

We could conceivably drop more of the high-level testing, but especially with big refactorings I wanted to be conservative.  Running the script body with multiple commits lots of times just to see that the script accepted the right options was going a bit far though; since it's isolated now I was able to test it at a much finer granularity.


== Tests ==

{{{
./bin/test -vvc lp.soyuz.scripts.tests.test_publishdistro
./bin/test -vvc lp.soyuz -t soyuz-upload.txt -t soyuz-set-of-uploads.txt
}}}


== Demo and Q/A ==

The publish-distro script should still work; we can test that on dogfood.  The publish-ftpmaster script also invokes publish-distro so we ought to test that as well.


= Launchpad lint =

I cleaned up some of it but there's still some left that I can't do much about:

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/soyuz/scripts/publishdistro.py
  lib/lp/soyuz/scripts/tests/test_publishdistro.py
  scripts/publish-distro.py
  lib/lp/archivepublisher/scripts/publish_ftpmaster.py
  lib/lp/soyuz/doc/soyuz-upload.txt
  lib/lp/soyuz/doc/soyuz-set-of-uploads.txt

./scripts/publish-distro.py
       8: '_pythonpath' imported but unused
./lib/lp/soyuz/doc/soyuz-upload.txt
     522: want exceeds 78 characters.
     523: want exceeds 78 characters.
     524: want exceeds 78 characters.
     620: want exceeds 78 characters.
     621: want exceeds 78 characters.
     622: want exceeds 78 characters.
     684: want exceeds 78 characters.
     687: want exceeds 78 characters.
     712: want exceeds 78 characters.
     713: want exceeds 78 characters.
     714: want exceeds 78 characters.
     715: want exceeds 78 characters.
-- 
https://code.launchpad.net/~jtv/launchpad/pre-788078/+merge/68374
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~jtv/launchpad/pre-788078 into lp:launchpad.
=== modified file 'lib/lp/archivepublisher/scripts/publish_ftpmaster.py'
--- lib/lp/archivepublisher/scripts/publish_ftpmaster.py	2011-05-09 08:48:12 +0000
+++ lib/lp/archivepublisher/scripts/publish_ftpmaster.py	2011-07-19 12:44:38 +0000
@@ -9,7 +9,6 @@
     ]
 
 from datetime import datetime
-from optparse import OptionParser
 import os
 from pytz import utc
 from zope.component import getUtility
@@ -26,7 +25,7 @@
 from lp.registry.interfaces.series import SeriesStatus
 from lp.services.utils import file_exists
 from lp.soyuz.enums import ArchivePurpose
-from lp.soyuz.scripts import publishdistro
+from lp.soyuz.scripts.publishdistro import PublishDistro
 from lp.soyuz.scripts.ftpmaster import LpQueryDistro
 from lp.soyuz.scripts.processaccepted import ProcessAccepted
 
@@ -363,10 +362,10 @@
             args +
             sum([['-s', suite] for suite in suites], []))
 
-        parser = OptionParser()
-        publishdistro.add_options(parser)
-        options, args = parser.parse_args(arguments)
-        publishdistro.run_publisher(options, self.txn, log=self.logger)
+        publish_distro = PublishDistro(test_args=arguments)
+        publish_distro.logger = self.logger
+        publish_distro.txn = self.txn
+        publish_distro.main()
 
     def publishDistroArchive(self, archive, security_suites=None):
         """Publish the results for an archive.

=== modified file 'lib/lp/soyuz/doc/soyuz-set-of-uploads.txt'
--- lib/lp/soyuz/doc/soyuz-set-of-uploads.txt	2011-06-28 15:04:29 +0000
+++ lib/lp/soyuz/doc/soyuz-set-of-uploads.txt	2011-07-19 12:44:38 +0000
@@ -710,11 +710,15 @@
     >>> fillLibrarianFile(70)
 
     >>> run_publish_distro(careful=True)
-    DEBUG   Initializing zopeless.
+    DEBUG   Enabled by DEFAULT section
     DEBUG   Distribution: ubuntutest
     ...
-    DEBUG   Added /var/tmp/archive/ubuntutest/pool/universe/b/bar/bar_1.0-2_i386.deb from library
-    DEBUG   Added /var/tmp/archive/ubuntutest/pool/universe/b/bar/bar_1.0-1_i386.deb from library
+    DEBUG   Added
+    /var/tmp/archive/ubuntutest/pool/universe/b/bar/bar_1.0-2_i386.deb from
+    library
+    DEBUG   Added
+    /var/tmp/archive/ubuntutest/pool/universe/b/bar/bar_1.0-1_i386.deb from
+    library
     ...
 
 
@@ -739,10 +743,11 @@
 uploaded in the test above don't break the assumptions of this test.
 
     >>> run_publish_distro(careful_publishing=True)
-    DEBUG   Initializing zopeless.
+    DEBUG   Enabled by DEFAULT section
     DEBUG   Distribution: ubuntutest
     ...
-    DEBUG   /var/tmp/archive/ubuntutest/pool/universe/b/bar/bar_1.0-2_i386.deb is already in pool with the same content.
+    DEBUG   /var/tmp/archive/ubuntutest/pool/universe/b/bar/bar_1.0-2_i386.deb
+    is already in pool with the same content.
     ...
     DEBUG   Skipping a-f stanza for breezy-autotest/RELEASE
     ...

=== modified file 'lib/lp/soyuz/doc/soyuz-upload.txt'
--- lib/lp/soyuz/doc/soyuz-upload.txt	2011-06-16 08:50:52 +0000
+++ lib/lp/soyuz/doc/soyuz-upload.txt	2011-07-19 12:44:38 +0000
@@ -517,7 +517,7 @@
     >>> print stdout
     <BLANKLINE>
     >>> print stderr
-    DEBUG   Initializing zopeless.
+    DEBUG Enabled by DEFAULT section
     DEBUG     Distribution: ubuntutest
     ...
     DEBUG   Added /var/tmp/archive/ubuntutest/pool/universe/e/etherwake/etherwake_1.08.orig.tar.gz from library
@@ -615,7 +615,7 @@
 already on disk, verify the contents are as expected.
 
     >>> print stderr
-    DEBUG   Initializing zopeless.
+    DEBUG Enabled by DEFAULT section
     DEBUG     Distribution: ubuntutest
     ...
     DEBUG   /var/tmp/archive/ubuntutest/pool/universe/e/etherwake/etherwake_1.08.orig.tar.gz is already in pool with the same content.

=== modified file 'lib/lp/soyuz/scripts/publishdistro.py'
--- lib/lp/soyuz/scripts/publishdistro.py	2011-04-27 08:20:37 +0000
+++ lib/lp/soyuz/scripts/publishdistro.py	2011-07-19 12:44:38 +0000
@@ -1,23 +1,22 @@
 # Copyright 2009-2011 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
-"""Publisher script functions."""
+"""Publisher script class."""
 
 __all__ = [
-    'add_options',
-    'run_publisher',
+    'PublishDistro',
     ]
 
+from optparse import OptionValueError
 from zope.component import getUtility
 
-from canonical.launchpad.scripts import (
-    logger,
-    logger_options,
-    )
 from lp.app.errors import NotFoundError
 from lp.archivepublisher.publishing import getPublisher
 from lp.registry.interfaces.distribution import IDistributionSet
-from lp.services.scripts.base import LaunchpadScriptFailure
+from lp.services.scripts.base import (
+    LaunchpadCronScript,
+    LaunchpadScriptFailure,
+    )
 from lp.soyuz.enums import (
     ArchivePurpose,
     ArchiveStatus,
@@ -27,194 +26,289 @@
     MAIN_ARCHIVE_PURPOSES,
     )
 
-# XXX Julian 2008-02-07 bug=189866:
-# These functions should be in a LaunchpadScript.
-
-def add_options(parser):
-    logger_options(parser)
-
-    parser.add_option("-C", "--careful", action="store_true",
-                      dest="careful", metavar="", default=False,
-                      help="Turns on all the below careful options.")
-
-    parser.add_option("-P", "--careful-publishing", action="store_true",
-                      dest="careful_publishing", metavar="", default=False,
-                      help="Make the package publishing process careful.")
-
-    parser.add_option("-D", "--careful-domination", action="store_true",
-                      dest="careful_domination", metavar="", default=False,
-                      help="Make the domination process careful.")
-
-    parser.add_option("-A", "--careful-apt", action="store_true",
-                      dest="careful_apt", metavar="", default=False,
-                      help="Make index generation (e.g. apt-ftparchive) careful.")
-
-    parser.add_option("-d", "--distribution",
-                      dest="distribution", metavar="DISTRO", default="ubuntu",
-                      help="The distribution to publish.")
-
-    parser.add_option('-s', '--suite', metavar='SUITE', dest='suite',
-                      action='append', type='string', default=[],
-                      help='The suite to publish')
-
-    parser.add_option("-R", "--distsroot",
-                      dest="distsroot", metavar="SUFFIX", default=None,
-                      help="Override the dists path for generation of the "
-                           "PRIMARY and PARTNER archives only.")
-
-    parser.add_option("--ppa", action="store_true",
-                      dest="ppa", metavar="PPA", default=False,
-                      help="Only run over PPA archives.")
-
-    parser.add_option("--private-ppa", action="store_true",
-                      dest="private_ppa", metavar="PRIVATEPPA", default=False,
-                      help="Only run over private PPA archives.")
-
-    parser.add_option("--partner", action="store_true",
-                      dest="partner", metavar="PARTNER", default=False,
-                      help="Only run over the partner archive.")
-
-    parser.add_option("--copy-archive", action="store_true",
-                      dest="copy_archive", metavar="COPYARCHIVE",
-                      default=False,
-                      help="Only run over the copy archives.")
-
-    parser.add_option(
-        "--primary-debug", action="store_true", default=False,
-        dest="primary_debug", metavar="PRIMARYDEBUG",
-        help="Only run over the debug-symbols for primary archive.")
-
-
-def run_publisher(options, txn, log=None):
-    if not log:
-        log = logger(options, "publish-distro")
-
-    def careful_msg(what):
-        """Quick handy util for the below."""
-        if options.careful:
+
+def is_ppa_private(ppa):
+    """Is `ppa` private?"""
+    return ppa.private
+
+
+def is_ppa_public(ppa):
+    """Is `ppa` public?"""
+    return not ppa.private
+
+
+class PublishDistro(LaunchpadCronScript):
+    """Distro publisher."""
+
+    def add_my_options(self):
+        self.parser.add_option(
+            "-C", "--careful", action="store_true", dest="careful",
+            default=False, help="Turns on all the below careful options.")
+
+        self.parser.add_option(
+            "-P", "--careful-publishing", action="store_true",
+            dest="careful_publishing", default=False,
+            help="Make the package publishing process careful.")
+
+        self.parser.add_option(
+            "-D", "--careful-domination", action="store_true",
+            dest="careful_domination", default=False,
+            help="Make the domination process careful.")
+
+        self.parser.add_option(
+            "-A", "--careful-apt", action="store_true", dest="careful_apt",
+            default=False,
+            help="Make index generation (e.g. apt-ftparchive) careful.")
+
+        self.parser.add_option(
+            "-d", "--distribution", dest="distribution", metavar="DISTRO",
+            default="ubuntu", help="The distribution to publish.")
+
+        self.parser.add_option(
+            '-s', '--suite', metavar='SUITE', dest='suite', action='append',
+            type='string', default=[], help='The suite to publish')
+
+        self.parser.add_option(
+            "-R", "--distsroot", dest="distsroot", metavar="SUFFIX",
+            default=None,
+            help=(
+                "Override the dists path for generation of the PRIMARY and "
+                "PARTNER archives only."))
+
+        self.parser.add_option(
+            "--ppa", action="store_true", dest="ppa", default=False,
+            help="Only run over PPA archives.")
+
+        self.parser.add_option(
+            "--private-ppa", action="store_true", dest="private_ppa",
+            default=False, help="Only run over private PPA archives.")
+
+        self.parser.add_option(
+            "--partner", action="store_true", dest="partner", default=False,
+            help="Only run over the partner archive.")
+
+        self.parser.add_option(
+            "--copy-archive", action="store_true", dest="copy_archive",
+            default=False, help="Only run over the copy archives.")
+
+        self.parser.add_option(
+            "--primary-debug", action="store_true", default=False,
+            dest="primary_debug",
+            help="Only run over the debug-symbols for primary archive.")
+
+    def isCareful(self, option):
+        """Is the given "carefulness" option enabled?
+
+        Yes if the option is True, but also if the global "careful" option
+        is set.
+
+        :param option: The specific "careful" option to test, e.g.
+            `self.options.careful_publishing`.
+        :return: Whether the option should be treated as asking us to be
+            careful.
+        """
+        return option or self.options.careful
+
+    def describeCare(self, option):
+        """Helper: describe carefulness setting of given option.
+
+        Produces a human-readable string saying whether the option is set
+        to careful mode; or "overridden" to careful mode by the global
+        "careful" option; or is left in normal mode.
+        """
+        if self.options.careful:
             return "Careful (Overridden)"
-        if what:
+        elif option:
             return "Careful"
-        return "Normal"
-
-    exclusive_options = (
-        options.partner, options.ppa, options.private_ppa,
-        options.primary_debug, options.copy_archive)
-
-    num_exclusive = [flag for flag in exclusive_options if flag]
-    if len(num_exclusive) > 1:
-        raise LaunchpadScriptFailure(
-            "Can only specify one of partner, ppa, private-ppa, copy-archive"
-            " and primary-debug.")
-
-    log.debug("  Distribution: %s" % options.distribution)
-    log.debug("    Publishing: %s" % careful_msg(options.careful_publishing))
-    log.debug("    Domination: %s" % careful_msg(options.careful_domination))
-    if num_exclusive == 0:
-        log.debug("Apt-FTPArchive: %s" % careful_msg(options.careful_apt))
-    else:
-        log.debug("      Indexing: %s" % careful_msg(options.careful_apt))
-
-    log.debug("Finding distribution object.")
-
-    try:
-        distribution = getUtility(IDistributionSet).getByName(
-            options.distribution)
-    except NotFoundError, info:
-        raise LaunchpadScriptFailure(info)
-
-    allowed_suites = set()
-    for suite in options.suite:
+        else:
+            return "Normal"
+
+    def logOption(self, name, value):
+        """Describe the state of `option` to the debug log."""
+        self.logger.debug("%14s: %s", name, value)
+
+    def countExclusiveOptions(self):
+        """Return the number of exclusive "mode" options that were set.
+
+        In valid use, at most one of them should be set.
+        """
+        exclusive_options = [
+            self.options.partner,
+            self.options.ppa,
+            self.options.private_ppa,
+            self.options.primary_debug,
+            self.options.copy_archive,
+            ]
+        return len(filter(None, exclusive_options))
+
+    def logOptions(self):
+        """Dump the selected options to the debug log."""
+        if self.countExclusiveOptions() == 0:
+            indexing_engine = "Apt-FTPArchive"
+        else:
+            indexing_engine = "Indexing"
+        self.logOption('Distribution', self.options.distribution)
+        log_items = [
+            ('Publishing', self.options.careful_publishing),
+            ('Domination', self.options.careful_domination),
+            (indexing_engine, self.options.careful_apt),
+            ]
+        for name, option in log_items:
+            self.logOption(name, self.describeCare(option))
+
+    def validateOptions(self):
+        """Check given options for user interface violations."""
+        if len(self.args) > 0:
+            raise OptionValueError(
+                "publish-distro takes no arguments, only options.")
+        if self.countExclusiveOptions() > 1:
+            raise OptionValueError(
+                "Can only specify one of partner, ppa, private-ppa, "
+                "copy-archive and primary-debug.")
+
+        for_ppa = (self.options.ppa or self.options.private_ppa)
+        if for_ppa and self.options.distsroot:
+            raise OptionValueError(
+                "We should not define 'distsroot' in PPA mode!", )
+
+    def findDistro(self):
+        """Find the selected distribution."""
+        self.logger.debug("Finding distribution object.")
+        name = self.options.distribution
+        distro = getUtility(IDistributionSet).getByName(name)
+        if distro is None:
+            raise OptionValueError("Distribution '%s' not found." % name)
+        return distro
+
+    def findSuite(self, suite):
+        """Find the named `suite` in the selected `Distribution`.
+
+        :param suite: The suite name to look for.
+        :return: A tuple of distroseries name and pocket.
+        """
         try:
-            distroseries, pocket = distribution.getDistroSeriesAndPocket(
-                suite)
-        except NotFoundError, info:
-            raise LaunchpadScriptFailure(info)
-        allowed_suites.add((distroseries.name, pocket))
-
-    if options.partner:
-        archives = [distribution.getArchiveByComponent('partner')]
-    elif options.ppa or options.private_ppa:
-        if options.careful or options.careful_publishing:
-            archives = distribution.getAllPPAs()
-        else:
-            archives = distribution.getPendingPublicationPPAs()
-
-        # Filter out non-private if we're publishing private PPAs only,
-        # or filter out private if we're doing non-private.
-        if options.private_ppa:
-            archives = [archive for archive in archives if archive.private]
-        else:
-            archives = [
-                archive for archive in archives if not archive.private]
-
-        if options.distsroot is not None:
-            raise LaunchpadScriptFailure(
-                "We should not define 'distsroot' in PPA mode !")
-    elif options.primary_debug:
+            series, pocket = self.distribution.getDistroSeriesAndPocket(suite)
+        except NotFoundError, e:
+            raise OptionValueError(e)
+        return series.name, pocket
+
+    def findAllowedSuites(self):
+        """Find the selected suite(s)."""
+        return set([self.findSuite(suite) for suite in self.options.suite])
+
+    def getDebugArchive(self):
+        """Find the debug archive for the selected distribution, as a list."""
         debug_archive = getUtility(IArchiveSet).getByDistroPurpose(
-            distribution, ArchivePurpose.DEBUG)
+            self.distribution, ArchivePurpose.DEBUG)
         if debug_archive is None:
-            raise LaunchpadScriptFailure(
-                "Could not find DEBUG archive for %s" % distribution.name)
-        archives = [debug_archive]
-    elif options.copy_archive:
-        archives = getUtility(IArchiveSet).getArchivesForDistribution(
-            distribution, purposes=[ArchivePurpose.COPY])
-        if not bool(archives):
+            raise OptionValueError(
+                "Could not find DEBUG archive for %s"
+                % self.distribution.name)
+        return [debug_archive]
+
+    def getCopyArchives(self):
+        """Find copy archives for the selected distribution."""
+        copy_archives = list(
+            getUtility(IArchiveSet).getArchivesForDistribution(
+                self.distribution, purposes=[ArchivePurpose.COPY]))
+        if copy_archives == []:
             raise LaunchpadScriptFailure("Could not find any COPY archives")
-    else:
-        archives = [distribution.main_archive]
-
-    # Consider only archives that have their "to be published" flag turned on
-    # or are pending deletion.
-    archives = [
-        archive for archive in archives
-        if archive.publish or archive.status == ArchiveStatus.DELETING]
-
-    for archive in archives:
+        return copy_archives
+
+    def getPPAs(self):
+        """Find private package archives for the selected distribution."""
+        if self.isCareful(self.options.careful_publishing):
+            return self.distribution.getAllPPAs()
+        else:
+            return self.distribution.getPendingPublicationPPAs()
+
+    def getTargetArchives(self):
+        """Find the archive(s) selected by the script's options."""
+        if self.options.partner:
+            return [self.distribution.getArchiveByComponent('partner')]
+        elif self.options.ppa:
+            return filter(is_ppa_public, self.getPPAs())
+        elif self.options.private_ppa:
+            return filter(is_ppa_private, self.getPPAs())
+        elif self.options.primary_debug:
+            return self.getDebugArchive()
+        elif self.options.copy_archive:
+            return self.getCopyArchives()
+        else:
+            return [self.distribution.main_archive]
+
+    def getPublisher(self, archive, allowed_suites):
+        """Get a publisher for the given options."""
         if archive.purpose in MAIN_ARCHIVE_PURPOSES:
-            log.info(
-                "Processing %s %s" % (distribution.name, archive.displayname))
+            description = "%s %s" % (
+                self.distribution.name,
+                archive.displayname)
             # Only let the primary/partner archives override the distsroot.
-            publisher = getPublisher(
-                archive, allowed_suites, log, options.distsroot)
-        else:
-            log.info("Processing %s" % archive.archive_url)
-            publisher = getPublisher(archive, allowed_suites, log)
-
-        # Do we need to delete the archive or publish it?
-        if archive.status == ArchiveStatus.DELETING:
-            if archive.purpose == ArchivePurpose.PPA:
-                publisher.deleteArchive()
-                txn.commit()
-            else:
-                # Other types of archives do not currently support deletion.
-                log.warning(
-                    "Deletion of %s skipped: operation not supported on %s"
-                    % archive.displayname)
-        else:
-            publisher.A_publish(options.careful or options.careful_publishing)
-            txn.commit()
-
-            # Flag dirty pockets for any outstanding deletions.
-            publisher.A2_markPocketsWithDeletionsDirty()
-            publisher.B_dominate(
-                options.careful or options.careful_domination)
-            txn.commit()
-
-            # The primary and copy archives use apt-ftparchive to generate the
-            # indexes, everything else uses the newer internal LP code.
-            if archive.purpose in (ArchivePurpose.PRIMARY, ArchivePurpose.COPY):
-                publisher.C_doFTPArchive(
-                    options.careful or options.careful_apt)
-            else:
-                publisher.C_writeIndexes(
-                    options.careful or options.careful_apt)
-            txn.commit()
-
-            publisher.D_writeReleaseFiles(
-                options.careful or options.careful_apt)
-            txn.commit()
-
-    log.debug("Ciao")
+            distsroot = self.options.distsroot
+        else:
+            description = archive.archive_url
+            distsroot = None
+
+        self.logger.info("Processing %s", description)
+        return getPublisher(archive, allowed_suites, self.logger, distsroot)
+
+    def deleteArchive(self, archive, publisher):
+        """Ask `publisher` to delete `archive`."""
+        if archive.purpose == ArchivePurpose.PPA:
+            publisher.deleteArchive()
+            return True
+        else:
+            # Other types of archives do not currently support deletion.
+            self.logger.warning(
+                "Deletion of %s skipped: operation not supported on %s",
+                archive.displayname, archive.purpose.title)
+            return False
+
+    def publishArchive(self, archive, publisher):
+        """Ask `publisher` to publish `archive`.
+
+        Commits transactions along the way.
+        """
+        publisher.A_publish(self.isCareful(self.options.careful_publishing))
+        self.txn.commit()
+
+        # Flag dirty pockets for any outstanding deletions.
+        publisher.A2_markPocketsWithDeletionsDirty()
+        publisher.B_dominate(self.isCareful(self.options.careful_domination))
+        self.txn.commit()
+
+        # The primary and copy archives use apt-ftparchive to
+        # generate the indexes, everything else uses the newer
+        # internal LP code.
+        careful_indexing = self.isCareful(self.options.careful_apt)
+        if archive.purpose in (ArchivePurpose.PRIMARY, ArchivePurpose.COPY):
+            publisher.C_doFTPArchive(careful_indexing)
+        else:
+            publisher.C_writeIndexes(careful_indexing)
+        self.txn.commit()
+
+        publisher.D_writeReleaseFiles(careful_indexing)
+        # The caller will commit this last step.
+
+    def main(self):
+        """See `LaunchpadScript`."""
+        self.validateOptions()
+        self.logOptions()
+        self.distribution = self.findDistro()
+        allowed_suites = self.findAllowedSuites()
+
+        for archive in self.getTargetArchives():
+            publisher = self.getPublisher(archive, allowed_suites)
+
+            if archive.status == ArchiveStatus.DELETING:
+                work_done = self.deleteArchive(archive, publisher)
+            elif archive.publish:
+                self.publishArchive(archive, publisher)
+                work_done = True
+            else:
+                work_done = False
+
+            if work_done:
+                self.txn.commit()
+
+        self.logger.debug("Ciao")

=== modified file 'lib/lp/soyuz/scripts/tests/test_publishdistro.py'
--- lib/lp/soyuz/scripts/tests/test_publishdistro.py	2011-03-15 14:46:27 +0000
+++ lib/lp/soyuz/scripts/tests/test_publishdistro.py	2011-07-19 12:44:38 +0000
@@ -1,37 +1,42 @@
-# Copyright 2009 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2011 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Functional tests for publish-distro.py script."""
 
 __metaclass__ = type
 
-from optparse import OptionParser
+from optparse import OptionValueError
 import os
 import shutil
 import subprocess
 import sys
-import unittest
-
 from zope.component import getUtility
 from zope.security.proxy import removeSecurityProxy
 
 from canonical.config import config
+from canonical.testing.layers import ZopelessDatabaseLayer
 from lp.archivepublisher.config import getPubConfig
 from lp.archivepublisher.interfaces.publisherconfig import IPublisherConfigSet
+from lp.archivepublisher.publishing import Publisher
 from lp.registry.interfaces.distribution import IDistributionSet
 from lp.registry.interfaces.person import IPersonSet
-from lp.services.log.logger import BufferLogger
+from lp.registry.interfaces.pocket import PackagePublishingPocket
+from lp.services.log.logger import (
+    BufferLogger,
+    DevNullLogger,
+    )
 from lp.services.scripts.base import LaunchpadScriptFailure
 from lp.soyuz.enums import (
     ArchivePurpose,
     BinaryPackageFormat,
     PackagePublishingStatus,
     )
-from lp.soyuz.interfaces.archive import (
-    IArchiveSet,
-    )
-from lp.soyuz.scripts import publishdistro
+from lp.soyuz.interfaces.archive import IArchiveSet
+from lp.soyuz.scripts.publishdistro import PublishDistro
 from lp.soyuz.tests.test_publishing import TestNativePublishingBase
+from lp.testing import TestCaseWithFactory
+from lp.testing.fakemethod import FakeMethod
+from lp.testing.faketransaction import FakeTransaction
 
 
 class TestPublishDistro(TestNativePublishingBase):
@@ -46,12 +51,11 @@
         args = ["-d", distribution]
         if extra_args is not None:
             args.extend(extra_args)
-        parser = OptionParser()
-        publishdistro.add_options(parser)
-        options, args = parser.parse_args(args=args)
+        publish_distro = PublishDistro(test_args=args)
+        publish_distro.logger = BufferLogger()
+        publish_distro.txn = self.layer.txn
         self.layer.switchDbUser(config.archivepublisher.dbuser)
-        publishdistro.run_publisher(
-            options, self.layer.txn, log=BufferLogger())
+        publish_distro.main()
         self.layer.switchDbUser('launchpad')
 
     def runPublishDistroScript(self):
@@ -250,7 +254,7 @@
             private=True, distribution=ubuntutest)
 
         # Publish something to the private PPA:
-        pub_source =  self.getPubSource(
+        pub_source = self.getPubSource(
             sourcename='baz', filecontent='baz', archive=private_ppa)
         self.layer.txn.commit()
 
@@ -271,7 +275,7 @@
         # 'ubuntutest' (default testing distribution) has no DEBUG
         # archive, Thus an error is raised.
         self.assertRaises(
-            LaunchpadScriptFailure,
+            OptionValueError,
             self.runPublishDistro, ['--primary-debug'])
 
         # The DEBUG repository path was not created.
@@ -338,7 +342,7 @@
         removeSecurityProxy(copy_archive).publish = True
 
         # Publish something.
-        pub_source =  self.getPubSource(
+        pub_source = self.getPubSource(
             sourcename='baz', filecontent='baz', archive=copy_archive)
 
         # Try a plain PPA run, to ensure the copy archive is not published.
@@ -391,37 +395,330 @@
             "%s/hoary-test/main/binary-i386/Packages" % self.config.distsroot)
         self.assertNotExists(index_path)
 
-    def testExclusiveOptions(self):
-        """Test that some command line options are mutually exclusive."""
-        self.assertRaises(
-            LaunchpadScriptFailure,
-            self.runPublishDistro,
-            ['--ppa', '--partner', '--primary-debug', '--copy-archive'])
-        self.assertRaises(
-            LaunchpadScriptFailure,
-            self.runPublishDistro, ['--ppa', '--partner'])
-        self.assertRaises(
-            LaunchpadScriptFailure,
-            self.runPublishDistro, ['--ppa', '--private-ppa'])
-        self.assertRaises(
-            LaunchpadScriptFailure,
-            self.runPublishDistro, ['--ppa', '--primary-debug'])
-        self.assertRaises(
-            LaunchpadScriptFailure,
-            self.runPublishDistro, ['--ppa', '--copy-archive'])
-        self.assertRaises(
-            LaunchpadScriptFailure,
-            self.runPublishDistro, ['--partner', '--private-ppa'])
-        self.assertRaises(
-            LaunchpadScriptFailure,
-            self.runPublishDistro, ['--partner', '--primary-debug'])
-        self.assertRaises(
-            LaunchpadScriptFailure,
-            self.runPublishDistro, ['--partner', '--copy-archive'])
-        self.assertRaises(
-            LaunchpadScriptFailure,
-            self.runPublishDistro, ['--primary-debug', '--copy-archive'])
-
-
-def test_suite():
-    return unittest.TestLoader().loadTestsFromName(__name__)
+
+class FakeArchive:
+    def __init__(self, purpose=ArchivePurpose.PRIMARY):
+        self.purpose = purpose
+
+
+class FakePublisher:
+    """A very simple fake `Publisher`."""
+    def __init__(self):
+        self.A_publish = FakeMethod()
+        self.A2_markPocketsWithDeletionsDirty = FakeMethod()
+        self.B_dominate = FakeMethod()
+        self.C_doFTPArchive = FakeMethod()
+        self.C_writeIndexes = FakeMethod()
+        self.D_writeReleaseFiles = FakeMethod()
+
+
+class TestPublishDistroMethods(TestCaseWithFactory):
+    """Fine-grained unit tests for `PublishDistro`."""
+
+    layer = ZopelessDatabaseLayer
+
+    def makeScript(self, distribution=None, args=[]):
+        """Create a `PublishDistro` for `distribution`."""
+        if distribution is None:
+            distribution = self.factory.makeDistribution()
+        full_args = ['-d', distribution.name] + args
+        script = PublishDistro(test_args=full_args)
+        script.distribution = distribution
+        script.logger = DevNullLogger()
+        return script
+
+    def test_isCareful_is_false_if_option_not_set(self):
+        self.assertFalse(self.makeScript().isCareful(False))
+
+    def test_isCareful_is_true_if_option_is_set(self):
+        self.assertTrue(self.makeScript().isCareful(True))
+
+    def test_isCareful_is_true_if_global_careful_option_is_set(self):
+        self.assertTrue(self.makeScript(args=['--careful']).isCareful(False))
+
+    def test_describeCare_reports_non_careful_option(self):
+        self.assertEqual("Normal", self.makeScript().describeCare(False))
+
+    def test_describeCare_reports_careful_option(self):
+        self.assertEqual("Careful", self.makeScript().describeCare(True))
+
+    def test_describeCare_reports_careful_override(self):
+        self.assertEqual(
+            "Careful (Overridden)",
+            self.makeScript(args=['--careful']).describeCare(False))
+
+    def test_countExclusiveOptions_is_zero_if_none_set(self):
+        self.assertEqual(0, self.makeScript().countExclusiveOptions())
+
+    def test_countExclusiveOptions_counts_partner(self):
+        self.assertEqual(
+            1, self.makeScript(args=['--partner']).countExclusiveOptions())
+
+    def test_countExclusiveOptions_counts_ppa(self):
+        self.assertEqual(
+            1, self.makeScript(args=['--ppa']).countExclusiveOptions())
+
+    def test_countExclusiveOptions_counts_private_ppa(self):
+        self.assertEqual(
+            1,
+            self.makeScript(args=['--private-ppa']).countExclusiveOptions())
+
+    def test_countExclusiveOptions_counts_primary_debug(self):
+        self.assertEqual(
+            1,
+            self.makeScript(args=['--primary-debug']).countExclusiveOptions())
+
+    def test_countExclusiveOptions_counts_copy_archive(self):
+        self.assertEqual(
+            1,
+            self.makeScript(args=['--copy-archive']).countExclusiveOptions())
+
+    def test_countExclusiveOptions_detects_conflict(self):
+        script = self.makeScript(args=['--ppa', '--partner'])
+        self.assertEqual(2, script.countExclusiveOptions())
+
+    def test_validateOptions_rejects_nonoption_arguments(self):
+        script = self.makeScript(args=['please'])
+        self.assertRaises(OptionValueError, script.validateOptions)
+
+    def test_validateOptions_rejects_exclusive_option_conflict(self):
+        # If more than one of the exclusive options are set,
+        # validateOptions raises that as an error.
+        script = self.makeScript()
+        script.countExclusiveOptions = FakeMethod(2)
+        self.assertRaises(OptionValueError, script.validateOptions)
+
+    def test_validateOptions_does_not_accept_distsroot_for_ppa(self):
+        script = self.makeScript(args=['--ppa', '--distsroot=/tmp'])
+        self.assertRaises(OptionValueError, script.validateOptions)
+
+    def test_validateOptions_does_not_accept_distsroot_for_private_ppa(self):
+        script = self.makeScript(args=['--private-ppa', '--distsroot=/tmp'])
+        self.assertRaises(OptionValueError, script.validateOptions)
+
+    def test_findDistro_finds_distribution(self):
+        distro = self.factory.makeDistribution()
+        self.assertEqual(distro, self.makeScript(distro).findDistro())
+
+    def test_findDistro_raises_if_distro_not_found(self):
+        wrong_name = self.factory.getUniqueString()
+        self.assertRaises(
+            OptionValueError,
+            PublishDistro(test_args=['-d', wrong_name]).findDistro)
+
+    def test_findSuite_finds_release_pocket(self):
+        series = self.factory.makeDistroSeries()
+        distro = series.distribution
+        self.assertEqual(
+            (series.name, PackagePublishingPocket.RELEASE),
+            self.makeScript(distro).findSuite(series.name))
+
+    def test_findSuite_finds_other_pocket(self):
+        series = self.factory.makeDistroSeries()
+        distro = series.distribution
+        self.assertEqual(
+            (series.name, PackagePublishingPocket.UPDATES),
+            self.makeScript(distro).findSuite(series.name + "-updates"))
+
+    def test_findSuite_raises_if_not_found(self):
+        self.assertRaises(
+            OptionValueError,
+            self.makeScript().findSuite, self.factory.getUniqueString())
+
+    def test_findAllowedSuites_finds_nothing_if_no_suites_given(self):
+        self.assertContentEqual([], self.makeScript().findAllowedSuites())
+
+    def test_findAllowedSuites_finds_series_and_pocket(self):
+        series = self.factory.makeDistroSeries()
+        suite = "%s-updates" % series.name
+        script = self.makeScript(series.distribution, ['--suite', suite])
+        self.assertContentEqual(
+            [(series.name, PackagePublishingPocket.UPDATES)],
+            script.findAllowedSuites())
+
+    def test_findAllowedSuites_finds_multiple(self):
+        series = self.factory.makeDistroSeries()
+        script = self.makeScript(series.distribution, [
+            '--suite', '%s-updates' % series.name,
+            '--suite', series.name])
+        expected_suites = [
+            (series.name, PackagePublishingPocket.UPDATES),
+            (series.name, PackagePublishingPocket.RELEASE),
+            ]
+        self.assertContentEqual(expected_suites, script.findAllowedSuites())
+
+    def test_getDebugArchive_returns_list(self):
+        distro = self.factory.makeDistribution()
+        script = self.makeScript(distro)
+        debug_archive = self.factory.makeArchive(
+            distro, purpose=ArchivePurpose.DEBUG)
+        self.assertEqual([debug_archive], script.getDebugArchive())
+
+    def test_getDebugArchive_raises_if_not_found(self):
+        self.assertRaises(OptionValueError, self.makeScript().getDebugArchive)
+
+    def test_getDebugArchive_ignores_other_archive_purposes(self):
+        distro = self.factory.makeDistribution()
+        script = self.makeScript(distro)
+        self.factory.makeArchive(distro, purpose=ArchivePurpose.PARTNER)
+        self.assertRaises(OptionValueError, script.getDebugArchive)
+
+    def test_getDebugArchive_ignores_other_distros(self):
+        self.factory.makeArchive(purpose=ArchivePurpose.DEBUG)
+        self.assertRaises(OptionValueError, self.makeScript().getDebugArchive)
+
+    def test_getCopyArchives_returns_list(self):
+        distro = self.factory.makeDistribution()
+        script = self.makeScript(distro)
+        copy_archive = self.factory.makeArchive(
+            distro, purpose=ArchivePurpose.COPY)
+        self.assertEqual([copy_archive], script.getCopyArchives())
+
+    def test_getCopyArchives_raises_if_not_found(self):
+        self.assertRaises(
+            LaunchpadScriptFailure, self.makeScript().getCopyArchives)
+
+    def test_getCopyArchives_ignores_other_archive_purposes(self):
+        distro = self.factory.makeDistribution()
+        script = self.makeScript(distro)
+        self.factory.makeArchive(distro, purpose=ArchivePurpose.PARTNER)
+        self.assertRaises(LaunchpadScriptFailure, script.getCopyArchives)
+
+    def test_getCopyArchives_ignores_other_distros(self):
+        self.factory.makeArchive(purpose=ArchivePurpose.COPY)
+        self.assertRaises(
+            LaunchpadScriptFailure, self.makeScript().getCopyArchives)
+
+    def test_getPPAs_gets_pending_distro_PPAs_if_careful(self):
+        distro = self.factory.makeDistribution()
+        script = self.makeScript(distro, ['--careful'])
+        ppa = self.factory.makeArchive(distro, purpose=ArchivePurpose.PPA)
+        self.factory.makeSourcePackagePublishingHistory(archive=ppa)
+        self.assertContentEqual([ppa], script.getPPAs())
+
+    def test_getPPAs_gets_nonpending_distro_PPAs_if_careful(self):
+        distro = self.factory.makeDistribution()
+        script = self.makeScript(distro, ['--careful'])
+        ppa = self.factory.makeArchive(distro, purpose=ArchivePurpose.PPA)
+        self.assertContentEqual([ppa], script.getPPAs())
+
+    def test_getPPAs_gets_pending_distro_PPAs_if_not_careful(self):
+        distro = self.factory.makeDistribution()
+        script = self.makeScript(distro)
+        ppa = self.factory.makeArchive(distro, purpose=ArchivePurpose.PPA)
+        self.factory.makeSourcePackagePublishingHistory(archive=ppa)
+        self.assertContentEqual([ppa], script.getPPAs())
+
+    def test_getPPAs_ignores_nonpending_distro_PPAs_if_not_careful(self):
+        distro = self.factory.makeDistribution()
+        script = self.makeScript(distro)
+        self.factory.makeArchive(distro, purpose=ArchivePurpose.PPA)
+        self.assertContentEqual([], script.getPPAs())
+
+    def test_getPPAs_returns_empty_if_careful_and_no_PPAs_found(self):
+        self.assertContentEqual(
+            [], self.makeScript(args=['--careful']).getPPAs())
+
+    def test_getPPAs_returns_empty_if_not_careful_and_no_PPAs_found(self):
+        self.assertContentEqual([], self.makeScript().getPPAs())
+
+    def test_getTargetArchives_gets_partner_archive(self):
+        distro = self.factory.makeDistribution()
+        partner = self.factory.makeArchive(
+            distro, purpose=ArchivePurpose.PARTNER)
+        script = self.makeScript(distro, ['--partner'])
+        self.assertContentEqual([partner], script.getTargetArchives())
+
+    def test_getTargetArchives_ignores_public_ppas_if_private(self):
+        distro = self.factory.makeDistribution()
+        self.factory.makeArchive(
+            distro, purpose=ArchivePurpose.PPA, private=False)
+        script = self.makeScript(distro, ['--private-ppa'])
+        self.assertContentEqual([], script.getTargetArchives())
+
+    def test_getTargetArchives_gets_private_ppas_if_private(self):
+        distro = self.factory.makeDistribution()
+        ppa = self.factory.makeArchive(
+            distro, purpose=ArchivePurpose.PPA, private=True)
+        script = self.makeScript(distro, ['--private-ppa', '--careful'])
+        self.assertContentEqual([ppa], script.getTargetArchives())
+
+    def test_getTargetArchives_gets_public_ppas_if_not_private(self):
+        distro = self.factory.makeDistribution()
+        ppa = self.factory.makeArchive(
+            distro, purpose=ArchivePurpose.PPA, private=False)
+        script = self.makeScript(distro, ['--ppa', '--careful'])
+        self.assertContentEqual([ppa], script.getTargetArchives())
+
+    def test_getTargetArchives_ignores_private_ppas_if_not_private(self):
+        distro = self.factory.makeDistribution()
+        self.factory.makeArchive(
+            distro, purpose=ArchivePurpose.PPA, private=True)
+        script = self.makeScript(distro, ['--ppa'])
+        self.assertContentEqual([], script.getTargetArchives())
+
+    def test_getTargetArchives_gets_primary_debug_archive(self):
+        distro = self.factory.makeDistribution()
+        debug = self.factory.makeArchive(distro, purpose=ArchivePurpose.DEBUG)
+        script = self.makeScript(distro, ['--primary-debug'])
+        self.assertContentEqual([debug], script.getTargetArchives())
+
+    def test_getTargetArchives_gets_copy_archives(self):
+        distro = self.factory.makeDistribution()
+        copy = self.factory.makeArchive(distro, purpose=ArchivePurpose.COPY)
+        script = self.makeScript(distro, ['--copy-archive'])
+        self.assertContentEqual([copy], script.getTargetArchives())
+
+    def test_getPublisher_returns_publisher(self):
+        distro = self.factory.makeDistribution()
+        script = self.makeScript(distro)
+        publisher = script.getPublisher(distro.main_archive, None)
+        self.assertIsInstance(publisher, Publisher)
+
+    def test_deleteArchive_deletes_ppa(self):
+        distro = self.factory.makeDistribution()
+        ppa = self.factory.makeArchive(distro, purpose=ArchivePurpose.PPA)
+        script = self.makeScript(distro)
+        deletion_done = script.deleteArchive(
+            ppa, script.getPublisher(ppa, []))
+        self.assertTrue(deletion_done)
+        self.assertContentEqual([], script.getPPAs())
+
+    def test_deleteArchive_ignores_non_ppa(self):
+        distro = self.factory.makeDistribution()
+        archive = self.factory.makeArchive(
+            distro, purpose=ArchivePurpose.DEBUG)
+        script = self.makeScript(distro)
+        deletion_done = script.deleteArchive(archive, None)
+        self.assertFalse(deletion_done)
+        self.assertContentEqual([archive], script.getDebugArchive())
+
+    def test_publishArchive_drives_publisher(self):
+        distro = self.factory.makeDistribution()
+        script = self.makeScript(distro)
+        script.txn = FakeTransaction()
+        publisher = FakePublisher()
+        script.publishArchive(FakeArchive(), publisher)
+        self.assertEqual(1, publisher.A_publish.call_count)
+        self.assertEqual(
+            1, publisher.A2_markPocketsWithDeletionsDirty.call_count)
+        self.assertEqual(1, publisher.B_dominate.call_count)
+        self.assertEqual(1, publisher.D_writeReleaseFiles.call_count)
+
+    def test_publishArchive_uses_apt_ftparchive_for_main_archive(self):
+        distro = self.factory.makeDistribution()
+        script = self.makeScript(distro)
+        script.txn = FakeTransaction()
+        publisher = FakePublisher()
+        script.publishArchive(FakeArchive(), publisher)
+        self.assertEqual(1, publisher.C_doFTPArchive.call_count)
+        self.assertEqual(0, publisher.C_writeIndexes.call_count)
+
+    def test_publishArchive_writes_own_indexes_for_ppa(self):
+        distro = self.factory.makeDistribution()
+        script = self.makeScript(distro)
+        script.txn = FakeTransaction()
+        publisher = FakePublisher()
+        script.publishArchive(FakeArchive(ArchivePurpose.PPA), publisher)
+        self.assertEqual(0, publisher.C_doFTPArchive.call_count)
+        self.assertEqual(1, publisher.C_writeIndexes.call_count)

=== modified file 'scripts/publish-distro.py'
--- scripts/publish-distro.py	2011-06-09 10:50:25 +0000
+++ scripts/publish-distro.py	2011-07-19 12:44:38 +0000
@@ -1,34 +1,16 @@
 #!/usr/bin/python -S
 #
-# Copyright 2009 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2011 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 # pylint: disable-msg=C0103,W0403
 
 import _pythonpath
 
-from optparse import OptionParser
-
 from canonical.config import config
-from canonical.launchpad.scripts import (
-    execute_zcml_for_scripts, logger)
-from lp.services.scripts.base import LaunchpadScriptFailure
-from canonical.lp import initZopeless
-from lp.soyuz.scripts import publishdistro
+from lp.soyuz.scripts.publishdistro import PublishDistro
 
 
 if __name__ == "__main__":
-    parser = OptionParser()
-    publishdistro.add_options(parser)
-    options, args = parser.parse_args()
-    assert len(args) == 0, "publish-distro takes no arguments, only options."
-
-    log = logger(options, "publish-distro")
-    log.debug("Initializing zopeless.")
-    execute_zcml_for_scripts()
-    txn = initZopeless(dbuser=config.archivepublisher.dbuser)
-
-    try:
-        publishdistro.run_publisher(options, txn)
-    except LaunchpadScriptFailure, err:
-        log.error(err)
+    script = PublishDistro(dbuser=config.archivepublisher.dbuser)
+    script.run()