launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #03411
[Merge] lp:~jtv/launchpad/db-bug-768342 into lp:launchpad/db-devel
Jeroen T. Vermeulen has proposed merging lp:~jtv/launchpad/db-bug-768342 into lp:launchpad/db-devel.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~jtv/launchpad/db-bug-768342/+merge/58682
= Summary =
The generate-contents script runs apt-ftparchive, which may take a long time. It's not very nice to save up all its log output and dump it just once at the end: we want live output.
== Proposed fix ==
Luckily this is all neatly isolated in a function called "execute." I replaced its subprocess.Popen implementation with one based on CommandSpawner (and its helpful friends for logging and result tracking).
== Pre-implementation notes ==
This was Julian's one gripe from testing the branch for bug 735621, currently landing, which is a requisit for this one. We couldn't think of any meaningful tests for it, since I just re-implemented an existing, tested function.
== Implementation details ==
CommandSpawner is really meant for running multiple commands in parallel, but there's nothing against using it for just one.
== Tests ==
{{{
./bin/test -vvc lp.archivepublisher.tests.test_generate_contents
}}}
== Demo and Q/A ==
Run the generate-contents script on dogfood (or some other sizable archive) and watch its output. It won't be exactly per-line granularity because of buffering, but there should be some output from apt-ftparchive before it completes.
= Launchpad lint =
This lint was previously present and is, as far as I can see, inevitable:
Checking for conflicts and issues in changed files.
Linting changed files:
lib/canonical/config/schema-lazr.conf
lib/lp/archivepublisher/tests/test_generate_contents_files.py
lib/lp/archivepublisher/scripts/generate_contents_files.py
lib/canonical/launchpad/ftests/script.py
cronscripts/publishing/gen-contents/apt_conf_dist.template
cronscripts/publishing/gen-contents/Contents.top
scripts/generate-contents-files.py
cronscripts/publishing/gen-contents/apt_conf_header.template
lib/lp/services/command_spawner.py
./lib/canonical/config/schema-lazr.conf
536: Line exceeds 78 characters.
619: Line exceeds 78 characters.
995: Line exceeds 78 characters.
1084: Line exceeds 78 characters.
./cronscripts/publishing/gen-contents/apt_conf_dist.template
4: Line exceeds 78 characters.
5: Line exceeds 78 characters.
./scripts/generate-contents-files.py
8: '_pythonpath' imported but unused
--
https://code.launchpad.net/~jtv/launchpad/db-bug-768342/+merge/58682
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~jtv/launchpad/db-bug-768342 into lp:launchpad/db-devel.
=== modified file 'cronscripts/publishing/gen-contents/Contents.top'
--- cronscripts/publishing/gen-contents/Contents.top 2006-05-31 22:27:14 +0000
+++ cronscripts/publishing/gen-contents/Contents.top 2011-04-21 14:25:52 +0000
@@ -1,6 +1,6 @@
-This file maps each file available in the Ubuntu Linux system to
-the package from which it originates. It includes packages from the
-DIST distribution for the ARCH architecture.
+This file maps each file available in the %(distrotitle)s
+system to the package from which it originates. It includes packages
+from the DIST distribution for the ARCH architecture.
You can use this list to determine which package contains a specific
file, or whether or not a specific file is available. The list is
=== modified file 'cronscripts/publishing/gen-contents/apt_conf_dist.template'
--- cronscripts/publishing/gen-contents/apt_conf_dist.template 2007-11-27 18:05:38 +0000
+++ cronscripts/publishing/gen-contents/apt_conf_dist.template 2011-04-21 14:25:52 +0000
@@ -1,13 +1,13 @@
-tree "dists/$SUITE"
+tree "dists/%(suite)s"
{
- FileList "/srv/launchpad.net/ubuntu-contents/ubuntu-overrides/$SUITE_$(SECTION)_binary-$(ARCH)";
- SourceFileList "/srv/launchpad.net/ubuntu-contents/ubuntu-overrides/$SUITE_$(SECTION)_source";
+ FileList "%(content_archive)s/%(distribution)s-overrides/%(suite)s_$(SECTION)_binary-$(ARCH)";
+ SourceFileList "%(content_archive)s/%(distribution)s-overrides/%(suite)s_$(SECTION)_source";
Sections "main restricted universe multiverse";
- Architectures "$ARCHS source";
- BinOverride "override.$SUITE.$(SECTION)";
- SrcOverride "override.$SUITE.$(SECTION).src";
- ExtraOverride "override.$SUITE.extra.$(SECTION)";
+ Architectures "%(architectures)s source";
+ BinOverride "override.%(suite)s.$(SECTION)";
+ SrcOverride "override.%(suite)s.$(SECTION).src";
+ ExtraOverride "override.%(suite)s.extra.$(SECTION)";
// we need the plain text content to compare before copy to the real tree
Packages::Compress ". gzip";
Sources::Compress ". gzip";
=== modified file 'cronscripts/publishing/gen-contents/apt_conf_header.template'
--- cronscripts/publishing/gen-contents/apt_conf_header.template 2007-11-27 18:24:35 +0000
+++ cronscripts/publishing/gen-contents/apt_conf_header.template 2011-04-21 14:25:52 +0000
@@ -2,9 +2,9 @@
{
// Content archive stores the results and caches, since they are
// incompatible with the normal ones.
- ArchiveDir "/srv/launchpad.net/ubuntu-contents/ubuntu";
- CacheDir "/srv/launchpad.net/ubuntu-contents/ubuntu-cache";
- OverrideDir "/srv/launchpad.net/ubuntu-contents/ubuntu-overrides";
+ ArchiveDir "%(content_archive)s/%(distribution)s";
+ CacheDir "%(content_archive)s/%(distribution)s-cache";
+ OverrideDir "%(content_archive)s/%(distribution)s-overrides";
};
@@ -16,6 +16,6 @@
TreeDefault
{
// Header for Contents file.
- Contents::Header "/srv/launchpad.net/ubuntu-contents/ubuntu-misc/Contents.top";
+ Contents::Header "%(content_archive)s/%(distribution)s-misc/Contents.top";
};
=== modified file 'lib/canonical/config/schema-lazr.conf'
--- lib/canonical/config/schema-lazr.conf 2011-04-13 18:48:42 +0000
+++ lib/canonical/config/schema-lazr.conf 2011-04-21 14:25:52 +0000
@@ -24,6 +24,17 @@
# datatype: string
dbuser: archivepublisher
+# Base directory for auxiliary archive where Contents files will be
+# generated.
+#
+# Subdirectories inside this directory will be named after the
+# distribution. For example, if content_archive_root is set to
+# /srv/launchpad.net then Ubuntu's Contents files will be created in
+# /srv/launchpad.net/ubuntu-contents
+#
+# datatype: string
+content_archive_root: /var/tmp/archive
+
# Location where the run-parts directories for publish-ftpmaster
# customization are to be found. Absolute path, or path relative to the
# Launchpad source tree, or "none" to skip execution of run-parts.
=== modified file 'lib/canonical/launchpad/ftests/script.py'
--- lib/canonical/launchpad/ftests/script.py 2009-06-25 05:30:52 +0000
+++ lib/canonical/launchpad/ftests/script.py 2011-04-21 14:25:52 +0000
@@ -4,7 +4,10 @@
"""Helper functions for running external commands."""
__metaclass__ = type
-__all__ = ['run_script']
+__all__ = [
+ 'run_command',
+ 'run_script',
+ ]
import subprocess
import sys
@@ -48,4 +51,3 @@
interpreter_args.extend(args)
return run_command(sys.executable, interpreter_args, input)
-
=== added file 'lib/lp/archivepublisher/scripts/generate_contents_files.py'
--- lib/lp/archivepublisher/scripts/generate_contents_files.py 1970-01-01 00:00:00 +0000
+++ lib/lp/archivepublisher/scripts/generate_contents_files.py 2011-04-21 14:25:52 +0000
@@ -0,0 +1,320 @@
+# Copyright 2011 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Archive Contents files generator."""
+
+__metaclass__ = type
+__all__ = [
+ 'GenerateContentsFiles',
+ ]
+
+from optparse import OptionValueError
+import os
+from zope.component import getUtility
+
+from canonical.config import config
+from lp.archivepublisher.config import getPubConfig
+from lp.registry.interfaces.distribution import IDistributionSet
+from lp.services.command_spawner import (
+ CommandSpawner,
+ OutputLineHandler,
+ ReturnCodeReceiver,
+ )
+from lp.services.scripts.base import (
+ LaunchpadScript,
+ LaunchpadScriptFailure,
+ )
+from lp.services.utils import file_exists
+from lp.soyuz.scripts.ftpmaster import LpQueryDistro
+
+
+COMPONENTS = [
+ 'main',
+ 'restricted',
+ 'universe',
+ 'multiverse',
+ ]
+
+
+def differ_in_content(one_file, other_file):
+ """Do the two named files have different contents?"""
+ one_exists = file_exists(one_file)
+ other_exists = file_exists(other_file)
+ if any([one_exists, other_exists]):
+ return (
+ one_exists != other_exists or
+ file(one_file).read() != file(other_file).read())
+ else:
+ return False
+
+
+class StoreArgument:
+ """Local helper for receiving `LpQueryDistro` results."""
+
+ def __call__(self, argument):
+ """Store call argument."""
+ self.argument = argument
+
+
+def get_template(template_name):
+ """Return path of given template in this script's templates directory."""
+ return os.path.join(
+ config.root, "cronscripts", "publishing", "gen-contents",
+ template_name)
+
+
+def execute(logger, command, args=None):
+ """Execute a shell command.
+
+ :param logger: Output from the command will be logged here.
+ :param command: Command to execute, as a string.
+ :param args: Optional list of arguments for `command`.
+ :raises LaunchpadScriptFailure: If the command returns failure.
+ """
+ command_line = [command]
+ if args is not None:
+ command_line += args
+ description = ' '.join(command_line)
+
+ logger.debug("Execute: %s", description)
+ # Some of these commands can take a long time. Use CommandSpawner
+ # and friends to provide "live" log output. Simpler ways of running
+ # commands tend to save it all up and then dump it at the end, or
+ # have trouble logging it as neat lines.
+ stderr_logger = OutputLineHandler(logger.warn)
+ stdout_logger = OutputLineHandler(logger.debug)
+ receiver = ReturnCodeReceiver()
+ spawner = CommandSpawner()
+ spawner.start(
+ command_line, completion_handler=receiver,
+ stderr_handler=stderr_logger, stdout_handler=stdout_logger)
+ spawner.complete()
+ stdout_logger.finalize()
+ stderr_logger.finalize()
+ if receiver.returncode != 0:
+ raise LaunchpadScriptFailure(
+ "Failure while running command: %s" % description)
+
+
+def move_file(old_path, new_path):
+ """Rename file `old_path` to `new_path`.
+
+ Mercilessly delete any file that may already exist at `new_path`.
+ """
+ if file_exists(new_path):
+ os.remove(new_path)
+ os.rename(old_path, new_path)
+
+
+class GenerateContentsFiles(LaunchpadScript):
+
+ distribution = None
+
+ def add_my_options(self):
+ """See `LaunchpadScript`."""
+ self.parser.add_option(
+ "-d", "--distribution", dest="distribution", default=None,
+ help="Distribution to generate Contents files for.")
+
+ @property
+ def name(self):
+ """See `LaunchpadScript`."""
+ # Include distribution name. Clearer to admins, but also
+ # puts runs for different distributions under separate
+ # locks so that they can run simultaneously.
+ return "%s-%s" % (self._name, self.options.distribution)
+
+ def processOptions(self):
+ """Handle command-line options."""
+ if self.options.distribution is None:
+ raise OptionValueError("Specify a distribution.")
+
+ self.distribution = getUtility(IDistributionSet).getByName(
+ self.options.distribution)
+ if self.distribution is None:
+ raise OptionValueError(
+ "Distribution '%s' not found." % self.options.distribution)
+
+ def setUpContentArchive(self):
+ """Make sure the `content_archive` directories exist."""
+ self.logger.debug("Ensuring that we have a private tree in place.")
+ for suffix in ['cache', 'misc']:
+ dirname = '-'.join([self.distribution.name, suffix])
+ path = os.path.join(self.content_archive, dirname)
+ if not file_exists(path):
+ os.makedirs(path)
+
+ def queryDistro(self, request, options=None):
+ """Call the query-distro script about `self.distribution`."""
+ args = ['-d', self.distribution.name]
+ if options is not None:
+ args += options
+ args.append(request)
+ query_distro = LpQueryDistro(test_args=args)
+ query_distro.logger = self.logger
+ query_distro.txn = self.txn
+ receiver = StoreArgument()
+ query_distro.runAction(presenter=receiver)
+ return receiver.argument
+
+ def getPocketSuffixes(self):
+ """Query the distribution's pocket suffixes."""
+ return self.queryDistro("pocket_suffixes").split()
+
+ def getSuites(self):
+ """Query the distribution's suites."""
+ return self.queryDistro("supported").split()
+
+ def getPockets(self):
+ """Return suites that are actually supported in this distribution."""
+ pockets = []
+ pocket_suffixes = self.getPocketSuffixes()
+ for suite in self.getSuites():
+ for pocket_suffix in pocket_suffixes:
+ pocket = suite + pocket_suffix
+ if file_exists(os.path.join(self.config.distsroot, pocket)):
+ pockets.append(pocket)
+ return pockets
+
+ def getArchs(self):
+ """Query architectures supported by the distribution."""
+ devel = self.queryDistro("development")
+ return self.queryDistro("archs", options=["-s", devel]).split()
+
+ def getDirs(self, archs):
+ """Subdirectories needed for each component."""
+ return ['source', 'debian-installer'] + [
+ 'binary-%s' % arch for arch in archs]
+
+ def writeAptContentsConf(self, suites, archs):
+ """Write apt-contents.conf file."""
+ output_dirname = '%s-misc' % self.distribution.name
+ output_path = os.path.join(
+ self.content_archive, output_dirname, "apt-contents.conf")
+ output_file = file(output_path, 'w')
+
+ parameters = {
+ 'architectures': ' '.join(archs),
+ 'content_archive': self.content_archive,
+ 'distribution': self.distribution.name,
+ }
+
+ header = get_template('apt_conf_header.template')
+ output_file.write(file(header).read() % parameters)
+
+ dist_template = file(get_template('apt_conf_dist.template')).read()
+ for suite in suites:
+ parameters['suite'] = suite
+ output_file.write(dist_template % parameters)
+
+ output_file.close()
+
+ def createComponentDirs(self, suites, archs):
+ """Create the content archive's tree for all of its components."""
+ for suite in suites:
+ for component in COMPONENTS:
+ for directory in self.getDirs(archs):
+ path = os.path.join(
+ self.content_archive, self.distribution.name, 'dists',
+ suite, component, directory)
+ if not file_exists(path):
+ self.logger.debug("Creating %s.", path)
+ os.makedirs(path)
+
+ def copyOverrides(self):
+ """Copy overrides into the content archive."""
+ if file_exists(self.config.overrideroot):
+ execute(self.logger, "cp", [
+ "-a",
+ self.config.overrideroot,
+ "%s/" % self.content_archive,
+ ])
+ else:
+ self.logger.debug("Did not find overrides; not copying.")
+
+ def writeContentsTop(self):
+ """Write Contents.top file."""
+ output_filename = os.path.join(
+ self.content_archive, '%s-misc' % self.distribution.name,
+ "Contents.top")
+ parameters = {
+ 'distrotitle': self.distribution.title,
+ }
+ output_file = file(output_filename, 'w')
+ text = file(get_template("Contents.top")).read() % parameters
+ output_file.write(text)
+ output_file.close()
+
+ def runAptFTPArchive(self):
+ """Run apt-ftparchive to produce the Contents files."""
+ execute(self.logger, "apt-ftparchive", [
+ "generate",
+ os.path.join(
+ self.content_archive, "%s-misc" % self.distribution.name,
+ "apt-contents.conf"),
+ ])
+
+ def generateContentsFiles(self):
+ """Generate Contents files."""
+ self.logger.debug(
+ "Running apt in private tree to generate new contents.")
+ self.copyOverrides()
+ self.writeContentsTop()
+ self.runAptFTPArchive()
+
+ def updateContentsFile(self, suite, arch):
+ """Update Contents file, if it has changed."""
+ contents_dir = os.path.join(
+ self.content_archive, self.distribution.name, 'dists', suite)
+ contents_filename = "Contents-%s" % arch
+ last_contents = os.path.join(contents_dir, ".%s" % contents_filename)
+ current_contents = os.path.join(contents_dir, contents_filename)
+
+ # Avoid rewriting unchanged files; mirrors would have to
+ # re-fetch them unnecessarily.
+ if differ_in_content(current_contents, last_contents):
+ self.logger.debug(
+ "Installing new Contents file for %s/%s.", suite, arch)
+
+ new_contents = os.path.join(
+ contents_dir, "%s.gz" % contents_filename)
+ contents_dest = os.path.join(
+ self.config.distsroot, suite, "%s.gz" % contents_filename)
+
+ move_file(current_contents, last_contents)
+ move_file(new_contents, contents_dest)
+ os.chmod(contents_dest, 0664)
+ else:
+ self.logger.debug(
+ "Skipping unmodified Contents file for %s/%s.", suite, arch)
+
+ def updateContentsFiles(self, suites, archs):
+ """Update all Contents files that have changed."""
+ self.logger.debug("Comparing contents files with public tree.")
+ for suite in suites:
+ for arch in archs:
+ self.updateContentsFile(suite, arch)
+
+ def setUp(self):
+ """Prepare configuration and filesystem state for the script's work.
+
+ This is idempotent: run it as often as you like. (For example,
+ a test may call `setUp` prior to calling `main` which again
+ invokes `setUp`).
+ """
+ self.processOptions()
+ self.config = getPubConfig(self.distribution.main_archive)
+ self.content_archive = os.path.join(
+ config.archivepublisher.content_archive_root,
+ self.distribution.name + "-contents")
+ self.setUpContentArchive()
+
+ def main(self):
+ """See `LaunchpadScript`."""
+ self.setUp()
+ suites = self.getPockets()
+ archs = self.getArchs()
+ self.writeAptContentsConf(suites, archs)
+ self.createComponentDirs(suites, archs)
+ self.generateContentsFiles()
+ self.updateContentsFiles(suites, archs)
=== added file 'lib/lp/archivepublisher/tests/test_generate_contents_files.py'
--- lib/lp/archivepublisher/tests/test_generate_contents_files.py 1970-01-01 00:00:00 +0000
+++ lib/lp/archivepublisher/tests/test_generate_contents_files.py 2011-04-21 14:25:52 +0000
@@ -0,0 +1,291 @@
+# Copyright 2011 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Test for the `generate-contents-files` script."""
+
+__metaclass__ = type
+
+from optparse import OptionValueError
+import os.path
+from textwrap import dedent
+
+from canonical.config import config
+from canonical.testing.layers import (
+ LaunchpadZopelessLayer,
+ ZopelessDatabaseLayer,
+ )
+from lp.archivepublisher.scripts.generate_contents_files import (
+ differ_in_content,
+ execute,
+ GenerateContentsFiles,
+ move_file,
+ )
+from lp.services.log.logger import DevNullLogger
+from lp.services.scripts.base import LaunchpadScriptFailure
+from lp.services.utils import file_exists
+from lp.testing import TestCaseWithFactory
+
+
+def write_file(filename, content=""):
+ """Write `content` to `filename`, and flush."""
+ output_file = file(filename, 'w')
+ output_file.write(content)
+ output_file.close()
+
+
+def fake_overrides(script, distroseries):
+ """Fake overrides files so `script` can run `apt-ftparchive`."""
+ os.makedirs(script.config.overrideroot)
+
+ components = ['main', 'restricted', 'universe', 'multiverse']
+ architectures = script.getArchs()
+ suffixes = components + ['extra.' + component for component in components]
+ for suffix in suffixes:
+ write_file(os.path.join(
+ script.config.overrideroot,
+ "override.%s.%s" % (distroseries.name, suffix)))
+
+ for component in components:
+ write_file(os.path.join(
+ script.config.overrideroot,
+ "%s_%s_source" % (distroseries.name, component)))
+ for arch in architectures:
+ write_file(os.path.join(
+ script.config.overrideroot,
+ "%s_%s_binary-%s" % (distroseries.name, component, arch)))
+
+
+class TestHelpers(TestCaseWithFactory):
+ """Tests for the module's helper functions."""
+
+ layer = ZopelessDatabaseLayer
+
+ def test_differ_in_content_returns_true_if_one_file_does_not_exist(self):
+ # A nonexistent file differs from an existing one.
+ self.useTempDir()
+ write_file('one', self.factory.getUniqueString())
+ self.assertTrue(differ_in_content('one', 'other'))
+
+ def test_differ_in_content_returns_false_for_identical_files(self):
+ # Identical files do not differ.
+ self.useTempDir()
+ text = self.factory.getUniqueString()
+ write_file('one', text)
+ write_file('other', text)
+ self.assertFalse(differ_in_content('one', 'other'))
+
+ def test_differ_in_content_returns_true_for_differing_files(self):
+ # Files with different contents differ.
+ self.useTempDir()
+ write_file('one', self.factory.getUniqueString())
+ write_file('other', self.factory.getUniqueString())
+ self.assertTrue(differ_in_content('one', 'other'))
+
+ def test_differ_in_content_returns_false_if_neither_file_exists(self):
+ # Nonexistent files do not differ.
+ self.useTempDir()
+ self.assertFalse(differ_in_content('one', 'other'))
+
+ def test_execute_raises_if_command_fails(self):
+ # execute checks its command's return value. If it's nonzero
+ # (as with /bin/false), it raises a LaunchpadScriptFailure.
+ logger = DevNullLogger()
+ self.assertRaises(
+ LaunchpadScriptFailure, execute, logger, "/bin/false")
+
+ def test_execute_executes_command(self):
+ # execute really does execute its command. If we tell it to
+ # "touch" a new file, that file really gets created.
+ self.useTempDir()
+ logger = DevNullLogger()
+ filename = self.factory.getUniqueString()
+ execute(logger, "touch", [filename])
+ self.assertTrue(file_exists(filename))
+
+ def test_move_file_renames_file(self):
+ # move_file renames a file from its old name to its new name.
+ self.useTempDir()
+ text = self.factory.getUniqueString()
+ write_file("old_name", text)
+ move_file("old_name", "new_name")
+ self.assertEqual(text, file("new_name").read())
+
+ def test_move_file_overwrites_old_file(self):
+ # If move_file finds another file in the way, that file gets
+ # deleted.
+ self.useTempDir()
+ write_file("new_name", self.factory.getUniqueString())
+ new_text = self.factory.getUniqueString()
+ write_file("old_name", new_text)
+ move_file("old_name", "new_name")
+ self.assertEqual(new_text, file("new_name").read())
+
+
+class TestGenerateContentsFiles(TestCaseWithFactory):
+ """Tests for the actual `GenerateContentsFiles` script."""
+
+ layer = LaunchpadZopelessLayer
+
+ def makeContentArchive(self):
+ """Prepare a "content archive" directory for script tests."""
+ content_archive = self.makeTemporaryDirectory()
+ config.push("content-archive", dedent("""\
+ [archivepublisher]
+ content_archive_root: %s
+ """ % content_archive))
+ self.addCleanup(config.pop, "content-archive")
+ return content_archive
+
+ def makeDistro(self):
+ """Create a distribution for testing.
+
+ The distribution will have a root directory set up, which will
+ be cleaned up after the test.
+ """
+ return self.factory.makeDistribution(
+ publish_root_dir=unicode(self.makeTemporaryDirectory()))
+
+ def makeScript(self, distribution=None):
+ """Create a script for testing."""
+ if distribution is None:
+ distribution = self.makeDistro()
+ script = GenerateContentsFiles(test_args=['-d', distribution.name])
+ script.logger = DevNullLogger()
+ script.setUp()
+ return script
+
+ def test_name_is_consistent(self):
+ # Script instances for the same distro get the same name.
+ distro = self.factory.makeDistribution()
+ self.assertEqual(
+ GenerateContentsFiles(test_args=['-d', distro.name]).name,
+ GenerateContentsFiles(test_args=['-d', distro.name]).name)
+
+ def test_name_is_unique_for_each_distro(self):
+ # Script instances for different distros get different names.
+ self.assertNotEqual(
+ GenerateContentsFiles(
+ test_args=['-d', self.factory.makeDistribution().name]).name,
+ GenerateContentsFiles(
+ test_args=['-d', self.factory.makeDistribution().name]).name)
+
+ def test_requires_distro(self):
+ # The --distribution or -d argument is mandatory.
+ script = GenerateContentsFiles(test_args=[])
+ self.assertRaises(OptionValueError, script.processOptions)
+
+ def test_requires_real_distro(self):
+ # An incorrect distribution name is flagged as an invalid option
+ # value.
+ script = GenerateContentsFiles(
+ test_args=['-d', self.factory.getUniqueString()])
+ self.assertRaises(OptionValueError, script.processOptions)
+
+ def test_looks_up_distro(self):
+ # The script looks up and keeps the distribution named on the
+ # command line.
+ distro = self.factory.makeDistribution()
+ script = self.makeScript(distro)
+ self.assertEqual(distro, script.distribution)
+
+ def test_queryDistro(self):
+ # queryDistro is a helper that invokes LpQueryDistro.
+ distroseries = self.factory.makeDistroSeries()
+ script = self.makeScript(distroseries.distribution)
+ script.processOptions()
+ self.assertEqual(distroseries.name, script.queryDistro('supported'))
+
+ def test_getArchs(self):
+ # getArchs returns a list of architectures in the distribution.
+ das = self.factory.makeDistroArchSeries()
+ script = self.makeScript(das.distroseries.distribution)
+ self.assertEqual([das.architecturetag], script.getArchs())
+
+ def test_getSuites(self):
+ # getSuites returns the suites in the distribution. The main
+ # suite has the same name as the distro, without suffix.
+ script = self.makeScript()
+ distroseries = self.factory.makeDistroSeries(
+ distribution=script.distribution)
+ self.assertIn(distroseries.name, script.getSuites())
+
+ def test_getPockets(self):
+ # getPockets returns the full names (distroseries-pocket) of the
+ # pockets that have packages to publish.
+ distro = self.makeDistro()
+ distroseries = self.factory.makeDistroSeries(distribution=distro)
+ package = self.factory.makeSuiteSourcePackage(distroseries)
+ script = self.makeScript(distro)
+ os.makedirs(os.path.join(script.config.distsroot, package.suite))
+ self.assertEqual([package.suite], script.getPockets())
+
+ def test_writeAptContentsConf_writes_header(self):
+ # writeAptContentsConf writes apt-contents.conf. At a minimum
+ # this will include a header based on apt_conf_header.template,
+ # with the right distribution name interpolated.
+ self.makeContentArchive()
+ distro = self.makeDistro()
+ script = self.makeScript(distro)
+ script.writeAptContentsConf([], [])
+ apt_contents_conf = file(
+ "%s/%s-misc/apt-contents.conf"
+ % (script.content_archive, distro.name)).read()
+ self.assertIn('\nDefault\n{', apt_contents_conf)
+ self.assertIn(distro.name, apt_contents_conf)
+
+ def test_writeAptContentsConf_writes_suite_sections(self):
+ # writeAptContentsConf adds sections based on
+ # apt_conf_dist.template for every suite, with certain
+ # parameters interpolated.
+ content_archive = self.makeContentArchive()
+ distro = self.makeDistro()
+ script = self.makeScript(distro)
+ suite = self.factory.getUniqueString('suite')
+ arch = self.factory.getUniqueString('arch')
+ script.writeAptContentsConf([suite], [arch])
+ apt_contents_conf = file(
+ "%s/%s-misc/apt-contents.conf"
+ % (script.content_archive, distro.name)).read()
+ self.assertIn('tree "dists/%s"\n' % suite, apt_contents_conf)
+ overrides_path = os.path.join(
+ content_archive, distro.name + "-contents",
+ distro.name + "-overrides")
+ self.assertIn('FileList "%s' % overrides_path, apt_contents_conf)
+ self.assertIn('Architectures "%s source";' % arch, apt_contents_conf)
+
+ def test_writeContentsTop(self):
+ # writeContentsTop writes a Contents.top file based on a
+ # standard template, with the distribution's title interpolated.
+ content_archive = self.makeContentArchive()
+ distro = self.makeDistro()
+ script = self.makeScript(distro)
+ script.writeContentsTop()
+ contents_top = file(
+ "%s/%s-contents/%s-misc/Contents.top"
+ % (content_archive, distro.name, distro.name)).read()
+ self.assertIn("This file maps", contents_top)
+ self.assertIn(distro.title, contents_top)
+
+ def test_main(self):
+ # If run end-to-end, the script generates Contents.gz files.
+ self.makeContentArchive()
+ distro = self.makeDistro()
+ distroseries = self.factory.makeDistroSeries(distribution=distro)
+ processor = self.factory.makeProcessor()
+ das = self.factory.makeDistroArchSeries(
+ distroseries=distroseries, processorfamily=processor.family)
+ package = self.factory.makeSuiteSourcePackage(distroseries)
+ self.factory.makeSourcePackagePublishingHistory(
+ distroseries=distroseries, pocket=package.pocket)
+ self.factory.makeBinaryPackageBuild(
+ distroarchseries=das, pocket=package.pocket,
+ processor=processor)
+ suite = package.suite
+ script = self.makeScript(distro)
+ os.makedirs(os.path.join(script.config.distsroot, package.suite))
+ self.assertNotEqual([], script.getPockets())
+ fake_overrides(script, distroseries)
+ script.main()
+ self.assertTrue(file_exists(os.path.join(
+ script.config.distsroot, suite,
+ "Contents-%s.gz" % das.architecturetag)))
=== modified file 'lib/lp/services/command_spawner.py'
--- lib/lp/services/command_spawner.py 2011-04-06 03:43:28 +0000
+++ lib/lp/services/command_spawner.py 2011-04-21 14:25:52 +0000
@@ -99,9 +99,9 @@
:param stderr_handler: Callback to handle error output received from
the sub-process. Must take the output as its sole argument. May
be called any number of times as output comes in.
- :param failure_handler: Callback to be invoked, exactly once, when the
- sub-process exits. Must take `command`'s numeric return code as
- its sole argument.
+ :param completion_handler: Callback to be invoked, exactly once, when
+ the sub-process exits. Must take `command`'s numeric return code
+ as its sole argument.
"""
process = self._spawn(command)
handlers = {
@@ -172,9 +172,9 @@
try:
output = pipe_file.read()
except IOError, e:
+ # "Resource temporarily unavailable"--not an error really,
+ # just means there's nothing to read.
if e.errno != errno.EAGAIN:
- # "Resource temporarily unavailable"--not an error
- # really, just means there's nothing to read.
raise
else:
if len(output) > 0:
=== added file 'scripts/generate-contents-files.py'
--- scripts/generate-contents-files.py 1970-01-01 00:00:00 +0000
+++ scripts/generate-contents-files.py 2011-04-21 14:25:52 +0000
@@ -0,0 +1,19 @@
+#!/usr/bin/python -S
+#
+# Copyright 2011 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Master distro publishing script."""
+
+import _pythonpath
+
+from canonical.config import config
+from lp.archivepublisher.scripts.generate_contents_files import (
+ GenerateContentsFiles,
+ )
+
+
+if __name__ == '__main__':
+ script = GenerateContentsFiles(
+ "generate-contents", dbuser=config.archivepublisher.dbuser)
+ script.lock_and_run()