Merge lp:~wallyworld/launchpad/recipe-webservice-api into lp:launchpad
- recipe-webservice-api
- Merge into devel
Status: | Merged | ||||||||
---|---|---|---|---|---|---|---|---|---|
Approved by: | Ian Booth | ||||||||
Approved revision: | no longer in the source branch. | ||||||||
Merged at revision: | 12459 | ||||||||
Proposed branch: | lp:~wallyworld/launchpad/recipe-webservice-api | ||||||||
Merge into: | lp:launchpad | ||||||||
Diff against target: |
758 lines (+201/-99) 20 files modified
lib/canonical/launchpad/interfaces/_schema_circular_imports.py (+17/-0) lib/lp/code/browser/branch.py (+1/-1) lib/lp/code/browser/sourcepackagerecipe.py (+3/-3) lib/lp/code/browser/sourcepackagerecipelisting.py (+2/-2) lib/lp/code/browser/tests/test_sourcepackagerecipe.py (+13/-11) lib/lp/code/interfaces/hasrecipes.py (+12/-3) lib/lp/code/interfaces/sourcepackagerecipe.py (+34/-9) lib/lp/code/model/branch.py (+3/-2) lib/lp/code/model/sourcepackagerecipe.py (+36/-19) lib/lp/code/model/tests/test_hasrecipes.py (+9/-15) lib/lp/code/model/tests/test_sourcepackagerecipe.py (+50/-14) lib/lp/code/scripts/tests/test_request_daily_builds.py (+5/-11) lib/lp/code/stories/webservice/xx-branch.txt (+2/-1) lib/lp/code/templates/sourcepackagerecipe-listing.pt (+1/-1) lib/lp/codehosting/scanner/bzrsync.py (+1/-1) lib/lp/registry/model/person.py (+4/-3) lib/lp/registry/model/product.py (+2/-1) lib/lp/registry/stories/webservice/xx-person.txt (+2/-0) lib/lp/registry/stories/webservice/xx-project-registry.txt (+2/-0) lib/lp/registry/tests/test_person.py (+2/-2) |
||||||||
To merge this branch: | bzr merge lp:~wallyworld/launchpad/recipe-webservice-api | ||||||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Leonard Richardson (community) | Approve | ||
Review via email: mp+50684@code.launchpad.net |
Commit message
[r=leonardr][bug=712329,712331] Add extra recipe methods to the web service.
Description of the change
Simple change to export via the webservice some recipe apis to list builds and recipes. See bugs 712329 and 712331.
== Implementation ==
Initial review discussions indicated that the current methods getBuilds() and getPendingBuilds() were poorly named for exporting. So the following methods were introduced:
getBuilds() - new, returns all builds
getPendingBuilds() - unchanged, returns builds where status == needsbuild
getCompleteBuilds() - renamed from getBuilds(), returns builds where status != needsbuild
Add required decorators to the exported methods:
IHasRecipe.
ISourcePackageR
ISourcePackageR
ISourcePackageR
ISourcePackageR
An additional small change was made to the order by statement used by the queries. The secondary order by term, BuildFarmJob.id, was changed to Desc() so that the semantics match the order imposed by the start_date, create_date ordering ie later builds go first
== Tests ==
Add/modify tests in test_sourcepack
test_getBuilds
test_getRecipes
test_requestBuild
bin/test -vvt test_sourcepack
== Lint ==
Linting changed files:
lib/canonical
lib/lp/
lib/lp/
lib/lp/
./lib/canonical
551: E501 line too long (82 characters)
551: Line exceeds 78 characters.
./lib/lp/
21: E302 expected 2 blank lines, found 1
./lib/lp/
185: E501 line too long (83 characters)
185: Line exceeds 78 characters.
Robert Collins (lifeless) wrote : | # |
Ian Booth (wallyworld) wrote : | # |
Hi Robert
I was implementing what the bug report (712331) asked for ie publish
ISourcePackageR
getBuildRecords() certainly is very different as you say, however this
method is exposed via the web service already.
On 22/02/11 16:54, Robert Collins wrote:
> Just to mention, this seems gratuitiously different to the existing api for getBuildRecords, which we could expose on your context - conceptually an IHasBuildRecords or similar. Is there a reason for this?
Leonard Richardson (leonardr) wrote : | # |
requestBuild is a classic case of a named operation, but there's no reason to publish the others as named operations.
They don't change the dataset and they don't take any arguments. Let's publish them as scoped collections and keep our public API cleaner:
recipes
builds
completed_builds
pending_builds
And a link:
last_build
To do this, define fields by these names in the interfaces, and implement them with @property method definitions that invoke the method behind the scenes.
@property
def recipes(self):
return self.getRecipes()
Or, if possible, get rid of getRecipes() altogether, and just implement .recipes for both the internal and external APIs. So 'recipes' would contain the getRecipes() implementation instead of invoking getRecipes().
Ian Booth (wallyworld) wrote : | # |
> requestBuild is a classic case of a named operation, but there's no reason to
> publish the others as named operations.
> They don't change the dataset and they don't take any arguments. Let's publish
> them as scoped collections and keep our public API cleaner:
>
> recipes
> builds
> completed_builds
> pending_builds
>
> And a link:
>
> last_build
>
Hi Leonard,
I have converted the getter methods to be properties as discussed. Tests pass and running up an instance locally works fine too. I ran some additional tests: test_person, test_hasrecipes, test_request_
Leonard Richardson (leonardr) wrote : | # |
This looks great now. My only comment is that "sorted by 'desc(date_
Robert Collins (lifeless) wrote : | # |
On Thu, Feb 24, 2011 at 2:18 AM, Leonard Richardson
<email address hidden> wrote:
> Review: Approve
> This looks great now. My only comment is that "sorted by 'desc(date_
An interesting thing about phrasing it like that, is that it makes it
obvious that the sort is inefficient: better to just sort on Desc(id)
- that is precisely the same sort because ids are allocated
sequentially, and under half the work for the DB.
Ian Booth (wallyworld) wrote : | # |
>
> An interesting thing about phrasing it like that, is that it makes it
> obvious that the sort is inefficient: better to just sort on Desc(id)
> - that is precisely the same sort because ids are allocated
> sequentially, and under half the work for the DB.
Done.
Preview Diff
1 | === modified file 'lib/canonical/launchpad/interfaces/_schema_circular_imports.py' |
2 | --- lib/canonical/launchpad/interfaces/_schema_circular_imports.py 2011-02-02 09:50:58 +0000 |
3 | +++ lib/canonical/launchpad/interfaces/_schema_circular_imports.py 2011-02-24 23:45:43 +0000 |
4 | @@ -74,6 +74,7 @@ |
5 | IHasMergeProposals, |
6 | IHasRequestedReviews, |
7 | ) |
8 | +from lp.code.interfaces.hasrecipes import IHasRecipes |
9 | from lp.code.interfaces.sourcepackagerecipe import ISourcePackageRecipe |
10 | from lp.code.interfaces.sourcepackagerecipebuild import ( |
11 | ISourcePackageRecipeBuild, |
12 | @@ -262,6 +263,10 @@ |
13 | |
14 | patch_entry_return_type(IPerson, 'getRecipe', ISourcePackageRecipe) |
15 | |
16 | +# IHasRecipe |
17 | +patch_collection_property( |
18 | + IHasRecipes, 'recipes', ISourcePackageRecipe) |
19 | + |
20 | IPerson['hardware_submissions'].value_type.schema = IHWSubmission |
21 | |
22 | # publishing.py |
23 | @@ -440,6 +445,18 @@ |
24 | ISourcePackageRelease, 'source_package_recipe_build', |
25 | ISourcePackageRecipeBuild) |
26 | |
27 | +# ISourcePackageRecipeView |
28 | +patch_entry_return_type( |
29 | + ISourcePackageRecipe, 'requestBuild', ISourcePackageRecipeBuild) |
30 | +patch_reference_property( |
31 | + ISourcePackageRecipe, 'last_build', ISourcePackageRecipeBuild) |
32 | +patch_collection_property( |
33 | + ISourcePackageRecipe, 'builds', ISourcePackageRecipeBuild) |
34 | +patch_collection_property( |
35 | + ISourcePackageRecipe, 'pending_builds', ISourcePackageRecipeBuild) |
36 | +patch_collection_property( |
37 | + ISourcePackageRecipe, 'completed_builds', ISourcePackageRecipeBuild) |
38 | + |
39 | # IHasBugs |
40 | patch_plain_parameter_type( |
41 | IHasBugs, 'searchTasks', 'assignee', IPerson) |
42 | |
43 | === modified file 'lib/lp/code/browser/branch.py' |
44 | --- lib/lp/code/browser/branch.py 2011-02-17 03:29:50 +0000 |
45 | +++ lib/lp/code/browser/branch.py 2011-02-24 23:45:43 +0000 |
46 | @@ -548,7 +548,7 @@ |
47 | |
48 | @property |
49 | def recipe_count_text(self): |
50 | - count = self.context.getRecipes().count() |
51 | + count = self.context.recipes.count() |
52 | if count == 0: |
53 | return 'No recipes' |
54 | elif count == 1: |
55 | |
56 | === modified file 'lib/lp/code/browser/sourcepackagerecipe.py' |
57 | --- lib/lp/code/browser/sourcepackagerecipe.py 2011-02-23 02:08:58 +0000 |
58 | +++ lib/lp/code/browser/sourcepackagerecipe.py 2011-02-24 23:45:43 +0000 |
59 | @@ -299,8 +299,8 @@ |
60 | This allows started but unfinished builds to show up in the view but |
61 | be discarded as more recent builds become available. |
62 | """ |
63 | - builds = list(recipe.getPendingBuilds()) |
64 | - for build in recipe.getBuilds(): |
65 | + builds = list(recipe.pending_builds) |
66 | + for build in recipe.completed_builds: |
67 | builds.append(build) |
68 | if len(builds) >= 5: |
69 | break |
70 | @@ -328,7 +328,7 @@ |
71 | The distroseries function as defaults for requesting a build. |
72 | """ |
73 | initial_values = {'distros': self.context.distroseries} |
74 | - build = self.context.getLastBuild() |
75 | + build = self.context.last_build |
76 | if build is not None: |
77 | initial_values['archive'] = build.archive |
78 | return initial_values |
79 | |
80 | === modified file 'lib/lp/code/browser/sourcepackagerecipelisting.py' |
81 | --- lib/lp/code/browser/sourcepackagerecipelisting.py 2011-02-17 03:17:54 +0000 |
82 | +++ lib/lp/code/browser/sourcepackagerecipelisting.py 2011-02-24 23:45:43 +0000 |
83 | @@ -29,7 +29,7 @@ |
84 | def view_recipes(self): |
85 | text = 'View source package recipes' |
86 | enabled = False |
87 | - if self.context.getRecipes().count(): |
88 | + if self.context.recipes.count(): |
89 | enabled = True |
90 | if not getFeatureFlag(RECIPE_ENABLED_FLAG): |
91 | enabled = False |
92 | @@ -51,7 +51,7 @@ |
93 | |
94 | def initialize(self): |
95 | super(RecipeListingView, self).initialize() |
96 | - recipes = self.context.getRecipes() |
97 | + recipes = self.context.recipes |
98 | if recipes.count() == 1: |
99 | recipe = recipes.one() |
100 | self.request.response.redirect(canonical_url(recipe)) |
101 | |
102 | === modified file 'lib/lp/code/browser/tests/test_sourcepackagerecipe.py' |
103 | --- lib/lp/code/browser/tests/test_sourcepackagerecipe.py 2011-02-22 23:07:17 +0000 |
104 | +++ lib/lp/code/browser/tests/test_sourcepackagerecipe.py 2011-02-24 23:45:43 +0000 |
105 | @@ -1194,8 +1194,10 @@ |
106 | def test_builds(self): |
107 | """Ensure SourcePackageRecipeView.builds is as described.""" |
108 | recipe = self.makeRecipe() |
109 | + # We create builds in time ascending order (oldest first) since we |
110 | + # use id as the ordering attribute and lower ids mean created earlier. |
111 | date_gen = time_counter( |
112 | - datetime(2010, 03, 16, tzinfo=utc), timedelta(days=-1)) |
113 | + datetime(2010, 03, 16, tzinfo=utc), timedelta(days=1)) |
114 | build1 = self.makeBuildJob(recipe, date_gen.next()) |
115 | build2 = self.makeBuildJob(recipe, date_gen.next()) |
116 | build3 = self.makeBuildJob(recipe, date_gen.next()) |
117 | @@ -1204,7 +1206,7 @@ |
118 | build6 = self.makeBuildJob(recipe, date_gen.next()) |
119 | view = SourcePackageRecipeView(recipe, None) |
120 | self.assertEqual( |
121 | - [build1, build2, build3, build4, build5, build6], |
122 | + [build6, build5, build4, build3, build2, build1], |
123 | view.builds) |
124 | |
125 | def set_status(build, status): |
126 | @@ -1214,19 +1216,19 @@ |
127 | if status == BuildStatus.FULLYBUILT: |
128 | naked_build.date_finished = ( |
129 | naked_build.date_created + timedelta(minutes=10)) |
130 | - set_status(build1, BuildStatus.FULLYBUILT) |
131 | - set_status(build2, BuildStatus.FAILEDTOBUILD) |
132 | + set_status(build6, BuildStatus.FULLYBUILT) |
133 | + set_status(build5, BuildStatus.FAILEDTOBUILD) |
134 | # When there are 4+ pending builds, only the the most |
135 | # recently-completed build is returned (i.e. build1, not build2) |
136 | self.assertEqual( |
137 | - [build3, build4, build5, build6, build1], |
138 | + [build4, build3, build2, build1, build6], |
139 | view.builds) |
140 | + set_status(build4, BuildStatus.FULLYBUILT) |
141 | set_status(build3, BuildStatus.FULLYBUILT) |
142 | - set_status(build4, BuildStatus.FULLYBUILT) |
143 | - set_status(build5, BuildStatus.FULLYBUILT) |
144 | - set_status(build6, BuildStatus.FULLYBUILT) |
145 | + set_status(build2, BuildStatus.FULLYBUILT) |
146 | + set_status(build1, BuildStatus.FULLYBUILT) |
147 | self.assertEqual( |
148 | - [build1, build2, build3, build4, build5], view.builds) |
149 | + [build6, build5, build4, build3, build2], view.builds) |
150 | |
151 | def test_request_daily_builds_button_stale(self): |
152 | # Recipes that are stale and are built daily have a build now link |
153 | @@ -1300,7 +1302,7 @@ |
154 | browser = self.getViewBrowser(recipe) |
155 | browser.getControl('Build now').click() |
156 | login(ANONYMOUS) |
157 | - builds = recipe.getPendingBuilds() |
158 | + builds = recipe.pending_builds |
159 | build_distros = [ |
160 | build.distroseries.displayname for build in builds] |
161 | build_distros.sort() |
162 | @@ -1346,7 +1348,7 @@ |
163 | browser.getControl('Request builds').click() |
164 | |
165 | login(ANONYMOUS) |
166 | - builds = recipe.getPendingBuilds() |
167 | + builds = recipe.pending_builds |
168 | build_distros = [ |
169 | build.distroseries.displayname for build in builds] |
170 | build_distros.sort() |
171 | |
172 | === modified file 'lib/lp/code/interfaces/hasrecipes.py' |
173 | --- lib/lp/code/interfaces/hasrecipes.py 2010-08-20 20:31:18 +0000 |
174 | +++ lib/lp/code/interfaces/hasrecipes.py 2011-02-24 23:45:43 +0000 |
175 | @@ -10,13 +10,22 @@ |
176 | |
177 | |
178 | from zope.interface import ( |
179 | - Attribute, |
180 | Interface, |
181 | ) |
182 | |
183 | +from lazr.restful.declarations import exported |
184 | +from lazr.restful.fields import ( |
185 | + CollectionField, |
186 | + Reference, |
187 | + ) |
188 | + |
189 | +from canonical.launchpad import _ |
190 | |
191 | class IHasRecipes(Interface): |
192 | """An object that has recipes.""" |
193 | |
194 | - def getRecipes(): |
195 | - """Returns all recipes associated with the object.""" |
196 | + recipes = exported( |
197 | + CollectionField( |
198 | + title=_("All recipes associated with the object."), |
199 | + value_type=Reference(schema=Interface), |
200 | + readonly=True)) |
201 | |
202 | === modified file 'lib/lp/code/interfaces/sourcepackagerecipe.py' |
203 | --- lib/lp/code/interfaces/sourcepackagerecipe.py 2011-02-22 23:07:17 +0000 |
204 | +++ lib/lp/code/interfaces/sourcepackagerecipe.py 2011-02-24 23:45:43 +0000 |
205 | @@ -29,6 +29,7 @@ |
206 | mutator_for, |
207 | operation_for_version, |
208 | operation_parameters, |
209 | + operation_returns_entry, |
210 | REQUEST_USER, |
211 | ) |
212 | from lazr.restful.fields import ( |
213 | @@ -104,6 +105,38 @@ |
214 | |
215 | recipe_text = exported(Text(readonly=True)) |
216 | |
217 | + pending_builds = exported( |
218 | + CollectionField( |
219 | + title=_("The pending builds of this recipe."), |
220 | + description=_('Pending builds of this recipe, sorted in ' |
221 | + 'descending order of creation.'), |
222 | + value_type=Reference(schema=Interface), |
223 | + readonly=True)) |
224 | + |
225 | + completed_builds = exported( |
226 | + CollectionField( |
227 | + title=_("The completed builds of this recipe."), |
228 | + description=_('Completed builds of this recipe, sorted in ' |
229 | + 'descending order of finishing (or starting if not' |
230 | + 'completed successfully).'), |
231 | + value_type=Reference(schema=Interface), |
232 | + readonly=True)) |
233 | + |
234 | + builds = exported( |
235 | + CollectionField( |
236 | + title=_("All builds of this recipe."), |
237 | + description=_('All builds of this recipe, sorted in ' |
238 | + 'descending order of finishing (or starting if not' |
239 | + 'completed successfully).'), |
240 | + value_type=Reference(schema=Interface), |
241 | + readonly=True)) |
242 | + |
243 | + last_build = exported( |
244 | + Reference( |
245 | + Interface, |
246 | + title=_("The the most recent build of this recipe."), |
247 | + readonly=True)) |
248 | + |
249 | def isOverQuota(requester, distroseries): |
250 | """True if the recipe/requester/distroseries combo is >= quota. |
251 | |
252 | @@ -111,20 +144,12 @@ |
253 | :param distroseries: The distroseries to build for. |
254 | """ |
255 | |
256 | - def getBuilds(): |
257 | - """Return a ResultSet of all the non-pending builds.""" |
258 | - |
259 | - def getPendingBuilds(): |
260 | - """Return a ResultSet of all the pending builds.""" |
261 | - |
262 | - def getLastBuild(): |
263 | - """Return the the most recent build of this recipe.""" |
264 | - |
265 | @call_with(requester=REQUEST_USER) |
266 | @operation_parameters( |
267 | archive=Reference(schema=IArchive), |
268 | distroseries=Reference(schema=IDistroSeries), |
269 | pocket=Choice(vocabulary=PackagePublishingPocket,)) |
270 | + @operation_returns_entry(Interface) |
271 | @export_write_operation() |
272 | def requestBuild(archive, distroseries, requester, pocket): |
273 | """Request that the recipe be built in to the specified archive. |
274 | |
275 | === modified file 'lib/lp/code/model/branch.py' |
276 | --- lib/lp/code/model/branch.py 2011-01-25 18:59:18 +0000 |
277 | +++ lib/lp/code/model/branch.py 2011-02-24 23:45:43 +0000 |
278 | @@ -665,7 +665,7 @@ |
279 | map(ClearOfficialPackageBranch, series_set.findForBranch(self))) |
280 | deletion_operations.extend( |
281 | DeletionCallable.forSourcePackageRecipe(recipe) |
282 | - for recipe in self.getRecipes()) |
283 | + for recipe in self.recipes) |
284 | return (alteration_operations, deletion_operations) |
285 | |
286 | def deletionRequirements(self): |
287 | @@ -1160,7 +1160,8 @@ |
288 | user, checked_branches) |
289 | return can_access |
290 | |
291 | - def getRecipes(self): |
292 | + @property |
293 | + def recipes(self): |
294 | """See `IHasRecipes`.""" |
295 | from lp.code.model.sourcepackagerecipedata import ( |
296 | SourcePackageRecipeData) |
297 | |
298 | === modified file 'lib/lp/code/model/sourcepackagerecipe.py' |
299 | --- lib/lp/code/model/sourcepackagerecipe.py 2011-02-19 00:30:04 +0000 |
300 | +++ lib/lp/code/model/sourcepackagerecipe.py 2011-02-24 23:45:43 +0000 |
301 | @@ -295,34 +295,51 @@ |
302 | continue |
303 | return builds |
304 | |
305 | - def getBuilds(self): |
306 | - """See `ISourcePackageRecipe`.""" |
307 | - where_clause = BuildFarmJob.status != BuildStatus.NEEDSBUILD |
308 | - order_by = Desc(Greatest( |
309 | - BuildFarmJob.date_started, |
310 | - BuildFarmJob.date_finished)), BuildFarmJob.id |
311 | - return self._getBuilds(where_clause, order_by) |
312 | - |
313 | - def getPendingBuilds(self): |
314 | - """See `ISourcePackageRecipe`.""" |
315 | - where_clause = BuildFarmJob.status == BuildStatus.NEEDSBUILD |
316 | - order_by = Desc(BuildFarmJob.date_created), BuildFarmJob.id |
317 | - return self._getBuilds(where_clause, order_by) |
318 | - |
319 | - def _getBuilds(self, where_clause, order_by): |
320 | + @property |
321 | + def builds(self): |
322 | + """See `ISourcePackageRecipe`.""" |
323 | + order_by = (Desc(Greatest( |
324 | + BuildFarmJob.date_started, |
325 | + BuildFarmJob.date_finished)), |
326 | + Desc(BuildFarmJob.date_created), Desc(BuildFarmJob.id)) |
327 | + return self._getBuilds(None, order_by) |
328 | + |
329 | + @property |
330 | + def completed_builds(self): |
331 | + """See `ISourcePackageRecipe`.""" |
332 | + filter_term = BuildFarmJob.status != BuildStatus.NEEDSBUILD |
333 | + order_by = (Desc(Greatest( |
334 | + BuildFarmJob.date_started, |
335 | + BuildFarmJob.date_finished)), |
336 | + Desc(BuildFarmJob.id)) |
337 | + return self._getBuilds(filter_term, order_by) |
338 | + |
339 | + @property |
340 | + def pending_builds(self): |
341 | + """See `ISourcePackageRecipe`.""" |
342 | + filter_term = BuildFarmJob.status == BuildStatus.NEEDSBUILD |
343 | + # We want to order by date_created but this is the same as ordering |
344 | + # by id (since id increases monotonically) and is less expensive. |
345 | + order_by = Desc(BuildFarmJob.id) |
346 | + return self._getBuilds(filter_term, order_by) |
347 | + |
348 | + def _getBuilds(self, filter_term, order_by): |
349 | """The actual query to get the builds.""" |
350 | - result = Store.of(self).find( |
351 | - SourcePackageRecipeBuild, |
352 | + query_args = [ |
353 | SourcePackageRecipeBuild.recipe==self, |
354 | SourcePackageRecipeBuild.package_build_id == PackageBuild.id, |
355 | PackageBuild.build_farm_job_id == BuildFarmJob.id, |
356 | And(PackageBuild.archive_id == Archive.id, |
357 | Archive._enabled == True), |
358 | - where_clause) |
359 | + ] |
360 | + if filter_term is not None: |
361 | + query_args.append(filter_term) |
362 | + result = Store.of(self).find(SourcePackageRecipeBuild, *query_args) |
363 | result.order_by(order_by) |
364 | return result |
365 | |
366 | - def getLastBuild(self): |
367 | + @property |
368 | + def last_build(self): |
369 | """See `ISourcePackageRecipeBuild`.""" |
370 | return self._getBuilds( |
371 | True, Desc(BuildFarmJob.date_finished)).first() |
372 | |
373 | === modified file 'lib/lp/code/model/tests/test_hasrecipes.py' |
374 | --- lib/lp/code/model/tests/test_hasrecipes.py 2010-10-04 19:50:45 +0000 |
375 | +++ lib/lp/code/model/tests/test_hasrecipes.py 2011-02-24 23:45:43 +0000 |
376 | @@ -7,8 +7,6 @@ |
377 | |
378 | __metaclass__ = type |
379 | |
380 | -import unittest |
381 | - |
382 | from canonical.testing.layers import DatabaseFunctionalLayer |
383 | from lp.code.interfaces.hasrecipes import IHasRecipes |
384 | from lp.testing import TestCaseWithFactory |
385 | @@ -24,16 +22,16 @@ |
386 | branch = self.factory.makeBranch() |
387 | self.assertProvides(branch, IHasRecipes) |
388 | |
389 | - def test_branch_getRecipes(self): |
390 | + def test_branch_recipes(self): |
391 | # IBranch.recipes should provide all the SourcePackageRecipes attached |
392 | # to that branch. |
393 | base_branch = self.factory.makeBranch() |
394 | recipe1 = self.factory.makeSourcePackageRecipe(branches=[base_branch]) |
395 | recipe2 = self.factory.makeSourcePackageRecipe(branches=[base_branch]) |
396 | recipe_ignored = self.factory.makeSourcePackageRecipe() |
397 | - self.assertEqual(2, base_branch.getRecipes().count()) |
398 | + self.assertEqual(2, base_branch.recipes.count()) |
399 | |
400 | - def test_branch_getRecipes_nonbase(self): |
401 | + def test_branch_recipes_nonbase(self): |
402 | # IBranch.recipes should provide all the SourcePackageRecipes |
403 | # that refer to the branch, even as a non-base branch. |
404 | base_branch = self.factory.makeBranch() |
405 | @@ -41,28 +39,28 @@ |
406 | recipe = self.factory.makeSourcePackageRecipe( |
407 | branches=[base_branch, nonbase_branch]) |
408 | recipe_ignored = self.factory.makeSourcePackageRecipe() |
409 | - self.assertEqual(recipe, nonbase_branch.getRecipes().one()) |
410 | + self.assertEqual(recipe, nonbase_branch.recipes.one()) |
411 | |
412 | def test_person_implements_hasrecipes(self): |
413 | # Person should implement IHasRecipes. |
414 | person = self.factory.makeBranch() |
415 | self.assertProvides(person, IHasRecipes) |
416 | |
417 | - def test_person_getRecipes(self): |
418 | - # IPerson.getRecipes should provide all the SourcePackageRecipes |
419 | + def test_person_recipes(self): |
420 | + # IPerson.recipes should provide all the SourcePackageRecipes |
421 | # owned by that person. |
422 | person = self.factory.makePerson() |
423 | recipe1 = self.factory.makeSourcePackageRecipe(owner=person) |
424 | recipe2 = self.factory.makeSourcePackageRecipe(owner=person) |
425 | recipe_ignored = self.factory.makeSourcePackageRecipe() |
426 | - self.assertEqual(2, person.getRecipes().count()) |
427 | + self.assertEqual(2, person.recipes.count()) |
428 | |
429 | def test_product_implements_hasrecipes(self): |
430 | # Product should implement IHasRecipes. |
431 | product = self.factory.makeProduct() |
432 | self.assertProvides(product, IHasRecipes) |
433 | |
434 | - def test_product_getRecipes(self): |
435 | + def test_product_recipes(self): |
436 | # IProduct.recipes should provide all the SourcePackageRecipes |
437 | # attached to that product's branches. |
438 | product = self.factory.makeProduct() |
439 | @@ -70,8 +68,4 @@ |
440 | recipe1 = self.factory.makeSourcePackageRecipe(branches=[branch]) |
441 | recipe2 = self.factory.makeSourcePackageRecipe(branches=[branch]) |
442 | recipe_ignored = self.factory.makeSourcePackageRecipe() |
443 | - self.assertEqual(2, product.getRecipes().count()) |
444 | - |
445 | - |
446 | -def test_suite(): |
447 | - return unittest.TestLoader().loadTestsFromName(__name__) |
448 | + self.assertEqual(2, product.recipes.count()) |
449 | |
450 | === modified file 'lib/lp/code/model/tests/test_sourcepackagerecipe.py' |
451 | --- lib/lp/code/model/tests/test_sourcepackagerecipe.py 2011-02-18 04:25:00 +0000 |
452 | +++ lib/lp/code/model/tests/test_sourcepackagerecipe.py 2011-02-24 23:45:43 +0000 |
453 | @@ -10,7 +10,6 @@ |
454 | timedelta, |
455 | ) |
456 | import textwrap |
457 | -import unittest |
458 | |
459 | from bzrlib.plugins.builder.recipe import ForbiddenInstructionError |
460 | from pytz import UTC |
461 | @@ -582,19 +581,32 @@ |
462 | timedelta(minutes=11), recipe.getMedianBuildDuration()) |
463 | |
464 | def test_getBuilds(self): |
465 | - # Builds that need building are pending. |
466 | + # Test the various getBuilds methods. |
467 | recipe = self.factory.makeSourcePackageRecipe() |
468 | - build = self.factory.makeSourcePackageRecipeBuild(recipe=recipe) |
469 | - self.assertEqual([], list(recipe.getBuilds())) |
470 | - self.assertEqual([build], list(recipe.getPendingBuilds())) |
471 | + builds = [ |
472 | + self.factory.makeSourcePackageRecipeBuild(recipe=recipe) |
473 | + for x in range(3)] |
474 | + # We want the latest builds first. |
475 | + builds.reverse() |
476 | + |
477 | + self.assertEqual([], list(recipe.completed_builds)) |
478 | + self.assertEqual(builds, list(recipe.pending_builds)) |
479 | + self.assertEqual(builds, list(recipe.builds)) |
480 | + |
481 | + # Change the status of one of the builds and retest. |
482 | + removeSecurityProxy(builds[0]).status = BuildStatus.FULLYBUILT |
483 | + self.assertEqual([builds[0]], list(recipe.completed_builds)) |
484 | + self.assertEqual(builds[1:], list(recipe.pending_builds)) |
485 | + self.assertEqual(builds, list(recipe.builds)) |
486 | |
487 | def test_getBuilds_cancelled(self): |
488 | # Cancelled builds are not considered pending. |
489 | recipe = self.factory.makeSourcePackageRecipe() |
490 | build = self.factory.makeSourcePackageRecipeBuild(recipe=recipe) |
491 | build.cancelBuild() |
492 | - self.assertEqual([build], list(recipe.getBuilds())) |
493 | - self.assertEqual([], list(recipe.getPendingBuilds())) |
494 | + self.assertEqual([build], list(recipe.builds)) |
495 | + self.assertEqual([build], list(recipe.completed_builds)) |
496 | + self.assertEqual([], list(recipe.pending_builds)) |
497 | |
498 | def test_setRecipeText_private_base_branch(self): |
499 | source_package_recipe = self.factory.makeSourcePackageRecipe() |
500 | @@ -633,8 +645,9 @@ |
501 | recipe=recipe, archive=archive) |
502 | with person_logged_in(archive.owner): |
503 | archive.disable() |
504 | - self.assertEqual([], list(recipe.getBuilds())) |
505 | - self.assertEqual([], list(recipe.getPendingBuilds())) |
506 | + self.assertEqual([], list(recipe.builds)) |
507 | + self.assertEqual([], list(recipe.completed_builds)) |
508 | + self.assertEqual([], list(recipe.pending_builds)) |
509 | |
510 | |
511 | class TestRecipeBranchRoundTripping(TestCaseWithFactory): |
512 | @@ -901,8 +914,14 @@ |
513 | recipe, user = self.makeRecipe()[:-1] |
514 | self.assertEqual(recipe, user.getRecipe(name=recipe.name)) |
515 | |
516 | + def test_recipes(self): |
517 | + """Person.recipes works as expected.""" |
518 | + recipe, user = self.makeRecipe()[:-1] |
519 | + [ws_recipe] = user.recipes |
520 | + self.assertEqual(recipe, ws_recipe) |
521 | + |
522 | def test_requestBuild(self): |
523 | - """Build requests can be performed.""" |
524 | + """Build requests can be performed and last_build works.""" |
525 | person = self.factory.makePerson() |
526 | archive = self.factory.makeArchive(owner=person) |
527 | distroseries = self.factory.makeSourcePackageRecipeDistroseries() |
528 | @@ -910,9 +929,10 @@ |
529 | recipe, user, launchpad = self.makeRecipe(person) |
530 | distroseries = ws_object(launchpad, distroseries) |
531 | archive = ws_object(launchpad, archive) |
532 | - recipe.requestBuild( |
533 | + build = recipe.requestBuild( |
534 | archive=archive, distroseries=distroseries, |
535 | pocket=PackagePublishingPocket.RELEASE.title) |
536 | + self.assertEqual(build, recipe.last_build) |
537 | |
538 | def test_requestBuildRejectRepeat(self): |
539 | """Build requests are rejected if already pending.""" |
540 | @@ -967,6 +987,22 @@ |
541 | pocket=PackagePublishingPocket.RELEASE.title) |
542 | self.assertIn('BuildNotAllowedForDistro', str(e)) |
543 | |
544 | - |
545 | -def test_suite(): |
546 | - return unittest.TestLoader().loadTestsFromName(__name__) |
547 | + def test_getBuilds(self): |
548 | + """SourcePackageRecipe.[pending_|completed_]builds is as expected.""" |
549 | + person = self.factory.makePerson() |
550 | + archives = [self.factory.makeArchive(owner=person) for x in range(4)] |
551 | + distroseries= self.factory.makeSourcePackageRecipeDistroseries() |
552 | + |
553 | + recipe, user, launchpad = self.makeRecipe(person) |
554 | + distroseries = ws_object(launchpad, distroseries) |
555 | + |
556 | + builds = [] |
557 | + for archive in archives: |
558 | + archive = ws_object(launchpad, archive) |
559 | + build = recipe.requestBuild( |
560 | + archive=archive, distroseries=distroseries, |
561 | + pocket=PackagePublishingPocket.RELEASE.title) |
562 | + builds.insert(0, build) |
563 | + self.assertEqual(builds, list(recipe.pending_builds)) |
564 | + self.assertEqual(builds, list(recipe.builds)) |
565 | + self.assertEqual([], list(recipe.completed_builds)) |
566 | |
567 | === modified file 'lib/lp/code/scripts/tests/test_request_daily_builds.py' |
568 | --- lib/lp/code/scripts/tests/test_request_daily_builds.py 2011-01-13 03:53:53 +0000 |
569 | +++ lib/lp/code/scripts/tests/test_request_daily_builds.py 2011-02-24 23:45:43 +0000 |
570 | @@ -5,8 +5,6 @@ |
571 | |
572 | """Test the request_daily_builds script""" |
573 | |
574 | -import unittest |
575 | - |
576 | import transaction |
577 | |
578 | from canonical.launchpad.scripts.tests import run_script |
579 | @@ -28,14 +26,14 @@ |
580 | pack_branch = self.factory.makePackageBranch() |
581 | pack_recipe = self.factory.makeSourcePackageRecipe( |
582 | build_daily=True, is_stale=True, branches=[pack_branch]) |
583 | - self.assertEqual(0, prod_recipe.getPendingBuilds().count()) |
584 | - self.assertEqual(0, pack_recipe.getPendingBuilds().count()) |
585 | + self.assertEqual(0, prod_recipe.pending_builds.count()) |
586 | + self.assertEqual(0, pack_recipe.pending_builds.count()) |
587 | transaction.commit() |
588 | retcode, stdout, stderr = run_script( |
589 | 'cronscripts/request_daily_builds.py', []) |
590 | self.assertIn('Requested 2 daily builds.', stderr) |
591 | - self.assertEqual(1, prod_recipe.getPendingBuilds().count()) |
592 | - self.assertEqual(1, pack_recipe.getPendingBuilds().count()) |
593 | + self.assertEqual(1, prod_recipe.pending_builds.count()) |
594 | + self.assertEqual(1, pack_recipe.pending_builds.count()) |
595 | self.assertFalse(prod_recipe.is_stale) |
596 | self.assertFalse(pack_recipe.is_stale) |
597 | |
598 | @@ -47,13 +45,9 @@ |
599 | transaction.commit() |
600 | retcode, stdout, stderr = run_script( |
601 | 'cronscripts/request_daily_builds.py', []) |
602 | - self.assertEqual(0, recipe.getPendingBuilds().count()) |
603 | + self.assertEqual(0, recipe.pending_builds.count()) |
604 | self.assertIn('Requested 0 daily builds.', stderr) |
605 | utility = ErrorReportingUtility() |
606 | utility.configure('request_daily_builds') |
607 | oops = utility.getLastOopsReport() |
608 | self.assertIn('NonPPABuildRequest', oops.tb_text) |
609 | - |
610 | - |
611 | -def test_suite(): |
612 | - return unittest.TestLoader().loadTestsFromName(__name__) |
613 | |
614 | === modified file 'lib/lp/code/stories/webservice/xx-branch.txt' |
615 | --- lib/lp/code/stories/webservice/xx-branch.txt 2011-01-31 17:08:35 +0000 |
616 | +++ lib/lp/code/stories/webservice/xx-branch.txt 2011-02-24 23:45:43 +0000 |
617 | @@ -130,6 +130,7 @@ |
618 | owner_link: u'.../~eric' |
619 | private: False |
620 | project_link: u'.../fooix' |
621 | + recipes_collection_link: u'http://.../~eric/fooix/trunk/recipes' |
622 | registrant_link: u'.../~eric' |
623 | repository_format: None |
624 | resource_type_link: u'.../#branch' |
625 | @@ -282,7 +283,7 @@ |
626 | >>> print deletable |
627 | False |
628 | |
629 | - Deleting only works on branches that do not have anything else |
630 | + Deleting only works on branches that do not have anything else |
631 | depending on them. |
632 | |
633 | >>> response = webservice.delete('/~eric/fooix/feature-branch') |
634 | |
635 | === modified file 'lib/lp/code/templates/sourcepackagerecipe-listing.pt' |
636 | --- lib/lp/code/templates/sourcepackagerecipe-listing.pt 2010-07-26 20:29:19 +0000 |
637 | +++ lib/lp/code/templates/sourcepackagerecipe-listing.pt 2011-02-24 23:45:43 +0000 |
638 | @@ -20,7 +20,7 @@ |
639 | </tr> |
640 | </thead> |
641 | <tbody> |
642 | - <tal:recipes repeat="recipe context/getRecipes"> |
643 | + <tal:recipes repeat="recipe context/recipes"> |
644 | <tr> |
645 | <td colspan="2"> |
646 | <a tal:attributes="href recipe/fmt:url" |
647 | |
648 | === modified file 'lib/lp/codehosting/scanner/bzrsync.py' |
649 | --- lib/lp/codehosting/scanner/bzrsync.py 2010-10-15 18:27:58 +0000 |
650 | +++ lib/lp/codehosting/scanner/bzrsync.py 2011-02-24 23:45:43 +0000 |
651 | @@ -319,5 +319,5 @@ |
652 | |
653 | |
654 | def update_recipes(tip_changed): |
655 | - for recipe in tip_changed.db_branch.getRecipes(): |
656 | + for recipe in tip_changed.db_branch.recipes: |
657 | recipe.is_stale = True |
658 | |
659 | === modified file 'lib/lp/registry/model/person.py' |
660 | --- lib/lp/registry/model/person.py 2011-02-16 11:03:04 +0000 |
661 | +++ lib/lp/registry/model/person.py 2011-02-24 23:45:43 +0000 |
662 | @@ -2851,7 +2851,8 @@ |
663 | from lp.hardwaredb.model.hwdb import HWSubmissionSet |
664 | return HWSubmissionSet().search(owner=self) |
665 | |
666 | - def getRecipes(self): |
667 | + @property |
668 | + def recipes(self): |
669 | """See `IHasRecipes`.""" |
670 | from lp.code.model.sourcepackagerecipe import SourcePackageRecipe |
671 | store = Store.of(self) |
672 | @@ -3454,8 +3455,8 @@ |
673 | |
674 | def _mergeSourcePackageRecipes(self, from_person, to_person): |
675 | # This shouldn't use removeSecurityProxy. |
676 | - recipes = from_person.getRecipes() |
677 | - existing_names = [r.name for r in to_person.getRecipes()] |
678 | + recipes = from_person.recipes |
679 | + existing_names = [r.name for r in to_person.recipes] |
680 | for recipe in recipes: |
681 | new_name = recipe.name |
682 | count = 1 |
683 | |
684 | === modified file 'lib/lp/registry/model/product.py' |
685 | --- lib/lp/registry/model/product.py 2011-01-24 20:53:10 +0000 |
686 | +++ lib/lp/registry/model/product.py 2011-02-24 23:45:43 +0000 |
687 | @@ -1315,7 +1315,8 @@ |
688 | return DecoratedResultSet( |
689 | self.getVersionSortedSeries(statuses=statuses), decorate) |
690 | |
691 | - def getRecipes(self): |
692 | + @property |
693 | + def recipes(self): |
694 | """See `IHasRecipes`.""" |
695 | from lp.code.model.branch import Branch |
696 | store = Store.of(self) |
697 | |
698 | === modified file 'lib/lp/registry/stories/webservice/xx-person.txt' |
699 | --- lib/lp/registry/stories/webservice/xx-person.txt 2011-02-13 22:54:48 +0000 |
700 | +++ lib/lp/registry/stories/webservice/xx-person.txt 2011-02-24 23:45:43 +0000 |
701 | @@ -50,6 +50,7 @@ |
702 | u'http://.../~salgado/+email/guilherme.salgado@canonical.com' |
703 | private: False |
704 | proposed_members_collection_link: u'http://.../~salgado/proposed_members' |
705 | + recipes_collection_link: u'http://.../~salgado/recipes' |
706 | resource_type_link: u'http://.../#person' |
707 | self_link: u'http://.../~salgado' |
708 | sshkeys_collection_link: u'http://.../~salgado/sshkeys' |
709 | @@ -110,6 +111,7 @@ |
710 | private: False |
711 | proposed_members_collection_link: |
712 | u'http://.../~ubuntu-team/proposed_members' |
713 | + recipes_collection_link: u'http://.../~ubuntu-team/recipes' |
714 | renewal_policy: u'invite them to apply for renewal' |
715 | resource_type_link: u'http://.../#team' |
716 | self_link: u'http://.../~ubuntu-team' |
717 | |
718 | === modified file 'lib/lp/registry/stories/webservice/xx-project-registry.txt' |
719 | --- lib/lp/registry/stories/webservice/xx-project-registry.txt 2011-02-13 22:54:48 +0000 |
720 | +++ lib/lp/registry/stories/webservice/xx-project-registry.txt 2011-02-24 23:45:43 +0000 |
721 | @@ -180,6 +180,7 @@ |
722 | programming_language: None |
723 | project_group_link: u'http://.../mozilla' |
724 | qualifies_for_free_hosting: False |
725 | + recipes_collection_link: u'http://.../firefox/recipes' |
726 | registrant_link: u'http://.../~name12' |
727 | releases_collection_link: u'http://.../firefox/releases' |
728 | remote_product: None |
729 | @@ -244,6 +245,7 @@ |
730 | programming_language: None |
731 | project_group_link: u'http://.../mozilla' |
732 | qualifies_for_free_hosting: False |
733 | + recipes_collection_link: u'http://.../firefox/recipes' |
734 | registrant_link: u'http://.../~name12' |
735 | releases_collection_link: u'http://.../firefox/releases' |
736 | remote_product: None |
737 | |
738 | === modified file 'lib/lp/registry/tests/test_person.py' |
739 | --- lib/lp/registry/tests/test_person.py 2011-02-18 13:26:58 +0000 |
740 | +++ lib/lp/registry/tests/test_person.py 2011-02-24 23:45:43 +0000 |
741 | @@ -734,7 +734,7 @@ |
742 | self._do_premerge(recipe.owner, person) |
743 | login_person(person) |
744 | self.person_set.merge(recipe.owner, person) |
745 | - self.assertEqual(1, person.getRecipes().count()) |
746 | + self.assertEqual(1, person.recipes.count()) |
747 | |
748 | def test_merge_with_duplicated_recipes(self): |
749 | # If both the from and to people have recipes with the same name, |
750 | @@ -750,7 +750,7 @@ |
751 | self._do_premerge(merge_from.owner, mergee) |
752 | login_person(mergee) |
753 | self.person_set.merge(merge_from.owner, merge_to.owner) |
754 | - recipes = mergee.getRecipes() |
755 | + recipes = mergee.recipes |
756 | self.assertEqual(2, recipes.count()) |
757 | descriptions = [r.description for r in recipes] |
758 | self.assertEqual([u'TO', u'FROM'], descriptions) |
Just to mention, this seems gratuitiously different to the existing api for getBuildRecords, which we could expose on your context - conceptually an IHasBuildRecords or similar. Is there a reason for this?