Merge lp:~thumper/launchpad/recipe-binary-builds into lp:launchpad

Proposed by Tim Penhey
Status: Merged
Approved by: Aaron Bentley
Approved revision: no longer in the source branch.
Merged at revision: 12000
Proposed branch: lp:~thumper/launchpad/recipe-binary-builds
Merge into: lp:launchpad
Diff against target: 435 lines (+189/-17)
8 files modified
lib/canonical/launchpad/doc/hierarchical-menu.txt (+6/-0)
lib/lp/code/browser/sourcepackagerecipebuild.py (+4/-3)
lib/lp/code/browser/tests/test_sourcepackagerecipe.py (+84/-5)
lib/lp/code/templates/sourcepackagerecipe-index.pt (+49/-4)
lib/lp/soyuz/browser/build.py (+33/-0)
lib/lp/soyuz/model/processor.py (+6/-0)
lib/lp/soyuz/templates/build-index.pt (+1/-2)
lib/lp/testing/factory.py (+6/-3)
To merge this branch: bzr merge lp:~thumper/launchpad/recipe-binary-builds
Reviewer Review Type Date Requested Status
Aaron Bentley (community) Approve
Paul Hummer (community) ui Approve
Review via email: mp+40686@code.launchpad.net

Commit message

[r=abentley][ui=rockstar][bug=602508] Show related binary builds on the recipe index page.

Description of the change

Adds binary build information to the latest builds section on the recipe index page.

http://people.canonical.com/~tim/recipe-latest-builds.png shows an example of how it looks.

I made some of the properties of the view that were accessed multiple times
cached properties. This required a few tweaks in the tests too.

The binary builds for the successful recipe builds are indented slightly and
show underneath the recipe build that they relate to.

During interactive hacking, I found that I wanted some nicer repr methods
for the processors.

A drive-by fix on the soyuz build-index.pt to just specify the content as
buildlog instead of using tal:content="string: buildlog"

To post a comment you must log in.
Revision history for this message
Paul Hummer (rockstar) wrote :

This looks really excellent!

review: Approve (ui)
Revision history for this message
Aaron Bentley (abentley) wrote :

You are showing the start time for binary builds, while the source package recipe builds show the end time. The time values in this column should have a consistent meaning.

review: Needs Fixing
Revision history for this message
Tim Penhey (thumper) wrote :

Aaron, I've gone with the expected completed time, and added the same properties to the BuildView. I feel that we should use a common mixin, but I can't think where to actually put the code.

My first thought was actually something like lp.shared.browser or lp.app.browser.shared...

Not sure.

Revision history for this message
Aaron Bentley (abentley) wrote :

Looks good. At some point, we might want to collapse eta and date into a single method call that returns a tuple.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/canonical/launchpad/doc/hierarchical-menu.txt'
2--- lib/canonical/launchpad/doc/hierarchical-menu.txt 2010-10-18 22:24:59 +0000
3+++ lib/canonical/launchpad/doc/hierarchical-menu.txt 2010-11-30 01:38:20 +0000
4@@ -90,6 +90,7 @@
5 # have to create fake views and other stuff to test breadcrumbs here. The
6 # functionality provided by that method is tested in
7 # webapp/tests/test_breadcrumbs.py.
8+ >>> make_breadcrumb_func = Hierarchy.makeBreadcrumbForRequestedPage
9 >>> Hierarchy.makeBreadcrumbForRequestedPage = lambda self: None
10
11 # Note that the Hierarchy assigns the breadcrumb's URL, but we need to
12@@ -269,3 +270,8 @@
13
14 >>> homepage_hierarchy.render().strip()
15 u''
16+
17+
18+Put the monkey patched method back.
19+
20+ >>> Hierarchy.makeBreadcrumbForRequestedPage = make_breadcrumb_func
21
22=== modified file 'lib/lp/code/browser/sourcepackagerecipebuild.py'
23--- lib/lp/code/browser/sourcepackagerecipebuild.py 2010-11-23 23:22:27 +0000
24+++ lib/lp/code/browser/sourcepackagerecipebuild.py 2010-11-30 01:38:20 +0000
25@@ -34,6 +34,7 @@
26 ISourcePackageRecipeBuild,
27 )
28 from lp.services.job.interfaces.job import JobStatus
29+from lp.services.propertycache import cachedproperty
30
31
32 UNEDITABLE_BUILD_STATES = (
33@@ -95,7 +96,7 @@
34 BuildStatus.FAILEDTOUPLOAD: 'Could not be uploaded correctly',
35 }.get(self.context.status, self.context.status.title)
36
37- @property
38+ @cachedproperty
39 def eta(self):
40 """The datetime when the build job is estimated to complete.
41
42@@ -114,14 +115,14 @@
43 duration = queue_record.estimated_duration
44 return start_time + duration
45
46- @property
47+ @cachedproperty
48 def date(self):
49 """The date when the build completed or is estimated to complete."""
50 if self.estimate:
51 return self.eta
52 return self.context.date_finished
53
54- @property
55+ @cachedproperty
56 def estimate(self):
57 """If true, the date value is an estimate."""
58 if self.context.date_finished is not None:
59
60=== modified file 'lib/lp/code/browser/tests/test_sourcepackagerecipe.py'
61--- lib/lp/code/browser/tests/test_sourcepackagerecipe.py 2010-11-24 18:40:48 +0000
62+++ lib/lp/code/browser/tests/test_sourcepackagerecipe.py 2010-11-30 01:38:20 +0000
63@@ -42,6 +42,7 @@
64 from lp.code.interfaces.sourcepackagerecipe import MINIMAL_RECIPE_TEXT
65 from lp.code.tests.helpers import recipe_parser_newest_version
66 from lp.registry.interfaces.pocket import PackagePublishingPocket
67+from lp.services.propertycache import clear_property_cache
68 from lp.soyuz.model.processor import ProcessorFamily
69 from lp.testing import (
70 ANONYMOUS,
71@@ -691,7 +692,7 @@
72
73 class TestSourcePackageRecipeView(TestCaseForRecipe):
74
75- layer = DatabaseFunctionalLayer
76+ layer = LaunchpadFunctionalLayer
77
78 def test_index(self):
79 recipe = self.makeRecipe()
80@@ -716,7 +717,7 @@
81 Distribution series: Secret Squirrel
82
83 Latest builds
84- Status Time Distribution series Archive
85+ Status When complete Distribution series Archive
86 Successful build on 2010-03-16 Secret Squirrel Secret PPA
87 Request build\(s\)
88
89@@ -724,12 +725,86 @@
90 # bzr-builder format 0.2 deb-version {debupstream}-0~{revno}
91 lp://dev/~chef/chocolate/cake""", self.getMainText(recipe))
92
93+ def test_index_success_with_buildlog(self):
94+ # The buildlog is shown if it is there.
95+ recipe = self.makeRecipe()
96+ build = removeSecurityProxy(self.factory.makeSourcePackageRecipeBuild(
97+ recipe=recipe, distroseries=self.squirrel, archive=self.ppa))
98+ build.status = BuildStatus.FULLYBUILT
99+ build.date_started = datetime(2010, 03, 16, tzinfo=utc)
100+ build.date_finished = datetime(2010, 03, 16, tzinfo=utc)
101+ build.log = self.factory.makeLibraryFileAlias()
102+
103+ self.assertTextMatchesExpressionIgnoreWhitespace("""\
104+ Latest builds
105+ Status .* Archive
106+ Successful build on 2010-03-16 buildlog \(.*\) Secret Squirrel Secret PPA
107+ Request build\(s\)""", self.getMainText(recipe))
108+
109+ def test_index_success_with_binary_builds(self):
110+ # Binary builds are shown after the recipe builds if there are any.
111+ recipe = self.makeRecipe()
112+ build = removeSecurityProxy(self.factory.makeSourcePackageRecipeBuild(
113+ recipe=recipe, distroseries=self.squirrel, archive=self.ppa))
114+ build.status = BuildStatus.FULLYBUILT
115+ build.date_started = datetime(2010, 03, 16, tzinfo=utc)
116+ build.date_finished = datetime(2010, 03, 16, tzinfo=utc)
117+ build.log = self.factory.makeLibraryFileAlias()
118+ package_name = self.factory.getOrMakeSourcePackageName('chocolate')
119+ source_package_release = self.factory.makeSourcePackageRelease(
120+ archive=self.ppa, sourcepackagename=package_name, distroseries=self.squirrel,
121+ source_package_recipe_build=build, version='0+r42')
122+ builder = self.factory.makeBuilder()
123+ binary_build = self.factory.makeBinaryPackageBuild(
124+ source_package_release=source_package_release,
125+ distroarchseries=self.squirrel.nominatedarchindep,
126+ processor=builder.processor)
127+ binary_build.queueBuild()
128+
129+ self.assertTextMatchesExpressionIgnoreWhitespace("""\
130+ Latest builds
131+ Status .* Archive
132+ Successful build on 2010-03-16 buildlog \(.*\) Secret Squirrel Secret PPA
133+ chocolate - 0\+r42 in .* \(estimated\) i386
134+ Request build\(s\)""", self.getMainText(recipe))
135+
136+ def test_index_success_with_completed_binary_build(self):
137+ # Binary builds show their buildlog too.
138+ recipe = self.makeRecipe()
139+ build = removeSecurityProxy(self.factory.makeSourcePackageRecipeBuild(
140+ recipe=recipe, distroseries=self.squirrel, archive=self.ppa))
141+ build.status = BuildStatus.FULLYBUILT
142+ build.date_started = datetime(2010, 03, 16, tzinfo=utc)
143+ build.date_finished = datetime(2010, 03, 16, tzinfo=utc)
144+ build.log = self.factory.makeLibraryFileAlias()
145+ package_name = self.factory.getOrMakeSourcePackageName('chocolate')
146+ source_package_release = self.factory.makeSourcePackageRelease(
147+ archive=self.ppa, sourcepackagename=package_name, distroseries=self.squirrel,
148+ source_package_recipe_build=build, version='0+r42')
149+ builder = self.factory.makeBuilder()
150+ binary_build = removeSecurityProxy(self.factory.makeBinaryPackageBuild(
151+ source_package_release=source_package_release,
152+ distroarchseries=self.squirrel.nominatedarchindep,
153+ processor=builder.processor))
154+ binary_build.queueBuild()
155+ binary_build.status = BuildStatus.FULLYBUILT
156+ binary_build.date_started = datetime(2010, 04, 16, tzinfo=utc)
157+ binary_build.date_finished = datetime(2010, 04, 16, tzinfo=utc)
158+ binary_build.log = self.factory.makeLibraryFileAlias()
159+
160+ self.assertTextMatchesExpressionIgnoreWhitespace("""\
161+ Latest builds
162+ Status .* Archive
163+ Successful build on 2010-03-16 buildlog \(.*\) Secret Squirrel Secret PPA
164+ chocolate - 0\+r42 on 2010-04-16 buildlog \(.*\) i386
165+ Request build\(s\)""", self.getMainText(recipe))
166+
167 def test_index_no_builds(self):
168 """A message should be shown when there are no builds."""
169 recipe = self.makeRecipe()
170 self.assertTextMatchesExpressionIgnoreWhitespace("""\
171 Latest builds
172- Status Time Distribution series Archive
173+ Status .* Archive
174 This recipe has not been built yet.""", self.getMainText(recipe))
175
176 def test_index_no_suitable_builders(self):
177@@ -738,7 +813,7 @@
178 recipe=recipe, distroseries=self.squirrel, archive=self.ppa))
179 self.assertTextMatchesExpressionIgnoreWhitespace("""
180 Latest builds
181- Status Time Distribution series Archive
182+ Status .* Archive
183 No suitable builders Secret Squirrel Secret PPA
184 Request build\(s\)""", self.getMainText(recipe))
185
186@@ -756,7 +831,7 @@
187 self.factory.makeBuilder()
188 pattern = """\
189 Latest builds
190- Status Time Distribution series Archive
191+ Status .* Archive
192 Pending build in .* \(estimated\) Secret Squirrel Secret PPA
193 Request build\(s\)
194
195@@ -946,8 +1021,10 @@
196 view = self.makeBuildView()
197 self.assertTrue(view.estimate)
198 view.context.buildqueue_record.job.start()
199+ clear_property_cache(view)
200 self.assertTrue(view.estimate)
201 removeSecurityProxy(view.context).date_finished = datetime.now(utc)
202+ clear_property_cache(view)
203 self.assertFalse(view.estimate)
204
205 def test_eta(self):
206@@ -966,11 +1043,13 @@
207 recipe_build=build)
208 queue_entry._now = lambda: datetime(1970, 1, 1, 0, 0, 0, 0, utc)
209 self.factory.makeBuilder()
210+ clear_property_cache(view)
211 self.assertIsNot(None, view.eta)
212 self.assertEqual(
213 queue_entry.getEstimatedJobStartTime() +
214 queue_entry.estimated_duration, view.eta)
215 queue_entry.job.start()
216+ clear_property_cache(view)
217 self.assertEqual(
218 queue_entry.job.date_started + queue_entry.estimated_duration,
219 view.eta)
220
221=== modified file 'lib/lp/code/templates/sourcepackagerecipe-index.pt'
222--- lib/lp/code/templates/sourcepackagerecipe-index.pt 2010-11-09 04:24:09 +0000
223+++ lib/lp/code/templates/sourcepackagerecipe-index.pt 2010-11-30 01:38:20 +0000
224@@ -7,6 +7,13 @@
225 i18n:domain="launchpad"
226 >
227
228+<metal:block fill-slot="head_epilogue">
229+ <style type="text/css">
230+ .binary-build .indent {
231+ padding-left: 2em;
232+ }
233+ </style>
234+</metal:block>
235
236 <body>
237
238@@ -86,14 +93,15 @@
239 <thead>
240 <tr>
241 <th>Status</th>
242- <th>Time</th>
243+ <th>When complete</th>
244 <th>Distribution series</th>
245 <th>Archive</th>
246 </tr>
247 </thead>
248 <tbody>
249- <tr tal:repeat="build view/builds">
250+ <tal:recipe-builds repeat="build view/builds">
251 <tal:build-view define="buildview nocall:build/@@+index">
252+ <tr>
253 <td>
254 <span tal:replace="structure build/image:icon" />
255 <a tal:content="buildview/status"
256@@ -102,8 +110,15 @@
257 <td>
258 <tal:date replace="buildview/date/fmt:displaydate" />
259 <tal:estimate condition="buildview/estimate">
260- (estimated)
261+ (estimated)
262 </tal:estimate>
263+
264+ <tal:build-log define="file build/log"
265+ tal:condition="file">
266+ <a class="sprite download"
267+ tal:attributes="href build/log_url">buildlog</a>
268+ (<span tal:replace="file/content/filesize/fmt:bytes" />)
269+ </tal:build-log>
270 </td>
271 <td>
272 <tal:distro
273@@ -112,8 +127,38 @@
274 <td>
275 <tal:archive replace="structure build/archive/fmt:link"/>
276 </td>
277- </tal:build-view>
278 </tr>
279+ <tal:binary-builds repeat="binary buildview/binary_builds">
280+ <tr tal:define="binaryview nocall:binary/@@+index"
281+ class="binary-build">
282+ <td class="indent">
283+ <span tal:replace="structure binary/image:icon"/>
284+ <a tal:content="binary/source_package_release/title"
285+ tal:attributes="href binary/fmt:url">package - version</a>
286+ </td>
287+ <td>
288+ <tal:date replace="binaryview/date/fmt:displaydate" />
289+ <tal:estimate condition="binaryview/estimate">
290+ (estimated)
291+ </tal:estimate>
292+
293+ <tal:build-log define="file binary/log"
294+ tal:condition="file">
295+ <a class="sprite download"
296+ tal:attributes="href binary/log_url">buildlog</a>
297+ (<span tal:replace="file/content/filesize/fmt:bytes" />)
298+ </tal:build-log>
299+ </td>
300+ <td class="indent">
301+ <a class="sprite distribution"
302+ tal:define="archseries binary/distro_arch_series"
303+ tal:attributes="href archseries/fmt:url"
304+ tal:content="archseries/architecturetag">i386</a>
305+ </td>
306+ </tr>
307+ </tal:binary-builds>
308+ </tal:build-view>
309+ </tal:recipe-builds>
310 </tbody>
311 </table>
312 <p tal:condition="not: view/builds">
313
314=== modified file 'lib/lp/soyuz/browser/build.py'
315--- lib/lp/soyuz/browser/build.py 2010-11-23 23:22:27 +0000
316+++ lib/lp/soyuz/browser/build.py 2010-11-30 01:38:20 +0000
317@@ -242,6 +242,39 @@
318 self.context.status == BuildStatus.NEEDSBUILD and
319 self.context.buildqueue_record.job.status == JobStatus.WAITING)
320
321+ @cachedproperty
322+ def eta(self):
323+ """The datetime when the build job is estimated to complete.
324+
325+ This is the BuildQueue.estimated_duration plus the
326+ Job.date_started or BuildQueue.getEstimatedJobStartTime.
327+ """
328+ if self.context.buildqueue_record is None:
329+ return None
330+ queue_record = self.context.buildqueue_record
331+ if queue_record.job.status == JobStatus.WAITING:
332+ start_time = queue_record.getEstimatedJobStartTime()
333+ if start_time is None:
334+ return None
335+ else:
336+ start_time = queue_record.job.date_started
337+ duration = queue_record.estimated_duration
338+ return start_time + duration
339+
340+ @cachedproperty
341+ def date(self):
342+ """The date when the build completed or is estimated to complete."""
343+ if self.estimate:
344+ return self.eta
345+ return self.context.date_finished
346+
347+ @cachedproperty
348+ def estimate(self):
349+ """If true, the date value is an estimate."""
350+ if self.context.date_finished is not None:
351+ return False
352+ return self.eta is not None
353+
354
355 class BuildRetryView(BuildView):
356 """View class for retrying `IBinaryPackageBuild`s"""
357
358=== modified file 'lib/lp/soyuz/model/processor.py'
359--- lib/lp/soyuz/model/processor.py 2010-08-20 20:31:18 +0000
360+++ lib/lp/soyuz/model/processor.py 2010-11-30 01:38:20 +0000
361@@ -38,6 +38,9 @@
362 title = StringCol(dbName='title', notNull=True)
363 description = StringCol(dbName='description', notNull=True)
364
365+ def __repr__(self):
366+ return "<Processor %r>" % self.title
367+
368
369 class ProcessorFamily(SQLBase):
370 implements(IProcessorFamily)
371@@ -55,6 +58,9 @@
372 return Processor(family=self, name=name, title=title,
373 description=description)
374
375+ def __repr__(self):
376+ return "<ProcessorFamily %r>" % self.title
377+
378
379 class ProcessorFamilySet:
380 implements(IProcessorFamilySet)
381
382=== modified file 'lib/lp/soyuz/templates/build-index.pt'
383--- lib/lp/soyuz/templates/build-index.pt 2010-08-06 16:01:38 +0000
384+++ lib/lp/soyuz/templates/build-index.pt 2010-11-30 01:38:20 +0000
385@@ -190,8 +190,7 @@
386 <li tal:define="file context/log"
387 tal:condition="file">
388 <a class="sprite download"
389- tal:attributes="href context/log_url"
390- tal:content="string: buildlog">BUILDLOG</a>
391+ tal:attributes="href context/log_url">buildlog</a>
392 (<span tal:replace="file/content/filesize/fmt:bytes" />)
393 </li>
394 <li tal:define="file context/upload_log"
395
396=== modified file 'lib/lp/testing/factory.py'
397--- lib/lp/testing/factory.py 2010-11-25 17:24:59 +0000
398+++ lib/lp/testing/factory.py 2010-11-30 01:38:20 +0000
399@@ -2771,7 +2771,7 @@
400
401 def makeBinaryPackageBuild(self, source_package_release=None,
402 distroarchseries=None, archive=None, builder=None,
403- status=None, pocket=None):
404+ status=None, pocket=None, date_created=None, processor=None):
405 """Create a BinaryPackageBuild.
406
407 If archive is not supplied, the source_package_release is used
408@@ -2795,7 +2795,8 @@
409 self.makeSourcePackagePublishingHistory(
410 distroseries=source_package_release.upload_distroseries,
411 archive=archive, sourcepackagerelease=source_package_release)
412- processor = self.makeProcessor()
413+ if processor is None:
414+ processor = self.makeProcessor()
415 if distroarchseries is None:
416 distroarchseries = self.makeDistroArchSeries(
417 distroseries=source_package_release.upload_distroseries,
418@@ -2804,6 +2805,8 @@
419 status = BuildStatus.NEEDSBUILD
420 if pocket is None:
421 pocket = PackagePublishingPocket.RELEASE
422+ if date_created is None:
423+ date_created = self.getUniqueDate()
424 binary_package_build = getUtility(IBinaryPackageBuildSet).new(
425 source_package_release=source_package_release,
426 processor=processor,
427@@ -2811,7 +2814,7 @@
428 status=status,
429 archive=archive,
430 pocket=pocket,
431- date_created=self.getUniqueDate())
432+ date_created=date_created)
433 naked_build = removeSecurityProxy(binary_package_build)
434 naked_build.builder = builder
435 return binary_package_build