Merge lp:~jtv/launchpad/bug-499404 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-499404
Merge into: lp:launchpad
Diff against target: 444 lines (+374/-6)
7 files modified
lib/lp/buildmaster/interfaces/buildfarmjob.py (+6/-6)
lib/lp/code/model/branchjob.py (+6/-0)
lib/lp/translations/configure.zcml (+29/-0)
lib/lp/translations/interfaces/translationtemplatesbuildjob.py (+37/-0)
lib/lp/translations/model/translationtemplatesbuildbehavior.py (+79/-0)
lib/lp/translations/model/translationtemplatesbuildjob.py (+85/-0)
lib/lp/translations/tests/test_translationtemplatesbuildjob.py (+132/-0)
To merge this branch: bzr merge lp:~jtv/launchpad/bug-499404
Reviewer Review Type Date Requested Status
Julian Edwards (community) code Approve
Review via email: mp+17120@code.launchpad.net

Commit message

IBuildFarmJob type for generating translation templates.

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

= Bug 499404 =

This implements some new classes that will be needed for Rosetta to run jobs on the generalised build farm. The job in question is generating translation templates based on a source-code branch, and will require a lot more work before it goes operational.

Generalisation of the build farm is ongoing work, so its APIs etc. are not complete yet. What you find here is:

1. An implementation of IBuildFarmJob—a new class representing a job that can be dispatched to the build farm. Called TranslationTemplatesBuildJob, it's a derivative of BranchJob found in the Code tree.

2. A BuildFarmJobSource utility—to produce and navigate jobs of the new type. This follows the pattern of the BranchJobSource utilities from the Code hierarchy. It also creates a BuildQueue record for each job it creates; these records are matched to the records of the new type in that they both refer to the same Job record.

3. A new IBuildFarmJobBehavior implementation for dispatching the new job type to the build farm, with an adapter associating it with TranslationTemplatesBuildJob. More may be needed there in the future.

This branch does not test how the Behavior class dispatches jobs to builders; I don't think that's tested for the existing classes either and I'm not sure that part of the build-farm generalization is quite ready for use yet.

To test:
{{{
./bin/test -vv -t translationtemplatesbuild
}}}

No lint.

Jeroen

Revision history for this message
Julian Edwards (julian-edwards) wrote :

Hi Jeroen! This is a great start, thanks also for cleaning up the docstrings. I'll just re-iterate here what we discussed in person for future reference:

 * You need a verifyObject on the ITranslationTemplatesBuildJobSource interface
 * Remove the XXX on line 171 next to build_type.
 * The fault handling in dispatchBuildToSlave needs to be refactored into BuildFarmJobBehaviorBase, can you file a bug for that please.
 * _getChroot is not returning a chroot! I can see what you were starting to do though, returning the nominatedarchindep arch chroot for the series in question is the right thing.
 * test_getChroot should poke a dummy file into the librarian for the chroot for the pertinent distroarchseries. It should then assert that the one that _getChroot returns is the same file.
 * There's no test for score().

Cheers
J

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

All done. Filed bug 506243.

Revision history for this message
Julian Edwards (julian-edwards) wrote :

Land it!

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/buildmaster/interfaces/buildfarmjob.py'
2--- lib/lp/buildmaster/interfaces/buildfarmjob.py 2009-12-24 14:18:35 +0000
3+++ lib/lp/buildmaster/interfaces/buildfarmjob.py 2010-01-12 03:38:15 +0000
4@@ -46,24 +46,24 @@
5 Build a package from a bazaar branch and a recipe.
6 """)
7
8- TRANSLATION = DBItem(4, """
9- TranslationJob
10+ TRANSLATIONTEMPLATESBUILD = DBItem(4, """
11+ TranslationTemplatesBuildJob
12
13- Perform a translation job.
14+ Generate translation templates from a bazaar branch.
15 """)
16
17
18 class IBuildFarmJob(Interface):
19- """Operations that Soyuz build farm jobs must implement."""
20+ """Operations that jobs for the build farm must implement."""
21
22 def score():
23 """Calculate a job score appropriate for the job type in question."""
24
25 def getLogFileName():
26- """The preferred file name for the log of this Soyuz job."""
27+ """The preferred file name for this job's log."""
28
29 def getName():
30- """An appropriate name for this Soyuz job."""
31+ """An appropriate name for this job."""
32
33 def jobStarted():
34 """'Job started' life cycle event, handle as appropriate."""
35
36=== modified file 'lib/lp/code/model/branchjob.py'
37--- lib/lp/code/model/branchjob.py 2010-01-07 04:57:13 +0000
38+++ lib/lp/code/model/branchjob.py 2010-01-12 03:38:15 +0000
39@@ -119,6 +119,12 @@
40 from disk.
41 """)
42
43+ TRANSLATION_TEMPLATES_BUILD = DBItem(6, """
44+ Generate translation templates
45+
46+ This job generates translations templates from a source branch.
47+ """)
48+
49
50 class BranchJob(SQLBase):
51 """Base class for jobs related to branches."""
52
53=== modified file 'lib/lp/translations/configure.zcml'
54--- lib/lp/translations/configure.zcml 2009-12-28 22:58:18 +0000
55+++ lib/lp/translations/configure.zcml 2010-01-12 03:38:15 +0000
56@@ -572,6 +572,35 @@
57 interface="lp.translations.interfaces.poexportrequest.IPOExportRequestSet"/>
58 </securedutility>
59
60+ <!-- TranslationTemplateBuildJob -->
61+ <class
62+ class="lp.translations.model.translationtemplatesbuildjob.TranslationTemplatesBuildJob">
63+ <allow
64+ interface="lp.translations.interfaces.translationtemplatesbuildjob.ITranslationTemplatesBuildJob"/>
65+ </class>
66+ <securedutility
67+ component="lp.translations.model.translationtemplatesbuildjob.TranslationTemplatesBuildJob"
68+ provides="lp.translations.interfaces.translationtemplatesbuildjob.ITranslationTemplatesBuildJobSource">
69+ <allow interface="lp.translations.interfaces.translationtemplatesbuildjob.ITranslationTemplatesBuildJobSource"/>
70+ </securedutility>
71+ <utility
72+ component="lp.translations.model.translationtemplatesbuildjob.TranslationTemplatesBuildJob"
73+ provides="lp.translations.interfaces.translationtemplatesbuildjob.ITranslationTemplatesBuildJob"
74+ name="TRANSLATIONTEMPLATESBUILD"/>
75+
76+ <!-- TranslationTemplateBuildBehavior -->
77+ <class
78+ class="lp.translations.model.translationtemplatesbuildbehavior.TranslationTemplatesBuildBehavior">
79+ <allow
80+ interface="lp.buildmaster.interfaces.buildfarmjobbehavior.IBuildFarmJobBehavior"/>
81+ </class>
82+
83+ <adapter
84+ provides="lp.buildmaster.interfaces.buildfarmjobbehavior.IBuildFarmJobBehavior"
85+ for="lp.translations.model.translationtemplatesbuildjob.TranslationTemplatesBuildJob"
86+ factory="lp.translations.model.translationtemplatesbuildbehavior.TranslationTemplatesBuildBehavior"
87+ />
88+
89 <webservice:register module="lp.translations.interfaces.webservice" />
90
91 </configure>
92
93=== added file 'lib/lp/translations/interfaces/translationtemplatesbuildjob.py'
94--- lib/lp/translations/interfaces/translationtemplatesbuildjob.py 1970-01-01 00:00:00 +0000
95+++ lib/lp/translations/interfaces/translationtemplatesbuildjob.py 2010-01-12 03:38:15 +0000
96@@ -0,0 +1,37 @@
97+# Copyright 2010 Canonical Ltd. This software is licensed under the
98+# GNU Affero General Public License version 3 (see the file LICENSE).
99+
100+# pylint: disable-msg=E0213
101+
102+__metaclass__ = type
103+
104+__all__ = [
105+ 'ITranslationTemplatesBuildJob',
106+ 'ITranslationTemplatesBuildJobSource',
107+ ]
108+
109+from zope.interface import Interface
110+
111+from lp.buildmaster.interfaces.buildfarmjob import IBuildFarmJob
112+from lp.code.interfaces.branchjob import IBranchJob
113+
114+
115+class ITranslationTemplatesBuildJob(IBranchJob, IBuildFarmJob):
116+ """Build-farm job type for generating translation templates."""
117+
118+
119+class ITranslationTemplatesBuildJobSource(Interface):
120+ """Container for `ITranslationTemplatesBuildJob`s."""
121+
122+ def create(branch):
123+ """Create new `ITranslationTemplatesBuildJob`.
124+
125+ Also creates the matching `IBuildQueue` and `IJob`.
126+
127+ :param branch: A `Branch` that this job will check out and
128+ generate templates for.
129+ :return: A new `ITranslationTemplatesBuildJob`.
130+ """
131+
132+ def getForJob(job):
133+ """Find `ITranslationTemplatesBuildJob` matching given `Job`."""
134
135=== added file 'lib/lp/translations/model/translationtemplatesbuildbehavior.py'
136--- lib/lp/translations/model/translationtemplatesbuildbehavior.py 1970-01-01 00:00:00 +0000
137+++ lib/lp/translations/model/translationtemplatesbuildbehavior.py 2010-01-12 03:38:15 +0000
138@@ -0,0 +1,79 @@
139+# Copyright 2010 Canonical Ltd. This software is licensed under the
140+# GNU Affero General Public License version 3 (see the file LICENSE).
141+
142+"""An `IBuildFarmJobBehavior` for `TranslationTemplatesBuildJob`.
143+
144+Dispatches translation template build jobs to build-farm slaves.
145+"""
146+
147+__metaclass__ = type
148+__all__ = [
149+ 'TranslationTemplatesBuildBehavior',
150+ ]
151+
152+import socket
153+import xmlrpclib
154+
155+from zope.component import getUtility
156+from zope.interface import implements
157+
158+from canonical.launchpad.interfaces import ILaunchpadCelebrities
159+
160+from lp.buildmaster.interfaces.buildfarmjobbehavior import (
161+ IBuildFarmJobBehavior)
162+from lp.buildmaster.model.buildfarmjobbehavior import (
163+ BuildFarmJobBehaviorBase)
164+from lp.soyuz.interfaces.builder import BuildSlaveFailure
165+from lp.translations.interfaces.translationtemplatesbuildjob import (
166+ ITranslationTemplatesBuildJobSource)
167+
168+
169+class TranslationTemplatesBuildBehavior(BuildFarmJobBehaviorBase):
170+ """Dispatches `TranslationTemplateBuildJob`s to slaves."""
171+ implements(IBuildFarmJobBehavior)
172+
173+ # Identify the type of job to the slave.
174+ build_type = 'translation-templates'
175+
176+ def dispatchBuildToSlave(self, build_queue_item, logger):
177+ """See `IBuildFarmJobBehavior`."""
178+ # XXX JeroenVermeulen 2009-12-24 bug=500110: This method is not
179+ # covered by tests yet. Either unify it with Soyuz code into a
180+ # generalised method, or test it.
181+ templatesbuildjob = self._findTranslationTemplatesBuildJob(
182+ build_queue_item)
183+ chroot = self._getChroot()
184+ chroot_sha1 = chroot.content.sha1
185+ self._builder.cacheFileOnSlave(logger, chroot)
186+ buildid = templatesbuildjob.getName()
187+
188+ args = { 'branch_url': build_queue_item.branch.url }
189+ filemap = {}
190+
191+ try:
192+ status, info = self._builder.slave.build(
193+ buildid, self.build_type, chroot_sha1, filemap, args)
194+ except xmlrpclib.Fault, info:
195+ # Mark builder as 'failed'.
196+ logger.debug(
197+ "Disabling builder: %s" % self._builder.url, exc_info=1)
198+ self._builder.failbuilder(
199+ "Exception (%s) when setting up to new job" % info)
200+ raise BuildSlaveFailure
201+ except socket.error, info:
202+ error_message = "Exception (%s) when setting up new job" % info
203+ self._builder.handleTimeout(logger, error_message)
204+ raise BuildSlaveFailure
205+
206+ def _getChroot(self):
207+ ubuntu = getUtility(ILaunchpadCelebrities).ubuntu
208+ return ubuntu.currentseries.nominatedarchindep.getChroot()
209+
210+ def _findTranslationTemplatesBuildJob(self, build_queue_item):
211+ """Find the `TranslationTemplatesBuildJob` for a job.
212+
213+ :param build_queue_item: A `BuildQueue` entry.
214+ :return: The matching `TranslationTemplatesBuildJob`.
215+ """
216+ jobsource = getUtility(ITranslationTemplatesBuildJobSource)
217+ return jobsource.getForJob(build_queue_item.job)
218
219=== added file 'lib/lp/translations/model/translationtemplatesbuildjob.py'
220--- lib/lp/translations/model/translationtemplatesbuildjob.py 1970-01-01 00:00:00 +0000
221+++ lib/lp/translations/model/translationtemplatesbuildjob.py 2010-01-12 03:38:15 +0000
222@@ -0,0 +1,85 @@
223+# Copyright 2010 Canonical Ltd. This software is licensed under the
224+# GNU Affero General Public License version 3 (see the file LICENSE).
225+
226+__metaclass__ = type
227+__all__ = [
228+ 'TranslationTemplatesBuildJob',
229+ ]
230+
231+import re
232+from datetime import timedelta
233+
234+from zope.component import getUtility
235+from zope.interface import classProvides, implements
236+
237+from canonical.launchpad.webapp.interfaces import (
238+ DEFAULT_FLAVOR, IStoreSelector, MAIN_STORE, MASTER_FLAVOR)
239+
240+from lp.buildmaster.interfaces.buildfarmjob import BuildFarmJobType
241+from lp.buildmaster.model.buildfarmjob import BuildFarmJob
242+from lp.code.model.branchjob import BranchJob, BranchJobDerived, BranchJobType
243+from lp.soyuz.model.buildqueue import BuildQueue
244+from lp.translations.interfaces.translationtemplatesbuildjob import (
245+ ITranslationTemplatesBuildJob, ITranslationTemplatesBuildJobSource)
246+
247+
248+class TranslationTemplatesBuildJob(BranchJobDerived, BuildFarmJob):
249+ """An `IBuildFarmJob` implementation that generates templates.
250+
251+ Implementation-wise, this is actually a `BranchJob`.
252+ """
253+ implements(ITranslationTemplatesBuildJob)
254+ classProvides(ITranslationTemplatesBuildJobSource)
255+
256+ duration_estimate = timedelta(seconds=10)
257+
258+ unsafe_chars = '[^a-zA-Z0-9_+-]'
259+
260+ def __init__(self, branch_job):
261+ super(TranslationTemplatesBuildJob, self).__init__(branch_job)
262+
263+ def score(self):
264+ """See `IBuildFarmJob`."""
265+ # Hard-code score for now; anything other than 1000 is probably
266+ # inappropriate.
267+ return 1000
268+
269+ def getLogFileName(self):
270+ """See `IBuildFarmJob`."""
271+ sanitized_name = re.sub(self.unsafe_chars, '_', self.getName())
272+ return "translationtemplates_%s" % sanitized_name
273+
274+ def getName(self):
275+ """See `IBuildFarmJob`."""
276+ return '%s-%d' % (self.branch.name, self.job.id)
277+
278+ @classmethod
279+ def create(cls, branch):
280+ """See `ITranslationTemplatesBuildJobSource`."""
281+ store = getUtility(IStoreSelector).get(MAIN_STORE, MASTER_FLAVOR)
282+
283+ # We don't have any JSON metadata for this BranchJob type.
284+ metadata = {}
285+ branch_job = BranchJob(
286+ branch, BranchJobType.TRANSLATION_TEMPLATES_BUILD, metadata)
287+ store.add(branch_job)
288+ specific_job = TranslationTemplatesBuildJob(branch_job)
289+
290+ duration_estimate = cls.duration_estimate
291+ build_queue_entry = BuildQueue(
292+ estimated_duration=duration_estimate,
293+ job_type=BuildFarmJobType.TRANSLATIONTEMPLATESBUILD,
294+ job=specific_job.job.id)
295+ store.add(build_queue_entry)
296+
297+ return specific_job
298+
299+ @classmethod
300+ def getForJob(cls, job):
301+ """See `ITranslationTemplatesBuildJobSource`."""
302+ store = getUtility(IStoreSelector).get(MAIN_STORE, DEFAULT_FLAVOR)
303+ branch_job = store.find(BranchJob, BranchJob.job == job).one()
304+ if branch_job is None:
305+ return None
306+ else:
307+ return cls(branch_job)
308
309=== added file 'lib/lp/translations/tests/test_translationtemplatesbuildjob.py'
310--- lib/lp/translations/tests/test_translationtemplatesbuildjob.py 1970-01-01 00:00:00 +0000
311+++ lib/lp/translations/tests/test_translationtemplatesbuildjob.py 2010-01-12 03:38:15 +0000
312@@ -0,0 +1,132 @@
313+# Copyright 2010 Canonical Ltd. This software is licensed under the
314+# GNU Affero General Public License version 3 (see the file LICENSE).
315+
316+__metaclass__ = type
317+
318+from unittest import TestLoader
319+
320+from zope.component import getUtility
321+from zope.security.proxy import removeSecurityProxy
322+
323+from canonical.launchpad.interfaces import (
324+ ILaunchpadCelebrities, ILibraryFileAliasSet)
325+from canonical.launchpad.webapp.testing import verifyObject
326+from canonical.testing import ZopelessDatabaseLayer
327+
328+from lp.testing import TestCaseWithFactory
329+
330+from lp.buildmaster.interfaces.buildfarmjob import IBuildFarmJob
331+from lp.buildmaster.interfaces.buildfarmjobbehavior import (
332+ IBuildFarmJobBehavior)
333+from lp.services.job.model.job import Job
334+from lp.soyuz.interfaces.buildqueue import IBuildQueueSet
335+from lp.soyuz.model.buildqueue import BuildQueue
336+from lp.translations.interfaces.translationtemplatesbuildjob import (
337+ ITranslationTemplatesBuildJob, ITranslationTemplatesBuildJobSource)
338+
339+
340+def get_job_id(job):
341+ """Peek inside a `Job` and retrieve its id."""
342+ return removeSecurityProxy(job).id
343+
344+
345+class TestTranslationTemplatesBuildJob(TestCaseWithFactory):
346+ """Test `TranslationTemplatesBuildJob`."""
347+
348+ layer = ZopelessDatabaseLayer
349+
350+ def setUp(self):
351+ super(TestTranslationTemplatesBuildJob, self).setUp()
352+ self.jobset = getUtility(ITranslationTemplatesBuildJobSource)
353+ self.branch = self.factory.makeBranch()
354+ self.specific_job = self.jobset.create(self.branch)
355+
356+ def test_new_TranslationTemplatesBuildJob(self):
357+ # TranslationTemplateBuildJob implements IBuildFarmJob and
358+ # ITranslationTemplatesBuildJob.
359+ verifyObject(IBuildFarmJob, self.specific_job)
360+ verifyObject(ITranslationTemplatesBuildJob, self.specific_job)
361+
362+ # The class also implements a utility.
363+ verifyObject(ITranslationTemplatesBuildJobSource, self.jobset)
364+
365+ # Each of these jobs knows the branch it will operate on.
366+ self.assertEqual(self.branch, self.specific_job.branch)
367+
368+ def test_has_Job(self):
369+ # Associated with each TranslationTemplateBuildJob is a Job.
370+ base_job = self.specific_job.job
371+ self.assertIsInstance(base_job, Job)
372+
373+ # From a Job, the TranslationTemplatesBuildJobSource can find the
374+ # TranslationTemplatesBuildJob back for us.
375+ specific_job_for_base_job = removeSecurityProxy(
376+ self.jobset.getForJob(base_job))
377+ self.assertEqual(self.specific_job, specific_job_for_base_job)
378+
379+ def test_has_BuildQueue(self):
380+ # There's also a BuildQueue item associated with the job.
381+ queueset = getUtility(IBuildQueueSet)
382+ job_id = get_job_id(self.specific_job.job)
383+ buildqueue = queueset.get(job_id)
384+
385+ self.assertIsInstance(buildqueue, BuildQueue)
386+ self.assertEqual(job_id, get_job_id(buildqueue.job))
387+
388+ def test_getName(self):
389+ # Each job gets a unique name.
390+ other_job = self.jobset.create(self.branch)
391+ self.assertNotEqual(self.specific_job.getName(), other_job.getName())
392+
393+ def test_getLogFileName(self):
394+ # Each job has a unique log file name.
395+ other_job = self.jobset.create(self.branch)
396+ self.assertNotEqual(
397+ self.specific_job.getLogFileName(), other_job.getLogFileName())
398+
399+ def test_score(self):
400+ # For now, these jobs always score themselves at 1,000. In the
401+ # future however the scoring system is to be revisited.
402+ self.assertEqual(1000, self.specific_job.score())
403+
404+
405+class TestTranslationTemplatesBuildBehavior(TestCaseWithFactory):
406+ """Test `TranslationTemplatesBuildBehavior`."""
407+
408+ layer = ZopelessDatabaseLayer
409+
410+ def setUp(self):
411+ super(TestTranslationTemplatesBuildBehavior, self).setUp()
412+ self.jobset = getUtility(ITranslationTemplatesBuildJobSource)
413+ self.branch = self.factory.makeBranch()
414+ self.specific_job = self.jobset.create(self.branch)
415+ self.behavior = IBuildFarmJobBehavior(self.specific_job)
416+
417+ def test_getChroot(self):
418+ # _getChroot produces the current chroot for the current Ubuntu
419+ # release, on the nominated architecture for
420+ # architecture-independent builds.
421+ ubuntu = getUtility(ILaunchpadCelebrities).ubuntu
422+ current_ubuntu = ubuntu.currentseries
423+ distroarchseries = current_ubuntu.nominatedarchindep
424+
425+ # Set an arbitrary chroot file.
426+ fake_chroot_file = getUtility(ILibraryFileAliasSet)[1]
427+ distroarchseries.addOrUpdateChroot(fake_chroot_file)
428+
429+ chroot = self.behavior._getChroot()
430+
431+ self.assertNotEqual(None, chroot)
432+ self.assertEqual(fake_chroot_file, chroot)
433+
434+ def test_findTranslationTemplatesBuildJob(self):
435+ job = self.specific_job.job
436+ job_id = removeSecurityProxy(job).id
437+ buildqueue = getUtility(IBuildQueueSet).get(job_id)
438+ specific_job_for_buildqueue = removeSecurityProxy(
439+ self.behavior._findTranslationTemplatesBuildJob(buildqueue))
440+ self.assertEqual(self.specific_job, specific_job_for_buildqueue)
441+
442+
443+def test_suite():
444+ return TestLoader().loadTestsFromName(__name__)