Merge lp:~abentley/launchpad/estimate-duration into lp:launchpad

Proposed by Aaron Bentley
Status: Merged
Merged at revision: 10920
Proposed branch: lp:~abentley/launchpad/estimate-duration
Merge into: lp:launchpad
Diff against target: 130 lines (+51/-9)
4 files modified
lib/lp/code/model/sourcepackagerecipe.py (+14/-1)
lib/lp/code/model/sourcepackagerecipebuild.py (+4/-2)
lib/lp/code/model/tests/test_sourcepackagerecipe.py (+22/-2)
lib/lp/code/model/tests/test_sourcepackagerecipebuild.py (+11/-4)
To merge this branch: bzr merge lp:~abentley/launchpad/estimate-duration
Reviewer Review Type Date Requested Status
Paul Hummer (community) code Approve
Review via email: mp+25553@code.launchpad.net

Commit message

Provide a data-based estimateDuration.

Description of the change

= Summary =
Fix bug #507764: Need proper SourcePackageRecipeBuild.estimateDuration()

== Proposed fix ==
For sourcepackagerecipes with one or more builds which has a duration, use the
median of those durations. The status of the these builds is not considered,
so if builds of this recipe always fail, this will estimate how long it will
take to fail.

Using the median means that extreme values will not unduly influence the
estimate.

If there are no builds with durations, the estimate is 10 minutes, which is a
reasonable value for small packages.

== Pre-implementation notes ==
bigjools impressed the importance of this bug upon me. Mid-implementation with
rockstar re: status

== Implementation details ==
This implements SourcePackageRecipe.getMedianBuildDuration as the underlying
calculation, and then implements estimateDuration on top of it.

Where the number of values is even, this is not a true median, because it takes
the lower-middle value instead of combining the lower-middle and higher-middle
values.

There are some small drive-by fixes, as well.

== Tests ==
bin/test -t estimateDuration -t getMedian test_sourcepackagerecipe

== Demo and Q/A ==
Request a build. The estimated duration should start at 10 minutes.
Request a second build. The estimated duration should be the actual previous
duration.

= Launchpad lint =

Checking for conflicts. and issues in doctests and templates.
Running jslint, xmllint, pyflakes, and pylint.
Using normal rules.

Linting changed files:
  lib/lp/code/model/tests/test_sourcepackagerecipebuild.py
  lib/lp/code/model/tests/test_sourcepackagerecipe.py
  lib/lp/code/model/sourcepackagerecipebuild.py
  lib/lp/code/model/sourcepackagerecipe.py

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

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/lp/code/model/sourcepackagerecipe.py'
--- lib/lp/code/model/sourcepackagerecipe.py 2010-05-10 15:50:23 +0000
+++ lib/lp/code/model/sourcepackagerecipe.py 2010-05-18 19:27:28 +0000
@@ -20,7 +20,7 @@
20from zope.interface import classProvides, implements20from zope.interface import classProvides, implements
2121
22from canonical.database.datetimecol import UtcDateTimeCol22from canonical.database.datetimecol import UtcDateTimeCol
23from canonical.launchpad.interfaces.lpstorm import IMasterStore23from canonical.launchpad.interfaces.lpstorm import IMasterStore, IStore
2424
25from lp.code.interfaces.sourcepackagerecipe import (25from lp.code.interfaces.sourcepackagerecipe import (
26 ISourcePackageRecipe, ISourcePackageRecipeSource,26 ISourcePackageRecipe, ISourcePackageRecipeSource,
@@ -176,3 +176,16 @@
176 *clauses)176 *clauses)
177 result.order_by(Desc(SourcePackageRecipeBuild.datebuilt))177 result.order_by(Desc(SourcePackageRecipeBuild.datebuilt))
178 return result178 return result
179
180 def getMedianBuildDuration(self):
181 """Return the median duration of builds of this recipe."""
182 store = IStore(self)
183 result = store.find(
184 SourcePackageRecipeBuild.buildduration,
185 SourcePackageRecipeBuild.recipe==self.id,
186 SourcePackageRecipeBuild.buildduration != None)
187 result.order_by(Desc(SourcePackageRecipeBuild.buildduration))
188 count = result.count()
189 if count == 0:
190 return None
191 return result[count/2]
179192
=== modified file 'lib/lp/code/model/sourcepackagerecipebuild.py'
--- lib/lp/code/model/sourcepackagerecipebuild.py 2010-05-14 21:24:48 +0000
+++ lib/lp/code/model/sourcepackagerecipebuild.py 2010-05-18 19:27:28 +0000
@@ -214,8 +214,10 @@
214214
215 def estimateDuration(self):215 def estimateDuration(self):
216 """See `IBuildBase`."""216 """See `IBuildBase`."""
217 # XXX: wgrant 2010-01-19 bug=507764: Need proper implementation.217 median = self.recipe.getMedianBuildDuration()
218 return datetime.timedelta(minutes=2)218 if median is not None:
219 return median
220 return datetime.timedelta(minutes=10)
219221
220 def verifySuccessfulUpload(self):222 def verifySuccessfulUpload(self):
221 return self.source_package_release is not None223 return self.source_package_release is not None
222224
=== modified file 'lib/lp/code/model/tests/test_sourcepackagerecipe.py'
--- lib/lp/code/model/tests/test_sourcepackagerecipe.py 2010-05-17 19:02:37 +0000
+++ lib/lp/code/model/tests/test_sourcepackagerecipe.py 2010-05-18 19:27:28 +0000
@@ -7,7 +7,7 @@
77
8__metaclass__ = type8__metaclass__ = type
99
10from datetime import datetime10from datetime import datetime, timedelta
11import textwrap11import textwrap
12import unittest12import unittest
1313
@@ -25,7 +25,8 @@
2525
26from canonical.launchpad.webapp.authorization import check_permission26from canonical.launchpad.webapp.authorization import check_permission
27from lp.soyuz.interfaces.archive import (27from lp.soyuz.interfaces.archive import (
28 ArchiveDisabled, ArchivePurpose, CannotUploadToArchive, InvalidPocketForPPA)28 ArchiveDisabled, ArchivePurpose, CannotUploadToArchive,
29 InvalidPocketForPPA)
29from lp.buildmaster.interfaces.buildqueue import IBuildQueue30from lp.buildmaster.interfaces.buildqueue import IBuildQueue
30from lp.buildmaster.model.buildqueue import BuildQueue31from lp.buildmaster.model.buildqueue import BuildQueue
31from lp.code.interfaces.sourcepackagerecipe import (32from lp.code.interfaces.sourcepackagerecipe import (
@@ -319,6 +320,25 @@
319 # Show no database constraints were violated320 # Show no database constraints were violated
320 Store.of(recipe).flush()321 Store.of(recipe).flush()
321322
323 def test_getMedianBuildDuration(self):
324 recipe = removeSecurityProxy(self.factory.makeSourcePackageRecipe())
325 self.assertIs(None, recipe.getMedianBuildDuration())
326 build = removeSecurityProxy(
327 self.factory.makeSourcePackageRecipeBuild(recipe=recipe))
328 build.buildduration = timedelta(minutes=10)
329 self.assertEqual(
330 timedelta(minutes=10), recipe.getMedianBuildDuration())
331 def addBuild(minutes):
332 build = removeSecurityProxy(
333 self.factory.makeSourcePackageRecipeBuild(recipe=recipe))
334 build.buildduration = timedelta(minutes=minutes)
335 addBuild(20)
336 self.assertEqual(
337 timedelta(minutes=10), recipe.getMedianBuildDuration())
338 addBuild(11)
339 self.assertEqual(
340 timedelta(minutes=11), recipe.getMedianBuildDuration())
341
322342
323class TestRecipeBranchRoundTripping(TestCaseWithFactory):343class TestRecipeBranchRoundTripping(TestCaseWithFactory):
324344
325345
=== modified file 'lib/lp/code/model/tests/test_sourcepackagerecipebuild.py'
--- lib/lp/code/model/tests/test_sourcepackagerecipebuild.py 2010-05-17 17:40:32 +0000
+++ lib/lp/code/model/tests/test_sourcepackagerecipebuild.py 2010-05-18 19:27:28 +0000
@@ -78,8 +78,8 @@
78 self.assertProvides(bq.specific_job, ISourcePackageRecipeBuildJob)78 self.assertProvides(bq.specific_job, ISourcePackageRecipeBuildJob)
79 self.assertEqual(True, bq.virtualized)79 self.assertEqual(True, bq.virtualized)
8080
81 # The processor for SourcePackageRecipeBuilds should not be None. They81 # The processor for SourcePackageRecipeBuilds should not be None.
82 # do require specific environments.82 # They do require specific environments.
83 self.assertNotEqual(None, bq.processor)83 self.assertNotEqual(None, bq.processor)
84 self.assertEqual(84 self.assertEqual(
85 spb.distroseries.nominatedarchindep.default_processor,85 spb.distroseries.nominatedarchindep.default_processor,
@@ -136,10 +136,17 @@
136 self.assertFalse(check_permission('launchpad.View', build))136 self.assertFalse(check_permission('launchpad.View', build))
137137
138 def test_estimateDuration(self):138 def test_estimateDuration(self):
139 # The duration estimate is currently hard-coded as two minutes.139 # If there are no successful builds, estimate 10 minutes.
140 spb = self.makeSourcePackageRecipeBuild()140 spb = self.makeSourcePackageRecipeBuild()
141 self.assertEqual(141 self.assertEqual(
142 datetime.timedelta(minutes=2), spb.estimateDuration())142 datetime.timedelta(minutes=10), spb.estimateDuration())
143 for minutes in [20, 5, 1]:
144 build = removeSecurityProxy(
145 self.factory.makeSourcePackageRecipeBuild(recipe=spb.recipe))
146 build.buildduration = datetime.timedelta(minutes=minutes)
147 self.assertEqual(
148 datetime.timedelta(minutes=5), spb.estimateDuration())
149
143150
144 def test_datestarted(self):151 def test_datestarted(self):
145 """Datestarted is taken from job if not specified in the build.152 """Datestarted is taken from job if not specified in the build.