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
1=== modified file 'daemons/buildd-slave.tac'
2--- daemons/buildd-slave.tac 2010-01-13 23:16:43 +0000
3+++ daemons/buildd-slave.tac 2010-02-18 14:41:20 +0000
4@@ -12,6 +12,7 @@
5 from canonical.buildd.binarypackage import BinaryPackageBuildManager
6 from canonical.buildd.sourcepackagerecipe import (
7 SourcePackageRecipeBuildManager)
8+from canonical.buildd.translationtemplates import TranslationTemplatesBuildManager
9 from canonical.launchpad.daemons import tachandler
10
11 from twisted.web import server, resource, static
12@@ -29,6 +30,7 @@
13 slave.registerBuilder(BinaryPackageBuildManager, "debian")
14 slave.registerBuilder(BinaryPackageBuildManager, "binarypackage")
15 slave.registerBuilder(SourcePackageRecipeBuildManager, "sourcepackagerecipe")
16+slave.registerBuilder(TranslationTemplatesBuildManager, 'translation-templates')
17
18 application = service.Application('BuildDSlave')
19 builddslaveService = service.IServiceCollection(application)
20
21=== modified file 'lib/canonical/buildd/debian/rules'
22--- lib/canonical/buildd/debian/rules 2010-01-13 23:16:43 +0000
23+++ lib/canonical/buildd/debian/rules 2010-02-18 14:41:20 +0000
24@@ -21,10 +21,10 @@
25 targetshare = $(target)/usr/share/launchpad-buildd
26 pytarget = $(targetshare)/canonical/buildd
27
28-pyfiles = debian.py slave.py binarypackage.py utils.py __init__.py sourcepackagerecipe.py
29+pyfiles = debian.py slave.py binarypackage.py utils.py __init__.py sourcepackagerecipe.py translationtemplates.py
30 slavebins = unpack-chroot mount-chroot update-debian-chroot sbuild-package \
31 scan-for-processes umount-chroot remove-build apply-ogre-model \
32-override-sources-list buildrecipe
33+override-sources-list buildrecipe generate_translation_templates.py
34
35 BUILDDUID=65500
36 BUILDDGID=65500
37
38=== added file 'lib/canonical/buildd/generate_translation_templates.py'
39--- lib/canonical/buildd/generate_translation_templates.py 1970-01-01 00:00:00 +0000
40+++ lib/canonical/buildd/generate_translation_templates.py 2010-02-18 14:41:20 +0000
41@@ -0,0 +1,57 @@
42+#! /usr/bin/python2.5
43+# Copyright 2010 Canonical Ltd. This software is licensed under the
44+# GNU Affero General Public License version 3 (see the file LICENSE).
45+
46+__metaclass__ = type
47+
48+import os.path
49+import sys
50+from subprocess import call
51+
52+
53+class GenerateTranslationTemplates:
54+ """Script to generate translation templates from a branch."""
55+
56+ def __init__(self, branch_spec):
57+ """Prepare to generate templates for a branch.
58+
59+ :param branch_spec: Either a branch URL or the path of a local
60+ branch. URLs are recognized by the occurrence of ':'. In
61+ the case of a URL, this will make up a path for the branch
62+ and check out the branch to there.
63+ """
64+ self.home = os.environ['HOME']
65+ self.branch_spec = branch_spec
66+
67+ def _getBranch(self):
68+ """Set `self.branch_dir`, and check out branch if needed."""
69+ if ':' in self.branch_spec:
70+ # This is a branch URL. Check out the branch.
71+ self.branch_dir = os.path.join(self.home, 'source-tree')
72+ self._checkout(self.branch_spec)
73+ else:
74+ # This is a local filesystem path. Use the branch in-place.
75+ self.branch_dir = self.branch_spec
76+
77+ def _checkout(self, branch_url):
78+ """Check out a source branch to generate from.
79+
80+ The branch is checked out to the location specified by
81+ `self.branch_dir`.
82+ """
83+ # XXX JeroenVermeulen 2010-02-11 bug=520651: Read the contents
84+ # of the branch using bzrlib.
85+
86+ def generate(self):
87+ """Do It. Generate templates."""
88+ self._getBranch()
89+ # XXX JeroenVermeulen 2010-01-19 bug=509557: Actual payload goes here.
90+ return 0
91+
92+
93+if __name__ == '__main__':
94+ if len(sys.argv) != 2:
95+ print "Usage: %s branch" % sys.argv[0]
96+ print "Where 'branch' is a branch URL or directory."
97+ sys.exit(1)
98+ sys.exit(GenerateTranslationTemplates(sys.argv[1]).generate())
99
100=== modified file 'lib/canonical/buildd/slave.py'
101--- lib/canonical/buildd/slave.py 2010-02-09 00:17:40 +0000
102+++ lib/canonical/buildd/slave.py 2010-02-18 14:41:20 +0000
103@@ -96,6 +96,11 @@
104 """Build Daemon slave build manager abstract parent"""
105
106 def __init__(self, slave, buildid):
107+ """Create a BuildManager.
108+
109+ :param slave: A `BuildDSlave`.
110+ :param buildid: Identifying string for this build.
111+ """
112 object.__init__(self)
113 self._buildid = buildid
114 self._slave = slave
115@@ -104,6 +109,7 @@
116 self._mountpath = slave._config.get("allmanagers", "mountpath")
117 self._umountpath = slave._config.get("allmanagers", "umountpath")
118 self.is_archive_private = False
119+ self.home = os.environ['HOME']
120
121 def runSubProcess(self, command, args):
122 """Run a sub process capturing the results in the log."""
123@@ -112,7 +118,7 @@
124 childfds = {0: devnull.fileno(), 1: "r", 2: "r"}
125 reactor.spawnProcess(
126 self._subprocess, command, args, env=os.environ,
127- path=os.environ["HOME"], childFDs=childfds)
128+ path=self.home, childFDs=childfds)
129
130 def doUnpack(self):
131 """Unpack the build chroot."""
132@@ -146,10 +152,10 @@
133 value keyed under the 'archive_private' string. If that value
134 evaluates to True the build at hand is for a private archive.
135 """
136- os.mkdir("%s/build-%s" % (os.environ["HOME"], self._buildid))
137+ os.mkdir("%s/build-%s" % (self.home, self._buildid))
138 for f in files:
139 os.symlink( self._slave.cachePath(files[f]),
140- "%s/build-%s/%s" % (os.environ["HOME"],
141+ "%s/build-%s/%s" % (self.home,
142 self._buildid, f))
143 self._chroottarfile = self._slave.cachePath(chroot)
144
145
146=== added file 'lib/canonical/buildd/test_buildd_generatetranslationtemplates'
147--- lib/canonical/buildd/test_buildd_generatetranslationtemplates 1970-01-01 00:00:00 +0000
148+++ lib/canonical/buildd/test_buildd_generatetranslationtemplates 2010-02-18 14:41:20 +0000
149@@ -0,0 +1,33 @@
150+#!/usr/bin/env python
151+# Copyright 2010 Canonical Ltd. This software is licensed under the
152+# GNU Affero General Public License version 3 (see the file LICENSE).
153+#
154+# Test script for manual use only. Exercises the
155+# TranslationTemplatesBuildManager through XMLRPC.
156+
157+import sys
158+
159+from xmlrpclib import ServerProxy
160+
161+if len(sys.argv) != 2:
162+ print "Usage: %s <chroot_sha1>" % sys.argv[0]
163+ print "Where <chroot_sha1> is the SHA1 of the chroot tarball to use."
164+ print "The chroot tarball must be in the local Librarian."
165+ print "See https://dev.launchpad.net/Soyuz/HowToUseSoyuzLocally"
166+ sys.exit(1)
167+
168+chroot_sha1 = sys.argv[1]
169+
170+proxy = ServerProxy('http://localhost:8221/rpc')
171+print proxy.info()
172+print proxy.status()
173+buildid = '1-2'
174+build_type = 'translation-templates'
175+filemap = {}
176+args = {'branch_url': 'no-branch-here-sorry'}
177+print proxy.build(buildid, build_type, chroot_sha1, filemap, args)
178+#status = proxy.status()
179+#for filename, sha1 in status[3].iteritems():
180+# print filename
181+#proxy.clean()
182+
183
184=== modified file 'lib/canonical/buildd/tests/harness.py'
185--- lib/canonical/buildd/tests/harness.py 2010-02-01 14:55:03 +0000
186+++ lib/canonical/buildd/tests/harness.py 2010-02-18 14:41:20 +0000
187@@ -79,8 +79,18 @@
188 >>> s.echo('Hello World')
189 ['Hello World']
190
191- >>> s.info()
192- ['1.0', 'i386', ['sourcepackagerecipe', 'binarypackage', 'debian']]
193+ >>> info = s.info()
194+ >>> len(info)
195+ 3
196+ >>> print info[:2]
197+ ['1.0', 'i386']
198+
199+ >>> for buildtype in sorted(info[2]):
200+ ... print buildtype
201+ binarypackage
202+ debian
203+ sourcepackagerecipe
204+ translation-templates
205
206 >>> s.status()
207 ['BuilderStatus.IDLE', '']
208
209=== added file 'lib/canonical/buildd/tests/test_generate_translation_templates.py'
210--- lib/canonical/buildd/tests/test_generate_translation_templates.py 1970-01-01 00:00:00 +0000
211+++ lib/canonical/buildd/tests/test_generate_translation_templates.py 2010-02-18 14:41:20 +0000
212@@ -0,0 +1,60 @@
213+# Copyright 2010 Canonical Ltd. This software is licensed under the
214+# GNU Affero General Public License version 3 (see the file LICENSE).
215+
216+import os
217+from unittest import TestLoader
218+
219+from lp.testing import TestCase
220+
221+from canonical.buildd.generate_translation_templates import (
222+ GenerateTranslationTemplates)
223+
224+from canonical.launchpad.ftests.script import run_script
225+from canonical.testing.layers import ZopelessDatabaseLayer
226+
227+
228+class MockGenerateTranslationTemplates(GenerateTranslationTemplates):
229+ """A GenerateTranslationTemplates with mocked _checkout."""
230+ # Records, for testing purposes, whether this object checked out a
231+ # branch.
232+ checked_out_branch = False
233+
234+ def _checkout(self, branch_url):
235+ assert not self.checked_out_branch, "Checking out branch again!"
236+ self.checked_out_branch = True
237+
238+
239+class TestGenerateTranslationTemplates(TestCase):
240+ """Test slave-side generate-translation-templates script."""
241+ def test_getBranch_url(self):
242+ # If passed a branch URL, the template generation script will
243+ # check out that branch into a directory called "source-tree."
244+ branch_url = 'lp://~my/translation/branch'
245+
246+ generator = MockGenerateTranslationTemplates(branch_url)
247+ generator._getBranch()
248+
249+ self.assertTrue(generator.checked_out_branch)
250+ self.assertTrue(generator.branch_dir.endswith('source-tree'))
251+
252+ def test_getBranch_dir(self):
253+ # If passed a branch directory, the template generation script
254+ # works directly in that directory.
255+ branch_dir = '/home/me/branch'
256+
257+ generator = MockGenerateTranslationTemplates(branch_dir)
258+ generator._getBranch()
259+
260+ self.assertFalse(generator.checked_out_branch)
261+ self.assertEqual(branch_dir, generator.branch_dir)
262+
263+ def test_script(self):
264+ tempdir = self.makeTemporaryDirectory()
265+ (retval, out, err) = run_script(
266+ 'lib/canonical/buildd/generate_translation_templates.py',
267+ args=[tempdir])
268+ self.assertEqual(0, retval)
269+
270+
271+def test_suite():
272+ return TestLoader().loadTestsFromName(__name__)
273
274=== added file 'lib/canonical/buildd/tests/test_translationtemplatesbuildmanager.py'
275--- lib/canonical/buildd/tests/test_translationtemplatesbuildmanager.py 1970-01-01 00:00:00 +0000
276+++ lib/canonical/buildd/tests/test_translationtemplatesbuildmanager.py 2010-02-18 14:41:20 +0000
277@@ -0,0 +1,133 @@
278+# Copyright 2010 Canonical Ltd. This software is licensed under the
279+# GNU Affero General Public License version 3 (see the file LICENSE).
280+
281+__metaclass__ = type
282+
283+import os
284+
285+from unittest import TestLoader
286+
287+from lp.testing import TestCase
288+
289+from canonical.buildd.translationtemplates import (
290+ TranslationTemplatesBuildManager, TranslationTemplatesBuildState)
291+
292+
293+class FakeConfig:
294+ def get(self, section, key):
295+ return key
296+
297+
298+class FakeSlave:
299+ def __init__(self, tempdir):
300+ self._cachepath = tempdir
301+ self._config = FakeConfig()
302+
303+ def cachePath(self, file):
304+ return os.path.join(self._cachepath, file)
305+
306+
307+class MockBuildManager(TranslationTemplatesBuildManager):
308+ def __init__(self, *args, **kwargs):
309+ super(MockBuildManager, self).__init__(*args, **kwargs)
310+ self.commands = []
311+
312+ def runSubProcess(self, path, command):
313+ self.commands.append(command)
314+ return 0
315+
316+
317+class TestTranslationTemplatesBuildManagerIteration(TestCase):
318+ """Run TranslationTemplatesBuildManager through its iteration steps."""
319+ def setUp(self):
320+ super(TestTranslationTemplatesBuildManagerIteration, self).setUp()
321+ self.working_dir = self.makeTemporaryDirectory()
322+ slave_dir = os.path.join(self.working_dir, 'slave')
323+ home_dir = os.path.join(self.working_dir, 'home')
324+ for dir in (slave_dir, home_dir):
325+ os.mkdir(dir)
326+ slave = FakeSlave(slave_dir)
327+ buildid = '123'
328+ self.buildmanager = MockBuildManager(slave, buildid)
329+ self.buildmanager.home = home_dir
330+ self.chrootdir = os.path.join(
331+ home_dir, 'build-%s' % buildid, 'chroot-autobuild')
332+
333+ def getState(self):
334+ """Retrieve build manager's state."""
335+ return self.buildmanager._state
336+
337+ def test_initiate(self):
338+ # Creating a BuildManager spawns no child processes.
339+ self.assertEqual([], self.buildmanager.commands)
340+
341+ # Initiating the build executes the first command. It leaves
342+ # the build manager in the INIT state.
343+ self.buildmanager.initiate({}, 'chroot.tar.gz', {'branch_url': 'foo'})
344+ self.assertEqual(1, len(self.buildmanager.commands))
345+ self.assertEqual(TranslationTemplatesBuildState.INIT, self.getState())
346+
347+ def test_iterate(self):
348+ url = 'lp:~my/branch'
349+ # The build manager's iterate() kicks off the consecutive states
350+ # after INIT.
351+ self.buildmanager.initiate({}, 'chroot.tar.gz', {'branch_url': url})
352+
353+ # UNPACK: execute unpack-chroot.
354+ self.buildmanager.iterate(0)
355+ self.assertEqual(
356+ TranslationTemplatesBuildState.UNPACK, self.getState())
357+ self.assertEqual('unpack-chroot', self.buildmanager.commands[-1][0])
358+
359+ # MOUNT: Set up realistic chroot environment.
360+ self.buildmanager.iterate(0)
361+ self.assertEqual(
362+ TranslationTemplatesBuildState.MOUNT, self.getState())
363+ self.assertEqual('mount-chroot', self.buildmanager.commands[-1][0])
364+
365+ # UPDATE: Get the latest versions of installed packages.
366+ self.buildmanager.iterate(0)
367+ self.assertEqual(
368+ TranslationTemplatesBuildState.UPDATE, self.getState())
369+ expected_command = [
370+ '/usr/bin/sudo',
371+ '/usr/sbin/chroot', self.chrootdir,
372+ 'update-debian-chroot',
373+ ]
374+ self.assertEqual(expected_command, self.buildmanager.commands[-1][:4])
375+
376+ # INSTALL: Install additional packages needed for this job into
377+ # the chroot.
378+ self.buildmanager.iterate(0)
379+ self.assertEqual(
380+ TranslationTemplatesBuildState.INSTALL, self.getState())
381+ expected_command = [
382+ '/usr/bin/sudo',
383+ '/usr/sbin/chroot', self.chrootdir,
384+ 'apt-get',
385+ ]
386+ self.assertEqual(expected_command, self.buildmanager.commands[-1][:4])
387+
388+ # GENERATE: Run the slave's payload, the script that generates
389+ # templates.
390+ self.buildmanager.iterate(0)
391+ self.assertEqual(
392+ TranslationTemplatesBuildState.GENERATE, self.getState())
393+ expected_command = [
394+ '/usr/bin/sudo',
395+ '/usr/sbin/chroot', self.chrootdir,
396+ '/usr/bin/sudo', '-u', self.buildmanager.username,
397+ 'generate-translation-templates.py',
398+ url,
399+ ]
400+ self.assertEqual(expected_command, self.buildmanager.commands[-1])
401+
402+ # CLEANUP.
403+ self.buildmanager.iterate(0)
404+ self.assertEqual(
405+ TranslationTemplatesBuildState.CLEANUP, self.getState())
406+ self.assertEqual('remove-build', self.buildmanager.commands[-1][0])
407+
408+
409+def test_suite():
410+ return TestLoader().loadTestsFromName(__name__)
411
412=== added file 'lib/canonical/buildd/translationtemplates.py'
413--- lib/canonical/buildd/translationtemplates.py 1970-01-01 00:00:00 +0000
414+++ lib/canonical/buildd/translationtemplates.py 2010-02-18 14:41:20 +0000
415@@ -0,0 +1,159 @@
416+# Copyright 2010 Canonical Ltd. This software is licensed under the
417+# GNU Affero General Public License version 3 (see the file LICENSE).
418+
419+__metaclass__ = type
420+
421+import os
422+import pwd
423+
424+from canonical.buildd.slave import BuildManager
425+
426+
427+class TranslationTemplatesBuildState(object):
428+ "States this kind of build goes through."""
429+ INIT = "INIT"
430+ UNPACK = "UNPACK"
431+ MOUNT = "MOUNT"
432+ UPDATE = "UPDATE"
433+ INSTALL = "INSTALL"
434+ GENERATE = "GENERATE"
435+ CLEANUP = "CLEANUP"
436+
437+
438+class TranslationTemplatesBuildManager(BuildManager):
439+ """Generate translation templates from branch.
440+
441+ This is the implementation of `TranslationTemplatesBuildJob`. The
442+ latter runs on the master server; TranslationTemplatesBuildManager
443+ runs on the build slave.
444+ """
445+ def __init__(self, *args, **kwargs):
446+ super(TranslationTemplatesBuildManager, self).__init__(
447+ *args, **kwargs)
448+ self._state = TranslationTemplatesBuildState.INIT
449+ self.alreadyfailed = False
450+
451+ def initiate(self, files, chroot, extra_args):
452+ """See `BuildManager`."""
453+ self.branch_url = extra_args['branch_url']
454+ self.username = pwd.getpwuid(os.getuid())[0]
455+
456+ super(TranslationTemplatesBuildManager, self).initiate(
457+ files, chroot, extra_args)
458+
459+ self.chroot_path = os.path.join(
460+ self.home, 'build-' + self._buildid, 'chroot-autobuild')
461+
462+ def iterate(self, success):
463+ func = getattr(self, 'iterate_' + self._state, None)
464+ if func is None:
465+ raise ValueError("Unknown %s state: %s" % (
466+ self.__class__.__name__, self._state))
467+ func(success)
468+
469+ def iterate_INIT(self, success):
470+ """Next step after initialization."""
471+ if success == 0:
472+ self._state = TranslationTemplatesBuildState.UNPACK
473+ self.doUnpack()
474+ else:
475+ if not self.alreadyfailed:
476+ self._slave.builderFail()
477+ self.alreadyfailed = True
478+ self._state = TranslationTemplatesBuildState.CLEANUP
479+ self.build_implementation.doCleanup()
480+
481+ def iterate_UNPACK(self, success):
482+ if success == 0:
483+ self._state = TranslationTemplatesBuildState.MOUNT
484+ self.doMounting()
485+ else:
486+ if not self.alreadyfailed:
487+ self._slave.chrootFail()
488+ self.alreadyfailed = True
489+ self._state = TranslationTemplatesBuildState.CLEANUP
490+ self.doCleanup()
491+
492+ def iterate_MOUNT(self, success):
493+ if success == 0:
494+ self._state = TranslationTemplatesBuildState.UPDATE
495+ self.doUpdate()
496+ else:
497+ if not self.alreadyfailed:
498+ self._slave.chrootFail()
499+ self.alreadyfailed = True
500+ self._state = TranslationTemplatesBuildState.CLEANUP
501+ self.doCleanup()
502+
503+ def iterate_UPDATE(self, success):
504+ if success == 0:
505+ self._state = TranslationTemplatesBuildState.INSTALL
506+ self.doInstall()
507+ else:
508+ if not self.alreadyfailed:
509+ self._slave.chrootFail()
510+ self.alreadyfailed = True
511+ self._state = TranslationTemplatesBuildState.CLEANUP
512+ self.doCleanup()
513+
514+ def iterate_INSTALL(self, success):
515+ if success == 0:
516+ self._state = TranslationTemplatesBuildState.GENERATE
517+ self.doGenerate()
518+ else:
519+ if not self.alreadyfailed:
520+ self._slave.chrootFail()
521+ self.alreadyfailed = True
522+ self._state = TranslationTemplatesBuildState.CLEANUP
523+ self.doCleanup()
524+
525+ def iterate_GENERATE(self, success):
526+ if success == 0:
527+ self._state = TranslationTemplatesBuildState.CLEANUP
528+ self.doCleanup()
529+ else:
530+ if not self.alreadyfailed:
531+ self._slave.buildFail()
532+ self.alreadyfailed = True
533+ self._state = TranslationTemplatesBuildState.CLEANUP
534+ self.doCleanup()
535+
536+ def iterate_CLEANUP(self, success):
537+ if success == 0:
538+ if not self.alreadyfailed:
539+ self._slave.buildOK()
540+ else:
541+ if not self.alreadyfailed:
542+ self._slave.builderFail()
543+ self.alreadyfailed = True
544+ self._slave.buildComplete()
545+
546+ def doInstall(self):
547+ """Install packages required."""
548+ required_packages = [
549+ 'bzr',
550+ 'intltool-debian',
551+ ]
552+ command = ['apt-get', 'install', '-y'] + required_packages
553+ self.runInChroot(self.home, command, as_root=True)
554+
555+ def doUpdate(self):
556+ """Update chroot."""
557+ command = ['update-debian-chroot', self._buildid]
558+ self.runInChroot(self.home, command, as_root=True)
559+
560+ def doGenerate(self):
561+ """Generate templates."""
562+ command = ['generate-translation-templates.py', self.branch_url]
563+ self.runInChroot(self.home, command)
564+
565+ def runInChroot(self, path, command, as_root=False):
566+ """Run command in chroot."""
567+ chroot = ['/usr/bin/sudo', '/usr/sbin/chroot', self.chroot_path]
568+ if as_root:
569+ sudo = []
570+ else:
571+ # We have to sudo to chroot, so if the command should _not_
572+ # be run as root, we then need to sudo back to who we were.
573+ sudo = ['/usr/bin/sudo', '-u', self.username]
574+ return self.runSubProcess(path, chroot + sudo + command)
575
576=== modified file 'lib/lp/testing/__init__.py'
577--- lib/lp/testing/__init__.py 2010-02-15 12:08:54 +0000
578+++ lib/lp/testing/__init__.py 2010-02-18 14:41:20 +0000
579@@ -208,6 +208,12 @@
580 fixture.setUp()
581 self.addCleanup(fixture.tearDown)
582
583+ def makeTemporaryDirectory(self):
584+ """Create a temporary directory, and return its path."""
585+ tempdir = tempfile.mkdtemp()
586+ self.addCleanup(lambda: shutil.rmtree(tempdir))
587+ return tempdir
588+
589 def assertProvides(self, obj, interface):
590 """Assert 'obj' correctly provides 'interface'."""
591 self.assertTrue(