Merge lp:~michael.nelson/launchpad/594492-present-bfjs-in-builder-history into lp:launchpad

Proposed by Michael Nelson
Status: Merged
Approved by: Michael Nelson
Approved revision: no longer in the source branch.
Merged at revision: 11041
Proposed branch: lp:~michael.nelson/launchpad/594492-present-bfjs-in-builder-history
Merge into: lp:launchpad
Prerequisite: lp:~michael.nelson/launchpad/536700-present-packagebuilds-in-ppa-context-2
Diff against target: 698 lines (+323/-51)
15 files modified
lib/lp/buildmaster/configure.zcml (+6/-0)
lib/lp/buildmaster/interfaces/buildfarmjob.py (+15/-0)
lib/lp/buildmaster/model/builder.py (+13/-8)
lib/lp/buildmaster/model/buildfarmjob.py (+71/-2)
lib/lp/buildmaster/tests/test_buildfarmjob.py (+132/-12)
lib/lp/registry/model/distribution.py (+3/-1)
lib/lp/soyuz/browser/build.py (+7/-1)
lib/lp/soyuz/browser/builder.py (+1/-0)
lib/lp/soyuz/interfaces/archive.py (+3/-9)
lib/lp/soyuz/interfaces/buildrecords.py (+8/-2)
lib/lp/soyuz/model/archive.py (+4/-3)
lib/lp/soyuz/model/distroarchseries.py (+3/-1)
lib/lp/soyuz/tests/test_binarypackagebuild.py (+2/-2)
lib/lp/soyuz/tests/test_hasbuildrecords.py (+42/-3)
lib/lp/testing/factory.py (+13/-7)
To merge this branch: bzr merge lp:~michael.nelson/launchpad/594492-present-bfjs-in-builder-history
Reviewer Review Type Date Requested Status
Jeroen T. Vermeulen (community) code Approve
Canonical Launchpad Engineering Pending
Review via email: mp+27717@code.launchpad.net

Commit message

Builder history presents general build farm jobs.

Description of the change

Please review my branch:

   bzr+ssh://bazaar.launchpad.net/~michael.nelson/launchpad/594492-present-bfjs-in-builder-history

revision 11026.

Test command:
bin/test -vvm test_buildfarmjob -m test_hasbuildrecords
Pre-implementation call with: Lots of discussion on bug 491330.

This is a fix for bug 491330 that doesn't yet have any visible effect. It
updates the Builder.getBuildRecords() query to accept binary_only=False, in
which case it will return BuildFarmJob records for that builder using the new
IBuildFarmJobSet.getBuildsForBuilder() method.

This is now used by the BuilderHistory view, so that when other BuildFarmJob
types are added, they will appear automatically in the builder history.

The main issue in this branch was *not* including BuildFarmJobs that are private. Currently this is only the case if the BuildFarmJob is also a PackageBuild, and the package build is associated with a private archive. I tried both using ResultSet.difference and a left join, and went with the left join in the end.

To post a comment you must log in.
Revision history for this message
Michael Nelson (michael.nelson) wrote :

Carried out an irc review with jtv yesterday:

http://irclogs.ubuntu.com/2010/06/16/%23launchpad-reviews.html#t14:53

The changes are pushed and attached from that discussion. Thanks jtv!

1=== modified file 'lib/lp/buildmaster/interfaces/buildfarmjob.py'
2--- lib/lp/buildmaster/interfaces/buildfarmjob.py 2010-06-15 16:17:17 +0000
3+++ lib/lp/buildmaster/interfaces/buildfarmjob.py 2010-06-16 14:12:20 +0000
4@@ -294,13 +294,14 @@
5
6
7 class IBuildFarmJobSet(Interface):
8- """A utility representing a set of package builds."""
9+ """A utility representing a set of build farm jobs."""
10
11 def getBuildsForBuilder(builder_id, status=None, user=None):
12 """Return `IBuildFarmJob` records touched by a builder.
13
14 :param builder_id: The id of the builder for which to find builds.
15- :param status: If status is provided, only builds with that status
16- will be returned.
17+ :param status: If given, limit the search to builds with this status.
18+ :param user: If given, this will be used to determine private builds
19+ that should be included.
20 :return: a `ResultSet` representing the requested builds.
21 """
22
23=== modified file 'lib/lp/buildmaster/model/buildfarmjob.py'
24--- lib/lp/buildmaster/model/buildfarmjob.py 2010-06-16 11:25:18 +0000
25+++ lib/lp/buildmaster/model/buildfarmjob.py 2010-06-16 16:01:08 +0000
26@@ -16,7 +16,7 @@
27
28 import pytz
29
30-from storm.expr import And, Desc, Join, LeftJoin, Or, Select
31+from storm.expr import And, Coalesce, Desc, Join, LeftJoin, Select
32 from storm.locals import Bool, DateTime, Int, Reference, Storm
33 from storm.store import Store
34
35@@ -28,7 +28,7 @@
36 from canonical.database.constants import UTC_NOW
37 from canonical.database.enumcol import DBEnum
38 from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities
39-from canonical.launchpad.interfaces.lpstorm import IMasterStore
40+from canonical.launchpad.interfaces.lpstorm import IMasterStore, IStore
41 from canonical.launchpad.webapp.interfaces import (
42 DEFAULT_FLAVOR, IStoreSelector, MAIN_STORE)
43
44@@ -353,16 +353,19 @@
45
46 def getBuildsForBuilder(self, builder_id, status=None, user=None):
47 """See `IBuildFarmJobSet`."""
48- store = getUtility(IStoreSelector).get(MAIN_STORE, DEFAULT_FLAVOR)
49 # Imported here to avoid circular imports.
50 from lp.buildmaster.model.packagebuild import PackageBuild
51 from lp.soyuz.model.archive import Archive
52
53+ extra_clauses = [BuildFarmJob.builder == builder_id]
54+ if status is not None:
55+ extra_clauses.append(BuildFarmJob.status == status)
56+
57 # We need to ensure that we don't include any private builds.
58 # Currently only package builds can be private (via their
59 # related archive), but not all build farm jobs will have a
60 # related package build - hence the left join.
61- bfjs_with_optional_package_builds = LeftJoin(
62+ left_join_pkg_builds = LeftJoin(
63 BuildFarmJob,
64 Join(
65 PackageBuild,
66@@ -370,35 +373,41 @@
67 And(PackageBuild.archive == Archive.id)),
68 PackageBuild.build_farm_job == BuildFarmJob.id)
69
70- filtered_builds = store.using(bfjs_with_optional_package_builds).find(
71- BuildFarmJob,
72- BuildFarmJob.builder == builder_id)
73-
74- if status is not None:
75- filtered_builds = filtered_builds.find(
76- BuildFarmJob.status == status)
77-
78- # Anonymous users can only see public builds.
79+ filtered_builds = IStore(BuildFarmJob).using(
80+ left_join_pkg_builds).find(BuildFarmJob, *extra_clauses)
81+
82 if user is None:
83- filtered_builds = filtered_builds.find(
84- Or(Archive.private == None, Archive.private == False))
85-
86- # All other non-admins can additionally see private builds for
87- # which they are a member of the team owning the archive.
88- elif not user.inTeam(getUtility(ILaunchpadCelebrities).admin):
89+ # Anonymous requests don't get to see private builds at all.
90+ filtered_builds = filtered_builds.find(
91+ Coalesce(Archive.private, False) == False)
92+
93+ elif user.inTeam(getUtility(ILaunchpadCelebrities).admin):
94+ # Admins get to see everything.
95+ pass
96+ else:
97+ # Everyone else sees a union of all public builds and the
98+ # specific private builds to which they have access.
99+ filtered_builds = filtered_builds.find(
100+ Coalesce(Archive.private, False) == False)
101+
102 user_teams_subselect = Select(
103 TeamParticipation.teamID,
104 where=And(
105 TeamParticipation.personID == user.id,
106 TeamParticipation.teamID == Archive.ownerID))
107- filtered_builds = filtered_builds.find(
108- Or(
109- Archive.private == None,
110- Or(
111- Archive.private == False,
112- Archive.ownerID.is_in(user_teams_subselect))))
113-
114- filtered_builds.order_by(Desc(BuildFarmJob.date_finished), BuildFarmJob.id)
115+ private_builds_for_user = IStore(BuildFarmJob).find(
116+ BuildFarmJob,
117+ PackageBuild.build_farm_job == BuildFarmJob.id,
118+ PackageBuild.archive == Archive.id,
119+ Archive.private == True,
120+ Archive.ownerID.is_in(user_teams_subselect),
121+ *extra_clauses)
122+
123+ filtered_builds = filtered_builds.union(
124+ private_builds_for_user)
125+
126+ filtered_builds.order_by(
127+ Desc(BuildFarmJob.date_finished), BuildFarmJob.id)
128
129 return filtered_builds
130
131
132=== modified file 'lib/lp/buildmaster/tests/test_buildfarmjob.py'
133--- lib/lp/buildmaster/tests/test_buildfarmjob.py 2010-06-16 11:25:18 +0000
134+++ lib/lp/buildmaster/tests/test_buildfarmjob.py 2010-06-17 08:26:34 +0000
135@@ -24,28 +24,37 @@
136 BuildFarmJobType, IBuildFarmJob, IBuildFarmJobSet, IBuildFarmJobSource,
137 InconsistentBuildFarmJobError)
138 from lp.buildmaster.model.buildfarmjob import BuildFarmJob
139-from lp.testing import login, TestCaseWithFactory
140-
141-
142-class TestBuildFarmJobBase(TestCaseWithFactory):
143+from lp.testing import ANONYMOUS, login, login_person, TestCaseWithFactory
144+
145+
146+class TestBuildFarmJobMixin:
147
148 layer = DatabaseFunctionalLayer
149
150 def setUp(self):
151 """Create a build farm job with which to test."""
152- super(TestBuildFarmJobBase, self).setUp()
153+ super(TestBuildFarmJobMixin, self).setUp()
154 self.build_farm_job = self.makeBuildFarmJob()
155
156 def makeBuildFarmJob(self, builder=None,
157 job_type=BuildFarmJobType.PACKAGEBUILD,
158- status=BuildStatus.FULLYBUILT):
159+ status=BuildStatus.NEEDSBUILD,
160+ date_finished=None):
161+ """A factory method for creating PackageBuilds.
162+
163+ This is not included in the launchpad test factory because
164+ a build farm job should never be instantiated outside the
165+ context of a derived class (such as a BinaryPackageBuild
166+ or eventually a SPRecipeBuild).
167+ """
168 build_farm_job = getUtility(IBuildFarmJobSource).new(
169 job_type=job_type, status=status)
170 removeSecurityProxy(build_farm_job).builder = builder
171+ removeSecurityProxy(build_farm_job).date_finished = date_finished
172 return build_farm_job
173
174
175-class TestBuildFarmJob(TestBuildFarmJobBase):
176+class TestBuildFarmJob(TestBuildFarmJobMixin, TestCaseWithFactory):
177 """Tests for the build farm job object."""
178
179 def test_providesInterface(self):
180@@ -62,7 +71,6 @@
181 self.assertEqual(self.build_farm_job, retrieved_job)
182
183 def test_default_values(self):
184- # A build farm job defaults to the NEEDSBUILD status.
185 # We flush the database updates to ensure sql defaults
186 # are set for various attributes.
187 flush_database_updates()
188@@ -172,7 +180,7 @@
189 InconsistentBuildFarmJobError, self.build_farm_job.getSpecificJob)
190
191
192-class TestBuildFarmJobSecurity(TestBuildFarmJobBase):
193+class TestBuildFarmJobSecurity(TestBuildFarmJobMixin, TestCaseWithFactory):
194
195 def test_view_build_farm_job(self):
196 # Anonymous access can read public builds, but not edit.
197@@ -190,100 +198,102 @@
198 BuildStatus.FULLYBUILT, self.build_farm_job.status)
199
200
201-class TestBuildFarmJobSet(TestBuildFarmJobBase):
202+class TestBuildFarmJobSet(TestBuildFarmJobMixin, TestCaseWithFactory):
203
204 layer = LaunchpadFunctionalLayer
205
206 def setUp(self):
207 super(TestBuildFarmJobSet, self).setUp()
208 self.builder = self.factory.makeBuilder()
209- self.build_farm_jobs = []
210- self.build_farm_jobs.append(
211- self.makeBuildFarmJob(builder=self.builder))
212- self.build_farm_jobs.append(self.makeBuildFarmJob(
213- builder=self.builder,
214- job_type=BuildFarmJobType.RECIPEBRANCHBUILD))
215- self.build_farm_jobs.append(self.makeBuildFarmJob(
216- builder=self.builder, status=BuildStatus.BUILDING))
217-
218- # For good measure, create a different type of build that will
219- # also have an associated PackageBuild.
220- owning_team = self.factory.makeTeam()
221- archive = self.factory.makeArchive(owner=owning_team)
222- self.binary_package_build = self.factory.makeBinaryPackageBuild(
223- archive=archive, builder=self.builder)
224- self.build_farm_jobs.append(self.binary_package_build.build_farm_job)
225-
226 self.build_farm_job_set = getUtility(IBuildFarmJobSet)
227
228 def test_getBuildsForBuilder_all(self):
229 # The default call without arguments returns all builds for the
230 # builder, and not those for other builders.
231+ build1 = self.makeBuildFarmJob(builder=self.builder)
232+ build2 = self.makeBuildFarmJob(builder=self.builder)
233 self.makeBuildFarmJob(builder=self.factory.makeBuilder())
234- self.assertContentEqual(
235- self.build_farm_jobs, self.build_farm_job_set.getBuildsForBuilder(
236- self.builder))
237+ result = self.build_farm_job_set.getBuildsForBuilder(self.builder)
238+
239+ self.assertContentEqual([build1, build2], result)
240
241 def test_getBuildsForBuilder_by_status(self):
242 # If the status arg is used, the results will be filtered by
243 # status.
244- self.assertContentEqual(
245- self.build_farm_jobs[:2],
246- self.build_farm_job_set.getBuildsForBuilder(
247- self.builder, status=BuildStatus.FULLYBUILT))
248-
249- def makeBuildPrivate(self, build):
250- """Helper to privatise a package build."""
251- removeSecurityProxy(build.archive).buildd_secret = "blah"
252- removeSecurityProxy(build.archive).private = True
253+ successful_builds = [
254+ self.makeBuildFarmJob(
255+ builder=self.builder, status=BuildStatus.FULLYBUILT),
256+ self.makeBuildFarmJob(
257+ builder=self.builder, status=BuildStatus.FULLYBUILT),
258+ ]
259+ self.makeBuildFarmJob(builder=self.builder)
260+
261+ query_by_status = self.build_farm_job_set.getBuildsForBuilder(
262+ self.builder, status=BuildStatus.FULLYBUILT)
263+
264+ self.assertContentEqual(successful_builds, query_by_status)
265+
266+ def _makePrivateAndNonPrivateBuilds(self, owning_team=None):
267+ """Return a tuple of a private and non-private build farm job."""
268+ if owning_team is None:
269+ owning_team = self.factory.makeTeam()
270+ archive = self.factory.makeArchive(owner=owning_team, private=True)
271+ login_person(owning_team.teamowner)
272+ private_build = self.factory.makeBinaryPackageBuild(
273+ archive=archive, builder=self.builder)
274+ private_build = private_build.build_farm_job
275+ login(ANONYMOUS)
276+ other_build = self.makeBuildFarmJob(builder=self.builder)
277+ return (private_build, other_build)
278
279 def test_getBuildsForBuilder_hides_private_from_anon(self):
280 # If no user is passed, all private builds are filtered out.
281-
282- # When private it is not included for anon requests.
283- self.makeBuildPrivate(self.binary_package_build)
284+ private_build, other_build = self._makePrivateAndNonPrivateBuilds()
285 result = self.build_farm_job_set.getBuildsForBuilder(self.builder)
286- self.assertTrue(
287- self.binary_package_build.build_farm_job not in result)
288+ self.assertContentEqual([other_build], result)
289
290 def test_getBuildsForBuilder_hides_private_other_users(self):
291 # Private builds are not returned for users without permission
292 # to view them.
293- self.makeBuildPrivate(self.binary_package_build)
294+ private_build, other_build = self._makePrivateAndNonPrivateBuilds()
295 result = self.build_farm_job_set.getBuildsForBuilder(
296 self.builder, user=self.factory.makePerson())
297- self.assertTrue(
298- self.binary_package_build.build_farm_job not in result)
299+ self.assertContentEqual([other_build], result)
300
301 def test_getBuildsForBuilder_shows_private_to_admin(self):
302 # Admin users can see private builds.
303 admin_team = getUtility(ILaunchpadCelebrities).admin
304- self.makeBuildPrivate(self.binary_package_build)
305+ private_build, other_build = self._makePrivateAndNonPrivateBuilds()
306 result = self.build_farm_job_set.getBuildsForBuilder(
307 self.builder, user=admin_team.teamowner)
308- self.assertTrue(self.binary_package_build.build_farm_job in result)
309+ self.assertContentEqual([private_build, other_build], result)
310
311 def test_getBuildsForBuilder_shows_private_to_authorised(self):
312 # Similarly, if the user is in the owning team they can see it.
313- self.makeBuildPrivate(self.binary_package_build)
314+ owning_team = self.factory.makeTeam()
315+ private_build, other_build = self._makePrivateAndNonPrivateBuilds(
316+ owning_team=owning_team)
317 result = self.build_farm_job_set.getBuildsForBuilder(
318 self.builder,
319- user=self.binary_package_build.archive.owner.teamowner)
320- self.assertTrue(self.binary_package_build.build_farm_job in result)
321+ user=owning_team.teamowner)
322+ self.assertContentEqual([private_build, other_build], result)
323
324 def test_getBuildsForBuilder_ordered_by_date_finished(self):
325 # Results are returned with the oldest build last.
326- naked_build_0 = removeSecurityProxy(self.build_farm_jobs[0])
327- naked_build_1 = removeSecurityProxy(self.build_farm_jobs[1])
328-
329- naked_build_0.date_finished = datetime(2008, 10, 10, tzinfo=pytz.UTC)
330- naked_build_1.date_finished = datetime(2008, 11, 10, tzinfo=pytz.UTC)
331- result = self.build_farm_job_set.getBuildsForBuilder(self.builder)
332- self.assertEqual(self.build_farm_jobs[0], result[3])
333-
334- naked_build_0.date_finished = datetime(2008, 12, 10, tzinfo=pytz.UTC)
335- result = self.build_farm_job_set.getBuildsForBuilder(self.builder)
336- self.assertEqual(self.build_farm_jobs[1], result[3])
337+ build_1 = self.makeBuildFarmJob(
338+ builder=self.builder,
339+ date_finished=datetime(2008, 10, 10, tzinfo=pytz.UTC))
340+ build_2 = self.makeBuildFarmJob(
341+ builder=self.builder,
342+ date_finished=datetime(2008, 11, 10, tzinfo=pytz.UTC))
343+
344+ result = self.build_farm_job_set.getBuildsForBuilder(self.builder)
345+ self.assertEqual([build_2, build_1], list(result))
346+
347+ removeSecurityProxy(build_2).date_finished = (
348+ datetime(2008, 8, 10, tzinfo=pytz.UTC))
349+ result = self.build_farm_job_set.getBuildsForBuilder(self.builder)
350+ self.assertEqual([build_1, build_2], list(result))
351
352
353 def test_suite():
Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :

A few more changes discussed on IRC:

 * The tests are much better, but hiding a login() in a helper is a bit obscure. Replaced with a removeSecurityProxy.
 * In test_binary_only_false, instead of just testing that builds.count() == 3 for a binary-only query, better to test separately that (1) all its elements are binary builds, (2) the result is nonempty so the test isn't trivial, and (3) the result is smaller than with a non-binary-only query so the test isn't meaningless.

Great shape now. Land that baby!

Quod subigo farinam,

Jeroen

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/buildmaster/configure.zcml'
2--- lib/lp/buildmaster/configure.zcml 2010-06-21 07:32:35 +0000
3+++ lib/lp/buildmaster/configure.zcml 2010-06-21 07:32:36 +0000
4@@ -58,6 +58,12 @@
5 <allow
6 interface="lp.buildmaster.interfaces.buildfarmjob.IBuildFarmJobSource" />
7 </securedutility>
8+ <securedutility
9+ class="lp.buildmaster.model.buildfarmjob.BuildFarmJobSet"
10+ provides="lp.buildmaster.interfaces.buildfarmjob.IBuildFarmJobSet">
11+ <allow
12+ interface="lp.buildmaster.interfaces.buildfarmjob.IBuildFarmJobSet" />
13+ </securedutility>
14
15 <!-- PackageBuild -->
16 <class
17
18=== modified file 'lib/lp/buildmaster/interfaces/buildfarmjob.py'
19--- lib/lp/buildmaster/interfaces/buildfarmjob.py 2010-06-21 07:32:35 +0000
20+++ lib/lp/buildmaster/interfaces/buildfarmjob.py 2010-06-21 07:32:36 +0000
21@@ -10,6 +10,7 @@
22 __all__ = [
23 'IBuildFarmJob',
24 'IBuildFarmJobOld',
25+ 'IBuildFarmJobSet',
26 'IBuildFarmJobSource',
27 'InconsistentBuildFarmJobError',
28 'ISpecificBuildFarmJob',
29@@ -290,3 +291,17 @@
30 :param virtualized: An optional boolean indicating whether
31 this job should be run virtualized.
32 """
33+
34+
35+class IBuildFarmJobSet(Interface):
36+ """A utility representing a set of build farm jobs."""
37+
38+ def getBuildsForBuilder(builder_id, status=None, user=None):
39+ """Return `IBuildFarmJob` records touched by a builder.
40+
41+ :param builder_id: The id of the builder for which to find builds.
42+ :param status: If given, limit the search to builds with this status.
43+ :param user: If given, this will be used to determine private builds
44+ that should be included.
45+ :return: a `ResultSet` representing the requested builds.
46+ """
47
48=== modified file 'lib/lp/buildmaster/model/builder.py'
49--- lib/lp/buildmaster/model/builder.py 2010-06-21 07:32:35 +0000
50+++ lib/lp/buildmaster/model/builder.py 2010-06-21 07:32:36 +0000
51@@ -45,6 +45,7 @@
52 BuildDaemonError, BuildSlaveFailure, CannotBuild, CannotFetchFile,
53 CannotResumeHost, CorruptBuildCookie, IBuilder, IBuilderSet,
54 ProtocolVersionMismatch)
55+from lp.buildmaster.interfaces.buildfarmjob import IBuildFarmJobSet
56 from lp.buildmaster.interfaces.buildfarmjobbehavior import (
57 BuildBehaviorMismatch)
58 from lp.buildmaster.interfaces.buildqueue import IBuildQueueSet
59@@ -59,7 +60,8 @@
60 # These dependencies on soyuz will be removed when getBuildRecords()
61 # is moved.
62 from lp.soyuz.interfaces.binarypackagebuild import IBinaryPackageBuildSet
63-from lp.soyuz.interfaces.buildrecords import IHasBuildRecords
64+from lp.soyuz.interfaces.buildrecords import (
65+ IHasBuildRecords, IncompatibleArguments)
66 from lp.soyuz.model.buildpackagejob import BuildPackageJob
67
68
69@@ -423,16 +425,19 @@
70 self.builderok = False
71 self.failnotes = reason
72
73- # XXX Michael Nelson 20091202 bug=491330. The current UI assumes
74- # that the builder history will display binary build records, as
75- # returned by getBuildRecords() below. See the bug for a discussion
76- # of the options.
77-
78 def getBuildRecords(self, build_state=None, name=None, arch_tag=None,
79 user=None, binary_only=True):
80 """See IHasBuildRecords."""
81- return getUtility(IBinaryPackageBuildSet).getBuildsForBuilder(
82- self.id, build_state, name, arch_tag, user)
83+ if binary_only:
84+ return getUtility(IBinaryPackageBuildSet).getBuildsForBuilder(
85+ self.id, build_state, name, arch_tag, user)
86+ else:
87+ if arch_tag is not None or name is not None:
88+ raise IncompatibleArguments(
89+ "The 'arch_tag' and 'name' parameters can be used only "
90+ "with binary_only=True.")
91+ return getUtility(IBuildFarmJobSet).getBuildsForBuilder(
92+ self, status=build_state, user=user)
93
94 def slaveStatus(self):
95 """See IBuilder."""
96
97=== modified file 'lib/lp/buildmaster/model/buildfarmjob.py'
98--- lib/lp/buildmaster/model/buildfarmjob.py 2010-06-21 07:32:35 +0000
99+++ lib/lp/buildmaster/model/buildfarmjob.py 2010-06-21 07:32:36 +0000
100@@ -16,6 +16,7 @@
101
102 import pytz
103
104+from storm.expr import And, Coalesce, Desc, Join, LeftJoin, Select
105 from storm.locals import Bool, DateTime, Int, Reference, Storm
106 from storm.store import Store
107
108@@ -26,15 +27,18 @@
109
110 from canonical.database.constants import UTC_NOW
111 from canonical.database.enumcol import DBEnum
112-from canonical.launchpad.interfaces.lpstorm import IMasterStore
113+from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities
114+from canonical.launchpad.interfaces.lpstorm import IMasterStore, IStore
115 from canonical.launchpad.webapp.interfaces import (
116 DEFAULT_FLAVOR, IStoreSelector, MAIN_STORE)
117
118 from lp.buildmaster.interfaces.buildbase import BuildStatus
119 from lp.buildmaster.interfaces.buildfarmjob import (
120 BuildFarmJobType, IBuildFarmJob, IBuildFarmJobOld,
121- IBuildFarmJobSource, InconsistentBuildFarmJobError, ISpecificBuildFarmJob)
122+ IBuildFarmJobSet, IBuildFarmJobSource,
123+ InconsistentBuildFarmJobError, ISpecificBuildFarmJob)
124 from lp.buildmaster.interfaces.buildqueue import IBuildQueueSet
125+from lp.registry.model.teammembership import TeamParticipation
126
127
128 class BuildFarmJobOld:
129@@ -342,3 +346,68 @@
130 class BuildFarmJobDerived:
131 implements(IBuildFarmJob)
132 delegates(IBuildFarmJob, context='build_farm_job')
133+
134+
135+class BuildFarmJobSet:
136+ implements(IBuildFarmJobSet)
137+
138+ def getBuildsForBuilder(self, builder_id, status=None, user=None):
139+ """See `IBuildFarmJobSet`."""
140+ # Imported here to avoid circular imports.
141+ from lp.buildmaster.model.packagebuild import PackageBuild
142+ from lp.soyuz.model.archive import Archive
143+
144+ extra_clauses = [BuildFarmJob.builder == builder_id]
145+ if status is not None:
146+ extra_clauses.append(BuildFarmJob.status == status)
147+
148+ # We need to ensure that we don't include any private builds.
149+ # Currently only package builds can be private (via their
150+ # related archive), but not all build farm jobs will have a
151+ # related package build - hence the left join.
152+ left_join_pkg_builds = LeftJoin(
153+ BuildFarmJob,
154+ Join(
155+ PackageBuild,
156+ Archive,
157+ And(PackageBuild.archive == Archive.id)),
158+ PackageBuild.build_farm_job == BuildFarmJob.id)
159+
160+ filtered_builds = IStore(BuildFarmJob).using(
161+ left_join_pkg_builds).find(BuildFarmJob, *extra_clauses)
162+
163+ if user is None:
164+ # Anonymous requests don't get to see private builds at all.
165+ filtered_builds = filtered_builds.find(
166+ Coalesce(Archive.private, False) == False)
167+
168+ elif user.inTeam(getUtility(ILaunchpadCelebrities).admin):
169+ # Admins get to see everything.
170+ pass
171+ else:
172+ # Everyone else sees a union of all public builds and the
173+ # specific private builds to which they have access.
174+ filtered_builds = filtered_builds.find(
175+ Coalesce(Archive.private, False) == False)
176+
177+ user_teams_subselect = Select(
178+ TeamParticipation.teamID,
179+ where=And(
180+ TeamParticipation.personID == user.id,
181+ TeamParticipation.teamID == Archive.ownerID))
182+ private_builds_for_user = IStore(BuildFarmJob).find(
183+ BuildFarmJob,
184+ PackageBuild.build_farm_job == BuildFarmJob.id,
185+ PackageBuild.archive == Archive.id,
186+ Archive.private == True,
187+ Archive.ownerID.is_in(user_teams_subselect),
188+ *extra_clauses)
189+
190+ filtered_builds = filtered_builds.union(
191+ private_builds_for_user)
192+
193+ filtered_builds.order_by(
194+ Desc(BuildFarmJob.date_finished), BuildFarmJob.id)
195+
196+ return filtered_builds
197+
198
199=== modified file 'lib/lp/buildmaster/tests/test_buildfarmjob.py'
200--- lib/lp/buildmaster/tests/test_buildfarmjob.py 2010-06-21 07:32:35 +0000
201+++ lib/lp/buildmaster/tests/test_buildfarmjob.py 2010-06-21 07:32:36 +0000
202@@ -15,31 +15,46 @@
203 from zope.security.proxy import removeSecurityProxy
204
205 from canonical.database.sqlbase import flush_database_updates
206-from canonical.testing.layers import DatabaseFunctionalLayer
207+from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities
208+from canonical.testing.layers import (
209+ DatabaseFunctionalLayer, LaunchpadFunctionalLayer)
210
211 from lp.buildmaster.interfaces.buildbase import BuildStatus
212 from lp.buildmaster.interfaces.buildfarmjob import (
213- BuildFarmJobType, IBuildFarmJob, IBuildFarmJobSource,
214+ BuildFarmJobType, IBuildFarmJob, IBuildFarmJobSet, IBuildFarmJobSource,
215 InconsistentBuildFarmJobError)
216 from lp.buildmaster.model.buildfarmjob import BuildFarmJob
217 from lp.testing import login, TestCaseWithFactory
218
219
220-class TestBuildFarmJobBase(TestCaseWithFactory):
221+class TestBuildFarmJobMixin:
222
223 layer = DatabaseFunctionalLayer
224
225 def setUp(self):
226 """Create a build farm job with which to test."""
227- super(TestBuildFarmJobBase, self).setUp()
228+ super(TestBuildFarmJobMixin, self).setUp()
229 self.build_farm_job = self.makeBuildFarmJob()
230
231- def makeBuildFarmJob(self):
232- return getUtility(IBuildFarmJobSource).new(
233- job_type=BuildFarmJobType.PACKAGEBUILD)
234-
235-
236-class TestBuildFarmJob(TestBuildFarmJobBase):
237+ def makeBuildFarmJob(self, builder=None,
238+ job_type=BuildFarmJobType.PACKAGEBUILD,
239+ status=BuildStatus.NEEDSBUILD,
240+ date_finished=None):
241+ """A factory method for creating PackageBuilds.
242+
243+ This is not included in the launchpad test factory because
244+ a build farm job should never be instantiated outside the
245+ context of a derived class (such as a BinaryPackageBuild
246+ or eventually a SPRecipeBuild).
247+ """
248+ build_farm_job = getUtility(IBuildFarmJobSource).new(
249+ job_type=job_type, status=status)
250+ removeSecurityProxy(build_farm_job).builder = builder
251+ removeSecurityProxy(build_farm_job).date_finished = date_finished
252+ return build_farm_job
253+
254+
255+class TestBuildFarmJob(TestBuildFarmJobMixin, TestCaseWithFactory):
256 """Tests for the build farm job object."""
257
258 def test_providesInterface(self):
259@@ -56,7 +71,6 @@
260 self.assertEqual(self.build_farm_job, retrieved_job)
261
262 def test_default_values(self):
263- # A build farm job defaults to the NEEDSBUILD status.
264 # We flush the database updates to ensure sql defaults
265 # are set for various attributes.
266 flush_database_updates()
267@@ -166,7 +180,7 @@
268 InconsistentBuildFarmJobError, self.build_farm_job.getSpecificJob)
269
270
271-class TestBuildFarmJobSecurity(TestBuildFarmJobBase):
272+class TestBuildFarmJobSecurity(TestBuildFarmJobMixin, TestCaseWithFactory):
273
274 def test_view_build_farm_job(self):
275 # Anonymous access can read public builds, but not edit.
276@@ -184,5 +198,111 @@
277 BuildStatus.FULLYBUILT, self.build_farm_job.status)
278
279
280+class TestBuildFarmJobSet(TestBuildFarmJobMixin, TestCaseWithFactory):
281+
282+ layer = LaunchpadFunctionalLayer
283+
284+ def setUp(self):
285+ super(TestBuildFarmJobSet, self).setUp()
286+ self.builder = self.factory.makeBuilder()
287+ self.build_farm_job_set = getUtility(IBuildFarmJobSet)
288+
289+ def test_getBuildsForBuilder_all(self):
290+ # The default call without arguments returns all builds for the
291+ # builder, and not those for other builders.
292+ build1 = self.makeBuildFarmJob(builder=self.builder)
293+ build2 = self.makeBuildFarmJob(builder=self.builder)
294+ self.makeBuildFarmJob(builder=self.factory.makeBuilder())
295+
296+ result = self.build_farm_job_set.getBuildsForBuilder(self.builder)
297+
298+ self.assertContentEqual([build1, build2], result)
299+
300+ def test_getBuildsForBuilder_by_status(self):
301+ # If the status arg is used, the results will be filtered by
302+ # status.
303+ successful_builds = [
304+ self.makeBuildFarmJob(
305+ builder=self.builder, status=BuildStatus.FULLYBUILT),
306+ self.makeBuildFarmJob(
307+ builder=self.builder, status=BuildStatus.FULLYBUILT),
308+ ]
309+ self.makeBuildFarmJob(builder=self.builder)
310+
311+ query_by_status = self.build_farm_job_set.getBuildsForBuilder(
312+ self.builder, status=BuildStatus.FULLYBUILT)
313+
314+ self.assertContentEqual(successful_builds, query_by_status)
315+
316+ def _makePrivateAndNonPrivateBuilds(self, owning_team=None):
317+ """Return a tuple of a private and non-private build farm job."""
318+ if owning_team is None:
319+ owning_team = self.factory.makeTeam()
320+ archive = self.factory.makeArchive(owner=owning_team, private=True)
321+ private_build = self.factory.makeBinaryPackageBuild(
322+ archive=archive, builder=self.builder)
323+ private_build = removeSecurityProxy(private_build).build_farm_job
324+ other_build = self.makeBuildFarmJob(builder=self.builder)
325+ return (private_build, other_build)
326+
327+ def test_getBuildsForBuilder_hides_private_from_anon(self):
328+ # If no user is passed, all private builds are filtered out.
329+ private_build, other_build = self._makePrivateAndNonPrivateBuilds()
330+
331+ result = self.build_farm_job_set.getBuildsForBuilder(self.builder)
332+
333+ self.assertContentEqual([other_build], result)
334+
335+ def test_getBuildsForBuilder_hides_private_other_users(self):
336+ # Private builds are not returned for users without permission
337+ # to view them.
338+ private_build, other_build = self._makePrivateAndNonPrivateBuilds()
339+
340+ result = self.build_farm_job_set.getBuildsForBuilder(
341+ self.builder, user=self.factory.makePerson())
342+
343+ self.assertContentEqual([other_build], result)
344+
345+ def test_getBuildsForBuilder_shows_private_to_admin(self):
346+ # Admin users can see private builds.
347+ admin_team = getUtility(ILaunchpadCelebrities).admin
348+ private_build, other_build = self._makePrivateAndNonPrivateBuilds()
349+
350+ result = self.build_farm_job_set.getBuildsForBuilder(
351+ self.builder, user=admin_team.teamowner)
352+
353+ self.assertContentEqual([private_build, other_build], result)
354+
355+ def test_getBuildsForBuilder_shows_private_to_authorised(self):
356+ # Similarly, if the user is in the owning team they can see it.
357+ owning_team = self.factory.makeTeam()
358+ private_build, other_build = self._makePrivateAndNonPrivateBuilds(
359+ owning_team=owning_team)
360+
361+ result = self.build_farm_job_set.getBuildsForBuilder(
362+ self.builder,
363+ user=owning_team.teamowner)
364+
365+ self.assertContentEqual([private_build, other_build], result)
366+
367+ def test_getBuildsForBuilder_ordered_by_date_finished(self):
368+ # Results are returned with the oldest build last.
369+ build_1 = self.makeBuildFarmJob(
370+ builder=self.builder,
371+ date_finished=datetime(2008, 10, 10, tzinfo=pytz.UTC))
372+ build_2 = self.makeBuildFarmJob(
373+ builder=self.builder,
374+ date_finished=datetime(2008, 11, 10, tzinfo=pytz.UTC))
375+
376+ result = self.build_farm_job_set.getBuildsForBuilder(self.builder)
377+ self.assertEqual([build_2, build_1], list(result))
378+
379+ removeSecurityProxy(build_2).date_finished = (
380+ datetime(2008, 8, 10, tzinfo=pytz.UTC))
381+ result = self.build_farm_job_set.getBuildsForBuilder(self.builder)
382+
383+ self.assertEqual([build_1, build_2], list(result))
384+
385+
386 def test_suite():
387 return unittest.TestLoader().loadTestsFromName(__name__)
388
389=== modified file 'lib/lp/registry/model/distribution.py'
390--- lib/lp/registry/model/distribution.py 2010-06-09 08:26:26 +0000
391+++ lib/lp/registry/model/distribution.py 2010-06-21 07:32:36 +0000
392@@ -797,11 +797,13 @@
393 raise NotFoundError(filename)
394
395 def getBuildRecords(self, build_state=None, name=None, pocket=None,
396- arch_tag=None, user=None):
397+ arch_tag=None, user=None, binary_only=True):
398 """See `IHasBuildRecords`"""
399 # Ignore "user", since it would not make any difference to the
400 # records returned here (private builds are only in PPA right
401 # now).
402+ # The "binary_only" option is not yet supported for
403+ # IDistribution.
404
405 # Find out the distroarchseries in question.
406 arch_ids = DistroArchSeriesSet().getIdsForArchitectures(
407
408=== modified file 'lib/lp/soyuz/browser/build.py'
409--- lib/lp/soyuz/browser/build.py 2010-06-21 07:32:35 +0000
410+++ lib/lp/soyuz/browser/build.py 2010-06-21 07:32:36 +0000
411@@ -376,10 +376,16 @@
412 # build self.state & self.available_states structures
413 self._setupMappedStates(state_tag)
414
415+ # By default, we use the binary_only class attribute, but we
416+ # ensure it is true if we are passed an arch tag or a name.
417+ binary_only = self.binary_only
418+ if self.text is not None or self.arch_tag is not None:
419+ binary_only = True
420+
421 # request context build records according the selected state
422 builds = self.context.getBuildRecords(
423 build_state=self.state, name=self.text, arch_tag=self.arch_tag,
424- user=self.user, binary_only=self.binary_only)
425+ user=self.user, binary_only=binary_only)
426 self.batchnav = BatchNavigator(builds, self.request)
427 # We perform this extra step because we don't what to issue one
428 # extra query to retrieve the BuildQueue for each Build (batch item)
429
430=== modified file 'lib/lp/soyuz/browser/builder.py'
431--- lib/lp/soyuz/browser/builder.py 2010-06-04 15:19:55 +0000
432+++ lib/lp/soyuz/browser/builder.py 2010-06-21 07:32:36 +0000
433@@ -265,6 +265,7 @@
434 __used_for__ = IBuilder
435
436 page_title = 'Build history'
437+ binary_only = False
438
439 @property
440 def label(self):
441
442=== modified file 'lib/lp/soyuz/interfaces/archive.py'
443--- lib/lp/soyuz/interfaces/archive.py 2010-06-21 07:32:35 +0000
444+++ lib/lp/soyuz/interfaces/archive.py 2010-06-21 07:32:36 +0000
445@@ -29,7 +29,6 @@
446 'IArchivePublic',
447 'IArchiveSet',
448 'IDistributionArchive',
449- 'IncompatibleArguments',
450 'InsufficientUploadRights',
451 'InvalidComponent',
452 'InvalidPocketForPartnerArchive',
453@@ -90,11 +89,6 @@
454 webservice_error(400) #Bad request.
455
456
457-class IncompatibleArguments(Exception):
458- """Raised when incompatible arguments are passed to a method."""
459- webservice_error(400) # Bad request.
460-
461-
462 class CannotSwitchPrivacy(Exception):
463 """Raised when switching the privacy of an archive that has
464 publishing records."""
465@@ -168,7 +162,7 @@
466 """Returned when a pocket is closed for uploads."""
467
468 def __init__(self, distroseries, pocket):
469- Exception.__init__(self,
470+ Exception.__init__(self,
471 "Not permitted to upload to the %s pocket in a series in the "
472 "'%s' state." % (pocket.name, distroseries.status.name))
473
474@@ -520,11 +514,11 @@
475 )
476 @export_operation_as("checkUpload")
477 @export_read_operation()
478- def _checkUpload(person, distroseries, sourcepackagename, component,
479+ def _checkUpload(person, distroseries, sourcepackagename, component,
480 pocket, strict_component=True):
481 """Wrapper around checkUpload for the web service API."""
482
483- def checkUpload(person, distroseries, sourcepackagename, component,
484+ def checkUpload(person, distroseries, sourcepackagename, component,
485 pocket, strict_component=True):
486 """Check if 'person' upload 'suitesourcepackage' to 'archive'.
487
488
489=== modified file 'lib/lp/soyuz/interfaces/buildrecords.py'
490--- lib/lp/soyuz/interfaces/buildrecords.py 2010-06-21 07:32:35 +0000
491+++ lib/lp/soyuz/interfaces/buildrecords.py 2010-06-21 07:32:36 +0000
492@@ -12,6 +12,7 @@
493
494 __all__ = [
495 'IHasBuildRecords',
496+ 'IncompatibleArguments',
497 ]
498
499 from zope.interface import Interface
500@@ -21,7 +22,12 @@
501 from canonical.launchpad import _
502 from lazr.restful.declarations import (
503 REQUEST_USER, call_with, export_read_operation, operation_parameters,
504- operation_returns_collection_of, rename_parameters_as)
505+ operation_returns_collection_of, rename_parameters_as, webservice_error)
506+
507+
508+class IncompatibleArguments(Exception):
509+ """Raised when incompatible arguments are passed to a method."""
510+ webservice_error(400) # Bad request.
511
512
513 class IHasBuildRecords(Interface):
514@@ -40,7 +46,7 @@
515 description=_("The pocket into which this entry is published"),
516 # Really a PackagePublishingPocket see _schema_circular_imports.
517 vocabulary=DBEnumeratedType))
518- @call_with(user=REQUEST_USER)
519+ @call_with(user=REQUEST_USER, binary_only=True)
520 # Really a IBuild see _schema_circular_imports.
521 @operation_returns_collection_of(Interface)
522 @export_read_operation()
523
524=== modified file 'lib/lp/soyuz/model/archive.py'
525--- lib/lp/soyuz/model/archive.py 2010-06-21 07:32:35 +0000
526+++ lib/lp/soyuz/model/archive.py 2010-06-21 07:32:36 +0000
527@@ -66,7 +66,7 @@
528 ArchiveNotPrivate, ArchivePurpose, ArchiveStatus, CannotCopy,
529 CannotSwitchPrivacy, CannotUploadToPPA, CannotUploadToPocket,
530 DistroSeriesNotFound, IArchive, IArchiveSet, IDistributionArchive,
531- IncompatibleArguments, InsufficientUploadRights, InvalidPocketForPPA,
532+ InsufficientUploadRights, InvalidPocketForPPA,
533 InvalidPocketForPartnerArchive, InvalidComponent, IPPA,
534 MAIN_ARCHIVE_PURPOSES, NoRightsForArchive, NoRightsForComponent,
535 NoSuchPPA, NoTokensForTeams, PocketNotFound, VersionRequiresName,
536@@ -79,7 +79,8 @@
537 ArchiveSubscriberStatus, IArchiveSubscriberSet, ArchiveSubscriptionError)
538 from lp.soyuz.interfaces.binarypackagerelease import BinaryPackageFileType
539 from lp.soyuz.interfaces.binarypackagebuild import IBinaryPackageBuildSet
540-from lp.soyuz.interfaces.buildrecords import IHasBuildRecords
541+from lp.soyuz.interfaces.buildrecords import (
542+ IHasBuildRecords, IncompatibleArguments)
543 from lp.soyuz.interfaces.component import IComponent, IComponentSet
544 from lp.registry.interfaces.distroseries import IDistroSeriesSet
545 from lp.registry.interfaces.person import PersonVisibility
546@@ -1044,7 +1045,7 @@
547 if isinstance(sourcepackagename, basestring):
548 sourcepackagename = getUtility(
549 ISourcePackageNameSet)[sourcepackagename]
550- reason = self.checkUpload(person, distroseries, sourcepackagename,
551+ reason = self.checkUpload(person, distroseries, sourcepackagename,
552 component, pocket, strict_component)
553 if reason is not None:
554 raise reason
555
556=== modified file 'lib/lp/soyuz/model/distroarchseries.py'
557--- lib/lp/soyuz/model/distroarchseries.py 2010-04-09 15:46:09 +0000
558+++ lib/lp/soyuz/model/distroarchseries.py 2010-06-21 07:32:36 +0000
559@@ -229,11 +229,13 @@
560 self, name)
561
562 def getBuildRecords(self, build_state=None, name=None, pocket=None,
563- arch_tag=None, user=None):
564+ arch_tag=None, user=None, binary_only=True):
565 """See IHasBuildRecords"""
566 # Ignore "user", since it would not make any difference to the
567 # records returned here (private builds are only in PPA right
568 # now).
569+ # Ignore "binary_only" as for a distro arch series it is only
570+ # the binaries that are relevant.
571
572 # For consistency we return an empty resultset if arch_tag
573 # is provided but doesn't match our architecture.
574
575=== modified file 'lib/lp/soyuz/tests/test_binarypackagebuild.py'
576--- lib/lp/soyuz/tests/test_binarypackagebuild.py 2010-06-21 07:32:35 +0000
577+++ lib/lp/soyuz/tests/test_binarypackagebuild.py 2010-06-21 07:32:36 +0000
578@@ -150,8 +150,8 @@
579 # If getSpecificJob is called on the binary build it is a noop.
580 store = Store.of(self.build)
581 store.flush()
582- build, statements = record_statements(self.build.getSpecificJob)
583- self.assertEqual(0, len(statements))
584+ self.assertStatementCount(
585+ 0, self.build.getSpecificJob)
586
587
588 class TestBuildUpdateDependencies(TestCaseWithFactory):
589
590=== modified file 'lib/lp/soyuz/tests/test_hasbuildrecords.py'
591--- lib/lp/soyuz/tests/test_hasbuildrecords.py 2010-06-21 07:32:35 +0000
592+++ lib/lp/soyuz/tests/test_hasbuildrecords.py 2010-06-21 07:32:36 +0000
593@@ -12,12 +12,14 @@
594
595 from lp.registry.model.sourcepackage import SourcePackage
596 from lp.buildmaster.interfaces.builder import IBuilderSet
597-from lp.buildmaster.interfaces.buildfarmjob import BuildFarmJobType
598+from lp.buildmaster.interfaces.buildfarmjob import (
599+ BuildFarmJobType, IBuildFarmJob)
600 from lp.buildmaster.interfaces.packagebuild import (
601 IPackageBuildSource)
602 from lp.registry.interfaces.pocket import PackagePublishingPocket
603-from lp.soyuz.interfaces.archive import IncompatibleArguments
604-from lp.soyuz.interfaces.buildrecords import IHasBuildRecords
605+from lp.soyuz.interfaces.binarypackagebuild import IBinaryPackageBuild
606+from lp.soyuz.interfaces.buildrecords import (
607+ IHasBuildRecords, IncompatibleArguments)
608 from lp.soyuz.model.processor import ProcessorFamilySet
609 from lp.soyuz.tests.test_binarypackagebuild import (
610 BaseTestCaseWithThreeBuilds)
611@@ -131,6 +133,43 @@
612 for build in self.builds:
613 build.builder = self.context
614
615+ def test_binary_only_false(self):
616+ # A builder can optionally return the more general
617+ # build farm job objects.
618+
619+ # Until we have different IBuildFarmJob types implemented, we
620+ # can only test this by creating a lone IBuildFarmJob of a
621+ # different type.
622+ from lp.buildmaster.interfaces.buildfarmjob import IBuildFarmJobSource
623+ from lp.buildmaster.interfaces.buildbase import BuildStatus
624+ build_farm_job = getUtility(IBuildFarmJobSource).new(
625+ job_type=BuildFarmJobType.RECIPEBRANCHBUILD, virtualized=True,
626+ status=BuildStatus.BUILDING)
627+ removeSecurityProxy(build_farm_job).builder = self.context
628+
629+ builds = self.context.getBuildRecords(binary_only=True)
630+ binary_only_count = builds.count()
631+
632+ self.assertTrue(
633+ all([IBinaryPackageBuild.providedBy(build) for build in builds]))
634+
635+ builds = self.context.getBuildRecords(binary_only=False)
636+ all_count = builds.count()
637+
638+ self.assertFalse(
639+ any([IBinaryPackageBuild.providedBy(build) for build in builds]))
640+ self.assertTrue(
641+ all([IBuildFarmJob.providedBy(build) for build in builds]))
642+ self.assertBetween(0, binary_only_count, all_count)
643+
644+ def test_incompatible_arguments(self):
645+ # binary_only=False is incompatible with arch_tag and name.
646+ self.failUnlessRaises(
647+ IncompatibleArguments, self.context.getBuildRecords,
648+ binary_only=False, arch_tag="anything")
649+ self.failUnlessRaises(
650+ IncompatibleArguments, self.context.getBuildRecords,
651+ binary_only=False, name="anything")
652
653 class TestSourcePackageHasBuildRecords(TestHasBuildRecordsInterface):
654 """Test the SourcePackage implementation of IHasBuildRecords."""
655
656=== modified file 'lib/lp/testing/factory.py'
657--- lib/lp/testing/factory.py 2010-06-18 08:39:22 +0000
658+++ lib/lp/testing/factory.py 2010-06-21 07:32:36 +0000
659@@ -2183,18 +2183,22 @@
660 source_package_recipe_build=source_package_recipe_build)
661
662 def makeBinaryPackageBuild(self, source_package_release=None,
663- distroarchseries=None):
664+ distroarchseries=None, archive=None, builder=None):
665 """Create a BinaryPackageBuild.
666
667- If supplied, the source_package_release is used to determine archive.
668+ If archive is not supplied, the source_package_release is used
669+ to determine archive.
670 :param source_package_release: The SourcePackageRelease this binary
671 build uses as its source.
672 :param distroarchseries: The DistroArchSeries to use.
673+ :param archive: The Archive to use.
674+ :param builder: An optional builder to assign.
675 """
676- if source_package_release is None:
677- archive = self.makeArchive()
678- else:
679- archive = source_package_release.upload_archive
680+ if archive is None:
681+ if source_package_release is None:
682+ archive = self.makeArchive()
683+ else:
684+ archive = source_package_release.upload_archive
685 if source_package_release is None:
686 multiverse = self.makeComponent(name='multiverse')
687 source_package_release = self.makeSourcePackageRelease(
688@@ -2212,7 +2216,9 @@
689 archive=archive,
690 pocket=PackagePublishingPocket.RELEASE,
691 date_created=self.getUniqueDate())
692- binary_package_build_job = binary_package_build.makeJob()
693+ naked_build = removeSecurityProxy(binary_package_build)
694+ naked_build.builder = builder
695+ binary_package_build_job = naked_build.makeJob()
696 BuildQueue(
697 job=binary_package_build_job.job,
698 job_type=BuildFarmJobType.PACKAGEBUILD)