Merge lp:~jtv/launchpad/bug-499405-translationtemplates-buildmanager into lp:launchpad

Proposed by Jeroen T. Vermeulen
Status: Merged
Approved by: Jeroen T. Vermeulen
Approved revision: not available
Merged at revision: not available
Proposed branch: lp:~jtv/launchpad/bug-499405-translationtemplates-buildmanager
Merge into: lp:launchpad
Diff against target: 591 lines (+473/-7)
10 files modified
daemons/buildd-slave.tac (+2/-0)
lib/canonical/buildd/debian/rules (+2/-2)
lib/canonical/buildd/generate_translation_templates.py (+57/-0)
lib/canonical/buildd/slave.py (+9/-3)
lib/canonical/buildd/test_buildd_generatetranslationtemplates (+33/-0)
lib/canonical/buildd/tests/harness.py (+12/-2)
lib/canonical/buildd/tests/test_generate_translation_templates.py (+60/-0)
lib/canonical/buildd/tests/test_translationtemplatesbuildmanager.py (+133/-0)
lib/canonical/buildd/translationtemplates.py (+159/-0)
lib/lp/testing/__init__.py (+6/-0)
To merge this branch: bzr merge lp:~jtv/launchpad/bug-499405-translationtemplates-buildmanager
Reviewer Review Type Date Requested Status
Paul Hummer (community) code Approve
Canonical Launchpad Engineering code Pending
Review via email: mp+17811@code.launchpad.net

Commit message

BuildManager for generating translation templates.

To post a comment you must log in.
Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :
Download full text (3.4 KiB)

= Bug 499405 =

This branch implements a build manager for generating translation templates from a branch. It provides no real functionality on its own; rather it's part of an ongoing project. Pre-imp discussions were done IRL at the Wellington build-farm generalisation sprint.

A build manager is the piece of code that runs on a build slave to coordinate its work. It iterates through a finite-state machine describing the job to be done. An important thing to bear in mind is that this is not "Launchpad code": it runs on a slave machine that has neither the Launchpad modules nor access to the database.

For this particular job, the finite-state machine consists of 7 states:
1. INIT: Initialize.
2. UNPACK: Create a chroot directory by unpacking a chroot tarball.
3. MOUNT: Set up a realistic system environment inside the chroot.
4. UPDATE: Get the latest versions of the packages installed inside the chroot.
5. INSTALL: Add a few job-specific packages to the chroot.
6. GENERATE: Check out the branch and produce templates based on it.
7. CLEANUP: Return the slave to its original, clean state.

Of these, step 6 is the "payload." It invokes a separate script, also found in this branch, that checks out a branch and will eventually generate templates inside it. The script is run under a non-privileged user inside the slave's chroot, providing maximum insulation from untrusted code inside the branches.

The implementations for most of the other steps are inherited from the existing BuildSlave base class.

Testing the actual shell commands is rather hard, which is why other build managers seem to go largely untested. I did find a way to unit-test at least the iteration of the state machine: mock up the BuildManager method that normally forks off an action of the state machine and hands it to twistd. Instead of running the action and triggering a state transition in twistd, the test just logs and verifies the command that would have been run in a sub-process.

In order to make this testable, I had to make just one small change in the base class. It made changes to the filesystem in the current user's home directory. Instead, I made it record the current user's home directory in its constructor; the other methods now get the location of the home directory from that same field. This allows the test to substitute a temporary directory of its own.

I also added a manual test to kick off a templates generation build through XMLRPC. I'm told this can't be run as part of the normal test suite, but it can be run manually to see if anything breaks spectacularly. It's a blatant ripoff of a similar test that abentley implemented for his own build-manager class. You "make run" in one shell then while that's active, run the test script in another with one argument: the SHA1 of a chroot tarball that's in your local Librarian.

Finally, you'll notice a helper method makeTemporaryDirectory in the Launchpad version of the TestCase class. This breaks naming convention with the existing useTempDir, but matches the convention of both a similar method in bzr test cases and the naming style in the standard tempfile module. I did update useTempDir to make use o...

Read more...

Revision history for this message
Paul Hummer (rockstar) wrote :
Download full text (5.0 KiB)

Hi Jeroen-

  This branch looks good. I do have some questions though, and would like to have answers before I Approve this branch.

=== added file 'lib/canonical/buildd/generate_translation_templates.py'
--- lib/canonical/buildd/generate_translation_templates.py 1970-01-01 00:00:00 +0000
+++ lib/canonical/buildd/generate_translation_templates.py 2010-01-21 12:50:24 +0000
@@ -0,0 +1,57 @@
+#! /usr/bin/python2.5
+# Copyright 2010 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+__metaclass__ = type
+
+import os.path
+import sys
+from subprocess import call
+
+
+class GenerateTranslationTemplates:
+ """Script to generate translation templates from a branch."""
+
+ def __init__(self, branch_spec):
+ """Prepare to generate templates for a branch.
+
+ :param branch_spec: Either a branch URL or the path of a local
+ branch. URLs are recognized by the occurrence of ':'. In
+ the case of a URL, this will make up a path for the branch
+ and check out the branch to there.
+ """
+ self.home = os.environ['HOME']
+ self.branch_spec = branch_spec
+
+ def _getBranch(self):
+ """Set `self.branch_dir`, and check out branch if needed."""
+ if ':' in self.branch_spec:
+ # This is a branch URL. Check out the branch.
+ self.branch_dir = os.path.join(self.home, 'source-tree')
+ self._checkout(self.branch_spec)
+ else:
+ # This is a local filesystem path. Use the branch in-place.
+ self.branch_dir = self.branch_spec
+
+ def _checkout(self, branch_url):
+ """Check out a source branch to generate from.
+
+ The branch is checked out to the location specified by
+ `self.branch_dir`.
+ """
+ command = ['/usr/bin/bzr', 'checkout', branch_url, self.branch_dir]
+ return call(command, cwd=self.home)

Is there a reason you're not using bzrlib? I know that the build slaves have special security there, but if you have bzr there, you have bzrlib.

+
+ def generate(self):
+ """Do It. Generate templates."""
+ self._getBranch()
+ # XXX JeroenVermeulen 2010-01-19 bug=509557: Actual payload goes here.
+ return 0
+

Does this mean that this is basically a no-op right now? I'm okay with this code, but I'd rather you wait for it to actually do something than land it now.

+
+if __name__ == '__main__':
+ if len(sys.argv) != 2:
+ print "Usage: %s branch" % sys.argv[0]
+ print "Where 'branch' is a branch URL or directory."
+ sys.exit(1)
+ sys.exit(GenerateTranslationTemplates(sys.argv[1]).generate())

=== added file 'lib/canonical/buildd/tests/test_generate_translation_templates.py'
--- lib/canonical/buildd/tests/test_generate_translation_templates.py 1970-01-01 00:00:00 +0000
+++ lib/canonical/buildd/tests/test_generate_translation_templates.py 2010-01-21 12:50:24 +0000
@@ -0,0 +1,57 @@
+# Copyright 2010 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+import os
+from unitte...

Read more...

review: Needs Information
Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :
Download full text (6.1 KiB)

> This branch looks good. I do have some questions though, and would like to
> have answers before I Approve this branch.

Thanks for the review—glad I could get someone who was involved in the sprint!

> === added file 'lib/canonical/buildd/generate_translation_templates.py'
> --- lib/canonical/buildd/generate_translation_templates.py 1970-01-01
> 00:00:00 +0000
> +++ lib/canonical/buildd/generate_translation_templates.py 2010-01-21
> 12:50:24 +0000
> @@ -0,0 +1,57 @@
> +#! /usr/bin/python2.5
> +# Copyright 2010 Canonical Ltd. This software is licensed under the
> +# GNU Affero General Public License version 3 (see the file LICENSE).
> +
> +__metaclass__ = type
> +
> +import os.path
> +import sys
> +from subprocess import call
> +
> +
> +class GenerateTranslationTemplates:
> + """Script to generate translation templates from a branch."""
> +
> + def __init__(self, branch_spec):
> + """Prepare to generate templates for a branch.
> +
> + :param branch_spec: Either a branch URL or the path of a local
> + branch. URLs are recognized by the occurrence of ':'. In
> + the case of a URL, this will make up a path for the branch
> + and check out the branch to there.
> + """
> + self.home = os.environ['HOME']
> + self.branch_spec = branch_spec
> +
> + def _getBranch(self):
> + """Set `self.branch_dir`, and check out branch if needed."""
> + if ':' in self.branch_spec:
> + # This is a branch URL. Check out the branch.
> + self.branch_dir = os.path.join(self.home, 'source-tree')
> + self._checkout(self.branch_spec)
> + else:
> + # This is a local filesystem path. Use the branch in-place.
> + self.branch_dir = self.branch_spec
> +
> + def _checkout(self, branch_url):
> + """Check out a source branch to generate from.
> +
> + The branch is checked out to the location specified by
> + `self.branch_dir`.
> + """
> + command = ['/usr/bin/bzr', 'checkout', branch_url, self.branch_dir]
> + return call(command, cwd=self.home)
>
> Is there a reason you're not using bzrlib? I know that the build slaves have
> special security there, but if you have bzr there, you have bzrlib.

Mainly laziness. But also, this being a corner of LP that's hard to test, I would like to minimize the risk of silly little mistakes that might not be reported back to me very effectively. Instead of writing my own python, I'm producing a command line here that I can just replicate in a shell when debugging.

By the way, I'm replacing the "checkout" with "export." All I need is the stored files; actual revision control is just useless baggage for this particular job.

> + def generate(self):
> + """Do It. Generate templates."""
> + self._getBranch()
> + # XXX JeroenVermeulen 2010-01-19 bug=509557: Actual payload goes
> here.
> + return 0
> +
>
> Does this mean that this is basically a no-op right now? I'm okay with this
> code, but I'd rather you wait for it to actually do something than land it
> now.

This is a pretty big project, so I'd...

Read more...

Revision history for this message
Paul Hummer (rockstar) wrote :

>> + def _checkout(self, branch_url):
>> + """Check out a source branch to generate from.
>> +
>> + The branch is checked out to the location specified by
>> + `self.branch_dir`.
>> + """
>> + command = ['/usr/bin/bzr', 'checkout', branch_url, self.branch_dir]
>> + return call(command, cwd=self.home)
>>
>> Is there a reason you're not using bzrlib? I know that the build slaves have
>> special security there, but if you have bzr there, you have bzrlib.
>
>Mainly laziness. But also, this being a corner of LP that's hard to test, I would >like to minimize the risk of silly little mistakes that might not be reported back to >me very effectively. Instead of writing my own python, I'm producing a command line >here that I can just replicate in a shell when debugging.

So there are a few issues with calling the bzr runtime instead of using bzrlib. The first is that we do not want to use the system bzr. Launchpad packages a local copy of bzr for this very purpose. The second is that we aren't guaranteed we'll have the plugins we need on the system, but we make sure they're available to bzrlib. There are also performance advantages to using bzrlib specifically. I really think that bzrlib needs to be used in this situation.

Other than that, I'm happy with your other changes.

review: Needs Fixing (code)
Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :

> So there are a few issues with calling the bzr runtime instead of using
> bzrlib. The first is that we do not want to use the system bzr. Launchpad
> packages a local copy of bzr for this very purpose. The second is that we
> aren't guaranteed we'll have the plugins we need on the system, but we make
> sure they're available to bzrlib. There are also performance advantages to
> using bzrlib specifically. I really think that bzrlib needs to be used in
> this situation.
>
> Other than that, I'm happy with your other changes.

This code runs on the slave, so Launchpad is not present. I think that also eliminates a lot of other complications there might otherwise be on the system.

Revision history for this message
Paul Hummer (rockstar) wrote :

I conferenced with abentley about this, so it's not just me saying "please use bzrlib" but also another code folk. If nothing else, it gives us a nice traceback, instead of a user friendly (and rather unhelpful) error message. It basically runs bzr as a subprocess, which could actually make things more complicated.

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

Alright then... I've replaced the code with an XXX (referring to a bug, of course) to say that in future the code needs to do the equivalent of "bzr export" using bzrlib.

Revision history for this message
Paul Hummer (rockstar) :
review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'daemons/buildd-slave.tac'
--- daemons/buildd-slave.tac 2010-01-13 23:16:43 +0000
+++ daemons/buildd-slave.tac 2010-02-18 14:41:20 +0000
@@ -12,6 +12,7 @@
12from canonical.buildd.binarypackage import BinaryPackageBuildManager12from canonical.buildd.binarypackage import BinaryPackageBuildManager
13from canonical.buildd.sourcepackagerecipe import (13from canonical.buildd.sourcepackagerecipe import (
14 SourcePackageRecipeBuildManager)14 SourcePackageRecipeBuildManager)
15from canonical.buildd.translationtemplates import TranslationTemplatesBuildManager
15from canonical.launchpad.daemons import tachandler16from canonical.launchpad.daemons import tachandler
1617
17from twisted.web import server, resource, static18from twisted.web import server, resource, static
@@ -29,6 +30,7 @@
29slave.registerBuilder(BinaryPackageBuildManager, "debian")30slave.registerBuilder(BinaryPackageBuildManager, "debian")
30slave.registerBuilder(BinaryPackageBuildManager, "binarypackage")31slave.registerBuilder(BinaryPackageBuildManager, "binarypackage")
31slave.registerBuilder(SourcePackageRecipeBuildManager, "sourcepackagerecipe")32slave.registerBuilder(SourcePackageRecipeBuildManager, "sourcepackagerecipe")
33slave.registerBuilder(TranslationTemplatesBuildManager, 'translation-templates')
3234
33application = service.Application('BuildDSlave')35application = service.Application('BuildDSlave')
34builddslaveService = service.IServiceCollection(application)36builddslaveService = service.IServiceCollection(application)
3537
=== modified file 'lib/canonical/buildd/debian/rules'
--- lib/canonical/buildd/debian/rules 2010-01-13 23:16:43 +0000
+++ lib/canonical/buildd/debian/rules 2010-02-18 14:41:20 +0000
@@ -21,10 +21,10 @@
21targetshare = $(target)/usr/share/launchpad-buildd21targetshare = $(target)/usr/share/launchpad-buildd
22pytarget = $(targetshare)/canonical/buildd22pytarget = $(targetshare)/canonical/buildd
2323
24pyfiles = debian.py slave.py binarypackage.py utils.py __init__.py sourcepackagerecipe.py24pyfiles = debian.py slave.py binarypackage.py utils.py __init__.py sourcepackagerecipe.py translationtemplates.py
25slavebins = unpack-chroot mount-chroot update-debian-chroot sbuild-package \25slavebins = unpack-chroot mount-chroot update-debian-chroot sbuild-package \
26scan-for-processes umount-chroot remove-build apply-ogre-model \26scan-for-processes umount-chroot remove-build apply-ogre-model \
27override-sources-list buildrecipe27override-sources-list buildrecipe generate_translation_templates.py
2828
29BUILDDUID=6550029BUILDDUID=65500
30BUILDDGID=6550030BUILDDGID=65500
3131
=== added file 'lib/canonical/buildd/generate_translation_templates.py'
--- lib/canonical/buildd/generate_translation_templates.py 1970-01-01 00:00:00 +0000
+++ lib/canonical/buildd/generate_translation_templates.py 2010-02-18 14:41:20 +0000
@@ -0,0 +1,57 @@
1#! /usr/bin/python2.5
2# Copyright 2010 Canonical Ltd. This software is licensed under the
3# GNU Affero General Public License version 3 (see the file LICENSE).
4
5__metaclass__ = type
6
7import os.path
8import sys
9from subprocess import call
10
11
12class GenerateTranslationTemplates:
13 """Script to generate translation templates from a branch."""
14
15 def __init__(self, branch_spec):
16 """Prepare to generate templates for a branch.
17
18 :param branch_spec: Either a branch URL or the path of a local
19 branch. URLs are recognized by the occurrence of ':'. In
20 the case of a URL, this will make up a path for the branch
21 and check out the branch to there.
22 """
23 self.home = os.environ['HOME']
24 self.branch_spec = branch_spec
25
26 def _getBranch(self):
27 """Set `self.branch_dir`, and check out branch if needed."""
28 if ':' in self.branch_spec:
29 # This is a branch URL. Check out the branch.
30 self.branch_dir = os.path.join(self.home, 'source-tree')
31 self._checkout(self.branch_spec)
32 else:
33 # This is a local filesystem path. Use the branch in-place.
34 self.branch_dir = self.branch_spec
35
36 def _checkout(self, branch_url):
37 """Check out a source branch to generate from.
38
39 The branch is checked out to the location specified by
40 `self.branch_dir`.
41 """
42 # XXX JeroenVermeulen 2010-02-11 bug=520651: Read the contents
43 # of the branch using bzrlib.
44
45 def generate(self):
46 """Do It. Generate templates."""
47 self._getBranch()
48 # XXX JeroenVermeulen 2010-01-19 bug=509557: Actual payload goes here.
49 return 0
50
51
52if __name__ == '__main__':
53 if len(sys.argv) != 2:
54 print "Usage: %s branch" % sys.argv[0]
55 print "Where 'branch' is a branch URL or directory."
56 sys.exit(1)
57 sys.exit(GenerateTranslationTemplates(sys.argv[1]).generate())
058
=== modified file 'lib/canonical/buildd/slave.py'
--- lib/canonical/buildd/slave.py 2010-02-09 00:17:40 +0000
+++ lib/canonical/buildd/slave.py 2010-02-18 14:41:20 +0000
@@ -96,6 +96,11 @@
96 """Build Daemon slave build manager abstract parent"""96 """Build Daemon slave build manager abstract parent"""
9797
98 def __init__(self, slave, buildid):98 def __init__(self, slave, buildid):
99 """Create a BuildManager.
100
101 :param slave: A `BuildDSlave`.
102 :param buildid: Identifying string for this build.
103 """
99 object.__init__(self)104 object.__init__(self)
100 self._buildid = buildid105 self._buildid = buildid
101 self._slave = slave106 self._slave = slave
@@ -104,6 +109,7 @@
104 self._mountpath = slave._config.get("allmanagers", "mountpath")109 self._mountpath = slave._config.get("allmanagers", "mountpath")
105 self._umountpath = slave._config.get("allmanagers", "umountpath")110 self._umountpath = slave._config.get("allmanagers", "umountpath")
106 self.is_archive_private = False111 self.is_archive_private = False
112 self.home = os.environ['HOME']
107113
108 def runSubProcess(self, command, args):114 def runSubProcess(self, command, args):
109 """Run a sub process capturing the results in the log."""115 """Run a sub process capturing the results in the log."""
@@ -112,7 +118,7 @@
112 childfds = {0: devnull.fileno(), 1: "r", 2: "r"}118 childfds = {0: devnull.fileno(), 1: "r", 2: "r"}
113 reactor.spawnProcess(119 reactor.spawnProcess(
114 self._subprocess, command, args, env=os.environ,120 self._subprocess, command, args, env=os.environ,
115 path=os.environ["HOME"], childFDs=childfds)121 path=self.home, childFDs=childfds)
116122
117 def doUnpack(self):123 def doUnpack(self):
118 """Unpack the build chroot."""124 """Unpack the build chroot."""
@@ -146,10 +152,10 @@
146 value keyed under the 'archive_private' string. If that value152 value keyed under the 'archive_private' string. If that value
147 evaluates to True the build at hand is for a private archive.153 evaluates to True the build at hand is for a private archive.
148 """154 """
149 os.mkdir("%s/build-%s" % (os.environ["HOME"], self._buildid))155 os.mkdir("%s/build-%s" % (self.home, self._buildid))
150 for f in files:156 for f in files:
151 os.symlink( self._slave.cachePath(files[f]),157 os.symlink( self._slave.cachePath(files[f]),
152 "%s/build-%s/%s" % (os.environ["HOME"],158 "%s/build-%s/%s" % (self.home,
153 self._buildid, f))159 self._buildid, f))
154 self._chroottarfile = self._slave.cachePath(chroot)160 self._chroottarfile = self._slave.cachePath(chroot)
155161
156162
=== added file 'lib/canonical/buildd/test_buildd_generatetranslationtemplates'
--- lib/canonical/buildd/test_buildd_generatetranslationtemplates 1970-01-01 00:00:00 +0000
+++ lib/canonical/buildd/test_buildd_generatetranslationtemplates 2010-02-18 14:41:20 +0000
@@ -0,0 +1,33 @@
1#!/usr/bin/env python
2# Copyright 2010 Canonical Ltd. This software is licensed under the
3# GNU Affero General Public License version 3 (see the file LICENSE).
4#
5# Test script for manual use only. Exercises the
6# TranslationTemplatesBuildManager through XMLRPC.
7
8import sys
9
10from xmlrpclib import ServerProxy
11
12if len(sys.argv) != 2:
13 print "Usage: %s <chroot_sha1>" % sys.argv[0]
14 print "Where <chroot_sha1> is the SHA1 of the chroot tarball to use."
15 print "The chroot tarball must be in the local Librarian."
16 print "See https://dev.launchpad.net/Soyuz/HowToUseSoyuzLocally"
17 sys.exit(1)
18
19chroot_sha1 = sys.argv[1]
20
21proxy = ServerProxy('http://localhost:8221/rpc')
22print proxy.info()
23print proxy.status()
24buildid = '1-2'
25build_type = 'translation-templates'
26filemap = {}
27args = {'branch_url': 'no-branch-here-sorry'}
28print proxy.build(buildid, build_type, chroot_sha1, filemap, args)
29#status = proxy.status()
30#for filename, sha1 in status[3].iteritems():
31# print filename
32#proxy.clean()
33
034
=== modified file 'lib/canonical/buildd/tests/harness.py'
--- lib/canonical/buildd/tests/harness.py 2010-02-01 14:55:03 +0000
+++ lib/canonical/buildd/tests/harness.py 2010-02-18 14:41:20 +0000
@@ -79,8 +79,18 @@
79 >>> s.echo('Hello World')79 >>> s.echo('Hello World')
80 ['Hello World']80 ['Hello World']
8181
82 >>> s.info()82 >>> info = s.info()
83 ['1.0', 'i386', ['sourcepackagerecipe', 'binarypackage', 'debian']]83 >>> len(info)
84 3
85 >>> print info[:2]
86 ['1.0', 'i386']
87
88 >>> for buildtype in sorted(info[2]):
89 ... print buildtype
90 binarypackage
91 debian
92 sourcepackagerecipe
93 translation-templates
8494
85 >>> s.status()95 >>> s.status()
86 ['BuilderStatus.IDLE', '']96 ['BuilderStatus.IDLE', '']
8797
=== added file 'lib/canonical/buildd/tests/test_generate_translation_templates.py'
--- lib/canonical/buildd/tests/test_generate_translation_templates.py 1970-01-01 00:00:00 +0000
+++ lib/canonical/buildd/tests/test_generate_translation_templates.py 2010-02-18 14:41:20 +0000
@@ -0,0 +1,60 @@
1# Copyright 2010 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4import os
5from unittest import TestLoader
6
7from lp.testing import TestCase
8
9from canonical.buildd.generate_translation_templates import (
10 GenerateTranslationTemplates)
11
12from canonical.launchpad.ftests.script import run_script
13from canonical.testing.layers import ZopelessDatabaseLayer
14
15
16class MockGenerateTranslationTemplates(GenerateTranslationTemplates):
17 """A GenerateTranslationTemplates with mocked _checkout."""
18 # Records, for testing purposes, whether this object checked out a
19 # branch.
20 checked_out_branch = False
21
22 def _checkout(self, branch_url):
23 assert not self.checked_out_branch, "Checking out branch again!"
24 self.checked_out_branch = True
25
26
27class TestGenerateTranslationTemplates(TestCase):
28 """Test slave-side generate-translation-templates script."""
29 def test_getBranch_url(self):
30 # If passed a branch URL, the template generation script will
31 # check out that branch into a directory called "source-tree."
32 branch_url = 'lp://~my/translation/branch'
33
34 generator = MockGenerateTranslationTemplates(branch_url)
35 generator._getBranch()
36
37 self.assertTrue(generator.checked_out_branch)
38 self.assertTrue(generator.branch_dir.endswith('source-tree'))
39
40 def test_getBranch_dir(self):
41 # If passed a branch directory, the template generation script
42 # works directly in that directory.
43 branch_dir = '/home/me/branch'
44
45 generator = MockGenerateTranslationTemplates(branch_dir)
46 generator._getBranch()
47
48 self.assertFalse(generator.checked_out_branch)
49 self.assertEqual(branch_dir, generator.branch_dir)
50
51 def test_script(self):
52 tempdir = self.makeTemporaryDirectory()
53 (retval, out, err) = run_script(
54 'lib/canonical/buildd/generate_translation_templates.py',
55 args=[tempdir])
56 self.assertEqual(0, retval)
57
58
59def test_suite():
60 return TestLoader().loadTestsFromName(__name__)
061
=== added file 'lib/canonical/buildd/tests/test_translationtemplatesbuildmanager.py'
--- lib/canonical/buildd/tests/test_translationtemplatesbuildmanager.py 1970-01-01 00:00:00 +0000
+++ lib/canonical/buildd/tests/test_translationtemplatesbuildmanager.py 2010-02-18 14:41:20 +0000
@@ -0,0 +1,133 @@
1# Copyright 2010 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4__metaclass__ = type
5
6import os
7
8from unittest import TestLoader
9
10from lp.testing import TestCase
11
12from canonical.buildd.translationtemplates import (
13 TranslationTemplatesBuildManager, TranslationTemplatesBuildState)
14
15
16class FakeConfig:
17 def get(self, section, key):
18 return key
19
20
21class FakeSlave:
22 def __init__(self, tempdir):
23 self._cachepath = tempdir
24 self._config = FakeConfig()
25
26 def cachePath(self, file):
27 return os.path.join(self._cachepath, file)
28
29
30class MockBuildManager(TranslationTemplatesBuildManager):
31 def __init__(self, *args, **kwargs):
32 super(MockBuildManager, self).__init__(*args, **kwargs)
33 self.commands = []
34
35 def runSubProcess(self, path, command):
36 self.commands.append(command)
37 return 0
38
39
40class TestTranslationTemplatesBuildManagerIteration(TestCase):
41 """Run TranslationTemplatesBuildManager through its iteration steps."""
42 def setUp(self):
43 super(TestTranslationTemplatesBuildManagerIteration, self).setUp()
44 self.working_dir = self.makeTemporaryDirectory()
45 slave_dir = os.path.join(self.working_dir, 'slave')
46 home_dir = os.path.join(self.working_dir, 'home')
47 for dir in (slave_dir, home_dir):
48 os.mkdir(dir)
49 slave = FakeSlave(slave_dir)
50 buildid = '123'
51 self.buildmanager = MockBuildManager(slave, buildid)
52 self.buildmanager.home = home_dir
53 self.chrootdir = os.path.join(
54 home_dir, 'build-%s' % buildid, 'chroot-autobuild')
55
56 def getState(self):
57 """Retrieve build manager's state."""
58 return self.buildmanager._state
59
60 def test_initiate(self):
61 # Creating a BuildManager spawns no child processes.
62 self.assertEqual([], self.buildmanager.commands)
63
64 # Initiating the build executes the first command. It leaves
65 # the build manager in the INIT state.
66 self.buildmanager.initiate({}, 'chroot.tar.gz', {'branch_url': 'foo'})
67 self.assertEqual(1, len(self.buildmanager.commands))
68 self.assertEqual(TranslationTemplatesBuildState.INIT, self.getState())
69
70 def test_iterate(self):
71 url = 'lp:~my/branch'
72 # The build manager's iterate() kicks off the consecutive states
73 # after INIT.
74 self.buildmanager.initiate({}, 'chroot.tar.gz', {'branch_url': url})
75
76 # UNPACK: execute unpack-chroot.
77 self.buildmanager.iterate(0)
78 self.assertEqual(
79 TranslationTemplatesBuildState.UNPACK, self.getState())
80 self.assertEqual('unpack-chroot', self.buildmanager.commands[-1][0])
81
82 # MOUNT: Set up realistic chroot environment.
83 self.buildmanager.iterate(0)
84 self.assertEqual(
85 TranslationTemplatesBuildState.MOUNT, self.getState())
86 self.assertEqual('mount-chroot', self.buildmanager.commands[-1][0])
87
88 # UPDATE: Get the latest versions of installed packages.
89 self.buildmanager.iterate(0)
90 self.assertEqual(
91 TranslationTemplatesBuildState.UPDATE, self.getState())
92 expected_command = [
93 '/usr/bin/sudo',
94 '/usr/sbin/chroot', self.chrootdir,
95 'update-debian-chroot',
96 ]
97 self.assertEqual(expected_command, self.buildmanager.commands[-1][:4])
98
99 # INSTALL: Install additional packages needed for this job into
100 # the chroot.
101 self.buildmanager.iterate(0)
102 self.assertEqual(
103 TranslationTemplatesBuildState.INSTALL, self.getState())
104 expected_command = [
105 '/usr/bin/sudo',
106 '/usr/sbin/chroot', self.chrootdir,
107 'apt-get',
108 ]
109 self.assertEqual(expected_command, self.buildmanager.commands[-1][:4])
110
111 # GENERATE: Run the slave's payload, the script that generates
112 # templates.
113 self.buildmanager.iterate(0)
114 self.assertEqual(
115 TranslationTemplatesBuildState.GENERATE, self.getState())
116 expected_command = [
117 '/usr/bin/sudo',
118 '/usr/sbin/chroot', self.chrootdir,
119 '/usr/bin/sudo', '-u', self.buildmanager.username,
120 'generate-translation-templates.py',
121 url,
122 ]
123 self.assertEqual(expected_command, self.buildmanager.commands[-1])
124
125 # CLEANUP.
126 self.buildmanager.iterate(0)
127 self.assertEqual(
128 TranslationTemplatesBuildState.CLEANUP, self.getState())
129 self.assertEqual('remove-build', self.buildmanager.commands[-1][0])
130
131
132def test_suite():
133 return TestLoader().loadTestsFromName(__name__)
0134
=== added file 'lib/canonical/buildd/translationtemplates.py'
--- lib/canonical/buildd/translationtemplates.py 1970-01-01 00:00:00 +0000
+++ lib/canonical/buildd/translationtemplates.py 2010-02-18 14:41:20 +0000
@@ -0,0 +1,159 @@
1# Copyright 2010 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4__metaclass__ = type
5
6import os
7import pwd
8
9from canonical.buildd.slave import BuildManager
10
11
12class TranslationTemplatesBuildState(object):
13 "States this kind of build goes through."""
14 INIT = "INIT"
15 UNPACK = "UNPACK"
16 MOUNT = "MOUNT"
17 UPDATE = "UPDATE"
18 INSTALL = "INSTALL"
19 GENERATE = "GENERATE"
20 CLEANUP = "CLEANUP"
21
22
23class TranslationTemplatesBuildManager(BuildManager):
24 """Generate translation templates from branch.
25
26 This is the implementation of `TranslationTemplatesBuildJob`. The
27 latter runs on the master server; TranslationTemplatesBuildManager
28 runs on the build slave.
29 """
30 def __init__(self, *args, **kwargs):
31 super(TranslationTemplatesBuildManager, self).__init__(
32 *args, **kwargs)
33 self._state = TranslationTemplatesBuildState.INIT
34 self.alreadyfailed = False
35
36 def initiate(self, files, chroot, extra_args):
37 """See `BuildManager`."""
38 self.branch_url = extra_args['branch_url']
39 self.username = pwd.getpwuid(os.getuid())[0]
40
41 super(TranslationTemplatesBuildManager, self).initiate(
42 files, chroot, extra_args)
43
44 self.chroot_path = os.path.join(
45 self.home, 'build-' + self._buildid, 'chroot-autobuild')
46
47 def iterate(self, success):
48 func = getattr(self, 'iterate_' + self._state, None)
49 if func is None:
50 raise ValueError("Unknown %s state: %s" % (
51 self.__class__.__name__, self._state))
52 func(success)
53
54 def iterate_INIT(self, success):
55 """Next step after initialization."""
56 if success == 0:
57 self._state = TranslationTemplatesBuildState.UNPACK
58 self.doUnpack()
59 else:
60 if not self.alreadyfailed:
61 self._slave.builderFail()
62 self.alreadyfailed = True
63 self._state = TranslationTemplatesBuildState.CLEANUP
64 self.build_implementation.doCleanup()
65
66 def iterate_UNPACK(self, success):
67 if success == 0:
68 self._state = TranslationTemplatesBuildState.MOUNT
69 self.doMounting()
70 else:
71 if not self.alreadyfailed:
72 self._slave.chrootFail()
73 self.alreadyfailed = True
74 self._state = TranslationTemplatesBuildState.CLEANUP
75 self.doCleanup()
76
77 def iterate_MOUNT(self, success):
78 if success == 0:
79 self._state = TranslationTemplatesBuildState.UPDATE
80 self.doUpdate()
81 else:
82 if not self.alreadyfailed:
83 self._slave.chrootFail()
84 self.alreadyfailed = True
85 self._state = TranslationTemplatesBuildState.CLEANUP
86 self.doCleanup()
87
88 def iterate_UPDATE(self, success):
89 if success == 0:
90 self._state = TranslationTemplatesBuildState.INSTALL
91 self.doInstall()
92 else:
93 if not self.alreadyfailed:
94 self._slave.chrootFail()
95 self.alreadyfailed = True
96 self._state = TranslationTemplatesBuildState.CLEANUP
97 self.doCleanup()
98
99 def iterate_INSTALL(self, success):
100 if success == 0:
101 self._state = TranslationTemplatesBuildState.GENERATE
102 self.doGenerate()
103 else:
104 if not self.alreadyfailed:
105 self._slave.chrootFail()
106 self.alreadyfailed = True
107 self._state = TranslationTemplatesBuildState.CLEANUP
108 self.doCleanup()
109
110 def iterate_GENERATE(self, success):
111 if success == 0:
112 self._state = TranslationTemplatesBuildState.CLEANUP
113 self.doCleanup()
114 else:
115 if not self.alreadyfailed:
116 self._slave.buildFail()
117 self.alreadyfailed = True
118 self._state = TranslationTemplatesBuildState.CLEANUP
119 self.doCleanup()
120
121 def iterate_CLEANUP(self, success):
122 if success == 0:
123 if not self.alreadyfailed:
124 self._slave.buildOK()
125 else:
126 if not self.alreadyfailed:
127 self._slave.builderFail()
128 self.alreadyfailed = True
129 self._slave.buildComplete()
130
131 def doInstall(self):
132 """Install packages required."""
133 required_packages = [
134 'bzr',
135 'intltool-debian',
136 ]
137 command = ['apt-get', 'install', '-y'] + required_packages
138 self.runInChroot(self.home, command, as_root=True)
139
140 def doUpdate(self):
141 """Update chroot."""
142 command = ['update-debian-chroot', self._buildid]
143 self.runInChroot(self.home, command, as_root=True)
144
145 def doGenerate(self):
146 """Generate templates."""
147 command = ['generate-translation-templates.py', self.branch_url]
148 self.runInChroot(self.home, command)
149
150 def runInChroot(self, path, command, as_root=False):
151 """Run command in chroot."""
152 chroot = ['/usr/bin/sudo', '/usr/sbin/chroot', self.chroot_path]
153 if as_root:
154 sudo = []
155 else:
156 # We have to sudo to chroot, so if the command should _not_
157 # be run as root, we then need to sudo back to who we were.
158 sudo = ['/usr/bin/sudo', '-u', self.username]
159 return self.runSubProcess(path, chroot + sudo + command)
0160
=== modified file 'lib/lp/testing/__init__.py'
--- lib/lp/testing/__init__.py 2010-02-15 12:08:54 +0000
+++ lib/lp/testing/__init__.py 2010-02-18 14:41:20 +0000
@@ -208,6 +208,12 @@
208 fixture.setUp()208 fixture.setUp()
209 self.addCleanup(fixture.tearDown)209 self.addCleanup(fixture.tearDown)
210210
211 def makeTemporaryDirectory(self):
212 """Create a temporary directory, and return its path."""
213 tempdir = tempfile.mkdtemp()
214 self.addCleanup(lambda: shutil.rmtree(tempdir))
215 return tempdir
216
211 def assertProvides(self, obj, interface):217 def assertProvides(self, obj, interface):
212 """Assert 'obj' correctly provides 'interface'."""218 """Assert 'obj' correctly provides 'interface'."""
213 self.assertTrue(219 self.assertTrue(