Merge lp:~jtv/launchpad/bug-499405-translationtemplates-buildmanager into lp:launchpad
- bug-499405-translationtemplates-buildmanager
- Merge into devel
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 |
Related bugs: |
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.
Description of the change
Jeroen T. Vermeulen (jtv) wrote : | # |
Paul Hummer (rockstar) wrote : | # |
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/
--- lib/canonical/
+++ lib/canonical/
@@ -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 GenerateTransla
+ """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.
+ self._checkout(
+ 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(
=== added file 'lib/canonical/
--- lib/canonical/
+++ lib/canonical/
@@ -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...
Jeroen T. Vermeulen (jtv) wrote : | # |
> 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/
> --- lib/canonical/
> 00:00:00 +0000
> +++ lib/canonical/
> 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 GenerateTransla
> + """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.
> + self._checkout(
> + 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...
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.
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.
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.
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.
Paul Hummer (rockstar) : | # |
Preview Diff
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( |
= 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 makeTemporaryDi rectory 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...