Merge lp:~cjwatson/launchpad/refactor-cron-germinate into lp:launchpad

Proposed by Colin Watson
Status: Merged
Approved by: Jeroen T. Vermeulen
Approved revision: no longer in the source branch.
Merged at revision: 14516
Proposed branch: lp:~cjwatson/launchpad/refactor-cron-germinate
Merge into: lp:launchpad
Diff against target: 1226 lines (+950/-135)
10 files modified
cronscripts/generate-extra-overrides.py (+18/-0)
cronscripts/publishing/cron.germinate (+5/-126)
database/schema/security.cfg (+4/-0)
lib/lp/archivepublisher/config.py (+3/-0)
lib/lp/archivepublisher/scripts/generate_extra_overrides.py (+339/-0)
lib/lp/archivepublisher/tests/publisher-config.txt (+7/-0)
lib/lp/archivepublisher/tests/test_generate_extra_overrides.py (+567/-0)
lib/lp/soyuz/scripts/tests/germinate-test-data/mock-bin/germinate (+0/-5)
lib/lp/soyuz/scripts/tests/germinate-test-data/mock-lp-root/cronscripts/generate-extra-overrides.py (+5/-0)
lib/lp/soyuz/scripts/tests/test_cron_germinate.py (+2/-4)
To merge this branch: bzr merge lp:~cjwatson/launchpad/refactor-cron-germinate
Reviewer Review Type Date Requested Status
Jeroen T. Vermeulen (community) Approve
Review via email: mp+84624@code.launchpad.net

Commit message

[r=jtv][bug=899972] Reimplement most of cron.germinate in Python using the new python-germinate package. This no longer needs to recompute the dependency expansion of common seeds over and over again, so is much faster.

Description of the change

= Summary =

cron.germinate currently takes on the order of ten minutes, and really doesn't have a particularly good excuse for taking so long. Fixing this would probably be enough to let us move back to 30-minute publisher cycles for Ubuntu (as we used to have with dak, back in the dawn of time), which would increase our velocity and make some of us very happy.

The main reason it's so slow is that it runs germinate as a separate process once for each of eight flavours (Ubuntu, Kubuntu, etc., each with its own seed collection) on each of five architectures. There are a few inefficiencies inherent in this approach, but the most significant is that it has to expand dependencies and build-dependencies of the seeds that are common to all the flavours eight times as often as necessary. Since the build-dependency chain in particular of the base system winds its way through a good fraction of main, this winds up being rather a lot of duplicated work.

== Proposed fix ==

I've been working on this problem for some time now, and have just released germinate 2.0 to support solving it properly. The most important change here is that germinate can now process multiple seed collections for a single architecture in a single instance of the core Germinator class, which allows reusing the expansions of common seeds. While I could technically have extended the command-line interface further for this, I felt that the command-line interface was already far too complicated, and decided instead to export a public, documented, and stable Python interface which can be used for this purpose.

This branch is the other half of that fix: a LaunchpadScript that uses germinate's new Python interface to do the same work as the top part of cron.germinate (i.e. everything except for maintenance-check.py). A stripped-down subset of this with just the logic (germinating seeds and producing output) runs in about three minutes on my laptop.

== Pre-implementation notes ==

The Launchpad position for a while has been that cron.germinate is owned by Ubuntu Engineering, and indeed the vast bulk of the work here was in preparing a new germinate release in Ubuntu that does what we need. However, I have talked with various Launchpad people over the last few months so that it was clear that I was working on this, and I did need some help figuring out how to approach testing. Initially I'd been planning on doing a full Soyuz publisher run so that I had Packages and Sources files to work with, but Julian said this would be too slow and pointed me towards methods like factory.makeSourcePackagePublishingHistory, which allow me to use getIndexStanza et al so that I don't have to write out RFC822 data by hand.

== Implementation details ==

I had to add a new attribute on PublisherConfig pointing to the germinate output directory; previously this was only known to the shell script.

I added a 'generate_extra_overrides' user to security.cfg, which seemed to be in line with other archivepublisher scripts. Was this the right approach? Does it require any special deployment steps?

The actual override generation was a fairly straightforward translation from shell to Python.

Predictably, the bulk of the work was in writing tests, since cron.germinate was previously untested and I didn't think continuing with untested code was likely to fly once it was in Python. The top-level shell script is still untested, but there's a lot less in it and I expect that it can eventually be removed entirely.

I ran into a couple of existing bugs that slowed me down. Firstly, I'd hoped to use the fake librarian to speed things up a bit, but ran into bug 713764, which was beyond my ability to fix (although I tried). Secondly, bug 694140 seriously confused me for a while; I added much the same workaround as is found elsewhere in Launchpad for that problem so that germinate's log output doesn't get lost.

I wanted to include a test that the cronscript could be run standalone, but I couldn't see how to ensure that a suitable PublisherConfig was in place.

== Tests ==

bin/test -vvct generate_extra_overrides

This branch makes some small changes to other archivepublisher tests, so:

bin/test -vvct archivepublisher

== Demo and Q/A ==

The deployment steps listed in bug 899972 and https://rt.admin.canonical.com/Ticket/Display.html?id=49745 need to be completed before we can land this branch.

Once that's done, we can Q/A this by doing a timed control publisher run on mawson, rolling out this change, taking a backup copy of ubuntu-germinate and ubuntu-misc, and then doing another timed publisher run and comparing the ubuntu-germinate and ubuntu-misc directories before and after. (There may be some harmless ordering changes and such.)

== Lint ==

./cronscripts/generate-extra-overrides.py
       8: '_pythonpath' imported but unused

Seems to be standard practice in cronscripts.

./lib/lp/archivepublisher/tests/publisher-config.txt
       1: narrative uses a moin header.
      26: narrative uses a moin header.
      46: narrative uses a moin header.
     114: narrative uses a moin header.
     138: narrative uses a moin header.
     163: narrative uses a moin header.
     178: want exceeds 78 characters.
     179: want exceeds 78 characters.
     180: want exceeds 78 characters.
     182: want exceeds 78 characters.

This was pre-existing lint; I only made the minimal changes necessary to add the 'germinateroot' attribute, which haven't fundamentally made the situation any worse.

To post a comment you must log in.
Revision history for this message
Colin Watson (cjwatson) wrote :

I've tested the core logic on mawson now (stripped out the database interaction and hardcoded paths to a local mirror so that I could run it in isolation). Compared to an output tree generated by the old cron.germinate with germinate 2.0, the substantive output is identical: various files are deliberately no longer generated, and the log output is different, but the actual germinate output and the more-extra.override.precise.main file are identical. It runs in about 7 minutes compared with about 14/15 minutes for the old cron.germinate, which I call a win (although these are single-run figures).

The weird part is that something is wrong that means we aren't even getting a lot of the benefit yet! It isn't actually sharing the results of germinating common seeds. Therefore, that 7/8 minutes of win must essentially come from avoiding duplicated seed parsing, avoiding duplicated Packages/Sources parsing, and a reduction in the number of files we write. I wasn't expecting that to account for so much of the runtime, but I'm not going to complain.

I will investigate further to find out why common seed output isn't being shared. That should bring the runtime down further.

Revision history for this message
Colin Watson (cjwatson) wrote :

Right. I've tracked down all the problems I could find and released germinate 2.1 to fix all of them; none of them were problems with this branch. The runtime is now down to about 4m50s on mawson; the only differences in output are (a) in places where it doesn't matter very much and (b) accountable to a pure-virtual dependency in libenchant1c2a, where germinate is entirely entitled to choose a provider at random, so I'm not worried about those.

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

A note on the problem of ensuring that a PublisherConfig is in place: factory.makeDistribution creates one for you. Just make sure to pass it a publish_root_dir, or I think it will use default system config. This should probably be considered a bug in the factory.

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

It looks to me like you did the right thing in creating a dedicated database user. We may ditch the per-user privileges, but we do want separate users for separate scripts.

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

=== added file 'lib/lp/archivepublisher/scripts/generate_extra_overrides.py'
--- lib/lp/archivepublisher/scripts/generate_extra_overrides.py 1970-01-01 00:00:00 +0000
+++ lib/lp/archivepublisher/scripts/generate_extra_overrides.py 2011-12-06 15:03:58 +0000

+class AtomicFile:
+ """Facilitate atomic writing of files."""
+
+ def __init__(self, filename):
+ self.filename = filename
+ self.fd = open('%s.new' % self.filename, 'w')
+
+ def __enter__(self):
+ return self.fd
+
+ def __exit__(self, exc_type, exc_value, exc_tb):
+ self.fd.close()
+ os.rename('%s.new' % self.filename, self.filename)

Does it make sense to put the file in place if the context exits with an exception?

Actually this is what I don't like about the design of python context managers: it puts you, as the author of a context manager, in the position where you may have to worry about failures in the context you manage — and yet it's easy for the author of the context to hide them from you. So maybe stupid is just better.

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

Next bit I looked at:

=== added file 'lib/lp/archivepublisher/scripts/generate_extra_overrides.py'

+ 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)
+
+ series = None
+ wanted_status = (SeriesStatus.DEVELOPMENT,
+ SeriesStatus.FROZEN)
+ for status in wanted_status:
+ series = self.distribution.getSeriesByStatus(status)
+ if series.count() > 0:
+ break
+ else:
+ raise LaunchpadScriptFailure(
+ 'There is no DEVELOPMENT distroseries for %s' %
+ self.options.distribution)
+ self.series = series[0]

The part from “series = None” onward seems to be an isolated unit of work. I think it looks for the first distroseries in development state, or failing that, the first in frozen state. But the structure of the code makes that hard to figure out, and then only afterwards I can start wondering why you do it exactly this way.

I fervently recommend extracting that code into a sensibly-named function. (It doesn't need to be a method: AFAICS a distribution goes in, a series comes out, and that is the full extent of its interaction with the rest of the world). Come to think of it, might there already be a suitable method in Distribution somewhere?

The structure of that loop is also a bit hard to follow. It gets easier if you have a single-purpose function that you can just return from instead of using a break!

Finally, a few cosmetic tips:

 * When line-breaking a tuple, list, or dict, our “house style” is to add a line break right after the opening parenthesis, bracket, or brace. Each entry ends in a comma and a newline — including the last entry.

    wanted_status = (
        SeriesStatus.DEVELOPMENT,
        SeriesStatus.FROZEN,
        )

 * You don't really need series.count(). In fact I think you can just retrieve the first matching series as series.first(), and you'll get None if there isn't any.

 * For Python strings I find consistent double quotes a good habit, at least for free-form texts that may just as well contain an apostrophe.

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

I'm going through the diff sequentially at the moment; the parts I'm ignoring are the parts that I have nothing to say about. When I get to the end I can give you a very brief formal review. :)

=== added file 'lib/lp/archivepublisher/scripts/generate_extra_overrides.py'

+ def getConfig(self):
+ """Set up a configuration object for this archive."""
+ for archive in self.distribution.all_distro_archives:
+ # We only work on the primary archive.
+ if archive.purpose == ArchivePurpose.PRIMARY:
+ return getPubConfig(archive)
+ else:
+ raise LaunchpadScriptFailure(
+ 'There is no PRIMARY archive for %s' %
+ self.options.distribution)

Why not just use self.distribution.main_archive?

(Also, remember to punctuate that error message!)

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

+ def outputPath(self, flavour, series_name, arch, base):
+ return os.path.join(
+ self.config.germinateroot,
+ '%s_%s_%s_%s' % (base, flavour, series_name, arch))

Our dromedary-cased method names normally start with a verb. Read that way, “outputPath” would suggest that the method prints a path.

So consider prefixing this method's name with a verb. The unimaginative catch-all verb is “get”; personally I prefer “compose” for this kind of thing.

Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :
Download full text (8.5 KiB)

+ def runGerminate(self, override_file, series_name, arch, flavours,
+ structures):

Rightly or wrongly, the “run” in that name led me to expect that this method would fire up a separate process. Maybe a very short docstring to dispel that notion?

+ germinator = Germinator(arch)
+
+ # Read archive metadata.
+ archive = TagFile(
+ series_name, self.components, arch,
+ 'file://%s' % self.config.archiveroot, cleanup=True)
+ germinator.parse_archive(archive)
+
+ for flavour in flavours:

To me, your branch is of very high quality but this is its Achilles' heel. The following loop body is massive! I can't see it all on one page, so it becomes hard even to see just how long it is. If I judged the indentations right, it makes up the entire rest of the method.

Please extract it. It's well nigh impossible to see (now or in the future, after some maintenance) whether any variables are carried across loop iterations. That sort of thing can easily trip you up, especially when it happens by accident.

Once you've extracted the loop body, given its size, it's probably still worth extracting chunks from that.

According to some, the pattern of “comment, block of code, blank line, comment, block of code, blank line” is a strong indication that you need to split things up. It shows that you have already done so in your own mind, but leaves each individual reader to figure out what the dependencies between consecutive blocks are.

Also, of course, small methods with simple purposes are easier to specify in detail and cheaper to test thoroughly.

+ self.logger.info('Germinating for %s/%s/%s',
+ flavour, series_name, arch)

We follow a different rule for line-breaking function calls than we do for function definitions.

For calls, line-break right after the opening parenthesis and then indent the arguments list by 4 spaces:

    self.logger.info(
        "Germinating for %s/%s/%s.", flavour, series_name, arch)

+ # Add this to the germinate log as well so that that can be
+ # debugged more easily. Log a separator line first.
+ self.germinate_logger.info('', extra={'progress': True})
+ self.germinate_logger.info('Germinating for %s/%s/%s',
+ flavour, series_name, arch,
+ extra={'progress': True})

What does the “extra” parameter do?

+ # Expand dependencies.
+ structure = structures[flavour]
+ germinator.plant_seeds(structure)
+ germinator.grow(structure)
+ germinator.add_extras(structure)
+ # Write output files.
+
+ # The structure file makes it possible to figure out how the
+ # other output files relate to each other.
+ structure.write(self.outputPath(
+ flavour, series_name, arch, 'structure'))
+
+ # "all" and "all.sources" list the full set of binary and source
+ # packages respectively for a given flavour/suite/architecture
+ # combination.
+ all_path = ...

Read more...

Revision history for this message
Colin Watson (cjwatson) wrote :

I think you're right that the file shouldn't be put in place if the
context exits with an exception. If I were writing this without context
managers, then rather than:

    with AtomicFile('foo') as handle:
        print >>handle, 'bar'

... I'd probably write:

    handle = open('foo.new', 'w')
    try:
        print >>handle, 'bar'
    finally:
        handle.close()
    os.rename('foo.new', 'foo')

And that's equivalent to the natural expression in (good) shell too:

    set -e
    echo bar >foo.new
    mv foo.new foo

That all suggests to me that skipping the rename on exception, and
leaving the .new file in place so that somebody with access can look at
it and find out how far the script got before it fell over, is a good
thing. I'll change the code to do that.

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

Getting a better feel for the structure of runGerminate now. Which makes me wonder:

+ # Generate apt-ftparchive "extra overrides" for Build-Essential
+ # fields.
+ if ('build-essential' in structure.names and
+ flavour == flavours[0]):
+ writeOverrides('build-essential', 'Build-Essential', 'yes')

I don't suppose you could just do this at the end, outside of all loops?

    if 'build-essential' in structures[flavours[0]].names:
        writeOverrides('build-essential', 'Build-Essential', 'yes')

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

Ah, I see now why you can't do that: writeOverrides is a closure. It picks up “structure” and “arch” from the surrounding method at its point of definition. There's another easy thing to break by accident! The structure should again be structures[flavours[0]], I guess.

Might it be worthwhile to separate the closure nature of writeOverrides from its functionality? If you had a separate function that takes all the necessary variables as arguments, it'd be easier to move that last paragraph out of the loop (still assuming that that makes sense at all) and it'd be easier to see that writeOverrides “borrows” some variables from runGerminate.

Revision history for this message
Colin Watson (cjwatson) wrote :

On Wed, Dec 07, 2011 at 07:20:29AM -0000, Jeroen T. Vermeulen wrote:
> The part from “series = None” onward seems to be an isolated unit of
> work. I think it looks for the first distroseries in development
> state, or failing that, the first in frozen state. But the structure
> of the code makes that hard to figure out, and then only afterwards I
> can start wondering why you do it exactly this way.
>
> I fervently recommend extracting that code into a sensibly-named
> function. (It doesn't need to be a method: AFAICS a distribution goes
> in, a series comes out, and that is the full extent of its interaction
> with the rest of the world). Come to think of it, might there already
> be a suitable method in Distribution somewhere?

Fair comment. Distribution.currentseries is nearly there: it just
sometimes returns series in statuses we don't want here; but if there's
one we can use then it will always return it, so we can just call
currentseries and then check the result.

> * For Python strings I find consistent double quotes a good habit, at
> least for free-form texts that may just as well contain an
> apostrophe.

This is an out-of-context habit of mine from Perl and shell programming:
since single and double quotes have different interpolation
characteristics there, I've trained myself to use single quotes unless
either I have an explicit reason to want interpolation or the string
contains an apostrophe and no double quotes. I realise this doesn't
make as much sense for Python, so I'll go through and amend this.

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

Some very nice tests there! To continue:

=== added file 'lib/lp/archivepublisher/tests/test_generate_extra_overrides.py'

+def file_contents(path):
+ """Return the contents of the file at path."""
+ with open(path) as handle:
+ return handle.read()

Since you're only reading the file, there's probably no hurry to close this file handle. So you could just return “open(path).read().”

Revision history for this message
Colin Watson (cjwatson) wrote :
Download full text (4.5 KiB)

[I've not responded to your entire review point-by-point, but I believe
I've addressed all points where I haven't given an explicit response.]

On Wed, Dec 07, 2011 at 09:22:24AM -0000, Jeroen T. Vermeulen wrote:
> + def runGerminate(self, override_file, series_name, arch, flavours,
> + structures):
>
> Rightly or wrongly, the “run” in that name led me to expect that this
> method would fire up a separate process. Maybe a very short docstring
> to dispel that notion?

The name doesn't actually make a lot of sense; I think originally I was
in the mindset where this actually did fire up a separate process. I've
renamed the script instance to germinateArch (with a docstring), and the
test instance to fetchGerminatedOverrides. Does that help?

> To me, your branch is of very high quality but this is its Achilles'
> heel. The following loop body is massive! I can't see it all on one
> page, so it becomes hard even to see just how long it is. If I judged
> the indentations right, it makes up the entire rest of the method.

That's fair; I went back and forward a bit on this, but clearly landed
in the wrong place. Actually, now that I've renamed runGerminate to
germinateArch, it's easier in my mind to turn a block of it into
germinateArchFlavour in turn. (Regularised naming may be boring, but I
like it anyway.)

The methods do start ending up with quite a few parameters, but that's
probably better than the alternative.

> Once you've extracted the loop body, given its size, it's probably
> still worth extracting chunks from that.

I've done a good deal of this, although will probably end up
restructuring a bit further per your later comments.

> According to some, the pattern of “comment, block of code, blank line,
> comment, block of code, blank line” is a strong indication that you
> need to split things up. It shows that you have already done so in
> your own mind, but leaves each individual reader to figure out what
> the dependencies between consecutive blocks are.

I don't necessarily agree with this in all cases (e.g. I don't know that
the new writeGerminateOutput method would benefit much from being split
further), but I agree that the original version of this code was rather
too far in the other direction.

> + # Add this to the germinate log as well so that that can be
> + # debugged more easily. Log a separator line first.
> + self.germinate_logger.info('', extra={'progress': True})
> + self.germinate_logger.info('Germinating for %s/%s/%s',
> + flavour, series_name, arch,
> + extra={'progress': True})
>
> What does the “extra” parameter do?

That attaches a dictionary of extra attributes to the log message which
can then be picked up by the formatter. Germinate uses this for certain
messages (arguably a wart in germinate's design; I should probably look
into doing that more neatly at some point). I've moved this to its own
method so that it can have a clarifying docstring.

> That's a lot of string manipulation. It may be clearer as a regex:
>
> task_header_regex = re.compile("ta...

Read more...

Revision history for this message
Colin Watson (cjwatson) wrote :

On Wed, Dec 07, 2011 at 09:56:26AM -0000, Jeroen T. Vermeulen wrote:
> Might it be worthwhile to separate the closure nature of
> writeOverrides from its functionality? If you had a separate function
> that takes all the necessary variables as arguments, it'd be easier to
> move that last paragraph out of the loop (still assuming that that
> makes sense at all) and it'd be easier to see that writeOverrides
> “borrows” some variables from runGerminate.

Good idea. Per our conversation on IRC, I've used functools.partial for
this. I also used it in writeGerminateOutput, which IMO this approach
notably improves. What do you think?

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

Thank you for bearing with me, making so many changes after producing an already large and difficult branch. I particularly like the way you seized on method extraction as an opportunity to document the algorithm's steps.

This is a massive step forward in terms of code maintenance alone, and I'm looking forward to seeing how it affects performance.

review: Approve
Revision history for this message
Colin Watson (cjwatson) wrote :

On Wed, Dec 07, 2011 at 10:14:27AM -0000, Jeroen T. Vermeulen wrote:
> +def file_contents(path):
> + """Return the contents of the file at path."""
> + with open(path) as handle:
> + return handle.read()
>
> Since you're only reading the file, there's probably no hurry to close this file handle. So you could just return “open(path).read().”

For the record, there was some discussion on this on IRC, and most
people seemed to think that it was better to retain the immediate close,
in particular since implementations other than cPython may not GC-close
it with any alacrity; so I've left this the way it is. (There is a lot
of variation on this in the Launchpad test suite, though ...)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== added file 'cronscripts/generate-extra-overrides.py'
--- cronscripts/generate-extra-overrides.py 1970-01-01 00:00:00 +0000
+++ cronscripts/generate-extra-overrides.py 2011-12-14 15:49:40 +0000
@@ -0,0 +1,18 @@
1#!/usr/bin/python -S
2#
3# Copyright 2011 Canonical Ltd. This software is licensed under the
4# GNU Affero General Public License version 3 (see the file LICENSE).
5
6"""Generate extra overrides using Germinate."""
7
8import _pythonpath
9
10from lp.archivepublisher.scripts.generate_extra_overrides import (
11 GenerateExtraOverrides,
12 )
13
14
15if __name__ == '__main__':
16 script = GenerateExtraOverrides(
17 "generate-extra-overrides", dbuser='generate_extra_overrides')
18 script.lock_and_run()
019
=== modified file 'cronscripts/publishing/cron.germinate'
--- cronscripts/publishing/cron.germinate 2011-10-27 11:36:13 +0000
+++ cronscripts/publishing/cron.germinate 2011-12-14 15:49:40 +0000
@@ -12,8 +12,12 @@
12GERMINATEROOT=$ARCHIVEROOT/../ubuntu-germinate12GERMINATEROOT=$ARCHIVEROOT/../ubuntu-germinate
1313
14LAUNCHPADROOT=${TEST_LAUNCHPADROOT:-/srv/launchpad.net/codelines/current}14LAUNCHPADROOT=${TEST_LAUNCHPADROOT:-/srv/launchpad.net/codelines/current}
15GENERATE=$LAUNCHPADROOT/cronscripts/generate-extra-overrides.py
15MAINTAINCE_CHECK=$LAUNCHPADROOT/cronscripts/publishing/maintenance-check.py16MAINTAINCE_CHECK=$LAUNCHPADROOT/cronscripts/publishing/maintenance-check.py
1617
18FLAVOURS="ubuntu kubuntu kubuntu-mobile edubuntu xubuntu mythbuntu lubuntu"
19FLAVOURS="$FLAVOURS ubuntustudio"
20
17## Check to see if another germinate run is in progress21## Check to see if another germinate run is in progress
1822
19LOCKFILE=$LOCKROOT/cron.germinate.lock23LOCKFILE=$LOCKROOT/cron.germinate.lock
@@ -28,134 +32,9 @@
2832
29trap cleanup EXIT33trap cleanup EXIT
3034
31suite=`$LAUNCHPADROOT/scripts/ftpmaster-tools/lp-query-distro.py development`
32archs=`$LAUNCHPADROOT/scripts/ftpmaster-tools/lp-query-distro.py -s "$suite" archs`
33
34echo -n "Running germinate... "
35cd $GERMINATEROOT35cd $GERMINATEROOT
3636
37# Clean up temporary files37$GENERATE -d ubuntu $FLAVOURS
38rm -f \
39 germinate.output ALL ALL.sources \
40 UBUNTU-* KUBUNTU-* EDUBUNTU-* XUBUNTU-* MYTHBUNTU-* LUBUNTU-* \
41 UBUNTUSTUDIO-*
42rm -f all_* all.sources_*
43rm -rf dists
44
45# Grab local copies of Sources and Packages files, to avoid problems in case
46# the archive changes under our feet.
47for component in main universe restricted multiverse; do
48 base="dists/$suite/$component"
49 mkdir -p "$base/source"
50 zcat "$ARCHIVEROOT/$base/source/Sources.gz" > "$base/source/Sources"
51 for arch in $archs; do
52 mkdir -p "$base/binary-$arch" "$base/debian-installer/binary-$arch"
53 zcat "$ARCHIVEROOT/$base/binary-$arch/Packages.gz" \
54 > "$base/binary-$arch/Packages"
55 zcat "$ARCHIVEROOT/$base/debian-installer/binary-$arch/Packages.gz" \
56 > "$base/debian-installer/binary-$arch/Packages"
57 done
58done
59
60> "$MISCROOT/more-extra.override.$suite.main.new"
61
62germinate_components=main,universe,restricted,multiverse
63for distro in \
64 ubuntu kubuntu kubuntu-mobile edubuntu xubuntu mythbuntu lubuntu \
65 ubuntustudio
66do
67 DISTRO="$(echo $distro | tr a-z A-Z)"
68 germinate_suite="$distro.$suite"
69 for arch in $archs; do
70 # Run germinate
71 echo " **************** $distro/$suite/$arch ********************* " >> germinate.output
72 germinate \
73 --no-rdepends \
74 -m "file://$(pwd)/" \
75 -s "$germinate_suite" \
76 -d "$suite" \
77 -c "$germinate_components" \
78 -a $arch >> germinate.output 2>&1
79
80 # The structure file is generally useful; keep per distro/suite/arch
81 # copies for convenience
82 cp structure structure_"$distro"_"$suite"_"$arch"
83
84 # Keep per distro/suite/arch copies of 'all' and 'all.sources' for
85 # anastacia.
86 cp all all_"$distro"_"$suite"_"$arch"
87 cp all.sources all.sources_"$distro"_"$suite"_"$arch"
88
89 # Keep per distro/suite/arch copies of 'minimal' and 'standard' for
90 # jessica.
91 cp minimal minimal_"$distro"_"$suite"_"$arch"
92 cp standard standard_"$distro"_"$suite"_"$arch"
93
94 # Keep amalgamated copies of 'all' and 'all.sources', just for convenience
95 cat all >> ALL; cat all.sources >> ALL.sources
96
97 # We need to fetch a number of seeds so that we can generate Task fields
98 # for them. This changes over time and differs from derivative to
99 # derivative, so it's best to just fetch them all.
100 taskseeds="$(cut -d: -f1 structure | xargs -n1 | sort -u)"
101
102 for seed in $taskseeds; do
103 cp "$seed" "$seed"_"$distro"_"$suite"_"$arch"
104 done
105 echo " ********************************************************************** " >> germinate.output
106 echo "" >> germinate.output
107 echo -n "."
108
109 ## Generate apt-ftparchive 'extra' overrides for Task: fields
110 for seed in $taskseeds; do
111 if ! grep -iq '^Task-' "$seed.seedtext"; then
112 continue
113 fi
114 # If the seed contains Task-Name header, override the normal behavior
115 if grep -iq '^Task-Name:' "$seed.seedtext"; then
116 task=$(grep '^Task-Name:' "$seed.seedtext" | cut -d: -f2)
117 elif grep -iq '^Task-Per-Derivative:' "$seed.seedtext"; then
118 task="$distro-$seed"
119 else
120 # If a seed is not per-derivative, then we only honour it for Ubuntu,
121 # and its task name is archive-global.
122 if [ "$distro" = ubuntu ]; then
123 task="$seed"
124 else
125 continue
126 fi
127 fi
128 if grep -iq '^Task-Seeds:' "$seed.seedtext"; then
129 scanseeds="$( (grep '^Task-Seeds:' "$seed.seedtext" | cut -d: -f2 | xargs -n1; echo "$seed") | sort -u )"
130 else
131 scanseeds="$seed"
132 fi
133 for scanseed in $scanseeds; do
134 egrep -v -- \
135 "^(-|Package| )" "$scanseed"_"$distro"_"$suite"_"$arch" |
136 awk '{print $1}' |
137 sed -e "s,$,/$arch Task $task," |
138 sort -u >> "$MISCROOT/more-extra.override.$suite.main.new"
139 done
140 done
141
142 # Generate apt-ftparchive 'extra' overrides for Build-Essential: fields
143 if [ -e build-essential ] && [ "$distro" = ubuntu ]; then
144 # Keep a copy, just for convenience
145 cp build-essential build-essential_"$distro"_"$suite"_"$arch"
146 egrep -v -- \
147 "^(-|Package| )" build-essential_"$distro"_"$suite"_"$arch" |
148 awk '{print $1}' |
149 sed -e "s,$,/$arch Build-Essential yes," |
150 sort -u >> "$MISCROOT/more-extra.override.$suite.main.new"
151 fi
152 done
153done
154echo " done."
155
156mv -f \
157 "$MISCROOT/more-extra.override.$suite.main.new" \
158 "$MISCROOT/more-extra.override.$suite.main"
15938
160# Now generate the Supported extra overrides for all supported distros.39# Now generate the Supported extra overrides for all supported distros.
161SUITES=`$LAUNCHPADROOT/scripts/ftpmaster-tools/lp-query-distro.py supported`40SUITES=`$LAUNCHPADROOT/scripts/ftpmaster-tools/lp-query-distro.py supported`
16241
=== modified file 'database/schema/security.cfg'
--- database/schema/security.cfg 2011-12-06 21:10:57 +0000
+++ database/schema/security.cfg 2011-12-14 15:49:40 +0000
@@ -2266,6 +2266,10 @@
2266type=user2266type=user
2267groups=archivepublisher2267groups=archivepublisher
22682268
2269[generate_extra_overrides]
2270type=user
2271groups=archivepublisher
2272
2269[process_death_row]2273[process_death_row]
2270type=user2274type=user
2271groups=archivepublisher2275groups=archivepublisher
22722276
=== modified file 'lib/lp/archivepublisher/config.py'
--- lib/lp/archivepublisher/config.py 2011-08-29 16:43:10 +0000
+++ lib/lp/archivepublisher/config.py 2011-12-14 15:49:40 +0000
@@ -74,10 +74,12 @@
74 pubconf.overrideroot = pubconf.archiveroot + '-overrides'74 pubconf.overrideroot = pubconf.archiveroot + '-overrides'
75 pubconf.cacheroot = pubconf.archiveroot + '-cache'75 pubconf.cacheroot = pubconf.archiveroot + '-cache'
76 pubconf.miscroot = pubconf.archiveroot + '-misc'76 pubconf.miscroot = pubconf.archiveroot + '-misc'
77 pubconf.germinateroot = pubconf.archiveroot + '-germinate'
77 else:78 else:
78 pubconf.overrideroot = None79 pubconf.overrideroot = None
79 pubconf.cacheroot = None80 pubconf.cacheroot = None
80 pubconf.miscroot = None81 pubconf.miscroot = None
82 pubconf.germinateroot = None
8183
82 pubconf.poolroot = os.path.join(pubconf.archiveroot, 'pool')84 pubconf.poolroot = os.path.join(pubconf.archiveroot, 'pool')
83 pubconf.distsroot = os.path.join(pubconf.archiveroot, 'dists')85 pubconf.distsroot = os.path.join(pubconf.archiveroot, 'dists')
@@ -106,6 +108,7 @@
106 self.cacheroot,108 self.cacheroot,
107 self.overrideroot,109 self.overrideroot,
108 self.miscroot,110 self.miscroot,
111 self.germinateroot,
109 self.temproot,112 self.temproot,
110 ]113 ]
111114
112115
=== added file 'lib/lp/archivepublisher/scripts/generate_extra_overrides.py'
--- lib/lp/archivepublisher/scripts/generate_extra_overrides.py 1970-01-01 00:00:00 +0000
+++ lib/lp/archivepublisher/scripts/generate_extra_overrides.py 2011-12-14 15:49:40 +0000
@@ -0,0 +1,339 @@
1# Copyright 2011 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Generate extra overrides using Germinate."""
5
6__metaclass__ = type
7__all__ = [
8 'GenerateExtraOverrides',
9 ]
10
11from functools import partial
12import logging
13from optparse import OptionValueError
14import os
15import re
16
17from germinate.germinator import Germinator
18from germinate.archive import TagFile
19from germinate.log import GerminateFormatter
20from germinate.seeds import SeedStructure
21
22from zope.component import getUtility
23
24from canonical.launchpad.webapp.dbpolicy import (
25 DatabaseBlockedPolicy,
26 SlaveOnlyDatabasePolicy,
27 )
28from lp.archivepublisher.config import getPubConfig
29from lp.registry.interfaces.distribution import IDistributionSet
30from lp.registry.interfaces.series import SeriesStatus
31from lp.services.scripts.base import (
32 LaunchpadScript,
33 LaunchpadScriptFailure,
34 )
35from lp.services.utils import file_exists
36
37
38class AtomicFile:
39 """Facilitate atomic writing of files."""
40
41 def __init__(self, filename):
42 self.filename = filename
43 self.fd = open("%s.new" % self.filename, "w")
44
45 def __enter__(self):
46 return self.fd
47
48 def __exit__(self, exc_type, exc_value, exc_tb):
49 self.fd.close()
50 if exc_type is None:
51 os.rename("%s.new" % self.filename, self.filename)
52
53
54def find_operable_series(distribution):
55 """Find a series we can operate on in this distribution.
56
57 We are allowed to modify DEVELOPMENT or FROZEN series, but should leave
58 series with any other status alone.
59 """
60 series = distribution.currentseries
61 if series.status in (SeriesStatus.DEVELOPMENT, SeriesStatus.FROZEN):
62 return series
63 else:
64 return None
65
66
67class GenerateExtraOverrides(LaunchpadScript):
68 """Main class for scripts/ftpmaster-tools/generate-task-overrides.py."""
69
70 def __init__(self, *args, **kwargs):
71 super(GenerateExtraOverrides, self).__init__(*args, **kwargs)
72 self.germinate_logger = None
73
74 def add_my_options(self):
75 """Add a 'distribution' context option."""
76 self.parser.add_option(
77 "-d", "--distribution", dest="distribution",
78 help="Context distribution name.")
79
80 @property
81 def name(self):
82 """See `LaunchpadScript`."""
83 # Include distribution name. Clearer to admins, but also
84 # puts runs for different distributions under separate
85 # locks so that they can run simultaneously.
86 return "%s-%s" % (self._name, self.options.distribution)
87
88 def processOptions(self):
89 """Handle command-line options."""
90 if self.options.distribution is None:
91 raise OptionValueError("Specify a distribution.")
92
93 self.distribution = getUtility(IDistributionSet).getByName(
94 self.options.distribution)
95 if self.distribution is None:
96 raise OptionValueError(
97 "Distribution '%s' not found." % self.options.distribution)
98
99 self.series = find_operable_series(self.distribution)
100 if self.series is None:
101 raise LaunchpadScriptFailure(
102 "There is no DEVELOPMENT distroseries for %s." %
103 self.options.distribution)
104
105 # Even if DistroSeries.component_names starts including partner, we
106 # don't want it; this applies to the primary archive only.
107 self.components = [component
108 for component in self.series.component_names
109 if component != "partner"]
110
111 def getConfig(self):
112 """Set up a configuration object for this archive."""
113 archive = self.distribution.main_archive
114 if archive:
115 return getPubConfig(archive)
116 else:
117 raise LaunchpadScriptFailure(
118 "There is no PRIMARY archive for %s." %
119 self.options.distribution)
120
121 def setUpDirs(self):
122 """Create output directories if they did not already exist."""
123 germinateroot = self.config.germinateroot
124 if not file_exists(germinateroot):
125 self.logger.debug("Creating germinate root %s.", germinateroot)
126 os.makedirs(germinateroot)
127 miscroot = self.config.miscroot
128 if not file_exists(miscroot):
129 self.logger.debug("Creating misc root %s.", miscroot)
130 os.makedirs(miscroot)
131
132 def addLogHandler(self):
133 """Send germinate's log output to a separate file."""
134 if self.germinate_logger is not None:
135 return
136
137 self.germinate_logger = logging.getLogger("germinate")
138 self.germinate_logger.setLevel(logging.INFO)
139 log_file = os.path.join(self.config.germinateroot, "germinate.output")
140 handler = logging.FileHandler(log_file, mode="w")
141 handler.setFormatter(GerminateFormatter())
142 self.germinate_logger.addHandler(handler)
143 self.germinate_logger.propagate = False
144
145 def setUp(self):
146 """Process options, and set up internal state."""
147 self.processOptions()
148 self.config = self.getConfig()
149 self.setUpDirs()
150 self.addLogHandler()
151
152 def makeSeedStructures(self, series_name, flavours, seed_bases=None):
153 structures = {}
154 for flavour in flavours:
155 structures[flavour] = SeedStructure(
156 "%s.%s" % (flavour, series_name), seed_bases=seed_bases)
157 return structures
158
159 def logGerminateProgress(self, *args):
160 """Log a "progress" entry to the germinate log file.
161
162 Germinate logs quite a bit of detailed information. To make it
163 easier to see the structure of its operation, GerminateFormatter
164 allows tagging some log entries as "progress" entries, which are
165 printed without a prefix.
166 """
167 self.germinate_logger.info(*args, extra={"progress": True})
168
169 def composeOutputPath(self, flavour, series_name, arch, base):
170 return os.path.join(
171 self.config.germinateroot,
172 "%s_%s_%s_%s" % (base, flavour, series_name, arch))
173
174 def writeGerminateOutput(self, germinator, structure, flavour,
175 series_name, arch):
176 """Write dependency-expanded output files.
177
178 These files are a reduced subset of those written by the germinate
179 command-line program.
180 """
181 path = partial(self.composeOutputPath, flavour, series_name, arch)
182
183 # The structure file makes it possible to figure out how the other
184 # output files relate to each other.
185 structure.write(path("structure"))
186
187 # "all" and "all.sources" list the full set of binary and source
188 # packages respectively for a given flavour/suite/architecture
189 # combination.
190 germinator.write_all_list(structure, path("all"))
191 germinator.write_all_source_list(structure, path("all.sources"))
192
193 # Write the dependency-expanded output for each seed. Several of
194 # these are used by archive administration tools, and others are
195 # useful for debugging, so it's best to just write them all.
196 for seedname in structure.names:
197 germinator.write_full_list(structure, path(seedname), seedname)
198
199 def parseTaskHeaders(self, seedtext):
200 """Parse a seed for Task headers.
201
202 seedtext is a file-like object. Return a dictionary of Task headers,
203 with keys canonicalised to lower-case.
204 """
205 task_headers = {}
206 task_header_regex = re.compile(
207 r"task-(.*?):(.*)", flags=re.IGNORECASE)
208 for line in seedtext:
209 match = task_header_regex.match(line)
210 if match is not None:
211 key, value = match.groups()
212 task_headers[key.lower()] = value.strip()
213 return task_headers
214
215 def getTaskName(self, task_headers, flavour, seedname, primary_flavour):
216 """Work out the name of the Task to be generated from this seed.
217
218 If there is a Task-Name header, it wins; otherwise, seeds with a
219 Task-Per-Derivative header are honoured for all flavours and put in
220 an appropriate namespace, while other seeds are only honoured for
221 the first flavour and have archive-global names.
222 """
223 if "name" in task_headers:
224 return task_headers["name"]
225 elif "per-derivative" in task_headers:
226 return "%s-%s" % (flavour, seedname)
227 elif primary_flavour:
228 return seedname
229 else:
230 return None
231
232 def getTaskSeeds(self, task_headers, seedname):
233 """Return the list of seeds used to generate a task from this seed.
234
235 The list of packages in this task comes from this seed plus any
236 other seeds listed in a Task-Seeds header.
237 """
238 scan_seeds = set([seedname])
239 if "seeds" in task_headers:
240 scan_seeds.update(task_headers["seeds"].split())
241 return sorted(scan_seeds)
242
243 def writeOverrides(self, override_file, germinator, structure, arch,
244 seedname, key, value):
245 packages = germinator.get_full(structure, seedname)
246 for package in sorted(packages):
247 print >>override_file, "%s/%s %s %s" % (
248 package, arch, key, value)
249
250 def germinateArchFlavour(self, override_file, germinator, series_name,
251 arch, flavour, structure, primary_flavour):
252 """Germinate seeds on a single flavour for a single architecture."""
253 # Expand dependencies.
254 germinator.plant_seeds(structure)
255 germinator.grow(structure)
256 germinator.add_extras(structure)
257
258 self.writeGerminateOutput(germinator, structure, flavour, series_name,
259 arch)
260
261 write_overrides = partial(
262 self.writeOverrides, override_file, germinator, structure, arch)
263
264 # Generate apt-ftparchive "extra overrides" for Task fields.
265 seednames = [name for name in structure.names if name != "extra"]
266 for seedname in seednames:
267 with structure[seedname] as seedtext:
268 task_headers = self.parseTaskHeaders(seedtext)
269 if task_headers:
270 task = self.getTaskName(
271 task_headers, flavour, seedname, primary_flavour)
272 if task is not None:
273 scan_seeds = self.getTaskSeeds(task_headers, seedname)
274 for scan_seed in scan_seeds:
275 write_overrides(scan_seed, "Task", task)
276
277 # Generate apt-ftparchive "extra overrides" for Build-Essential
278 # fields.
279 if "build-essential" in structure.names and primary_flavour:
280 write_overrides("build-essential", "Build-Essential", "yes")
281
282 def germinateArch(self, override_file, series_name, arch, flavours,
283 structures):
284 """Germinate seeds on all flavours for a single architecture."""
285 germinator = Germinator(arch)
286
287 # Read archive metadata.
288 archive = TagFile(
289 series_name, self.components, arch,
290 "file://%s" % self.config.archiveroot, cleanup=True)
291 germinator.parse_archive(archive)
292
293 for flavour in flavours:
294 self.logger.info(
295 "Germinating for %s/%s/%s", flavour, series_name, arch)
296 # Add this to the germinate log as well so that that can be
297 # debugged more easily. Log a separator line first.
298 self.logGerminateProgress("")
299 self.logGerminateProgress(
300 "Germinating for %s/%s/%s", flavour, series_name, arch)
301
302 self.germinateArchFlavour(
303 override_file, germinator, series_name, arch, flavour,
304 structures[flavour], flavour == flavours[0])
305
306 def generateExtraOverrides(self, series_name, series_architectures,
307 flavours, seed_bases=None):
308 structures = self.makeSeedStructures(
309 series_name, flavours, seed_bases=seed_bases)
310
311 override_path = os.path.join(
312 self.config.miscroot,
313 "more-extra.override.%s.main" % series_name)
314 with AtomicFile(override_path) as override_file:
315 for arch in series_architectures:
316 self.germinateArch(
317 override_file, series_name, arch, flavours, structures)
318
319 def process(self, seed_bases=None):
320 """Do the bulk of the work."""
321 self.setUp()
322
323 series_name = self.series.name
324 series_architectures = sorted(
325 [arch.architecturetag for arch in self.series.architectures])
326
327 # This takes a while. Ensure that we do it without keeping a
328 # database transaction open.
329 self.txn.commit()
330 with DatabaseBlockedPolicy():
331 self.generateExtraOverrides(
332 series_name, series_architectures, self.args,
333 seed_bases=seed_bases)
334
335 def main(self):
336 """See `LaunchpadScript`."""
337 # This code has no need to alter the database.
338 with SlaveOnlyDatabasePolicy():
339 self.process()
0340
=== modified file 'lib/lp/archivepublisher/tests/publisher-config.txt'
--- lib/lp/archivepublisher/tests/publisher-config.txt 2010-10-17 13:35:20 +0000
+++ lib/lp/archivepublisher/tests/publisher-config.txt 2011-12-14 15:49:40 +0000
@@ -14,6 +14,7 @@
14 ... 'overrideroot',14 ... 'overrideroot',
15 ... 'cacheroot',15 ... 'cacheroot',
16 ... 'miscroot',16 ... 'miscroot',
17 ... 'germinateroot',
17 ... 'temproot',18 ... 'temproot',
18 ... ]19 ... ]
1920
@@ -38,6 +39,7 @@
38 overrideroot: /var/tmp/archive/ubuntutest-overrides39 overrideroot: /var/tmp/archive/ubuntutest-overrides
39 cacheroot: /var/tmp/archive/ubuntutest-cache40 cacheroot: /var/tmp/archive/ubuntutest-cache
40 miscroot: /var/tmp/archive/ubuntutest-misc41 miscroot: /var/tmp/archive/ubuntutest-misc
42 germinateroot: /var/tmp/archive/ubuntutest-germinate
41 temproot: /var/tmp/archive/ubuntutest-temp43 temproot: /var/tmp/archive/ubuntutest-temp
4244
4345
@@ -80,6 +82,7 @@
80 overrideroot: None82 overrideroot: None
81 cacheroot: None83 cacheroot: None
82 miscroot: None84 miscroot: None
85 germinateroot: None
83 temproot: /var/tmp/archive/ubuntutest-temp86 temproot: /var/tmp/archive/ubuntutest-temp
8487
85There is a separate location for private PPAs that is used if the88There is a separate location for private PPAs that is used if the
@@ -108,6 +111,7 @@
108 overrideroot: None111 overrideroot: None
109 cacheroot: None112 cacheroot: None
110 miscroot: None113 miscroot: None
114 germinateroot: None
111 temproot: /var/tmp/archive/ubuntutest-temp115 temproot: /var/tmp/archive/ubuntutest-temp
112116
113117
@@ -131,6 +135,7 @@
131 overrideroot: None135 overrideroot: None
132 cacheroot: None136 cacheroot: None
133 miscroot: None137 miscroot: None
138 germinateroot: None
134 temproot: /var/tmp/archive/ubuntutest-temp139 temproot: /var/tmp/archive/ubuntutest-temp
135140
136141
@@ -155,6 +160,7 @@
155 overrideroot: None160 overrideroot: None
156 cacheroot: None161 cacheroot: None
157 miscroot: None162 miscroot: None
163 germinateroot: None
158 temproot: /var/tmp/archive/ubuntutest-temp164 temproot: /var/tmp/archive/ubuntutest-temp
159165
160166
@@ -177,4 +183,5 @@
177 overrideroot: /var/tmp/archive/ubuntutest-rebuildtest99/ubuntutest-overrides183 overrideroot: /var/tmp/archive/ubuntutest-rebuildtest99/ubuntutest-overrides
178 cacheroot: /var/tmp/archive/ubuntutest-rebuildtest99/ubuntutest-cache184 cacheroot: /var/tmp/archive/ubuntutest-rebuildtest99/ubuntutest-cache
179 miscroot: /var/tmp/archive/ubuntutest-rebuildtest99/ubuntutest-misc185 miscroot: /var/tmp/archive/ubuntutest-rebuildtest99/ubuntutest-misc
186 germinateroot: /var/tmp/archive/ubuntutest-rebuildtest99/ubuntutest-germinate
180 temproot: /var/tmp/archive/ubuntutest-rebuildtest99/ubuntutest-temp187 temproot: /var/tmp/archive/ubuntutest-rebuildtest99/ubuntutest-temp
181188
=== added file 'lib/lp/archivepublisher/tests/test_generate_extra_overrides.py'
--- lib/lp/archivepublisher/tests/test_generate_extra_overrides.py 1970-01-01 00:00:00 +0000
+++ lib/lp/archivepublisher/tests/test_generate_extra_overrides.py 2011-12-14 15:49:40 +0000
@@ -0,0 +1,567 @@
1# Copyright 2011 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Test for the `generate-extra-overrides` script."""
5
6__metaclass__ = type
7
8import logging
9from optparse import OptionValueError
10import os
11import tempfile
12
13from germinate import (
14 archive,
15 germinator,
16 seeds,
17 )
18
19import transaction
20
21from canonical.testing.layers import (
22 LaunchpadZopelessLayer,
23 ZopelessDatabaseLayer,
24 )
25from lp.archivepublisher.scripts.generate_extra_overrides import (
26 AtomicFile,
27 GenerateExtraOverrides,
28 )
29from lp.archivepublisher.utils import RepositoryIndexFile
30from lp.registry.interfaces.pocket import PackagePublishingPocket
31from lp.registry.interfaces.series import SeriesStatus
32from lp.services.log.logger import DevNullLogger
33from lp.services.osutils import (
34 ensure_directory_exists,
35 open_for_writing,
36 )
37from lp.services.scripts.base import LaunchpadScriptFailure
38from lp.services.utils import file_exists
39from lp.soyuz.enums import PackagePublishingStatus
40from lp.testing import TestCaseWithFactory
41from lp.testing.faketransaction import FakeTransaction
42
43
44def file_contents(path):
45 """Return the contents of the file at path."""
46 with open(path) as handle:
47 return handle.read()
48
49
50class TestAtomicFile(TestCaseWithFactory):
51 """Tests for the AtomicFile helper class."""
52
53 layer = ZopelessDatabaseLayer
54
55 def test_atomic_file_creates_file(self):
56 # AtomicFile creates the named file with the requested contents.
57 self.useTempDir()
58 filename = self.factory.getUniqueString()
59 text = self.factory.getUniqueString()
60 with AtomicFile(filename) as test:
61 test.write(text)
62 self.assertEqual(text, file_contents(filename))
63
64 def test_atomic_file_removes_dot_new(self):
65 # AtomicFile does not leave .new files lying around.
66 self.useTempDir()
67 filename = self.factory.getUniqueString()
68 with AtomicFile(filename):
69 pass
70 self.assertFalse(file_exists("%s.new" % filename))
71
72
73class TestGenerateExtraOverrides(TestCaseWithFactory):
74 """Tests for the actual `GenerateExtraOverrides` script."""
75
76 layer = LaunchpadZopelessLayer
77
78 def setUp(self):
79 super(TestGenerateExtraOverrides, self).setUp()
80 self.seeddir = self.makeTemporaryDirectory()
81 # XXX cjwatson 2011-12-06 bug=694140: Make sure germinate doesn't
82 # lose its loggers between tests, due to Launchpad's messing with
83 # global log state.
84 archive._logger = logging.getLogger("germinate.archive")
85 germinator._logger = logging.getLogger("germinate.germinator")
86 seeds._logger = logging.getLogger("germinate.seeds")
87
88 def assertFilesEqual(self, expected_path, observed_path):
89 self.assertEqual(
90 file_contents(expected_path), file_contents(observed_path))
91
92 def makeDistro(self):
93 """Create a distribution for testing.
94
95 The distribution will have a root directory set up, which will
96 be cleaned up after the test. It will have an attached archive.
97 """
98 return self.factory.makeDistribution(
99 publish_root_dir=unicode(self.makeTemporaryDirectory()))
100
101 def makeScript(self, distribution, run_setup=True, extra_args=None):
102 """Create a script for testing."""
103 test_args = []
104 if distribution is not None:
105 test_args.extend(["-d", distribution.name])
106 if extra_args is not None:
107 test_args.extend(extra_args)
108 script = GenerateExtraOverrides(test_args=test_args)
109 script.logger = DevNullLogger()
110 script.txn = FakeTransaction()
111 if distribution is not None and run_setup:
112 script.setUp()
113 else:
114 script.distribution = distribution
115 return script
116
117 def makePackage(self, component, dases, **kwargs):
118 """Create a published source and binary package for testing."""
119 package = self.factory.makeDistributionSourcePackage(
120 distribution=dases[0].distroseries.distribution)
121 spph = self.factory.makeSourcePackagePublishingHistory(
122 distroseries=dases[0].distroseries,
123 pocket=PackagePublishingPocket.RELEASE,
124 status=PackagePublishingStatus.PUBLISHED,
125 sourcepackagename=package.name, component=component)
126 for das in dases:
127 build = self.factory.makeBinaryPackageBuild(
128 source_package_release=spph.sourcepackagerelease,
129 distroarchseries=das, processor=das.default_processor)
130 bpr = self.factory.makeBinaryPackageRelease(
131 binarypackagename=package.name, build=build,
132 component=component, **kwargs)
133 lfa = self.factory.makeLibraryFileAlias(
134 filename="%s.deb" % package.name)
135 transaction.commit()
136 bpr.addFile(lfa)
137 self.factory.makeBinaryPackagePublishingHistory(
138 binarypackagerelease=bpr, distroarchseries=das,
139 pocket=PackagePublishingPocket.RELEASE,
140 status=PackagePublishingStatus.PUBLISHED)
141 return package
142
143 def makeIndexFiles(self, script, distroseries):
144 """Create a limited subset of index files for testing."""
145 ensure_directory_exists(script.config.temproot)
146
147 for component in distroseries.components:
148 index_root = os.path.join(
149 script.config.distsroot, distroseries.name, component.name)
150
151 source_index_root = os.path.join(index_root, "source")
152 source_index = RepositoryIndexFile(
153 source_index_root, script.config.temproot, "Sources")
154 for spp in distroseries.getSourcePackagePublishing(
155 PackagePublishingStatus.PUBLISHED,
156 PackagePublishingPocket.RELEASE, component=component):
157 stanza = spp.getIndexStanza().encode("utf-8") + "\n\n"
158 source_index.write(stanza)
159 source_index.close()
160
161 for arch in distroseries.architectures:
162 package_index_root = os.path.join(
163 index_root, "binary-%s" % arch.architecturetag)
164 package_index = RepositoryIndexFile(
165 package_index_root, script.config.temproot, "Packages")
166 for bpp in distroseries.getBinaryPackagePublishing(
167 archtag=arch.architecturetag,
168 pocket=PackagePublishingPocket.RELEASE,
169 component=component):
170 stanza = bpp.getIndexStanza().encode("utf-8") + "\n\n"
171 package_index.write(stanza)
172 package_index.close()
173
174 def composeSeedPath(self, flavour, series_name, seed_name):
175 return os.path.join(
176 self.seeddir, "%s.%s" % (flavour, series_name), seed_name)
177
178 def makeSeedStructure(self, flavour, series_name, seed_names,
179 seed_inherit=None):
180 """Create a simple seed structure file."""
181 if seed_inherit is None:
182 seed_inherit = {}
183
184 structure_path = self.composeSeedPath(
185 flavour, series_name, "STRUCTURE")
186 with open_for_writing(structure_path, "w") as structure:
187 for seed_name in seed_names:
188 inherit = seed_inherit.get(seed_name, [])
189 line = "%s: %s" % (seed_name, " ".join(inherit))
190 print >>structure, line.strip()
191
192 def makeSeed(self, flavour, series_name, seed_name, entries,
193 headers=None):
194 """Create a simple seed file."""
195 seed_path = self.composeSeedPath(flavour, series_name, seed_name)
196 with open_for_writing(seed_path, "w") as seed:
197 if headers is not None:
198 for header in headers:
199 print >>seed, header
200 print >>seed
201 for entry in entries:
202 print >>seed, " * %s" % entry
203
204 def getTaskNameFromSeed(self, script, flavour, series_name, seed,
205 primary_flavour):
206 """Use script to parse a seed and return its task name."""
207 seed_path = self.composeSeedPath(flavour, series_name, seed)
208 with open(seed_path) as seed_text:
209 task_headers = script.parseTaskHeaders(seed_text)
210 return script.getTaskName(
211 task_headers, flavour, seed, primary_flavour)
212
213 def getTaskSeedsFromSeed(self, script, flavour, series_name, seed):
214 """Use script to parse a seed and return its task seed list."""
215 seed_path = self.composeSeedPath(flavour, series_name, seed)
216 with open(seed_path) as seed_text:
217 task_headers = script.parseTaskHeaders(seed_text)
218 return script.getTaskSeeds(task_headers, seed)
219
220 def test_name_is_consistent(self):
221 # Script instances for the same distro get the same name.
222 distro = self.factory.makeDistribution()
223 self.assertEqual(
224 GenerateExtraOverrides(test_args=["-d", distro.name]).name,
225 GenerateExtraOverrides(test_args=["-d", distro.name]).name)
226
227 def test_name_is_unique_for_each_distro(self):
228 # Script instances for different distros get different names.
229 self.assertNotEqual(
230 GenerateExtraOverrides(
231 test_args=["-d", self.factory.makeDistribution().name]).name,
232 GenerateExtraOverrides(
233 test_args=["-d", self.factory.makeDistribution().name]).name)
234
235 def test_requires_distro(self):
236 # The --distribution or -d argument is mandatory.
237 script = self.makeScript(None)
238 self.assertRaises(OptionValueError, script.processOptions)
239
240 def test_requires_real_distro(self):
241 # An incorrect distribution name is flagged as an invalid option
242 # value.
243 script = self.makeScript(
244 None, extra_args=["-d", self.factory.getUniqueString()])
245 self.assertRaises(OptionValueError, script.processOptions)
246
247 def test_looks_up_distro(self):
248 # The script looks up and keeps the distribution named on the
249 # command line.
250 distro = self.makeDistro()
251 self.factory.makeDistroSeries(distro)
252 script = self.makeScript(distro)
253 self.assertEqual(distro, script.distribution)
254
255 def test_prefers_development_distro_series(self):
256 # The script prefers a DEVELOPMENT series for the named
257 # distribution over CURRENT and SUPPORTED series.
258 distro = self.makeDistro()
259 self.factory.makeDistroSeries(distro, status=SeriesStatus.SUPPORTED)
260 self.factory.makeDistroSeries(distro, status=SeriesStatus.CURRENT)
261 development_distroseries = self.factory.makeDistroSeries(
262 distro, status=SeriesStatus.DEVELOPMENT)
263 script = self.makeScript(distro)
264 self.assertEqual(development_distroseries, script.series)
265
266 def test_permits_frozen_distro_series(self):
267 # If there is no DEVELOPMENT series, a FROZEN one will do.
268 distro = self.makeDistro()
269 self.factory.makeDistroSeries(distro, status=SeriesStatus.SUPPORTED)
270 self.factory.makeDistroSeries(distro, status=SeriesStatus.CURRENT)
271 frozen_distroseries = self.factory.makeDistroSeries(
272 distro, status=SeriesStatus.FROZEN)
273 script = self.makeScript(distro)
274 self.assertEqual(frozen_distroseries, script.series)
275
276 def test_requires_development_frozen_distro_series(self):
277 # If there is no DEVELOPMENT or FROZEN series, the script fails.
278 distro = self.makeDistro()
279 self.factory.makeDistroSeries(distro, status=SeriesStatus.SUPPORTED)
280 self.factory.makeDistroSeries(distro, status=SeriesStatus.CURRENT)
281 script = self.makeScript(distro, run_setup=False)
282 self.assertRaises(LaunchpadScriptFailure, script.processOptions)
283
284 def test_components_exclude_partner(self):
285 # If a 'partner' component exists, it is excluded.
286 distro = self.makeDistro()
287 distroseries = self.factory.makeDistroSeries(distro)
288 self.factory.makeComponentSelection(
289 distroseries=distroseries, component="main")
290 self.factory.makeComponentSelection(
291 distroseries=distroseries, component="partner")
292 script = self.makeScript(distro)
293 self.assertEqual(["main"], script.components)
294
295 def test_compose_output_path_in_germinateroot(self):
296 # Output files are written to the correct locations under
297 # germinateroot.
298 distro = self.makeDistro()
299 distroseries = self.factory.makeDistroSeries(distro)
300 script = self.makeScript(distro)
301 flavour = self.factory.getUniqueString()
302 arch = self.factory.getUniqueString()
303 base = self.factory.getUniqueString()
304 output = script.composeOutputPath(
305 flavour, distroseries.name, arch, base)
306 self.assertEqual(
307 "%s/%s_%s_%s_%s" % (
308 script.config.germinateroot, base, flavour, distroseries.name,
309 arch),
310 output)
311
312 def fetchGerminatedOverrides(self, script, series_name, arch, flavours):
313 """Helper to call script.germinateArch and return overrides."""
314 structures = script.makeSeedStructures(
315 series_name, flavours, seed_bases=["file://%s" % self.seeddir])
316
317 override_fd, override_path = tempfile.mkstemp()
318 with os.fdopen(override_fd, "w") as override_file:
319 script.germinateArch(
320 override_file, series_name, arch, flavours, structures)
321 return file_contents(override_path).splitlines()
322
323 def test_germinate_output(self):
324 # A single call to germinateArch produces output for all flavours on
325 # one architecture.
326 distro = self.makeDistro()
327 distroseries = self.factory.makeDistroSeries(distribution=distro)
328 series_name = distroseries.name
329 component = self.factory.makeComponent()
330 self.factory.makeComponentSelection(
331 distroseries=distroseries, component=component)
332 das = self.factory.makeDistroArchSeries(distroseries=distroseries)
333 arch = das.architecturetag
334 one = self.makePackage(component, [das])
335 two = self.makePackage(component, [das])
336 script = self.makeScript(distro)
337 self.makeIndexFiles(script, distroseries)
338
339 flavour_one = self.factory.getUniqueString()
340 flavour_two = self.factory.getUniqueString()
341 seed = self.factory.getUniqueString()
342 self.makeSeedStructure(flavour_one, series_name, [seed])
343 self.makeSeed(flavour_one, series_name, seed, [one.name])
344 self.makeSeedStructure(flavour_two, series_name, [seed])
345 self.makeSeed(flavour_two, series_name, seed, [two.name])
346
347 overrides = self.fetchGerminatedOverrides(
348 script, series_name, arch, [flavour_one, flavour_two])
349 self.assertEqual([], overrides)
350
351 seed_dir_one = os.path.join(
352 self.seeddir, "%s.%s" % (flavour_one, series_name))
353 self.assertFilesEqual(
354 os.path.join(seed_dir_one, "STRUCTURE"),
355 script.composeOutputPath(
356 flavour_one, series_name, arch, "structure"))
357 self.assertTrue(file_exists(script.composeOutputPath(
358 flavour_one, series_name, arch, "all")))
359 self.assertTrue(file_exists(script.composeOutputPath(
360 flavour_one, series_name, arch, "all.sources")))
361 self.assertTrue(file_exists(script.composeOutputPath(
362 flavour_one, series_name, arch, seed)))
363
364 seed_dir_two = os.path.join(
365 self.seeddir, "%s.%s" % (flavour_two, series_name))
366 self.assertFilesEqual(
367 os.path.join(seed_dir_two, "STRUCTURE"),
368 script.composeOutputPath(
369 flavour_two, series_name, arch, "structure"))
370 self.assertTrue(file_exists(script.composeOutputPath(
371 flavour_two, series_name, arch, "all")))
372 self.assertTrue(file_exists(script.composeOutputPath(
373 flavour_two, series_name, arch, "all.sources")))
374 self.assertTrue(file_exists(script.composeOutputPath(
375 flavour_two, series_name, arch, seed)))
376
377 def test_germinate_output_task(self):
378 # germinateArch produces Task extra overrides.
379 distro = self.makeDistro()
380 distroseries = self.factory.makeDistroSeries(distribution=distro)
381 series_name = distroseries.name
382 component = self.factory.makeComponent()
383 self.factory.makeComponentSelection(
384 distroseries=distroseries, component=component)
385 das = self.factory.makeDistroArchSeries(distroseries=distroseries)
386 arch = das.architecturetag
387 one = self.makePackage(component, [das])
388 two = self.makePackage(component, [das], depends=one.name)
389 three = self.makePackage(component, [das])
390 self.makePackage(component, [das])
391 script = self.makeScript(distro)
392 self.makeIndexFiles(script, distroseries)
393
394 flavour = self.factory.getUniqueString()
395 seed_one = self.factory.getUniqueString()
396 seed_two = self.factory.getUniqueString()
397 self.makeSeedStructure(flavour, series_name, [seed_one, seed_two])
398 self.makeSeed(
399 flavour, series_name, seed_one, [two.name],
400 headers=["Task-Description: one"])
401 self.makeSeed(
402 flavour, series_name, seed_two, [three.name],
403 headers=["Task-Description: two"])
404
405 overrides = self.fetchGerminatedOverrides(
406 script, series_name, arch, [flavour])
407 expected_overrides = [
408 "%s/%s Task %s" % (one.name, arch, seed_one),
409 "%s/%s Task %s" % (two.name, arch, seed_one),
410 "%s/%s Task %s" % (three.name, arch, seed_two),
411 ]
412 self.assertContentEqual(expected_overrides, overrides)
413
414 def test_task_name(self):
415 # The Task-Name field is honoured.
416 series_name = self.factory.getUniqueString()
417 package = self.factory.getUniqueString()
418 script = self.makeScript(None)
419
420 flavour = self.factory.getUniqueString()
421 seed = self.factory.getUniqueString()
422 task = self.factory.getUniqueString()
423 self.makeSeed(
424 flavour, series_name, seed, [package],
425 headers=["Task-Name: %s" % task])
426
427 observed_task = self.getTaskNameFromSeed(
428 script, flavour, series_name, seed, True)
429 self.assertEqual(task, observed_task)
430
431 def test_task_per_derivative(self):
432 # The Task-Per-Derivative field is honoured.
433 series_name = self.factory.getUniqueString()
434 package = self.factory.getUniqueString()
435 script = self.makeScript(None)
436
437 flavour_one = self.factory.getUniqueString()
438 flavour_two = self.factory.getUniqueString()
439 seed_one = self.factory.getUniqueString()
440 seed_two = self.factory.getUniqueString()
441 self.makeSeed(
442 flavour_one, series_name, seed_one, [package],
443 headers=["Task-Description: one"])
444 self.makeSeed(
445 flavour_one, series_name, seed_two, [package],
446 headers=["Task-Per-Derivative: 1"])
447 self.makeSeed(
448 flavour_two, series_name, seed_one, [package],
449 headers=["Task-Description: one"])
450 self.makeSeed(
451 flavour_two, series_name, seed_two, [package],
452 headers=["Task-Per-Derivative: 1"])
453
454 observed_task_one_one = self.getTaskNameFromSeed(
455 script, flavour_one, series_name, seed_one, True)
456 observed_task_one_two = self.getTaskNameFromSeed(
457 script, flavour_one, series_name, seed_two, True)
458 observed_task_two_one = self.getTaskNameFromSeed(
459 script, flavour_two, series_name, seed_one, False)
460 observed_task_two_two = self.getTaskNameFromSeed(
461 script, flavour_two, series_name, seed_two, False)
462
463 # seed_one is not per-derivative, so it is honoured only for
464 # flavour_one and has a global name.
465 self.assertEqual(seed_one, observed_task_one_one)
466 self.assertIsNone(observed_task_two_one)
467
468 # seed_two is per-derivative, so it is honoured for both flavours
469 # and has the flavour name prefixed.
470 self.assertEqual(
471 "%s-%s" % (flavour_one, seed_two), observed_task_one_two)
472 self.assertEqual(
473 "%s-%s" % (flavour_two, seed_two), observed_task_two_two)
474
475 def test_task_seeds(self):
476 # The Task-Seeds field is honoured.
477 series_name = self.factory.getUniqueString()
478 one = self.getUniqueString()
479 two = self.getUniqueString()
480 script = self.makeScript(None)
481
482 flavour = self.factory.getUniqueString()
483 seed_one = self.factory.getUniqueString()
484 seed_two = self.factory.getUniqueString()
485 self.makeSeed(flavour, series_name, seed_one, [one])
486 self.makeSeed(
487 flavour, series_name, seed_two, [two],
488 headers=["Task-Seeds: %s" % seed_one])
489
490 task_seeds = self.getTaskSeedsFromSeed(
491 script, flavour, series_name, seed_two)
492 self.assertContentEqual([seed_one, seed_two], task_seeds)
493
494 def test_germinate_output_build_essential(self):
495 # germinateArch produces Build-Essential extra overrides.
496 distro = self.makeDistro()
497 distroseries = self.factory.makeDistroSeries(distribution=distro)
498 series_name = distroseries.name
499 component = self.factory.makeComponent()
500 self.factory.makeComponentSelection(
501 distroseries=distroseries, component=component)
502 das = self.factory.makeDistroArchSeries(distroseries=distroseries)
503 arch = das.architecturetag
504 package = self.makePackage(component, [das])
505 script = self.makeScript(distro)
506 self.makeIndexFiles(script, distroseries)
507
508 flavour = self.factory.getUniqueString()
509 seed = "build-essential"
510 self.makeSeedStructure(flavour, series_name, [seed])
511 self.makeSeed(flavour, series_name, seed, [package.name])
512
513 overrides = self.fetchGerminatedOverrides(
514 script, series_name, arch, [flavour])
515 self.assertContentEqual(
516 ["%s/%s Build-Essential yes" % (package.name, arch)], overrides)
517
518 def test_main(self):
519 # If run end-to-end, the script generates override files containing
520 # output for all architectures, and sends germinate's log output to
521 # a file.
522 distro = self.makeDistro()
523 distroseries = self.factory.makeDistroSeries(distribution=distro)
524 series_name = distroseries.name
525 component = self.factory.makeComponent()
526 self.factory.makeComponentSelection(
527 distroseries=distroseries, component=component)
528 das_one = self.factory.makeDistroArchSeries(distroseries=distroseries)
529 arch_one = das_one.architecturetag
530 das_two = self.factory.makeDistroArchSeries(distroseries=distroseries)
531 arch_two = das_two.architecturetag
532 package = self.makePackage(component, [das_one, das_two])
533 flavour = self.factory.getUniqueString()
534 script = self.makeScript(distro, extra_args=[flavour])
535 self.makeIndexFiles(script, distroseries)
536
537 seed = self.factory.getUniqueString()
538 self.makeSeedStructure(flavour, series_name, [seed])
539 self.makeSeed(
540 flavour, series_name, seed, [package.name],
541 headers=["Task-Description: task"])
542
543 script.process(seed_bases=["file://%s" % self.seeddir])
544 override_path = os.path.join(
545 script.config.miscroot,
546 "more-extra.override.%s.main" % series_name)
547 expected_overrides = [
548 "%s/%s Task %s" % (package.name, arch_one, seed),
549 "%s/%s Task %s" % (package.name, arch_two, seed),
550 ]
551 self.assertContentEqual(
552 expected_overrides, file_contents(override_path).splitlines())
553
554 log_file = os.path.join(
555 script.config.germinateroot, "germinate.output")
556 self.assertIn("Downloading file://", file_contents(log_file))
557
558 def test_run_script(self):
559 # The script will run stand-alone.
560 from canonical.launchpad.scripts.tests import run_script
561 distro = self.makeDistro()
562 self.factory.makeDistroSeries(distro)
563 transaction.commit()
564 retval, out, err = run_script(
565 "cronscripts/generate-extra-overrides.py",
566 ["-d", distro.name, "-q"])
567 self.assertEqual(0, retval)
0568
=== removed file 'lib/lp/soyuz/scripts/tests/germinate-test-data/mock-bin/germinate'
--- lib/lp/soyuz/scripts/tests/germinate-test-data/mock-bin/germinate 2010-10-20 13:23:05 +0000
+++ lib/lp/soyuz/scripts/tests/germinate-test-data/mock-bin/germinate 1970-01-01 00:00:00 +0000
@@ -1,5 +0,0 @@
1#!/bin/sh
2#
3# This is a mock germinate script that just produces enough (empty)
4# files so that the cron.germinate shell script can run.
5touch structure all all.sources minimal standard
60
=== added file 'lib/lp/soyuz/scripts/tests/germinate-test-data/mock-lp-root/cronscripts/generate-extra-overrides.py'
--- lib/lp/soyuz/scripts/tests/germinate-test-data/mock-lp-root/cronscripts/generate-extra-overrides.py 1970-01-01 00:00:00 +0000
+++ lib/lp/soyuz/scripts/tests/germinate-test-data/mock-lp-root/cronscripts/generate-extra-overrides.py 2011-12-14 15:49:40 +0000
@@ -0,0 +1,5 @@
1#! /usr/bin/python
2#
3# We don't need to run generate-extra-overrides.py when testing
4# maintenance-check.py. We could, but it would slow down the tests and its
5# output is not interesting here.
06
=== modified file 'lib/lp/soyuz/scripts/tests/test_cron_germinate.py'
--- lib/lp/soyuz/scripts/tests/test_cron_germinate.py 2011-01-19 17:49:10 +0000
+++ lib/lp/soyuz/scripts/tests/test_cron_germinate.py 2011-12-14 15:49:40 +0000
@@ -148,10 +148,8 @@
148 os.path.join(archive_dir, "ubuntu"))148 os.path.join(archive_dir, "ubuntu"))
149 fake_environ["TEST_LAUNCHPADROOT"] = os.path.abspath(149 fake_environ["TEST_LAUNCHPADROOT"] = os.path.abspath(
150 os.path.join(basepath, "germinate-test-data/mock-lp-root"))150 os.path.join(basepath, "germinate-test-data/mock-lp-root"))
151 # Set the PATH in the fake environment so that our mock germinate151 # Set the PATH in the fake environment so that our mock lockfile is
152 # is used. We could use the real germinate as well, but that will152 # used.
153 # slow down the tests a lot and its also not interessting for this
154 # test as we do not use any of the germinate information.
155 fake_environ["PATH"] = "%s:%s" % (153 fake_environ["PATH"] = "%s:%s" % (
156 os.path.abspath(os.path.join(154 os.path.abspath(os.path.join(
157 basepath, "germinate-test-data/mock-bin")),155 basepath, "germinate-test-data/mock-bin")),