Merge lp:~jtv/launchpad/bug-536797 into lp:launchpad

Proposed by Jeroen T. Vermeulen
Status: Merged
Merged at revision: not available
Proposed branch: lp:~jtv/launchpad/bug-536797
Merge into: lp:launchpad
Diff against target: 814 lines (+356/-89)
23 files modified
lib/canonical/launchpad/interfaces/_schema_circular_imports.py (+2/-1)
lib/canonical/launchpad/security.py (+52/-2)
lib/canonical/launchpad/webapp/configure.zcml (+7/-0)
lib/canonical/launchpad/webapp/tales.py (+27/-0)
lib/lp/buildmaster/interfaces/buildfarmbranchjob.py (+21/-0)
lib/lp/buildmaster/interfaces/buildfarmjob.py (+0/-1)
lib/lp/buildmaster/interfaces/buildqueue.py (+4/-0)
lib/lp/buildmaster/model/buildqueue.py (+12/-2)
lib/lp/buildmaster/tests/test_buildqueue.py (+36/-7)
lib/lp/code/interfaces/sourcepackagerecipebuild.py (+2/-7)
lib/lp/soyuz/browser/build.py (+0/-1)
lib/lp/soyuz/browser/builder.py (+2/-8)
lib/lp/soyuz/browser/configure.zcml (+31/-1)
lib/lp/soyuz/doc/build-estimated-dispatch-time.txt (+3/-4)
lib/lp/soyuz/interfaces/buildfarmbuildjob.py (+21/-0)
lib/lp/soyuz/interfaces/buildpackagejob.py (+4/-4)
lib/lp/soyuz/templates/builder-index.pt (+5/-47)
lib/lp/soyuz/templates/buildfarmbranchjob-current.pt (+11/-0)
lib/lp/soyuz/templates/buildfarmbuildjob-current.pt (+8/-0)
lib/lp/soyuz/templates/buildfarmjob-current.pt (+10/-0)
lib/lp/soyuz/templates/buildqueue-current.pt (+24/-0)
lib/lp/translations/model/translationtemplatesbuildjob.py (+5/-4)
lib/lp/translations/stories/buildfarm/xx-build-summary.txt (+69/-0)
To merge this branch: bzr merge lp:~jtv/launchpad/bug-536797
Reviewer Review Type Date Requested Status
Brad Crittenden (community) code Approve
Canonical Launchpad Engineering ui Pending
Review via email: mp+21439@code.launchpad.net

Commit message

Minimal support for non-Soyuz build-farm jobs in builder UI.

Description of the change

= Bug 536797 =

The Builder UI is not very good at dealing with non-Soyuz build-farm jobs. Most build-farm job types have an IBuildBase associated with them, but not all. (It's not required by the IBuildFarmJob interface either).

This branch, after discussion with al-maisan, bigjools, noodles, wgrant, and probably others introduces a bit more UI support for coping with different types of build-farm job. It does this by introducing a few new interfaces: IBuildFarmBuildJob represents an IBuildFarmJob that also has an IBuild reference, and IBuildFarmBranchJob represents one that has an IBranch reference. These inherit from IBuildFarmJob so that zope can pick the most specific applicable interface to render in the UI.

You'll see a bunch of new templates: buildqueue-current.pt summarizes the IBuildQueue a Builder is currently working on. This then renders the associated IBuildQueue.specific_job, which can be one of several IBuildFarmJob-implementing types. These are differentiated along the lines of the IBuildFarmJob / IBuildFarmBuildJob / IBuildFarmBranchJob interface hierarchy. For the existing Soyuz-style build jobs this doesn't change the UI at all, apart from an icon that renders in a slightly different place.

I also added a link formatter for IBuildBase, to help cut down on complicated logic in the TAL.

In the same vein you'll see the access check for showing a job's log tail moved into security.py. Doing this in the TAL became too complicated with the different build types. It's quite possible that the current check is too strict; it may make sense to show a build's log if either its branch or its build is accessible. I think experience will have to show this. Soyuz is not the kind of thing you perfect all in one branch. In any case it's hard to exercise just now: our jobs that generate translation templates don't support private branches.

A BuildQueue's "current build duration" moves from the view into the model, which seems more appropriate and lets it reuse a "fake clock" fixture that's already there. I did replace some use of datetime.utcnow() with datetime.now(timezone('UTC')); the former returns timezone-unaware times which breaks several tests.

You'll note that some of the files I introduced deserve to be in better places. I filed a separate bug about this, because I've just about hit the limit of what I can do in one branch, and a step like that would need broader discussion.

To test (runs a lot of tests, but many exercise one change or another):
{{{
./bin/test -vv -t buildmaster -t soyuz -t 'translations.*build'
}}}

To Q/A, first off, browser around the /builders page. Then follow the instructions on https://dev.launchpad.net/Translations/BuildTemplatesOnLocalBuildFarm to create a TranslationTemplatesBuildJob, and look again. Now you'll see a different summary of the currently running job, since it has no associated IBuild.

I found no lint. I'm firing off an EC2 run to make sure I didn't miss any tests.

Jeroen

To post a comment you must log in.
Revision history for this message
Brad Crittenden (bac) wrote :
Download full text (16.0 KiB)

Hi Jeroen,

Thanks for the nice agrarian branch.

Running your test command (was that just 'make check' in disguise?) I
got a failure on test_dispatchBuildToSlave.

Jeroen I tried on two different occasions to follow your instructions
in order to exercise the new UI. Both times were very time consuming
and ultimately failed. I give up. I approve the code but not the UI
-- you needed to get a separate UI review anyway but as a code
reviewer I like to have a once-over.

> === modified file 'lib/canonical/launchpad/security.py'
> --- lib/canonical/launchpad/security.py 2010-03-12 08:59:36 +0000
> +++ lib/canonical/launchpad/security.py 2010-03-16 11:13:21 +0000
> @@ -1505,6 +1508,41 @@
> return auth_spr.checkUnauthenticated()
>
>
> +class ViewBuildFarmJob(AuthorizationBase):
> + """Permission to view an `IBuildFarmJob`.
> +
> + This permission is based entirely on permission to view the
> + associated `IBuild` and/or `IBranch`.
> + """
> + permission = 'launchpad.View'
> + usedfor = IBuildFarmJob
> +
> + def _getBuildPermission(self):
> + return ViewBuildRecord(self.obj.build)
> +
> + def _checkBranchVisibility(self, user=None):
> + """Is the user free to view any branches associated with this job?"""
> + if not IBuildFarmBranchJob.providedBy(self.obj):
> + return True
> + else:
> + return self.obj.branch.visibleByUser(user)

I don't like the inverted test (our coding standards recommend against
having a 'not' test if both paths are present) and the fact a return
value of True has dual meanings.

> + def checkAuthenticated(self, user):
> + if not self._checkBranchVisibility(user):
> + return False
> +
> + if IBuildFarmBuildJob.providedBy(self.obj):
> + if not self._getBuildPermission().checkAuthenticated(user):
> + return False
> +
> + return True

Could you write a single method that provides checkAuthenticated and
checkUnauthenticated for your two possiblities? A nice little wrapper
would make the real methods more readable.

> + def checkUnauthenticated(self):
> + if not self._checkBranchVisibility():
> + return False
> + return self._getBuildPermission().checkUnauthenticated()

Again I find this implementation confusing.

> +
> class AdminQuestion(AdminByAdminsTeam):
> permission = 'launchpad.Admin'
> usedfor = IQuestion

> === modified file 'lib/canonical/launchpad/webapp/tales.py'
> --- lib/canonical/launchpad/webapp/tales.py 2010-03-10 12:50:18 +0000
> +++ lib/canonical/launchpad/webapp/tales.py 2010-03-16 11:13:21 +0000
> @@ -1514,6 +1514,33 @@
> return url
>
>
> +class BuildBaseFormatterAPI(ObjectFormatterAPI):
> + """Adapter providing fmt support for `IBuildBase` objects."""

You have plenty of room so please spell out formatter.

> + def _composeArchiveReference(self, archive):
> + if archive.is_ppa:
> + return " [%s/%s]" % (
> + cgi.escape(archive.owner.name), cgi.escape(archive.name))
> + else:
> + return ""
> +
> + def icon(self, view_name):
> + if not check_permi...

review: Approve (code)
Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :
Download full text (11.1 KiB)

Hi Brad,

This is a really good review. Thanks for that! It does show that I was past my saturation point for this branch, doesn't it?

> Running your test command (was that just 'make check' in disguise?) I
> got a failure on test_dispatchBuildToSlave.

Hmm... my EC2 test came through clean though, as do my manual runs. You don't happen to have a log of this failure somewhere, do you? I wouldn't normally ask, but IIRC you were the one who wrote the retest tool, so you may have a more reliable habit of keeping them.

> Jeroen I tried on two different occasions to follow your instructions
> in order to exercise the new UI. Both times were very time consuming
> and ultimately failed. I give up. I approve the code but not the UI
> -- you needed to get a separate UI review anyway but as a code
> reviewer I like to have a once-over.

Alright. Sorry to put you to the trouble; this is indeed very hard work and it's been slowing me down no end. If you have any notes at all about what made it hard, please let me know so I can improve the documentation. I believe it's important to us to make this accessible to encourage development.

> > === modified file 'lib/canonical/launchpad/security.py'
> > --- lib/canonical/launchpad/security.py 2010-03-12 08:59:36 +0000
> > +++ lib/canonical/launchpad/security.py 2010-03-16 11:13:21 +0000
> > @@ -1505,6 +1508,41 @@
> > return auth_spr.checkUnauthenticated()
> >
> >
> > +class ViewBuildFarmJob(AuthorizationBase):
> > + """Permission to view an `IBuildFarmJob`.
> > +
> > + This permission is based entirely on permission to view the
> > + associated `IBuild` and/or `IBranch`.
> > + """
> > + permission = 'launchpad.View'
> > + usedfor = IBuildFarmJob
> > +
> > + def _getBuildPermission(self):
> > + return ViewBuildRecord(self.obj.build)
> > +
> > + def _checkBranchVisibility(self, user=None):
> > + """Is the user free to view any branches associated with this
> job?"""
> > + if not IBuildFarmBranchJob.providedBy(self.obj):
> > + return True
> > + else:
> > + return self.obj.branch.visibleByUser(user)
>
> I don't like the inverted test (our coding standards recommend against
> having a 'not' test if both paths are present) and the fact a return
> value of True has dual meanings.

You're right, of course. This is a missing final cleanup after various refactorings. I put the interface checks in separate "get {IBranch,IBuildBase} for this job if it has one" methods to isolate the complexity.

> > + def checkAuthenticated(self, user):
> > + if not self._checkBranchVisibility(user):
> > + return False
> > +
> > + if IBuildFarmBuildJob.providedBy(self.obj):
> > + if not self._getBuildPermission().checkAuthenticated(user):
> > + return False
> > +
> > + return True
>
> Could you write a single method that provides checkAuthenticated and
> checkUnauthenticated for your two possiblities? A nice little wrapper
> would make the real methods more readable.

TBH I still don't see why these security classes don't work like that in the first place. What cou...

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

Went over this with wgrant, and (IIRC) noodles & bigjools. The conclusion was that the status icon for regular builds is now in the wrong place; even though it's the build status, it's shown in the UI as the builder status. And since I was traveling, wgrant has already landed a fix.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/canonical/launchpad/interfaces/_schema_circular_imports.py'
2--- lib/canonical/launchpad/interfaces/_schema_circular_imports.py 2010-03-06 04:57:40 +0000
3+++ lib/canonical/launchpad/interfaces/_schema_circular_imports.py 2010-03-17 06:01:34 +0000
4@@ -322,7 +322,8 @@
5 IStructuralSubscriptionTarget)
6
7 patch_reference_property(
8- ISourcePackageRelease, 'source_package_recipe_build', ISourcePackageRecipeBuild)
9+ ISourcePackageRelease, 'source_package_recipe_build',
10+ ISourcePackageRecipeBuild)
11
12 # IHasBugs
13 patch_plain_parameter_type(
14
15=== modified file 'lib/canonical/launchpad/security.py'
16--- lib/canonical/launchpad/security.py 2010-03-16 10:47:00 +0000
17+++ lib/canonical/launchpad/security.py 2010-03-17 06:01:34 +0000
18@@ -1,4 +1,4 @@
19-# Copyright 2009 Canonical Ltd. This software is licensed under the
20+# Copyright 2009-2010 Canonical Ltd. This software is licensed under the
21 # GNU Affero General Public License version 3 (see the file LICENSE).
22
23 """Security policies for using content objects."""
24@@ -32,8 +32,9 @@
25 from lp.bugs.interfaces.bugnomination import IBugNomination
26 from lp.bugs.interfaces.bugsubscription import IBugSubscription
27 from lp.bugs.interfaces.bugtracker import IBugTracker
28-from lp.soyuz.interfaces.build import IBuild
29 from lp.buildmaster.interfaces.builder import IBuilder, IBuilderSet
30+from lp.buildmaster.interfaces.buildfarmbranchjob import IBuildFarmBranchJob
31+from lp.buildmaster.interfaces.buildfarmjob import IBuildFarmJob
32 from lp.code.interfaces.codeimport import ICodeImport
33 from lp.code.interfaces.codeimportjob import (
34 ICodeImportJobSet, ICodeImportJobWorkflow)
35@@ -70,6 +71,8 @@
36 from lp.soyuz.interfaces.packageset import IPackageset, IPackagesetSet
37 from lp.translations.interfaces.pofile import IPOFile
38 from lp.translations.interfaces.potemplate import IPOTemplate
39+from lp.soyuz.interfaces.build import IBuild
40+from lp.soyuz.interfaces.buildfarmbuildjob import IBuildFarmBuildJob
41 from lp.soyuz.interfaces.publishing import (
42 IBinaryPackagePublishingHistory, IPublishingEdit,
43 ISourcePackagePublishingHistory)
44@@ -1508,6 +1511,53 @@
45 return auth_spr.checkUnauthenticated()
46
47
48+class ViewBuildFarmJob(AuthorizationBase):
49+ """Permission to view an `IBuildFarmJob`.
50+
51+ This permission is based entirely on permission to view the
52+ associated `IBuild` and/or `IBranch`.
53+ """
54+ permission = 'launchpad.View'
55+ usedfor = IBuildFarmJob
56+
57+ def _getBranch(self):
58+ """Get `IBranch` associated with this job, if any."""
59+ if IBuildFarmBranchJob.providedBy(self.obj):
60+ return self.obj.branch
61+ else:
62+ return None
63+
64+ def _getBuild(self):
65+ """Get `IBuildBase` associated with this job, if any."""
66+ if IBuildFarmBuildJob.providedBy(self.obj):
67+ return self.obj.build
68+ else:
69+ return None
70+
71+ def _checkBuildPermission(self, user=None):
72+ """Check access to `IBuildBase` for this job."""
73+ permission = ViewBuildRecord(self.obj.build)
74+ if user is None:
75+ return permission.checkUnauthenticated()
76+ else:
77+ return permission.checkAuthenticated(user)
78+
79+ def _checkAccess(self, user=None):
80+ """Unified access check for anonymous and authenticated users."""
81+ branch = self._getBranch()
82+ if branch is not None and not branch.visibleByUser(user):
83+ return False
84+
85+ build = self._getBuild()
86+ if build is not None and not self._checkBuildPermission(user):
87+ return False
88+
89+ return True
90+
91+ checkAuthenticated = _checkAccess
92+ checkUnauthenticated = _checkAccess
93+
94+
95 class AdminQuestion(AdminByAdminsTeam):
96 permission = 'launchpad.Admin'
97 usedfor = IQuestion
98
99=== modified file 'lib/canonical/launchpad/webapp/configure.zcml'
100--- lib/canonical/launchpad/webapp/configure.zcml 2010-02-17 11:13:06 +0000
101+++ lib/canonical/launchpad/webapp/configure.zcml 2010-03-17 06:01:34 +0000
102@@ -426,6 +426,13 @@
103 />
104
105 <adapter
106+ for="lp.buildmaster.interfaces.buildbase.IBuildBase"
107+ provides="zope.traversing.interfaces.IPathAdapter"
108+ factory="canonical.launchpad.webapp.tales.BuildBaseFormatterAPI"
109+ name="fmt"
110+ />
111+
112+ <adapter
113 for="lp.code.interfaces.codeimportmachine.ICodeImportMachine"
114 provides="zope.traversing.interfaces.IPathAdapter"
115 factory="canonical.launchpad.webapp.tales.CodeImportMachineFormatterAPI"
116
117=== modified file 'lib/canonical/launchpad/webapp/tales.py'
118--- lib/canonical/launchpad/webapp/tales.py 2010-03-10 12:50:18 +0000
119+++ lib/canonical/launchpad/webapp/tales.py 2010-03-17 06:01:34 +0000
120@@ -1514,6 +1514,33 @@
121 return url
122
123
124+class BuildBaseFormatterAPI(ObjectFormatterAPI):
125+ """Adapter providing fmt support for `IBuildBase` objects."""
126+ def _composeArchiveReference(self, archive):
127+ if archive.is_ppa:
128+ return " [%s/%s]" % (
129+ cgi.escape(archive.owner.name), cgi.escape(archive.name))
130+ else:
131+ return ""
132+
133+ def icon(self, view_name):
134+ if not check_permission('launchpad.View', self._context):
135+ return '<img src="/@@/processing" alt="[build]" />'
136+
137+ return BuildImageDisplayAPI(self._context).icon()
138+
139+ def link(self, view_name, rootsite=None):
140+ icon = self.icon(view_name)
141+ build = self._context
142+ if not check_permission('launchpad.View', build):
143+ return '%s private source' % icon
144+
145+ url = self.url(view_name=view_name, rootsite=rootsite)
146+ title = cgi.escape(build.title)
147+ archive = self._composeArchiveReference(build.archive)
148+ return '<a href="%s">%s%s</a>%s' % (url, icon, title, archive)
149+
150+
151 class CodeImportMachineFormatterAPI(CustomizableFormatter):
152 """Adapter providing fmt support for CodeImport objects"""
153
154
155=== added file 'lib/lp/buildmaster/interfaces/buildfarmbranchjob.py'
156--- lib/lp/buildmaster/interfaces/buildfarmbranchjob.py 1970-01-01 00:00:00 +0000
157+++ lib/lp/buildmaster/interfaces/buildfarmbranchjob.py 2010-03-17 06:01:34 +0000
158@@ -0,0 +1,21 @@
159+# Copyright 2010 Canonical Ltd. This software is licensed under the
160+# GNU Affero General Public License version 3 (see the file LICENSE).
161+
162+"""Interface for `IBuildFarmJob`s that are also `IBranchJob`s."""
163+
164+__metaclass__ = type
165+__all__ = [
166+ 'IBuildFarmBranchJob'
167+ ]
168+
169+from lp.buildmaster.interfaces.buildfarmjob import IBuildFarmJob
170+from lp.code.interfaces.branchjob import IBranchJob
171+
172+
173+class IBuildFarmBranchJob(IBuildFarmJob, IBranchJob):
174+ """An `IBuildFarmJob` that's also an `IBranchJob`.
175+
176+ Use this interface for `IBuildFarmJob` implementations that do not
177+ have a "build" attribute but do implement `IBranchJob`, so that the
178+ UI can render appropriate status information.
179+ """
180
181=== modified file 'lib/lp/buildmaster/interfaces/buildfarmjob.py'
182--- lib/lp/buildmaster/interfaces/buildfarmjob.py 2010-01-30 05:27:48 +0000
183+++ lib/lp/buildmaster/interfaces/buildfarmjob.py 2010-03-17 06:01:34 +0000
184@@ -91,7 +91,6 @@
185 "return None."))
186
187
188-
189 class ISpecificBuildFarmJobClass(Interface):
190 """Class interface provided by `IBuildFarmJob` classes.
191
192
193=== modified file 'lib/lp/buildmaster/interfaces/buildqueue.py'
194--- lib/lp/buildmaster/interfaces/buildqueue.py 2010-03-05 13:52:32 +0000
195+++ lib/lp/buildmaster/interfaces/buildqueue.py 2010-03-17 06:01:34 +0000
196@@ -72,6 +72,10 @@
197 title=_("Estimated Job Duration"), required=True,
198 description=_("Estimated job duration interval."))
199
200+ current_build_duration = Timedelta(
201+ title=_("Current build duration"), required=False,
202+ description=_("Time spent building so far."))
203+
204 def manualScore(value):
205 """Manually set a score value to a queue item and lock it."""
206
207
208=== modified file 'lib/lp/buildmaster/model/buildqueue.py'
209--- lib/lp/buildmaster/model/buildqueue.py 2010-03-08 12:53:59 +0000
210+++ lib/lp/buildmaster/model/buildqueue.py 2010-03-17 06:01:34 +0000
211@@ -14,6 +14,7 @@
212 from collections import defaultdict
213 from datetime import datetime, timedelta
214 import logging
215+import pytz
216
217 from sqlobject import (
218 StringCol, ForeignKey, BoolCol, IntCol, IntervalCol, SQLObjectNotFound)
219@@ -118,6 +119,15 @@
220 """See `IBuildQueue`."""
221 return self.job.date_started
222
223+ @property
224+ def current_build_duration(self):
225+ """See `IBuildQueue`."""
226+ date_started = self.date_started
227+ if date_started is None:
228+ return None
229+ else:
230+ return self._now() - date_started
231+
232 def destroySelf(self):
233 """Remove this record and associated job/specific_job."""
234 job = self.job
235@@ -464,8 +474,8 @@
236
237 @staticmethod
238 def _now():
239- """Provide utcnow() while allowing test code to monkey-patch this."""
240- return datetime.utcnow()
241+ """Return current time (UTC). Overridable for test purposes."""
242+ return datetime.now(pytz.UTC)
243
244
245 class BuildQueueSet(object):
246
247=== modified file 'lib/lp/buildmaster/tests/test_buildqueue.py'
248--- lib/lp/buildmaster/tests/test_buildqueue.py 2010-03-10 21:30:57 +0000
249+++ lib/lp/buildmaster/tests/test_buildqueue.py 2010-03-17 06:01:34 +0000
250@@ -1,4 +1,4 @@
251-# Copyright 2009 Canonical Ltd. This software is licensed under the
252+# Copyright 2009-2010 Canonical Ltd. This software is licensed under the
253 # GNU Affero General Public License version 3 (see the file LICENSE).
254 # pylint: disable-msg=C0324
255
256@@ -6,13 +6,14 @@
257
258 from datetime import datetime, timedelta
259 from pytz import utc
260+from unittest import TestLoader
261
262 from zope.component import getUtility
263 from zope.interface.verify import verifyObject
264
265 from canonical.launchpad.webapp.interfaces import (
266 IStoreSelector, MAIN_STORE, DEFAULT_FLAVOR)
267-from canonical.testing import LaunchpadZopelessLayer
268+from canonical.testing import LaunchpadZopelessLayer, ZopelessDatabaseLayer
269
270 from lp.buildmaster.interfaces.buildbase import BuildStatus
271 from lp.buildmaster.interfaces.builder import IBuilderSet
272@@ -28,6 +29,7 @@
273 from lp.soyuz.model.build import Build
274 from lp.soyuz.tests.test_publishing import SoyuzTestPublisher
275 from lp.testing import TestCaseWithFactory
276+from lp.testing.fakemethod import FakeMethod
277
278
279 def find_job(test, name, processor='386'):
280@@ -140,10 +142,12 @@
281 This avoids spurious test failures.
282 """
283 # Use the date/time the job started if available.
284- time_stamp = buildqueue.job.date_started
285- if not time_stamp:
286- time_stamp = datetime.now(utc)
287- buildqueue._now = lambda: time_stamp
288+ if buildqueue.job.date_started:
289+ time_stamp = buildqueue.job.date_started
290+ else:
291+ time_stamp = buildqueue._now()
292+
293+ buildqueue._now = FakeMethod(result=time_stamp)
294 return time_stamp
295
296
297@@ -209,7 +213,6 @@
298 "An active build job must have a builder.")
299
300
301-
302 class TestBuildQueueBase(TestCaseWithFactory):
303 """Setup the test publisher and some builders."""
304
305@@ -605,6 +608,7 @@
306 # fact that no builders capable of running the job are available.
307 check_mintime_to_builder(self, job, 0)
308
309+
310 class MultiArchBuildsBase(TestBuildQueueBase):
311 """Set up a test environment with builds and multiple processors."""
312 def setUp(self):
313@@ -805,6 +809,27 @@
314 check_mintime_to_builder(self, apg_job, 0)
315
316
317+class TestBuildQueueDuration(TestCaseWithFactory):
318+ layer = ZopelessDatabaseLayer
319+
320+ def _makeBuildQueue(self):
321+ """Produce a `BuildQueue` object to test."""
322+ return self.factory.makeSourcePackageRecipeBuildJob()
323+
324+ def test_current_build_duration_not_started(self):
325+ buildqueue = self._makeBuildQueue()
326+ self.assertEqual(None, buildqueue.current_build_duration)
327+
328+ def test_current_build_duration(self):
329+ buildqueue = self._makeBuildQueue()
330+ now = buildqueue._now()
331+ buildqueue._now = FakeMethod(result=now)
332+ age = timedelta(minutes=3)
333+ buildqueue.job.date_started = now - age
334+
335+ self.assertEqual(age, buildqueue.current_build_duration)
336+
337+
338 class TestJobClasses(TestCaseWithFactory):
339 """Tests covering build farm job type classes."""
340 layer = LaunchpadZopelessLayer
341@@ -1301,3 +1326,7 @@
342 assign_to_builder(self, 'xxr-daptup', 1, None)
343 postgres_build, postgres_job = find_job(self, 'postgres', '386')
344 check_estimate(self, postgres_job, 120)
345+
346+
347+def test_suite():
348+ return TestLoader().loadTestsFromName(__name__)
349
350=== modified file 'lib/lp/code/interfaces/sourcepackagerecipebuild.py'
351--- lib/lp/code/interfaces/sourcepackagerecipebuild.py 2010-02-19 06:34:18 +0000
352+++ lib/lp/code/interfaces/sourcepackagerecipebuild.py 2010-03-17 06:01:34 +0000
353@@ -21,7 +21,7 @@
354 from canonical.launchpad import _
355
356 from lp.buildmaster.interfaces.buildbase import IBuildBase
357-from lp.buildmaster.interfaces.buildfarmjob import IBuildFarmJob
358+from lp.soyuz.interfaces.buildfarmbuildjob import IBuildFarmBuildJob
359 from lp.code.interfaces.sourcepackagerecipe import ISourcePackageRecipe
360 from lp.registry.interfaces.person import IPerson
361 from lp.registry.interfaces.distroseries import IDistroSeries
362@@ -79,18 +79,13 @@
363 """
364
365
366-class ISourcePackageRecipeBuildJob(IBuildFarmJob):
367+class ISourcePackageRecipeBuildJob(IBuildFarmBuildJob):
368 """A read-only interface for recipe build jobs."""
369
370 job = Reference(
371 IJob, title=_("Job"), required=True, readonly=True,
372 description=_("Data common to all job types."))
373
374- build = Reference(
375- ISourcePackageRecipeBuild, title=_("Build"),
376- required=True, readonly=True,
377- description=_("Build record associated with this job."))
378-
379
380 class ISourcePackageRecipeBuildJobSource(Interface):
381 """A utility of this interface used to create _things_."""
382
383=== modified file 'lib/lp/soyuz/browser/build.py'
384--- lib/lp/soyuz/browser/build.py 2010-03-10 13:28:55 +0000
385+++ lib/lp/soyuz/browser/build.py 2010-03-17 06:01:34 +0000
386@@ -525,4 +525,3 @@
387 @property
388 def no_results(self):
389 return self.form_submitted and not self.complete_builds
390-
391
392=== modified file 'lib/lp/soyuz/browser/builder.py'
393--- lib/lp/soyuz/browser/builder.py 2010-03-08 05:30:13 +0000
394+++ lib/lp/soyuz/browser/builder.py 2010-03-17 06:01:34 +0000
395@@ -18,9 +18,7 @@
396 'BuilderView',
397 ]
398
399-import datetime
400 import operator
401-import pytz
402
403 from zope.component import getUtility
404 from zope.event import notify
405@@ -235,14 +233,10 @@
406
407 @property
408 def current_build_duration(self):
409- """Return the delta representing the duration of the current job."""
410- if (self.context.currentjob is None or
411- self.context.currentjob.date_started is None):
412+ if self.context.currentjob is None:
413 return None
414 else:
415- UTC = pytz.timezone('UTC')
416- date_started = self.context.currentjob.date_started
417- return datetime.datetime.now(UTC) - date_started
418+ return self.context.currentjob.current_build_duration
419
420 @property
421 def page_title(self):
422
423=== modified file 'lib/lp/soyuz/browser/configure.zcml'
424--- lib/lp/soyuz/browser/configure.zcml 2010-03-08 05:30:13 +0000
425+++ lib/lp/soyuz/browser/configure.zcml 2010-03-17 06:01:34 +0000
426@@ -1,4 +1,4 @@
427-<!-- Copyright 2009 Canonical Ltd. This software is licensed under the
428+<!-- Copyright 2009-2010 Canonical Ltd. This software is licensed under the
429 GNU Affero General Public License version 3 (see the file LICENSE).
430 -->
431
432@@ -798,4 +798,34 @@
433 path_expression="string:+dependency/${dependency/id}"
434 attribute_to_parent="archive"
435 />
436+
437+ <!-- XXX JeroenVermeulen 2010-03-13 bug=539395: This is buildmaster, not soyuz -->
438+ <browser:page
439+ for="lp.buildmaster.interfaces.buildqueue.IBuildQueue"
440+ name="+current"
441+ class="canonical.launchpad.webapp.publisher.LaunchpadView"
442+ facet="overview"
443+ permission="launchpad.View"
444+ template="../templates/buildqueue-current.pt"/>
445+ <browser:page
446+ for="lp.buildmaster.interfaces.buildqueue.IBuildFarmJob"
447+ name="+current"
448+ class="canonical.launchpad.webapp.publisher.LaunchpadView"
449+ facet="overview"
450+ permission="launchpad.View"
451+ template="../templates/buildfarmjob-current.pt"/>
452+ <browser:page
453+ for="lp.buildmaster.interfaces.buildfarmbranchjob.IBuildFarmBranchJob"
454+ name="+current"
455+ class="canonical.launchpad.webapp.publisher.LaunchpadView"
456+ facet="overview"
457+ permission="launchpad.View"
458+ template="../templates/buildfarmbranchjob-current.pt"/>
459+ <browser:page
460+ for="lp.soyuz.interfaces.buildfarmbuildjob.IBuildFarmBuildJob"
461+ name="+current"
462+ class="canonical.launchpad.webapp.publisher.LaunchpadView"
463+ facet="overview"
464+ permission="launchpad.View"
465+ template="../templates/buildfarmbuildjob-current.pt"/>
466 </configure>
467
468=== modified file 'lib/lp/soyuz/doc/build-estimated-dispatch-time.txt'
469--- lib/lp/soyuz/doc/build-estimated-dispatch-time.txt 2010-03-06 04:57:40 +0000
470+++ lib/lp/soyuz/doc/build-estimated-dispatch-time.txt 2010-03-17 06:01:34 +0000
471@@ -56,7 +56,6 @@
472
473 >>> from datetime import datetime
474 >>> import pytz
475- >>> UTC = pytz.timezone('UTC')
476 >>> bob_the_builder = builder_set.get(1)
477 >>> cur_bqueue = bob_the_builder.currentjob
478 >>> from lp.soyuz.interfaces.build import IBuildSet
479@@ -76,7 +75,7 @@
480 >>> from zope.security.proxy import removeSecurityProxy
481 >>> cur_bqueue.lastscore = 1111
482 >>> cur_bqueue.setDateStarted(
483- ... datetime(2008, 4, 1, 10, 45, 39, tzinfo=UTC))
484+ ... datetime(2008, 4, 1, 10, 45, 39, tzinfo=pytz.UTC))
485 >>> print cur_bqueue.date_started
486 2008-04-01 10:45:39+00:00
487
488@@ -89,7 +88,7 @@
489 The estimated start time for the pending job is either now or lies
490 in the future.
491
492- >>> now = datetime.utcnow()
493+ >>> now = datetime.now(pytz.UTC)
494 >>> def job_start_estimate(build):
495 ... return build.buildqueue_record.getEstimatedJobStartTime()
496 >>> estimate = job_start_estimate(alsa_build)
497@@ -147,7 +146,7 @@
498 Since the 'iceweasel' build has a higher score (666) than the 'pmount'
499 build (66) its estimated dispatch time is essentially "now".
500
501- >>> now = datetime.utcnow()
502+ >>> now = datetime.now(pytz.UTC)
503 >>> estimate = job_start_estimate(iceweasel_build)
504 >>> estimate > now
505 True
506
507=== added file 'lib/lp/soyuz/interfaces/buildfarmbuildjob.py'
508--- lib/lp/soyuz/interfaces/buildfarmbuildjob.py 1970-01-01 00:00:00 +0000
509+++ lib/lp/soyuz/interfaces/buildfarmbuildjob.py 2010-03-17 06:01:34 +0000
510@@ -0,0 +1,21 @@
511+# Copyright 2010 Canonical Ltd. This software is licensed under the
512+# GNU Affero General Public License version 3 (see the file LICENSE).
513+
514+"""Interface to support UI for most build-farm jobs."""
515+
516+__metaclass__ = type
517+__all__ = [
518+ 'IBuildFarmBuildJob'
519+ ]
520+
521+from canonical.launchpad import _
522+from lazr.restful.fields import Reference
523+from lp.buildmaster.interfaces.buildfarmjob import IBuildFarmJob
524+from lp.soyuz.interfaces.build import IBuild
525+
526+
527+class IBuildFarmBuildJob(IBuildFarmJob):
528+ """An `IBuildFarmJob` with an `IBuild` reference."""
529+ build = Reference(
530+ IBuild, title=_("Build"), required=True, readonly=True,
531+ description=_("Build record associated with this job."))
532
533=== modified file 'lib/lp/soyuz/interfaces/buildpackagejob.py'
534--- lib/lp/soyuz/interfaces/buildpackagejob.py 2010-01-20 22:09:26 +0000
535+++ lib/lp/soyuz/interfaces/buildpackagejob.py 2010-03-17 06:01:34 +0000
536@@ -15,12 +15,12 @@
537
538 from canonical.launchpad import _
539 from lazr.restful.fields import Reference
540-from lp.buildmaster.interfaces.buildfarmjob import IBuildFarmJob
541 from lp.services.job.interfaces.job import IJob
542 from lp.soyuz.interfaces.build import IBuild
543-
544-
545-class IBuildPackageJob(IBuildFarmJob):
546+from lp.soyuz.interfaces.buildfarmbuildjob import IBuildFarmBuildJob
547+
548+
549+class IBuildPackageJob(IBuildFarmBuildJob):
550 """A read-only interface for build package jobs."""
551
552 id = Int(title=_('ID'), required=True, readonly=True)
553
554=== modified file 'lib/lp/soyuz/templates/builder-index.pt'
555--- lib/lp/soyuz/templates/builder-index.pt 2009-11-12 16:19:18 +0000
556+++ lib/lp/soyuz/templates/builder-index.pt 2010-03-17 06:01:34 +0000
557@@ -104,32 +104,10 @@
558 </tal:buildernok>
559 </tal:no_job>
560
561- <tal:comment replace="nothing">
562- In the very near future, 'job' will not just be a Build job.
563- The template needs to cope with that as and when new job types are
564- added.
565- </tal:comment>
566- <tal:job condition="job">
567+ <tal:job-header condition="job">
568 <span class="sortkey" tal:content="job/id" />
569- <tal:build define="build job/specific_job/build">
570- <tal:visible condition="build/required:launchpad.View">
571- <tal:icon replace="structure build/image:icon" />
572- Building
573- <a tal:attributes="href build/fmt:url"
574- tal:content="build/title"
575- >i386 build of mozilla-firefox 0.9 in ubuntu hoary RELEASE</a>
576- <tal:ppa condition="build/archive/is_ppa"
577- define="ppa build/archive;">
578- <span tal:replace="string: [${ppa/owner/name}/${ppa/name}]"
579- >[cprov/ppa]</span>
580- </tal:ppa>
581- </tal:visible>
582- <tal:restricted condition="not: build/required:launchpad.View">
583- <img src="/@@/processing" alt="[building]" />
584- Building private source
585- </tal:restricted>
586- </tal:build>
587- </tal:job>
588+ <tal:specific-job replace="structure job/specific_job/@@+current" />
589+ </tal:job-header>
590
591 </metal:macro>
592
593@@ -147,7 +125,7 @@
594 </span>
595 Current status</h2>
596
597- <p tal:define="builder context">
598+ <p tal:define="builder context" id="current-build-summary">
599 <metal:summary use-macro="template/macros/status-summary" />
600 </p>
601
602@@ -156,27 +134,7 @@
603 context/failnotes/fmt:text-to-html" />
604 </tal:buildernok>
605
606- <tal:job condition="job">
607- <p class="sprite">Started
608- <span tal:attributes="title job/job/date_started/fmt:datetime"
609- tal:content="view/current_build_duration/fmt:exactduration"
610- /> ago.</p>
611- <tal:visible
612- define="build job/specific_job/build"
613- condition="build/required:launchpad.View">
614- <tal:logtail condition="job/logtail">
615- <h3>Buildlog</h3>
616- <div tal:content="structure job/logtail/fmt:text-to-html"
617- id="buildlog-tail" class="logtail">
618- Things are crashing and burning all over the place.
619- </div>
620- <p class="discreet" tal:condition="view/user">
621- Updated on
622- <span tal:replace="structure view/user/fmt:local-time"/>
623- </p>
624- </tal:logtail>
625- </tal:visible>
626- </tal:job>
627+ <tal:job condition="job" replace="structure job/@@+current" />
628 </div>
629
630 </metal:macro>
631
632=== added file 'lib/lp/soyuz/templates/buildfarmbranchjob-current.pt'
633--- lib/lp/soyuz/templates/buildfarmbranchjob-current.pt 1970-01-01 00:00:00 +0000
634+++ lib/lp/soyuz/templates/buildfarmbranchjob-current.pt 2010-03-17 06:01:34 +0000
635@@ -0,0 +1,11 @@
636+<tal:root
637+ xmlns:tal="http://xml.zope.org/namespaces/tal"
638+ xmlns:metal="http://xml.zope.org/namespaces/metal"
639+ xmlns:i18n="http://xml.zope.org/namespaces/i18n"
640+ omit-tag="">
641+
642+ <img src="/@@/processing" alt="[building]" />
643+ Working on
644+ <tal:jobtype replace="context/__class__/__name__" />
645+ for branch <tal:branch replace="structure context/branch/fmt:link" />.
646+</tal:root>
647
648=== added file 'lib/lp/soyuz/templates/buildfarmbuildjob-current.pt'
649--- lib/lp/soyuz/templates/buildfarmbuildjob-current.pt 1970-01-01 00:00:00 +0000
650+++ lib/lp/soyuz/templates/buildfarmbuildjob-current.pt 2010-03-17 06:01:34 +0000
651@@ -0,0 +1,8 @@
652+<tal:root
653+ xmlns:tal="http://xml.zope.org/namespaces/tal"
654+ xmlns:metal="http://xml.zope.org/namespaces/metal"
655+ xmlns:i18n="http://xml.zope.org/namespaces/i18n"
656+ omit-tag="">
657+
658+ Building <tal:build replace="structure context/build/fmt:link" />
659+</tal:root>
660
661=== added file 'lib/lp/soyuz/templates/buildfarmjob-current.pt'
662--- lib/lp/soyuz/templates/buildfarmjob-current.pt 1970-01-01 00:00:00 +0000
663+++ lib/lp/soyuz/templates/buildfarmjob-current.pt 2010-03-17 06:01:34 +0000
664@@ -0,0 +1,10 @@
665+<tal:root
666+ xmlns:tal="http://xml.zope.org/namespaces/tal"
667+ xmlns:metal="http://xml.zope.org/namespaces/metal"
668+ xmlns:i18n="http://xml.zope.org/namespaces/i18n"
669+ omit-tag="">
670+
671+ <img src="/@@/processing" alt="[building]" />
672+ Working on
673+ <tal:jobtype replace="context/__class__/__name__" />.
674+</tal:root>
675
676=== added file 'lib/lp/soyuz/templates/buildqueue-current.pt'
677--- lib/lp/soyuz/templates/buildqueue-current.pt 1970-01-01 00:00:00 +0000
678+++ lib/lp/soyuz/templates/buildqueue-current.pt 2010-03-17 06:01:34 +0000
679@@ -0,0 +1,24 @@
680+<tal:root
681+ xmlns:tal="http://xml.zope.org/namespaces/tal"
682+ xmlns:metal="http://xml.zope.org/namespaces/metal"
683+ xmlns:i18n="http://xml.zope.org/namespaces/i18n"
684+ omit-tag="">
685+
686+ <p class="sprite">Started
687+ <span
688+ tal:attributes="title context/job/date_started/fmt:datetime"
689+ tal:content="context/current_build_duration/fmt:exactduration" />
690+ ago.
691+ </p>
692+ <tal:logtail condition="context/specific_job/required:launchpad.View">
693+ <h2>Buildlog</h2>
694+ <div tal:content="structure context/logtail/fmt:text-to-html"
695+ id="buildlog-tail"
696+ class="logtail">
697+ Things are crashing and burning all over the place.
698+ </div>
699+ <p class="discreet" tal:condition="view/user">
700+ Updated on <tal:date replace="structure view/user/fmt:local-time" />
701+ </p>
702+ </tal:logtail>
703+</tal:root>
704
705=== modified file 'lib/lp/translations/model/translationtemplatesbuildjob.py'
706--- lib/lp/translations/model/translationtemplatesbuildjob.py 2010-03-06 00:21:57 +0000
707+++ lib/lp/translations/model/translationtemplatesbuildjob.py 2010-03-17 06:01:34 +0000
708@@ -19,10 +19,11 @@
709 DEFAULT_FLAVOR, IStoreSelector, MAIN_STORE, MASTER_FLAVOR)
710
711 from lp.buildmaster.interfaces.buildfarmjob import (
712- BuildFarmJobType, IBuildFarmJob, ISpecificBuildFarmJobClass)
713+ BuildFarmJobType, ISpecificBuildFarmJobClass)
714 from lp.buildmaster.model.buildfarmjob import BuildFarmJob
715 from lp.buildmaster.model.buildqueue import BuildQueue
716-from lp.code.interfaces.branchjob import IBranchJob, IRosettaUploadJobSource
717+from lp.code.interfaces.branchjob import IRosettaUploadJobSource
718+from lp.buildmaster.interfaces.buildfarmbranchjob import IBuildFarmBranchJob
719 from lp.code.model.branchjob import BranchJob, BranchJobDerived, BranchJobType
720 from lp.translations.interfaces.translationtemplatesbuildjob import (
721 ITranslationTemplatesBuildJobSource)
722@@ -34,7 +35,7 @@
723
724 Implementation-wise, this is actually a `BranchJob`.
725 """
726- implements(IBranchJob, IBuildFarmJob)
727+ implements(IBuildFarmBranchJob)
728
729 class_job_type = BranchJobType.TRANSLATION_TEMPLATES_BUILD
730
731@@ -101,7 +102,7 @@
732 """See `ITranslationTemplatesBuildJobSource`."""
733 store = getUtility(IStoreSelector).get(MAIN_STORE, MASTER_FLAVOR)
734
735- # We don't have any JSON metadata for this BranchJob type.
736+ # Pass public HTTP URL for the branch.
737 metadata = {'branch_url': branch.composePublicURL()}
738 branch_job = BranchJob(
739 branch, BranchJobType.TRANSLATION_TEMPLATES_BUILD, metadata)
740
741=== added directory 'lib/lp/translations/stories/buildfarm'
742=== added file 'lib/lp/translations/stories/buildfarm/xx-build-summary.txt'
743--- lib/lp/translations/stories/buildfarm/xx-build-summary.txt 1970-01-01 00:00:00 +0000
744+++ lib/lp/translations/stories/buildfarm/xx-build-summary.txt 2010-03-17 06:01:34 +0000
745@@ -0,0 +1,69 @@
746+= TranslationTemplatesBuildJob Build Summary =
747+
748+The builders UI can show TranslationTemplateBuildJobs, although they
749+look a little different from Soyuz-style jobs.
750+
751+== Setup ==
752+
753+Create a builder working on a TranslationTemplatesBuildJob for a branch.
754+
755+ >>> from zope.component import getUtility
756+ >>> from canonical.launchpad.interfaces.launchpad import (
757+ ... ILaunchpadCelebrities)
758+ >>> from canonical.launchpad.interfaces.librarian import (
759+ ... ILibraryFileAliasSet)
760+ >>> from canonical.launchpad.scripts.logger import QuietFakeLogger
761+ >>> from lp.buildmaster.interfaces.buildqueue import IBuildQueueSet
762+ >>> from lp.testing.fakemethod import FakeMethod
763+ >>> from lp.translations.interfaces.translations import (
764+ ... TranslationsBranchImportMode)
765+
766+ >>> class FakeSlave:
767+ ... resume = FakeMethod(result=('stdout', 'stderr', 0))
768+ ... build = FakeMethod()
769+ ... cacheFile = FakeMethod()
770+
771+ >>> login(ANONYMOUS)
772+ >>> owner_email = factory.getUniqueString() + '@example.com'
773+ >>> owner = factory.makePerson(email=owner_email, password='test')
774+
775+ >>> productseries = factory.makeProductSeries(owner=owner)
776+ >>> product = productseries.product
777+ >>> product.official_rosetta = True
778+ >>> branch = factory.makeProductBranch(product=product, owner=owner)
779+ >>> branch_url = branch.unique_name
780+
781+ >>> productseries.branch = factory.makeBranch()
782+ >>> productseries.translations_autoimport_mode = (
783+ ... TranslationsBranchImportMode.IMPORT_TEMPLATES)
784+ >>> specific_job = factory.makeTranslationTemplatesBuildJob(branch=branch)
785+ >>> buildqueue = getUtility(IBuildQueueSet).getByJob(specific_job.job)
786+
787+ >>> fake_chroot = getUtility(ILibraryFileAliasSet)[1]
788+ >>> ubuntu = getUtility(ILaunchpadCelebrities).ubuntu
789+ >>> unused = ubuntu.currentseries.nominatedarchindep.addOrUpdateChroot(
790+ ... fake_chroot)
791+
792+ >>> builder = factory.makeBuilder(vm_host=factory.getUniqueString())
793+ >>> builder.setSlaveForTesting(FakeSlave())
794+ >>> builder.startBuild(buildqueue, QuietFakeLogger())
795+
796+ >>> builder_page = canonical_url(builder)
797+ >>> logout()
798+
799+Helper: find the current build's summary on a browser's current page.
800+
801+ >>> def find_build_summary(browser):
802+ ... return find_tag_by_id(browser.contents, 'current-build-summary')
803+
804+
805+== Show summary ==
806+
807+The job's summary shows that what type of job this is. It also links
808+to the branch.
809+
810+ >>> user_browser.open(builder_page)
811+ >>> print extract_text(find_build_summary(user_browser))
812+ Working on TranslationTemplatesBuildJob for branch ...
813+
814+ >>> user_browser.getLink(branch_url).click()