← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~abentley/launchpad/package-build-recipe into lp:launchpad

 

Aaron Bentley has proposed merging lp:~abentley/launchpad/package-build-recipe into lp:launchpad.

Requested reviews:
  Stuart Bishop (stub): db
  Launchpad code reviewers (launchpad-reviewers): code
Related bugs:
  #609266 sourcepackagerecipebuilds should be packagebuilds
  https://bugs.launchpad.net/bugs/609266


Summary: Fix bug Bug 609266 sourcepackagerecipebuilds should be packagebuilds.

Changes: Add a foreign key reference from sourcepackagerecipebuild to packagebuild.  Drop all columns from sourcepackagerecipe that are now provided by packagebuild or buildfarmjob.  Update the model and calling code.

Preimplementation was with stub and noodles.

Lint omitted because there's way too much of it-- all kinds of places were affected by this change.
-- 
https://code.launchpad.net/~abentley/launchpad/package-build-recipe/+merge/30818
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~abentley/launchpad/package-build-recipe into lp:launchpad.
=== modified file 'database/sampledata/current-dev.sql'
--- database/sampledata/current-dev.sql	2010-07-23 09:04:18 +0000
+++ database/sampledata/current-dev.sql	2010-07-23 19:32:55 +0000
@@ -4321,6 +4321,13 @@
 ALTER TABLE featuredproject ENABLE TRIGGER ALL;
 
 
+ALTER TABLE featureflag DISABLE TRIGGER ALL;
+
+
+
+ALTER TABLE featureflag ENABLE TRIGGER ALL;
+
+
 ALTER TABLE flatpackagesetinclusion DISABLE TRIGGER ALL;
 
 

=== modified file 'database/sampledata/current.sql'
--- database/sampledata/current.sql	2010-07-23 09:04:18 +0000
+++ database/sampledata/current.sql	2010-07-23 19:32:55 +0000
@@ -4259,6 +4259,13 @@
 ALTER TABLE featuredproject ENABLE TRIGGER ALL;
 
 
+ALTER TABLE featureflag DISABLE TRIGGER ALL;
+
+
+
+ALTER TABLE featureflag ENABLE TRIGGER ALL;
+
+
 ALTER TABLE flatpackagesetinclusion DISABLE TRIGGER ALL;
 
 

=== modified file 'database/schema/comments.sql'
--- database/schema/comments.sql	2010-07-23 09:04:18 +0000
+++ database/schema/comments.sql	2010-07-23 19:32:55 +0000
@@ -1384,19 +1384,10 @@
 
 COMMENT ON TABLE SourcePackageRecipeBuild IS 'The build record for the process of building a source package as described by a recipe.';
 COMMENT ON COLUMN SourcePackageRecipeBuild.distroseries IS 'The distroseries the build was for.';
-COMMENT ON COLUMN SourcePackageRecipeBuild.build_state IS 'The state of the build.';
-COMMENT ON COLUMN SourcePackageRecipeBuild.date_built IS 'When the build record was processed.';
-COMMENT ON COLUMN SourcePackageRecipeBuild.build_duration IS 'How long this build took to be processed.';
-COMMENT ON COLUMN SourcePackageRecipeBuild.build_log IS 'Points to the build_log file stored in librarian.';
-COMMENT ON COLUMN SourcePackageRecipeBuild.builder IS 'Points to the builder which has once processed it.';
-COMMENT ON COLUMN SourcePackageRecipeBuild.date_first_dispatched IS 'The instant the build was dispatched the first time. This value will not get overridden if the build is retried.';
+COMMENT ON COLUMN SourcePackageRecipeBuild.package_build IS 'The package_build with the base information about this build.';
 COMMENT ON COLUMN SourcePackageRecipeBuild.requester IS 'Who requested the build.';
 COMMENT ON COLUMN SourcePackageRecipeBuild.recipe IS 'The recipe being processed.';
 COMMENT ON COLUMN SourcePackageRecipeBuild.manifest IS 'The evaluated recipe that was built.';
-COMMENT ON COLUMN SourcePackageRecipeBuild.archive IS 'The archive the source package will be built in and uploaded to.';
-COMMENT ON COLUMN SourcePackageRecipeBuild.pocket IS 'The pocket the source package will be built in and uploaded to.';
-COMMENT ON COLUMN SourcePackageRecipeBuild.dependencies IS 'The missing build dependencies, if any.';
-COMMENT ON COLUMN SourcePackageRecipeBuild.upload_log IS 'The output from uploading the source package to the archive.';
 
 -- SourcePackageRecipeBuildJob
 

=== added file 'database/schema/patch-2207-99-0.sql'
--- database/schema/patch-2207-99-0.sql	1970-01-01 00:00:00 +0000
+++ database/schema/patch-2207-99-0.sql	2010-07-23 19:32:55 +0000
@@ -0,0 +1,29 @@
+-- Copyright 2010 Canonical Ltd. This software is licensed under the
+-- GNU Affero General Public License version 3 (see the file LICENSE).
+
+SET client_min_messages=ERROR;
+
+CREATE TEMPORARY TABLE NewBuildFarmJob AS SELECT
+  nextval('buildfarmjob_id_seq') AS id, 1 AS processor, True AS virtualized, date_created, date_built - build_duration AS date_started, date_built AS date_finished, date_first_dispatched, builder, build_state, build_log, 3 AS job_type, id AS sprb_id FROM SourcePackageRecipeBuild;
+
+INSERT INTO BuildFarmJob SELECT id, processor, virtualized, date_created, date_started, date_finished, date_first_dispatched, builder, build_state, build_log, job_type FROM NewBuildFarmJob;
+
+CREATE TEMPORARY TABLE NewPackageBuild AS SELECT
+  nextval('packagebuild_id_seq') AS id, NewBuildFarmJob.id AS new_build_farm_job, archive, pocket, upload_log, dependencies, SourcePackageRecipeBuild.id AS sprb_id FROM SourcePackageRecipeBuild, NewBuildFarmJob;
+
+ALTER TABLE SourcePackageRecipeBuild
+  ADD COLUMN package_build INTEGER REFERENCES PackageBuild NOT NULL;
+
+UPDATE SourcePackageRecipebuild
+  SET package_build=NewPackageBuild.id
+  FROM NewPackageBuild
+  WHERE sprb_id = SourcePackageRecipeBuild.id;
+
+ALTER TABLE SourcePackageRecipeBuild
+  ALTER COLUMN package_build SET NOT NULL, DROP COLUMN date_created,
+  DROP COLUMN build_duration, DROP COLUMN date_built,
+  DROP COLUMN date_first_dispatched, DROP COLUMN builder,
+  DROP COLUMN build_state, DROP COLUMN build_log, DROP COLUMN archive,
+  DROP COLUMN pocket, DROP COLUMN upload_log, DROP COLUMN dependencies;
+
+INSERT INTO LaunchpadDatabaseRevision VALUES (2207, 99, 0);

=== modified file 'database/schema/security.cfg'
--- database/schema/security.cfg	2010-07-23 09:04:18 +0000
+++ database/schema/security.cfg	2010-07-23 19:32:55 +0000
@@ -700,12 +700,14 @@
 public.archivepermission                        = SELECT
 public.buildqueue                               = SELECT, INSERT, UPDATE
 public.branch                                   = SELECT
+public.buildfarmjob                             = SELECT, INSERT
 public.component                                = SELECT
 public.distribution                             = SELECT
 public.distroseries                             = SELECT
 public.distroarchseries                         = SELECT
 public.job                                      = SELECT, INSERT
 public.person                                   = SELECT
+public.packagebuild                             = SELECT, INSERT
 public.processor                                = SELECT
 public.processorfamily                          = SELECT
 public.sourcepackagerecipe                      = SELECT, UPDATE

=== modified file 'lib/lp/archiveuploader/dscfile.py'
--- lib/lp/archiveuploader/dscfile.py	2010-07-15 11:30:58 +0000
+++ lib/lp/archiveuploader/dscfile.py	2010-07-23 19:32:55 +0000
@@ -573,7 +573,7 @@
         build = getUtility(ISourcePackageRecipeBuildSource).getById(build_id)
 
         # The master verifies the status to confirm successful upload.
-        build.buildstate = BuildStatus.FULLYBUILT
+        build.status = BuildStatus.FULLYBUILT
         # If this upload is successful, any existing log is wrong and
         # unuseful.
         build.upload_log = None

=== modified file 'lib/lp/archiveuploader/tests/test_recipeuploads.py'
--- lib/lp/archiveuploader/tests/test_recipeuploads.py	2010-07-18 00:26:33 +0000
+++ lib/lp/archiveuploader/tests/test_recipeuploads.py	2010-07-23 19:32:55 +0000
@@ -49,7 +49,7 @@
         # Ensure that the upload processor correctly links the SPR to
         # the SPRB, and that the status is set properly.
         # This test depends on write access being granted to anybody
-        # (it does not matter who) on SPRB.{buildstate,upload_log}.
+        # (it does not matter who) on SPRB.{status,upload_log}.
         self.assertIs(None, self.build.source_package_release)
         self.assertEqual(False, self.build.verifySuccessfulUpload())
         self.queueUpload('bar_1.0-1', '%d/ubuntu' % self.build.archive.id)
@@ -67,5 +67,5 @@
         spr = queue_item.sources[0].sourcepackagerelease
         self.assertEqual(self.build, spr.source_package_recipe_build)
         self.assertEqual(spr, self.build.source_package_release)
-        self.assertEqual(BuildStatus.FULLYBUILT, self.build.buildstate)
+        self.assertEqual(BuildStatus.FULLYBUILT, self.build.status)
         self.assertEqual(True, self.build.verifySuccessfulUpload())

=== modified file 'lib/lp/buildmaster/interfaces/packagebuild.py'
--- lib/lp/buildmaster/interfaces/packagebuild.py	2010-06-14 08:11:33 +0000
+++ lib/lp/buildmaster/interfaces/packagebuild.py	2010-07-23 19:32:55 +0000
@@ -106,7 +106,7 @@
             stored.
         """
 
-    def getLogFromSlave():
+    def getLogFromSlave(package_build):
         """Get last buildlog from slave. """
 
     def getUploadLogContent(root, leaf):
@@ -120,7 +120,7 @@
     def estimateDuration():
         """Estimate the build duration."""
 
-    def storeBuildInfo(librarian, slave_status):
+    def storeBuildInfo(package_build, librarian, slave_status):
         """Store available information for the build job.
 
         Derived classes can override this as needed, and call it from
@@ -193,4 +193,3 @@
             will be returned.
         :return: a `ResultSet` representing the requested package builds.
         """
-

=== modified file 'lib/lp/code/browser/sourcepackagerecipebuild.py'
--- lib/lp/code/browser/sourcepackagerecipebuild.py	2010-07-14 10:27:35 +0000
+++ lib/lp/code/browser/sourcepackagerecipebuild.py	2010-07-23 19:32:55 +0000
@@ -64,7 +64,7 @@
     @property
     def status(self):
         """A human-friendly status string."""
-        if (self.context.buildstate == BuildStatus.NEEDSBUILD
+        if (self.context.status == BuildStatus.NEEDSBUILD
             and self.eta is None):
             return 'No suitable builders'
         return {
@@ -77,7 +77,7 @@
             BuildStatus.SUPERSEDED: (
                 'Could not build because source package was superseded'),
             BuildStatus.FAILEDTOUPLOAD: 'Could not be uploaded correctly',
-            }.get(self.context.buildstate, self.context.buildstate.title)
+            }.get(self.context.status, self.context.status.title)
 
     @property
     def eta(self):
@@ -103,12 +103,12 @@
         """The date when the build completed or is estimated to complete."""
         if self.estimate:
             return self.eta
-        return self.context.datebuilt
+        return self.context.date_finished
 
     @property
     def estimate(self):
         """If true, the date value is an estimate."""
-        if self.context.datebuilt is not None:
+        if self.context.date_finished is not None:
             return False
         return self.eta is not None
 

=== modified file 'lib/lp/code/browser/tests/test_sourcepackagerecipe.py'
--- lib/lp/code/browser/tests/test_sourcepackagerecipe.py	2010-07-22 08:39:05 +0000
+++ lib/lp/code/browser/tests/test_sourcepackagerecipe.py	2010-07-23 19:32:55 +0000
@@ -28,8 +28,13 @@
 from lp.code.interfaces.sourcepackagerecipe import MINIMAL_RECIPE_TEXT
 from lp.registry.interfaces.pocket import PackagePublishingPocket
 from lp.soyuz.model.processor import ProcessorFamily
+<<<<<<< TREE
 from lp.testing import ANONYMOUS, BrowserTestCase, login, logout
 from lp.testing.factory import remove_security_proxy_and_shout_at_engineer
+=======
+from lp.testing import (
+    ANONYMOUS, BrowserTestCase, login, logout)
+>>>>>>> MERGE-SOURCE
 
 
 class TestCaseForRecipe(BrowserTestCase):
@@ -450,8 +455,8 @@
         recipe = self.makeRecipe()
         build = removeSecurityProxy(self.factory.makeSourcePackageRecipeBuild(
             recipe=recipe, distroseries=self.squirrel, archive=self.ppa))
-        build.buildstate = BuildStatus.FULLYBUILT
-        build.datebuilt = datetime(2010, 03, 16, tzinfo=utc)
+        build.status = BuildStatus.FULLYBUILT
+        build.date_finished = datetime(2010, 03, 16, tzinfo=utc)
 
         self.assertTextMatchesExpressionIgnoreWhitespace("""\
             Master Chef Recipes cake_recipe
@@ -532,7 +537,7 @@
             set(view.builds))
 
         def set_day(build, day):
-            removeSecurityProxy(build).datebuilt = datetime(
+            removeSecurityProxy(build).date_finished = datetime(
                 2010, 03, day, tzinfo=utc)
         set_day(build1, 16)
         set_day(build2, 15)
@@ -617,7 +622,7 @@
         for x in range(5):
             build = recipe.requestBuild(
                 self.ppa, self.chef, woody, PackagePublishingPocket.RELEASE)
-            removeSecurityProxy(build).buildstate = BuildStatus.FULLYBUILT
+            removeSecurityProxy(build).status = BuildStatus.FULLYBUILT
 
         browser = self.getViewBrowser(recipe, '+request-builds')
         browser.getControl('Woody').click()
@@ -658,7 +663,7 @@
         self.user = self.factory.makePerson(
             displayname='Owner', name='build-owner', password='test')
 
-    def makeBuild(self, buildstate=None):
+    def makeBuild(self):
         """Make a build suitabe for testing."""
         archive = self.factory.makeArchive(name='build',
             owner=self.user)
@@ -683,7 +688,7 @@
         self.assertTrue(view.estimate)
         view.context.buildqueue_record.job.start()
         self.assertTrue(view.estimate)
-        removeSecurityProxy(view.context).datebuilt = datetime.now(utc)
+        removeSecurityProxy(view.context).date_finished = datetime.now(utc)
         self.assertFalse(view.estimate)
 
     def test_eta(self):
@@ -739,11 +744,12 @@
         release = self.makeBuildAndRelease()
         self.makeBinaryBuild(release, 'itanic')
         naked_build = removeSecurityProxy(release.source_package_recipe_build)
-        naked_build.buildstate = BuildStatus.FULLYBUILT
-        naked_build.buildduration = timedelta(minutes=1)
-        naked_build.datebuilt = datetime(2009, 1, 1, tzinfo=utc)
+        naked_build.status = BuildStatus.FULLYBUILT
+        naked_build.date_finished = datetime(2009, 1, 1, tzinfo=utc)
+        naked_build.date_started = (
+            naked_build.date_finished - timedelta(minutes=1))
         naked_build.buildqueue_record.destroySelf()
-        naked_build.buildlog = self.factory.makeLibraryFileAlias(
+        naked_build.log = self.factory.makeLibraryFileAlias(
             content='buildlog')
         naked_build.upload_log = self.factory.makeLibraryFileAlias(
             content='upload_log')
@@ -805,10 +811,10 @@
         build.buildqueue_record.builder = self.factory.makeBuilder()
         main_text = self.getMainText(build, '+index')
         self.assertNotIn('Logs have no tails!', main_text)
-        removeSecurityProxy(build).buildstate = BuildStatus.BUILDING
+        removeSecurityProxy(build).status = BuildStatus.BUILDING
         main_text = self.getMainText(build, '+index')
         self.assertIn('Logs have no tails!', main_text)
-        removeSecurityProxy(build).buildstate = BuildStatus.FULLYBUILT
+        removeSecurityProxy(build).status = BuildStatus.FULLYBUILT
         self.assertIn('Logs have no tails!', main_text)
 
     def getMainText(self, build, view_name=None):
@@ -819,12 +825,11 @@
     def test_buildlog(self):
         """A link to the build log is shown if available."""
         build = self.makeBuild()
-        removeSecurityProxy(build).buildlog = (
+        removeSecurityProxy(build).log = (
             self.factory.makeLibraryFileAlias())
-        build_log_url = build.build_log_url
         browser = self.getViewBrowser(build)
         link = browser.getLink('buildlog')
-        self.assertEqual(build_log_url, link.url)
+        self.assertEqual(build.log_url, link.url)
 
     def test_uploadlog(self):
         """A link to the upload log is shown if available."""

=== modified file 'lib/lp/code/configure.zcml'
--- lib/lp/code/configure.zcml	2010-07-23 01:53:05 +0000
+++ lib/lp/code/configure.zcml	2010-07-23 19:32:55 +0000
@@ -920,7 +920,7 @@
     <require permission="launchpad.View" interface="lp.code.interfaces.sourcepackagerecipebuild.ISourcePackageRecipeBuild"/>
     <!-- This is needed for UploadProcessor to run. The permission isn't
          important; launchpad.Edit isn't actually held by anybody. -->
-    <require permission="launchpad.Edit" set_attributes="buildstate upload_log" />
+    <require permission="launchpad.Edit" set_attributes="status upload_log" />
   </class>
 
   <securedutility

=== modified file 'lib/lp/code/interfaces/sourcepackagerecipebuild.py'
--- lib/lp/code/interfaces/sourcepackagerecipebuild.py	2010-07-14 08:42:01 +0000
+++ lib/lp/code/interfaces/sourcepackagerecipebuild.py	2010-07-23 19:32:55 +0000
@@ -17,11 +17,11 @@
 from lazr.restful.declarations import export_as_webservice_entry
 
 from zope.interface import Interface
-from zope.schema import Bool, Datetime, Int, Object
+from zope.schema import Bool, Int, Object
 
 from canonical.launchpad import _
 
-from lp.buildmaster.interfaces.buildbase import IBuildBase
+from lp.buildmaster.interfaces.packagebuild import IPackageBuild
 from lp.soyuz.interfaces.binarypackagebuild import IBinaryPackageBuild
 from lp.soyuz.interfaces.buildfarmbuildjob import IBuildFarmBuildJob
 from lp.code.interfaces.sourcepackagerecipe import (
@@ -32,7 +32,7 @@
 from lp.soyuz.interfaces.sourcepackagerelease import ISourcePackageRelease
 
 
-class ISourcePackageRecipeBuild(IBuildBase):
+class ISourcePackageRecipeBuild(IPackageBuild):
     """A build of a source package."""
     export_as_webservice_entry()
 
@@ -42,15 +42,9 @@
         Reference(IBinaryPackageBuild),
         title=_("The binary builds that resulted from this."), readonly=True)
 
-    datestarted = Datetime(title=u'The time the build started.')
-
     distroseries = Reference(
         IDistroSeries, title=_("The distroseries being built for"),
         readonly=True)
-    # XXX michaeln 2010-05-18 bug=567922
-    # Temporarily alias distro_series until SPRecipeBuild is
-    # implementing IPackageBuild.
-    distro_series = distroseries
 
     requester = Object(
         schema=IPerson, required=False,
@@ -89,7 +83,8 @@
     def new(distroseries, recipe, requester, archive, date_created=None):
         """Create an `ISourcePackageRecipeBuild`.
 
-        :param distroseries: The `IDistroSeries` that this is building against.
+        :param distroseries: The `IDistroSeries` that this is building
+            against.
         :param recipe: The `ISourcePackageRecipe` that this is building.
         :param requester: The `IPerson` who wants to build it.
         :param date_created: The date this build record was created. If not

=== modified file 'lib/lp/code/mail/sourcepackagerecipebuild.py'
--- lib/lp/code/mail/sourcepackagerecipebuild.py	2010-06-09 20:24:11 +0000
+++ lib/lp/code/mail/sourcepackagerecipebuild.py	2010-07-23 19:32:55 +0000
@@ -50,7 +50,7 @@
         params = super(
             SourcePackageRecipeBuildMailer, self)._getTemplateParams(email)
         params.update({
-            'status': self.build.buildstate.title,
+            'status': self.build.status.title,
             'distroseries': self.build.distroseries.name,
             'recipe': self.build.recipe.name,
             'recipe_owner': self.build.recipe.owner.name,

=== modified file 'lib/lp/code/model/sourcepackagerecipe.py'
--- lib/lp/code/model/sourcepackagerecipe.py	2010-07-21 14:41:26 +0000
+++ lib/lp/code/model/sourcepackagerecipe.py	2010-07-23 19:32:55 +0000
@@ -26,6 +26,8 @@
 from canonical.launchpad.interfaces.lpstorm import IMasterStore, IStore
 
 from lp.buildmaster.interfaces.buildbase import BuildStatus
+from lp.buildmaster.model.buildfarmjob import BuildFarmJob
+from lp.buildmaster.model.packagebuild import PackageBuild
 from lp.code.errors import (BuildAlreadyPending, BuildNotAllowedForDistro,
     TooManyBuilds)
 from lp.code.interfaces.sourcepackagerecipe import (
@@ -217,14 +219,16 @@
         pending = IStore(self).find(SourcePackageRecipeBuild,
             SourcePackageRecipeBuild.recipe_id == self.id,
             SourcePackageRecipeBuild.distroseries_id == distroseries.id,
-            SourcePackageRecipeBuild.archive_id == archive.id,
-            SourcePackageRecipeBuild.buildstate == BuildStatus.NEEDSBUILD)
+            PackageBuild.archive_id == archive.id,
+            PackageBuild.id == SourcePackageRecipeBuild.package_build_id,
+            BuildFarmJob.id == PackageBuild.build_farm_job_id,
+            BuildFarmJob.status == BuildStatus.NEEDSBUILD)
         if pending.any() is not None:
             raise BuildAlreadyPending(self, distroseries)
 
         build = getUtility(ISourcePackageRecipeBuildSource).new(distroseries,
             self, requester, archive)
-        build.queueBuild(build)
+        build.queueBuild()
         if manual:
             build.buildqueue_record.manualScore(1000)
         return build
@@ -232,32 +236,41 @@
     def getBuilds(self, pending=False):
         """See `ISourcePackageRecipe`."""
         if pending:
-            clauses = [SourcePackageRecipeBuild.datebuilt == None]
+            clauses = [BuildFarmJob.date_finished == None]
         else:
-            clauses = [SourcePackageRecipeBuild.datebuilt != None]
+            clauses = [BuildFarmJob.date_finished != None]
         result = Store.of(self).find(
-            SourcePackageRecipeBuild, SourcePackageRecipeBuild.recipe==self,
+            SourcePackageRecipeBuild,
+            SourcePackageRecipeBuild.recipe==self,
+            SourcePackageRecipeBuild.package_build_id == PackageBuild.id,
+            PackageBuild.build_farm_job_id == BuildFarmJob.id,
             *clauses)
-        result.order_by(Desc(SourcePackageRecipeBuild.datebuilt))
+        result.order_by(Desc(BuildFarmJob.date_finished))
         return result
 
     def getLastBuild(self):
         """See `ISourcePackageRecipeBuild`."""
         store = Store.of(self)
         result = store.find(
-            SourcePackageRecipeBuild, SourcePackageRecipeBuild.recipe == self)
-        result.order_by(Desc(SourcePackageRecipeBuild.datebuilt))
+            (SourcePackageRecipeBuild),
+            SourcePackageRecipeBuild.recipe == self,
+            SourcePackageRecipeBuild.package_build_id == PackageBuild.id,
+            PackageBuild.build_farm_job_id == BuildFarmJob.id)
+        result.order_by(Desc(BuildFarmJob.date_finished))
         return result.first()
 
     def getMedianBuildDuration(self):
         """Return the median duration of builds of this recipe."""
         store = IStore(self)
         result = store.find(
-            SourcePackageRecipeBuild.buildduration,
-            SourcePackageRecipeBuild.recipe==self.id,
-            SourcePackageRecipeBuild.buildduration != None)
-        result.order_by(Desc(SourcePackageRecipeBuild.buildduration))
-        count = result.count()
-        if count == 0:
+            BuildFarmJob,
+            SourcePackageRecipeBuild.recipe == self.id,
+            BuildFarmJob.date_finished != None,
+            BuildFarmJob.id == PackageBuild.build_farm_job_id,
+            SourcePackageRecipeBuild.package_build_id == PackageBuild.id)
+        durations = [build.date_finished - build.date_started for build in
+                     result]
+        if len(durations) == 0:
             return None
-        return result[count/2]
+        durations.sort(reverse=True)
+        return durations[len(durations) / 2]

=== modified file 'lib/lp/code/model/sourcepackagerecipebuild.py'
--- lib/lp/code/model/sourcepackagerecipebuild.py	2010-07-14 09:35:20 +0000
+++ lib/lp/code/model/sourcepackagerecipebuild.py	2010-07-23 19:32:55 +0000
@@ -16,19 +16,22 @@
 from pytz import utc
 
 from canonical.database.constants import UTC_NOW
-from canonical.database.datetimecol import UtcDateTimeCol
-from canonical.database.enumcol import DBEnum
+from canonical.launchpad.browser.librarian import (
+    ProxiedLibraryFileAlias)
 from canonical.launchpad.interfaces.lpstorm import IMasterStore, IStore
 from canonical.launchpad.interfaces.launchpad import NotFoundError
 
-from storm.locals import Int, Reference, Storm, TimeDelta, Unicode
+from psycopg2 import ProgrammingError
+from storm.locals import Int, Reference, Storm
 from storm.store import Store
 
 from zope.component import getUtility
 from zope.interface import classProvides, implements
 
 from canonical.launchpad.webapp import errorlog
-from lp.buildmaster.interfaces.buildbase import BuildStatus, IBuildBase
+from lp.buildmaster.model.packagebuild import (
+    PackageBuild, PackageBuildDerived)
+from lp.buildmaster.interfaces.buildbase import BuildStatus
 from lp.buildmaster.interfaces.buildfarmjob import BuildFarmJobType
 from lp.buildmaster.model.buildbase import BuildBase
 from lp.buildmaster.model.buildqueue import BuildQueue
@@ -50,23 +53,24 @@
 from lp.soyuz.model.sourcepackagerelease import SourcePackageRelease
 
 
-class SourcePackageRecipeBuild(BuildBase, Storm):
+class SourcePackageRecipeBuild(PackageBuildDerived, Storm):
+
     __storm_table__ = 'SourcePackageRecipeBuild'
 
     policy_name = 'recipe'
 
-    implements(IBuildBase, ISourcePackageRecipeBuild)
+    implements(ISourcePackageRecipeBuild)
     classProvides(ISourcePackageRecipeBuildSource)
 
+    package_build_id = Int(name='package_build', allow_none=False)
+    package_build = Reference(package_build_id, 'PackageBuild.id')
+
     build_farm_job_type = BuildFarmJobType.RECIPEBRANCHBUILD
 
     id = Int(primary=True)
 
     is_private = False
 
-    archive_id = Int(name='archive', allow_none=False)
-    archive = Reference(archive_id, 'Archive.id')
-
     @property
     def binary_builds(self):
         """See `ISourcePackageRecipeBuild`."""
@@ -75,53 +79,10 @@
             SourcePackageRelease.id,
             SourcePackageRelease.source_package_recipe_build==self.id)
 
-    buildduration = TimeDelta(name='build_duration', default=None)
-
-    builder_id = Int(name='builder', allow_none=True)
-    builder = Reference(builder_id, 'Builder.id')
-
-    buildlog_id = Int(name='build_log', allow_none=True)
-    buildlog = Reference(buildlog_id, 'LibraryFileAlias.id')
-
-    buildstate = DBEnum(enum=BuildStatus, name='build_state')
-    dependencies = Unicode(allow_none=True)
-
-    upload_log_id = Int(name='upload_log', allow_none=True)
-    upload_log = Reference(upload_log_id, 'LibraryFileAlias.id')
-
     @property
     def current_component(self):
         return getUtility(IComponentSet)[default_component_dependency_name]
 
-    datecreated = UtcDateTimeCol(notNull=True, dbName='date_created')
-    datebuilt = UtcDateTimeCol(notNull=False, dbName='date_built')
-
-    # See `IBuildBase` - the following attributes are aliased
-    # to allow a shared implementation of the handleStatus methods
-    # until IBuildBase is removed.
-    status = buildstate
-    date_finished = datebuilt
-    log = buildlog
-
-    @property
-    def datestarted(self):
-        """See `IBuild`."""
-        # datestarted is not stored on Build.  It can be calculated from
-        # self.datebuilt and self.buildduration, if both are set.  This does
-        # not happen until the build is complete.
-        #
-        # Before the build is complete, there will be a buildqueue_record.
-        # If buildqueue_record is set, buildqueue_record.job.date_started can
-        # be used.  Otherwise, None is returned.
-        if None not in (self.datebuilt, self.buildduration):
-            return self.datebuilt - self.buildduration
-        queue_record = self.buildqueue_record
-        if queue_record is None:
-            return None
-        return queue_record.job.date_started
-
-    date_first_dispatched = UtcDateTimeCol(notNull=False)
-
     distroseries_id = Int(name='distroseries', allow_none=True)
     distroseries = Reference(distroseries_id, 'DistroSeries.id')
     distro_series = distroseries
@@ -133,8 +94,6 @@
 
     is_virtualized = True
 
-    pocket = DBEnum(enum=PackagePublishingPocket)
-
     recipe_id = Int(name='recipe', allow_none=False)
     recipe = Reference(recipe_id, 'SourcePackageRecipe.id')
 
@@ -180,22 +139,10 @@
     def title(self):
         return '%s recipe build' % self.recipe.base_branch.unique_name
 
-    def __init__(self, distroseries, recipe, requester,
-                 archive, pocket, date_created=None,
-                 date_first_dispatched=None, date_built=None, builder=None,
-                 build_state=BuildStatus.NEEDSBUILD, build_log=None,
-                 build_duration=None):
+    def __init__(self, package_build, distroseries, recipe, requester):
         """Construct a SourcePackageRecipeBuild."""
         super(SourcePackageRecipeBuild, self).__init__()
-        self.archive = archive
-        self.pocket = pocket
-        self.buildduration = build_duration
-        self.buildlog = build_log
-        self.builder = builder
-        self.buildstate = build_state
-        self.datebuilt = date_built
-        self.datecreated = date_created
-        self.date_first_dispatched = date_first_dispatched
+        self.package_build = package_build
         self.distroseries = distroseries
         self.recipe = recipe
         self.requester = requester
@@ -209,13 +156,13 @@
             pocket = PackagePublishingPocket.RELEASE
         if date_created is None:
             date_created = UTC_NOW
+        packagebuild = PackageBuild.new(cls.build_farm_job_type,
+            True, archive, pocket, date_created=date_created)
         spbuild = cls(
+            packagebuild,
             distroseries,
             recipe,
-            requester,
-            archive,
-            pocket,
-            date_created=date_created)
+            requester)
         store.add(spbuild)
         return spbuild
 
@@ -232,6 +179,8 @@
                         distroseries, PackagePublishingPocket.RELEASE)
                 except BuildAlreadyPending:
                     continue
+                except ProgrammingError:
+                    raise
                 except:
                     info = sys.exc_info()
                     errorlog.globalErrorUtility.raising(info)
@@ -269,13 +218,16 @@
 
     @classmethod
     def getRecentBuilds(cls, requester, recipe, distroseries, _now=None):
+        from lp.buildmaster.model.buildfarmjob import BuildFarmJob
         if _now is None:
             _now = datetime.datetime.now(utc)
         store = IMasterStore(SourcePackageRecipeBuild)
         old_threshold = _now - datetime.timedelta(days=1)
         return store.find(cls, cls.distroseries_id == distroseries.id,
             cls.requester_id == requester.id, cls.recipe_id == recipe.id,
-            cls.datecreated > old_threshold)
+            BuildFarmJob.date_created > old_threshold,
+            BuildFarmJob.id == PackageBuild.build_farm_job_id,
+            PackageBuild.id == cls.package_build_id)
 
     def makeJob(self):
         """See `ISourcePackageRecipeBuildJob`."""
@@ -301,10 +253,35 @@
         mailer = SourcePackageRecipeBuildMailer.forStatus(self)
         mailer.sendAll()
 
+    def lfaUrl(self, lfa):
+        """Return the URL for a LibraryFileAlias, in the context of self.
+        """
+        if lfa is None:
+            return None
+        return ProxiedLibraryFileAlias(lfa, self).http_url
+
+    @property
+    def log_url(self):
+        """See `IPackageBuild`.
+
+        Overridden here so that it uses the SourcePackageRecipeBuild as
+        context.
+        """
+        return self.lfaUrl(self.log)
+
+    @property
+    def upload_log_url(self):
+        """See `IPackageBuild`.
+
+        Overridden here so that it uses the SourcePackageRecipeBuild as
+        context.
+        """
+        return self.lfaUrl(self.upload_log)
+
     def getFileByName(self, filename):
         """See `ISourcePackageRecipeBuild`."""
         files = dict((lfa.filename, lfa)
-                     for lfa in [self.buildlog, self.upload_log]
+                     for lfa in [self.log, self.upload_log]
                      if lfa is not None)
         try:
             return files[filename]

=== modified file 'lib/lp/code/model/tests/test_sourcepackagerecipe.py'
--- lib/lp/code/model/tests/test_sourcepackagerecipe.py	2010-07-22 08:39:05 +0000
+++ lib/lp/code/model/tests/test_sourcepackagerecipe.py	2010-07-23 19:32:55 +0000
@@ -304,7 +304,7 @@
         def request_build():
             build = recipe.requestBuild(archive, requester, series,
                     PackagePublishingPocket.RELEASE)
-            removeSecurityProxy(build).buildstate = BuildStatus.FULLYBUILT
+            removeSecurityProxy(build).status = BuildStatus.FULLYBUILT
         [request_build() for num in range(5)]
         e = self.assertRaises(TooManyBuilds, request_build)
         self.assertIn(
@@ -331,7 +331,7 @@
         recipe.requestBuild(archive, recipe.owner,
             new_distroseries, PackagePublishingPocket.RELEASE)
         # Changing status of old build allows new build.
-        removeSecurityProxy(old_build).buildstate = BuildStatus.FULLYBUILT
+        removeSecurityProxy(old_build).status = BuildStatus.FULLYBUILT
         recipe.requestBuild(archive, recipe.owner, series,
                 PackagePublishingPocket.RELEASE)
 
@@ -425,18 +425,21 @@
             SourcePackageRecipe.findStaleDailyBuilds())
 
     def test_getMedianBuildDuration(self):
+        def set_duration(build, minutes):
+            duration = timedelta(minutes=minutes)
+            build = removeSecurityProxy(build)
+            build.date_started = self.factory.getUniqueDate()
+            build.date_finished = build.date_started + duration
         recipe = removeSecurityProxy(self.factory.makeSourcePackageRecipe())
         self.assertIs(None, recipe.getMedianBuildDuration())
-        build = removeSecurityProxy(
-            self.factory.makeSourcePackageRecipeBuild(recipe=recipe))
-        build.buildduration = timedelta(minutes=10)
+        build = self.factory.makeSourcePackageRecipeBuild(recipe=recipe)
+        set_duration(build, 10)
         self.assertEqual(
             timedelta(minutes=10), recipe.getMedianBuildDuration())
 
         def addBuild(minutes):
-            build = removeSecurityProxy(
-                self.factory.makeSourcePackageRecipeBuild(recipe=recipe))
-            build.buildduration = timedelta(minutes=minutes)
+            build = self.factory.makeSourcePackageRecipeBuild(recipe=recipe)
+            set_duration(build, minutes)
         addBuild(20)
         self.assertEqual(
             timedelta(minutes=10), recipe.getMedianBuildDuration())

=== modified file 'lib/lp/code/model/tests/test_sourcepackagerecipebuild.py'
--- lib/lp/code/model/tests/test_sourcepackagerecipebuild.py	2010-07-22 08:15:29 +0000
+++ lib/lp/code/model/tests/test_sourcepackagerecipebuild.py	2010-07-23 19:32:55 +0000
@@ -11,7 +11,6 @@
 import re
 import unittest
 
-from pytz import utc
 import transaction
 from storm.locals import Store
 from zope.component import getUtility
@@ -23,7 +22,7 @@
 from canonical.launchpad.interfaces.lpstorm import IStore
 from canonical.launchpad.webapp.authorization import check_permission
 from canonical.launchpad.webapp.testing import verifyObject
-from lp.buildmaster.interfaces.buildbase import BuildStatus, IBuildBase
+from lp.buildmaster.interfaces.buildbase import BuildStatus
 from lp.buildmaster.interfaces.buildqueue import IBuildQueue
 from lp.buildmaster.tests.test_buildbase import (
     TestGetUploadMethodsMixin, TestHandleStatusMixin)
@@ -69,7 +68,6 @@
         # SourcePackageRecipeBuild provides IBuildBase and
         # ISourcePackageRecipeBuild.
         spb = self.makeSourcePackageRecipeBuild()
-        self.assertProvides(spb, IBuildBase)
         self.assertProvides(spb, ISourcePackageRecipeBuild)
 
     def test_implements_interface(self):
@@ -156,44 +154,28 @@
     def test_estimateDuration(self):
         # If there are no successful builds, estimate 10 minutes.
         spb = self.makeSourcePackageRecipeBuild()
+        cur_date = self.factory.getUniqueDate()
         self.assertEqual(
             datetime.timedelta(minutes=10), spb.estimateDuration())
         for minutes in [20, 5, 1]:
             build = removeSecurityProxy(
                 self.factory.makeSourcePackageRecipeBuild(recipe=spb.recipe))
-            build.buildduration = datetime.timedelta(minutes=minutes)
+            build.date_started = cur_date
+            build.date_finished = (
+                cur_date + datetime.timedelta(minutes=minutes))
         self.assertEqual(
             datetime.timedelta(minutes=5), spb.estimateDuration())
 
-    def test_datestarted(self):
-        """Datestarted is taken from job if not specified in the build.
-
-        Specifying datestarted in the build requires datebuilt and
-        buildduration to be specified.
-        """
-        spb = self.makeSourcePackageRecipeBuild()
-        self.assertIs(None, spb.datestarted)
-        job = self.factory.makeSourcePackageRecipeBuildJob(
-            recipe_build=spb).job
-        job.start()
-        self.assertEqual(job.date_started, spb.datestarted)
-        now = datetime.datetime.now(utc)
-        removeSecurityProxy(spb).datebuilt = now
-        self.assertEqual(job.date_started, spb.datestarted)
-        duration = datetime.timedelta(minutes=1)
-        removeSecurityProxy(spb).buildduration = duration
-        self.assertEqual(now - duration, spb.datestarted)
-
     def test_getFileByName(self):
         """getFileByName returns the logs when requested by name."""
         spb = self.factory.makeSourcePackageRecipeBuild()
-        removeSecurityProxy(spb).buildlog = (
+        removeSecurityProxy(spb).log = (
             self.factory.makeLibraryFileAlias(filename='buildlog.txt.gz'))
-        self.assertEqual(spb.buildlog, spb.getFileByName('buildlog.txt.gz'))
+        self.assertEqual(spb.log, spb.getFileByName('buildlog.txt.gz'))
         self.assertRaises(NotFoundError, spb.getFileByName, 'foo')
-        removeSecurityProxy(spb).buildlog = (
+        removeSecurityProxy(spb).log = (
             self.factory.makeLibraryFileAlias(filename='foo'))
-        self.assertEqual(spb.buildlog, spb.getFileByName('foo'))
+        self.assertEqual(spb.log, spb.getFileByName('foo'))
         self.assertRaises(NotFoundError, spb.getFileByName, 'buildlog.txt.gz')
         removeSecurityProxy(spb).upload_log = (
             self.factory.makeLibraryFileAlias(filename='upload.txt.gz'))
@@ -287,7 +269,7 @@
             date_created=yesterday)
         self.assertContentEqual([], get_recent())
         a_second = datetime.timedelta(seconds=1)
-        removeSecurityProxy(recent_build).datecreated += a_second
+        removeSecurityProxy(recent_build).date_created += a_second
         self.assertContentEqual([recent_build], get_recent())
 
     def test_destroySelf(self):
@@ -320,7 +302,7 @@
         secret = self.factory.makeDistroSeries(name=u'distroseries')
         build = self.factory.makeSourcePackageRecipeBuild(
             recipe=cake, distroseries=secret, archive=pantry)
-        removeSecurityProxy(build).buildstate = BuildStatus.FULLYBUILT
+        removeSecurityProxy(build).status = BuildStatus.FULLYBUILT
         IStore(build).flush()
         build.notify()
         (message, ) = pop_notifications()
@@ -342,7 +324,7 @@
         def prepare_build():
             queue_record = self.factory.makeSourcePackageRecipeBuildJob()
             build = queue_record.specific_job.build
-            removeSecurityProxy(build).buildstate = BuildStatus.FULLYBUILT
+            removeSecurityProxy(build).status = BuildStatus.FULLYBUILT
             queue_record.builder = self.factory.makeBuilder()
             slave = WaitingSlave('BuildStatus.OK')
             queue_record.builder.setSlaveForTesting(slave)

=== modified file 'lib/lp/code/templates/sourcepackagerecipebuild-index.pt'
--- lib/lp/code/templates/sourcepackagerecipebuild-index.pt	2010-07-21 10:48:23 +0000
+++ lib/lp/code/templates/sourcepackagerecipebuild-index.pt	2010-07-23 19:32:55 +0000
@@ -12,8 +12,8 @@
     <tal:registering metal:fill-slot="registering">
       <p>
         created
-        <span tal:content="context/datecreated/fmt:displaydate"
-              tal:attributes="title context/datecreated/fmt:datetime"
+        <span tal:content="context/date_created/fmt:displaydate"
+              tal:attributes="title context/date_created/fmt:datetime"
           >on 2005-01-01</span>
       </p>
     </tal:registering>
@@ -37,7 +37,7 @@
       </div> <!-- yui-g  -->
 
       <div id="buildlog" class="portlet"
-           tal:condition="context/buildstate/enumvalue:BUILDING">
+           tal:condition="context/status/enumvalue:BUILDING">
         <div metal:use-macro="template/macros/buildlog" />
       </div>
 
@@ -95,9 +95,9 @@
     <p>
       <span tal:replace="structure context/image:icon" />
       <span tal:attributes="
-            class string:buildstatus${context/buildstate/name};"
-            tal:content="context/buildstate/title">Fully built</span>
-      <tal:building condition="context/buildstate/enumvalue:BUILDING">
+            class string:buildstatus${context/status/name};"
+            tal:content="context/status/title">Fully built</span>
+      <tal:building condition="context/status/enumvalue:BUILDING">
         on <a tal:content="context/buildqueue_record/builder/title"
               tal:attributes="href context/buildqueue_record/builder/fmt:url"/>
       </tal:building>
@@ -120,33 +120,33 @@
         </li>
       </tal:pending>
       </tal:reallypending>
-      <tal:started condition="context/datestarted">
-        <li tal:condition="context/datestarted">
+      <tal:started condition="context/date_started">
+        <li tal:condition="context/date_started">
           Started <span
-           tal:define="start context/datestarted"
+           tal:define="start context/date_started"
            tal:attributes="title start/fmt:datetime"
            tal:content="start/fmt:displaydate">2008-01-01</span>
         </li>
       </tal:started>
-      <tal:finish condition="not: context/datebuilt">
+      <tal:finish condition="not: context/date_finished">
         <li tal:define="eta view/eta" tal:condition="view/eta">
           Estimated finish <tal:eta
             replace="eta/fmt:approximatedate">in 3 hours</tal:eta>
         </li>
       </tal:finish>
 
-      <li tal:condition="context/datebuilt">
+      <li tal:condition="context/date_finished">
         Finished <span
-          tal:attributes="title context/datebuilt/fmt:datetime"
-          tal:content="context/datebuilt/fmt:displaydate">2008-01-01</span>
-        <tal:duration condition="context/buildduration">
-          (took <span tal:replace="context/buildduration/fmt:exactduration"/>)
+          tal:attributes="title context/date_finished/fmt:datetime"
+          tal:content="context/date_finished/fmt:displaydate">2008-01-01</span>
+        <tal:duration condition="context/duration">
+          (took <span tal:replace="context/duration/fmt:exactduration"/>)
         </tal:duration>
       </li>
-      <li tal:define="file context/buildlog"
+      <li tal:define="file context/log"
           tal:condition="file">
         <a class="sprite download"
-           tal:attributes="href context/build_log_url">buildlog</a>
+           tal:attributes="href context/log_url">buildlog</a>
         (<span tal:replace="file/content/filesize/fmt:bytes" />)
       </li>
       <li tal:define="file context/upload_log"

=== modified file 'lib/lp/testing/factory.py'
--- lib/lp/testing/factory.py	2010-07-22 14:06:45 +0000
+++ lib/lp/testing/factory.py	2010-07-23 19:32:55 +0000
@@ -158,7 +158,6 @@
     ANONYMOUS,
     login,
     login_as,
-    logout,
     run_with_login,
     temp_dir,
     time_counter,
@@ -1864,7 +1863,7 @@
             requester=requester,
             pocket=pocket,
             date_created=date_created)
-        removeSecurityProxy(spr_build).buildstate = status
+        removeSecurityProxy(spr_build).status = status
         return spr_build
 
     def makeSourcePackageRecipeBuildJob(