Merge lp:~mwhudson/launchpad/recipe-model-code into lp:launchpad/db-devel
- recipe-model-code
- Merge into db-devel
Status: | Merged |
---|---|
Approved by: | Michael Hudson-Doyle |
Approved revision: | not available |
Merged at revision: | not available |
Proposed branch: | lp:~mwhudson/launchpad/recipe-model-code |
Merge into: | lp:launchpad/db-devel |
Prerequisite: | lp:~mwhudson/launchpad/recipe-db-schema |
Diff against target: |
837 lines (+799/-0) 6 files modified
lib/lp/soyuz/configure.zcml (+30/-0) lib/lp/soyuz/interfaces/sourcepackagerecipe.py (+86/-0) lib/lp/soyuz/model/sourcepackagerecipe.py (+82/-0) lib/lp/soyuz/model/sourcepackagerecipedata.py (+230/-0) lib/lp/soyuz/tests/test_sourcepackagerecipe.py (+370/-0) utilities/sourcedeps.conf (+1/-0) |
To merge this branch: | bzr merge lp:~mwhudson/launchpad/recipe-model-code |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Paul Hummer (community) | code | Approve | |
Jonathan Lange (community) | Needs Resubmitting | ||
Review via email: mp+16272@code.launchpad.net |
Commit message
Model classes and code for storing bzr-builder recipes in the database
Description of the change
Michael Hudson-Doyle (mwhudson) wrote : | # |
Jonathan Lange (jml) wrote : | # |
This is going to have to change as a result of the schema changes, I think.
Michael Hudson-Doyle (mwhudson) wrote : | # |
> This is going to have to change as a result of the schema changes, I think.
And now it has. The guts of this branch is converting bzr-builder's view of a recipe into and out of the database views. The tests are copy-paste-hacked from bzr-builder's own tests -- it's possible we could/should submit a patch to bzr-builder that would let us reuse bzr-builders tests. But first we'd like to land this :-)
Jonathan Lange (jml) wrote : | # |
On Tue, Jan 12, 2010 at 11:46 AM, Michael Hudson
<email address hidden> wrote:
>> This is going to have to change as a result of the schema changes, I think.
>
> And now it has. The guts of this branch is converting bzr-builder's view of a recipe into and out of the database views. The tests are copy-paste-hacked from bzr-builder's own tests -- it's possible we could/should submit a patch to bzr-builder that would let us reuse bzr-builders tests. But first we'd like to land this :-)
Does that mean you want this reviewed again?
jml
Paul Hummer (rockstar) wrote : | # |
Hi Michael-
This branch is quite big, and has a lot of really hairy things going on. I know all of this has been discussed on the mailing list, but that's a terrible place to document things. Has anyone put any effort into saying "This is how this works and how you work with it?" Just something to think about.
This branch looks good. As we discussed, ForbiddenInstru
Cheers,
Paul
Paul Hummer (rockstar) wrote : | # |
_SourcePackageR
Preview Diff
1 | === added symlink 'bzrplugins/builder' |
2 | === target is u'../sourcecode/bzr-builder' |
3 | === modified file 'lib/lp/soyuz/configure.zcml' |
4 | --- lib/lp/soyuz/configure.zcml 2010-01-13 04:41:19 +0000 |
5 | +++ lib/lp/soyuz/configure.zcml 2010-01-13 20:46:22 +0000 |
6 | @@ -915,4 +915,34 @@ |
7 | factory="lp.soyuz.model.binarypackagebuildbehavior.BinaryPackageBuildBehavior" |
8 | permission="zope.Public" /> |
9 | |
10 | + <!-- SourcePackageRecipe --> |
11 | + <class |
12 | + class="lp.soyuz.model.sourcepackagerecipe.SourcePackageRecipe"> |
13 | + <allow interface="lp.soyuz.interfaces.sourcepackagerecipe.ISourcePackageRecipe"/> |
14 | + <require |
15 | + permission="launchpad.Edit" |
16 | + set_attributes="builder_recipe"/> |
17 | + </class> |
18 | + <class |
19 | + class="bzrlib.plugins.builder.recipe.BaseRecipeBranch"> |
20 | + <allow attributes="name url revspec revid child_branches deb_version" /> |
21 | + </class> |
22 | + <class |
23 | + class="bzrlib.plugins.builder.recipe.RecipeBranch"> |
24 | + <allow attributes="name url revspec revid child_branches" /> |
25 | + </class> |
26 | + <class |
27 | + class="bzrlib.plugins.builder.recipe.MergeInstruction"> |
28 | + <allow attributes="as_tuple recipe_branch nest_path" /> |
29 | + </class> |
30 | + <class |
31 | + class="bzrlib.plugins.builder.recipe.NestInstruction"> |
32 | + <allow attributes="as_tuple recipe_branch nest_path" /> |
33 | + </class> |
34 | + <securedutility |
35 | + component="lp.soyuz.model.sourcepackagerecipe.SourcePackageRecipe" |
36 | + provides="lp.soyuz.interfaces.sourcepackagerecipe.ISourcePackageRecipeSource"> |
37 | + <allow interface="lp.soyuz.interfaces.sourcepackagerecipe.ISourcePackageRecipeSource"/> |
38 | + </securedutility> |
39 | + |
40 | </configure> |
41 | |
42 | === added file 'lib/lp/soyuz/interfaces/sourcepackagerecipe.py' |
43 | --- lib/lp/soyuz/interfaces/sourcepackagerecipe.py 1970-01-01 00:00:00 +0000 |
44 | +++ lib/lp/soyuz/interfaces/sourcepackagerecipe.py 2010-01-13 20:46:22 +0000 |
45 | @@ -0,0 +1,86 @@ |
46 | +# Copyright 2009 Canonical Ltd. This software is licensed under the |
47 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
48 | + |
49 | +"""Interface of the `SourcePackageRecipe` content type.""" |
50 | + |
51 | +__metaclass__ = type |
52 | +__all__ = [ |
53 | + 'ForbiddenInstruction', |
54 | + 'ISourcePackageRecipe', |
55 | + 'ISourcePackageRecipeSource', |
56 | + 'TooNewRecipeFormat', |
57 | + ] |
58 | + |
59 | +from lazr.restful.fields import Reference |
60 | + |
61 | +from zope.interface import Attribute, Interface |
62 | + |
63 | +from zope.schema import Datetime, TextLine |
64 | + |
65 | +from canonical.launchpad import _ |
66 | +from canonical.launchpad.validators.name import name_validator |
67 | + |
68 | +from lp.registry.interfaces.person import IPerson |
69 | +from lp.registry.interfaces.role import IHasOwner |
70 | +from lp.registry.interfaces.distroseries import IDistroSeries |
71 | +from lp.registry.interfaces.sourcepackagename import ISourcePackageName |
72 | + |
73 | + |
74 | +class ForbiddenInstruction(Exception): |
75 | + """A forbidden instruction was found in the recipe.""" |
76 | + |
77 | + def __init__(self, instruction_name): |
78 | + self.instruction_name = instruction_name |
79 | + |
80 | + |
81 | +class TooNewRecipeFormat(Exception): |
82 | + """The format of the recipe supplied was too new.""" |
83 | + |
84 | + def __init__(self, supplied_format, newest_supported): |
85 | + self.supplied_format = supplied_format |
86 | + self.newest_supported = newest_supported |
87 | + |
88 | + |
89 | +class ISourcePackageRecipe(IHasOwner): |
90 | + """An ISourcePackageRecipe describes how to build a source package. |
91 | + |
92 | + More precisely, it describes how to combine a number of branches into a |
93 | + debianized source tree. |
94 | + """ |
95 | + |
96 | + date_created = Datetime(required=True, readonly=True) |
97 | + date_last_modified = Datetime(required=True, readonly=True) |
98 | + |
99 | + registrant = Reference( |
100 | + IPerson, title=_("The person who created this recipe"), readonly=True) |
101 | + owner = Reference( |
102 | + IPerson, title=_("The person or team who can edit this recipe"), |
103 | + readonly=False) |
104 | + distroseries = Reference( |
105 | + IDistroSeries, title=_("The distroseries this recipe will build a " |
106 | + "source package for"), |
107 | + readonly=True) |
108 | + sourcepackagename = Reference( |
109 | + ISourcePackageName, title=_("The name of the source package this " |
110 | + "recipe will build a source package"), |
111 | + readonly=True) |
112 | + |
113 | + name = TextLine( |
114 | + title=_("Name"), required=True, |
115 | + constraint=name_validator, |
116 | + description=_("The name of this recipe.")) |
117 | + |
118 | + builder_recipe = Attribute( |
119 | + _("The bzr-builder data structure for the recipe.")) |
120 | + |
121 | + def getReferencedBranches(): |
122 | + """An iterator of the branches referenced by this recipe.""" |
123 | + |
124 | + |
125 | +class ISourcePackageRecipeSource(Interface): |
126 | + """A utility of this interface can be used to create and access recipes. |
127 | + """ |
128 | + |
129 | + def new(registrant, owner, distroseries, sourcepackagename, name, |
130 | + builder_recipe): |
131 | + """Create an `ISourcePackageRecipe`.""" |
132 | |
133 | === added file 'lib/lp/soyuz/model/sourcepackagerecipe.py' |
134 | --- lib/lp/soyuz/model/sourcepackagerecipe.py 1970-01-01 00:00:00 +0000 |
135 | +++ lib/lp/soyuz/model/sourcepackagerecipe.py 2010-01-13 20:46:22 +0000 |
136 | @@ -0,0 +1,82 @@ |
137 | +# Copyright 2009 Canonical Ltd. This software is licensed under the |
138 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
139 | + |
140 | +"""Implementation of the `SourcePackageRecipe` content type.""" |
141 | + |
142 | +__metaclass__ = type |
143 | +__all__ = ['SourcePackageRecipe'] |
144 | + |
145 | +from storm.locals import Int, Reference, Store, Storm, Unicode |
146 | + |
147 | +from zope.interface import classProvides, implements |
148 | + |
149 | +from canonical.database.datetimecol import UtcDateTimeCol |
150 | +from canonical.launchpad.interfaces.lpstorm import IMasterStore |
151 | + |
152 | +from lp.soyuz.interfaces.sourcepackagerecipe import ( |
153 | + ISourcePackageRecipe, ISourcePackageRecipeSource) |
154 | +from lp.soyuz.model.sourcepackagerecipedata import _SourcePackageRecipeData |
155 | + |
156 | + |
157 | +class SourcePackageRecipe(Storm): |
158 | + """See `ISourcePackageRecipe` and `ISourcePackageRecipeSource`.""" |
159 | + |
160 | + __storm_table__ = 'SourcePackageRecipe' |
161 | + |
162 | + implements(ISourcePackageRecipe) |
163 | + classProvides(ISourcePackageRecipeSource) |
164 | + |
165 | + id = Int(primary=True) |
166 | + |
167 | + date_created = UtcDateTimeCol(notNull=True) |
168 | + date_last_modified = UtcDateTimeCol(notNull=True) |
169 | + |
170 | + owner_id = Int(name='owner', allow_none=True) |
171 | + owner = Reference(owner_id, 'Person.id') |
172 | + |
173 | + registrant_id = Int(name='registrant', allow_none=True) |
174 | + registrant = Reference(registrant_id, 'Person.id') |
175 | + |
176 | + distroseries_id = Int(name='distroseries', allow_none=True) |
177 | + distroseries = Reference(distroseries_id, 'DistroSeries.id') |
178 | + |
179 | + sourcepackagename_id = Int(name='sourcepackagename', allow_none=True) |
180 | + sourcepackagename = Reference( |
181 | + sourcepackagename_id, 'SourcePackageName.id') |
182 | + |
183 | + name = Unicode(allow_none=True) |
184 | + |
185 | + @property |
186 | + def _recipe_data(self): |
187 | + return Store.of(self).find( |
188 | + _SourcePackageRecipeData, |
189 | + _SourcePackageRecipeData.sourcepackage_recipe == self).one() |
190 | + |
191 | + def _get_builder_recipe(self): |
192 | + """Accesses of the recipe go to the _SourcePackageRecipeData.""" |
193 | + return self._recipe_data.getRecipe() |
194 | + |
195 | + def _set_builder_recipe(self, value): |
196 | + """Setting of the recipe goes to the _SourcePackageRecipeData.""" |
197 | + self._recipe_data.setRecipe(value) |
198 | + |
199 | + builder_recipe = property(_get_builder_recipe, _set_builder_recipe) |
200 | + |
201 | + def getReferencedBranches(self): |
202 | + """See `ISourcePackageRecipe.getReferencedBranches`.""" |
203 | + return self._recipe_data.getReferencedBranches() |
204 | + |
205 | + @staticmethod |
206 | + def new(registrant, owner, distroseries, sourcepackagename, name, |
207 | + builder_recipe): |
208 | + """See `ISourcePackageRecipeSource.new`.""" |
209 | + store = IMasterStore(SourcePackageRecipe) |
210 | + sprecipe = SourcePackageRecipe() |
211 | + _SourcePackageRecipeData(builder_recipe, sprecipe) |
212 | + sprecipe.registrant = registrant |
213 | + sprecipe.owner = owner |
214 | + sprecipe.distroseries = distroseries |
215 | + sprecipe.sourcepackagename = sourcepackagename |
216 | + sprecipe.name = name |
217 | + store.add(sprecipe) |
218 | + return sprecipe |
219 | |
220 | === added file 'lib/lp/soyuz/model/sourcepackagerecipedata.py' |
221 | --- lib/lp/soyuz/model/sourcepackagerecipedata.py 1970-01-01 00:00:00 +0000 |
222 | +++ lib/lp/soyuz/model/sourcepackagerecipedata.py 2010-01-13 20:46:22 +0000 |
223 | @@ -0,0 +1,230 @@ |
224 | +# Copyright 2009 Canonical Ltd. This software is licensed under the |
225 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
226 | + |
227 | +"""Implementation of the recipe storage. |
228 | + |
229 | +This is purely an implementation detail of SourcePackageRecipe.recipe_data and |
230 | +SourcePackageRecipeBuild.manifest, the classes in this file have no public |
231 | +interfaces. |
232 | +""" |
233 | + |
234 | +__metaclass__ = type |
235 | +__all__ = ['_SourcePackageRecipeData'] |
236 | + |
237 | +from bzrlib.plugins.builder.recipe import ( |
238 | + BaseRecipeBranch, MergeInstruction, NestInstruction, RecipeBranch) |
239 | + |
240 | +from lazr.enum import DBEnumeratedType, DBItem |
241 | + |
242 | +from storm.locals import Int, Reference, ReferenceSet, Store, Storm, Unicode |
243 | + |
244 | +from zope.component import getUtility |
245 | + |
246 | +from canonical.database.enumcol import EnumCol |
247 | +from canonical.launchpad.interfaces.lpstorm import IStore |
248 | + |
249 | +from lp.code.model.branch import Branch |
250 | +from lp.code.interfaces.branchlookup import IBranchLookup |
251 | +from lp.soyuz.interfaces.sourcepackagerecipe import ( |
252 | + ForbiddenInstruction, TooNewRecipeFormat) |
253 | + |
254 | + |
255 | +class InstructionType(DBEnumeratedType): |
256 | + """The instruction type, for _SourcePackageRecipeDataInstruction.type.""" |
257 | + |
258 | + MERGE = DBItem(1, """ |
259 | + Merge instruction |
260 | + |
261 | + A merge instruction.""") |
262 | + |
263 | + NEST = DBItem(2, """ |
264 | + Nest instruction |
265 | + |
266 | + A nest instruction.""") |
267 | + |
268 | + |
269 | +class _SourcePackageRecipeDataInstruction(Storm): |
270 | + """A single line from a recipe.""" |
271 | + |
272 | + __storm_table__ = "SourcePackageRecipeDataInstruction" |
273 | + |
274 | + def __init__(self, name, type, comment, line_number, branch, revspec, |
275 | + directory, recipe_data, parent_instruction): |
276 | + self.name = unicode(name) |
277 | + self.type = type |
278 | + self.comment = comment |
279 | + self.line_number = line_number |
280 | + self.branch = branch |
281 | + if revspec is not None: |
282 | + revspec = unicode(revspec) |
283 | + self.revspec = revspec |
284 | + if directory is not None: |
285 | + directory = unicode(directory) |
286 | + self.directory = directory |
287 | + self.recipe_data = recipe_data |
288 | + self.parent_instruction = parent_instruction |
289 | + |
290 | + id = Int(primary=True) |
291 | + |
292 | + name = Unicode(allow_none=False) |
293 | + type = EnumCol(notNull=True, schema=InstructionType) |
294 | + comment = Unicode(allow_none=True) |
295 | + line_number = Int(allow_none=False) |
296 | + |
297 | + branch_id = Int(name='branch', allow_none=False) |
298 | + branch = Reference(branch_id, 'Branch.id') |
299 | + |
300 | + revspec = Unicode(allow_none=True) |
301 | + directory = Unicode(allow_none=True) |
302 | + |
303 | + recipe_data_id = Int(name='recipe_data', allow_none=False) |
304 | + recipe_data = Reference(recipe_data_id, '_SourcePackageRecipeData.id') |
305 | + |
306 | + parent_instruction_id = Int(name='parent_instruction', allow_none=True) |
307 | + parent_instruction = Reference( |
308 | + parent_instruction_id, '_SourcePackageRecipeDataInstruction.id') |
309 | + |
310 | + def appendToRecipe(self, recipe_branch): |
311 | + """Append a bzr-builder instruction to the recipe_branch object.""" |
312 | + branch = RecipeBranch( |
313 | + self.name, self.branch.bzr_identity, self.revspec) |
314 | + if self.type == InstructionType.MERGE: |
315 | + recipe_branch.merge_branch(branch) |
316 | + elif self.type == InstructionType.NEST: |
317 | + recipe_branch.nest_branch(self.directory, branch) |
318 | + else: |
319 | + raise AssertionError("Unknown type %r" % self.type) |
320 | + return branch |
321 | + |
322 | + |
323 | +class _SourcePackageRecipeData(Storm): |
324 | + """The database representation of a BaseRecipeBranch from bzr-builder. |
325 | + |
326 | + This is referenced from the SourcePackageRecipe table as the 'recipe_data' |
327 | + column and from the SourcePackageRecipeBuild table as the 'manifest' |
328 | + column. |
329 | + """ |
330 | + |
331 | + __storm_table__ = "SourcePackageRecipeData" |
332 | + |
333 | + id = Int(primary=True) |
334 | + |
335 | + base_branch_id = Int(name='base_branch', allow_none=False) |
336 | + base_branch = Reference(base_branch_id, 'Branch.id') |
337 | + |
338 | + recipe_format = Unicode(allow_none=False) |
339 | + deb_version_template = Unicode(allow_none=False) |
340 | + revspec = Unicode(allow_none=True) |
341 | + |
342 | + instructions = ReferenceSet( |
343 | + id, _SourcePackageRecipeDataInstruction.recipe_data_id, |
344 | + order_by=_SourcePackageRecipeDataInstruction.line_number) |
345 | + |
346 | + sourcepackage_recipe_id = Int( |
347 | + name='sourcepackage_recipe', allow_none=True) |
348 | + sourcepackage_recipe = Reference( |
349 | + sourcepackage_recipe_id, 'SourcePackageRecipe.id') |
350 | + |
351 | + sourcepackage_recipe_build_id = Int( |
352 | + name='sourcepackage_recipe_build', allow_none=True) |
353 | + #sourcepackage_recipe_build = Reference( |
354 | + # sourcepackage_recipe_build_id, 'SourcePackageRecipeBuild.id') |
355 | + |
356 | + def getRecipe(self): |
357 | + """The BaseRecipeBranch version of the recipe.""" |
358 | + base_branch = BaseRecipeBranch( |
359 | + self.base_branch.bzr_identity, self.deb_version_template, |
360 | + self.recipe_format, self.revspec) |
361 | + insn_stack = [] |
362 | + for insn in self.instructions: |
363 | + while insn_stack and \ |
364 | + insn_stack[-1]['insn'] != insn.parent_instruction: |
365 | + insn_stack.pop() |
366 | + if insn_stack: |
367 | + target_branch = insn_stack[-1]['recipe_branch'] |
368 | + else: |
369 | + target_branch = base_branch |
370 | + recipe_branch = insn.appendToRecipe(target_branch) |
371 | + insn_stack.append( |
372 | + dict(insn=insn, recipe_branch=recipe_branch)) |
373 | + return base_branch |
374 | + |
375 | + def _scanInstructions(self, recipe_branch): |
376 | + """Check the recipe_branch doesn't use 'run' and look up the branches. |
377 | + |
378 | + We do all the lookups before we start constructing database objects to |
379 | + avoid flushing half-constructed objects to the database. |
380 | + |
381 | + :return: A map ``{branch_url: db_branch}``. |
382 | + """ |
383 | + r = {} |
384 | + for instruction in recipe_branch.child_branches: |
385 | + if not (isinstance(instruction, MergeInstruction) or |
386 | + isinstance(instruction, NestInstruction)): |
387 | + raise ForbiddenInstruction(str(instruction)) |
388 | + db_branch = getUtility(IBranchLookup).getByUrl( |
389 | + instruction.recipe_branch.url) |
390 | + r[instruction.recipe_branch.url] = db_branch |
391 | + r.update(self._scanInstructions(instruction.recipe_branch)) |
392 | + return r |
393 | + |
394 | + def _recordInstructions(self, recipe_branch, parent_insn, branch_map, |
395 | + line_number=0): |
396 | + """Build _SourcePackageRecipeDataInstructions for the recipe_branch. |
397 | + """ |
398 | + for instruction in recipe_branch.child_branches: |
399 | + if isinstance(instruction, MergeInstruction): |
400 | + type = InstructionType.MERGE |
401 | + elif isinstance(instruction, NestInstruction): |
402 | + type = InstructionType.NEST |
403 | + else: |
404 | + # Unsupported instructions should have been filtered out by |
405 | + # _scanInstructions; if we get surprised here, that's a bug. |
406 | + raise AssertionError( |
407 | + "Unsupported instruction %r" % instruction) |
408 | + line_number += 1 |
409 | + comment = None |
410 | + db_branch = branch_map[instruction.recipe_branch.url] |
411 | + insn = _SourcePackageRecipeDataInstruction( |
412 | + instruction.recipe_branch.name, type, comment, |
413 | + line_number, db_branch, instruction.recipe_branch.revspec, |
414 | + instruction.nest_path, self, parent_insn) |
415 | + line_number = self._recordInstructions( |
416 | + instruction.recipe_branch, insn, branch_map, line_number) |
417 | + return line_number |
418 | + |
419 | + def setRecipe(self, builder_recipe): |
420 | + """Convert the BaseRecipeBranch `builder_recipe` to the db form.""" |
421 | + if builder_recipe.format > 0.2: |
422 | + raise TooNewRecipeFormat(builder_recipe.format, 0.2) |
423 | + branch_map = self._scanInstructions(builder_recipe) |
424 | + # If this object hasn't been added to a store yet, there can't be any |
425 | + # instructions linking to us yet. |
426 | + if Store.of(self) is not None: |
427 | + self.instructions.find().remove() |
428 | + branch_lookup = getUtility(IBranchLookup) |
429 | + base_branch = branch_lookup.getByUrl(builder_recipe.url) |
430 | + if builder_recipe.revspec is not None: |
431 | + self.revspec = unicode(builder_recipe.revspec) |
432 | + self._recordInstructions( |
433 | + builder_recipe, parent_insn=None, branch_map=branch_map) |
434 | + self.base_branch = base_branch |
435 | + self.deb_version_template = unicode(builder_recipe.deb_version) |
436 | + self.recipe_format = unicode(builder_recipe.format) |
437 | + |
438 | + def __init__(self, recipe, sourcepackage_recipe): |
439 | + """Initialize from the bzr-builder recipe and link it to a db recipe. |
440 | + """ |
441 | + self.setRecipe(recipe) |
442 | + self.sourcepackage_recipe = sourcepackage_recipe |
443 | + |
444 | + def getReferencedBranches(self): |
445 | + """Return an iterator of the Branch objects referenced by this recipe. |
446 | + """ |
447 | + yield self.base_branch |
448 | + sub_branches = IStore(self).find( |
449 | + Branch, |
450 | + _SourcePackageRecipeDataInstruction.recipe_data == self, |
451 | + Branch.id == _SourcePackageRecipeDataInstruction.branch_id) |
452 | + for branch in sub_branches: |
453 | + yield branch |
454 | |
455 | === added file 'lib/lp/soyuz/tests/test_sourcepackagerecipe.py' |
456 | --- lib/lp/soyuz/tests/test_sourcepackagerecipe.py 1970-01-01 00:00:00 +0000 |
457 | +++ lib/lp/soyuz/tests/test_sourcepackagerecipe.py 2010-01-13 20:46:22 +0000 |
458 | @@ -0,0 +1,370 @@ |
459 | +# Copyright 2009 Canonical Ltd. This software is licensed under the |
460 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
461 | + |
462 | +"""Tests for the SourcePackageRecipe content type.""" |
463 | + |
464 | +__metaclass__ = type |
465 | + |
466 | +import textwrap |
467 | +import unittest |
468 | + |
469 | +from bzrlib.plugins.builder.recipe import RecipeParser |
470 | + |
471 | +from zope.component import getUtility |
472 | +from zope.security.interfaces import Unauthorized |
473 | + |
474 | +from canonical.testing.layers import DatabaseFunctionalLayer |
475 | + |
476 | +from lp.soyuz.interfaces.sourcepackagerecipe import ( |
477 | + ForbiddenInstruction, ISourcePackageRecipe, ISourcePackageRecipeSource, |
478 | + TooNewRecipeFormat) |
479 | +from lp.testing import login_person, TestCaseWithFactory |
480 | + |
481 | + |
482 | +MINIMAL_RECIPE_TEXT = u'''\ |
483 | +# bzr-builder format 0.2 deb-version 1.0 |
484 | +%s |
485 | +''' |
486 | + |
487 | +class TestSourcePackageRecipe(TestCaseWithFactory): |
488 | + """Tests for `SourcePackageRecipe` objects.""" |
489 | + |
490 | + layer = DatabaseFunctionalLayer |
491 | + |
492 | + def makeBuilderRecipe(self, *branches): |
493 | + """Make a builder recipe that references `branches`. |
494 | + |
495 | + If no branches are passed, return a recipe text that references an |
496 | + arbitrary branch. |
497 | + """ |
498 | + if len(branches) == 0: |
499 | + branches = (self.factory.makeAnyBranch(),) |
500 | + base_branch = branches[0] |
501 | + other_branches = branches[1:] |
502 | + text = MINIMAL_RECIPE_TEXT % base_branch.bzr_identity |
503 | + for i, branch in enumerate(other_branches): |
504 | + text += 'merge dummy-%s %s\n' % (i, branch.bzr_identity) |
505 | + parser = RecipeParser(text) |
506 | + return parser.parse() |
507 | + |
508 | + def makeSourcePackageRecipeFromBuilderRecipe(self, builder_recipe): |
509 | + """Make a SourcePackageRecipe from a recipe with arbitrary other data. |
510 | + """ |
511 | + registrant = self.factory.makePerson() |
512 | + owner = self.factory.makeTeam(owner=registrant) |
513 | + distroseries = self.factory.makeDistroSeries() |
514 | + sourcepackagename = self.factory.makeSourcePackageName() |
515 | + name = self.factory.getUniqueString(u'recipe-name') |
516 | + return getUtility(ISourcePackageRecipeSource).new( |
517 | + registrant=registrant, owner=owner, distroseries=distroseries, |
518 | + sourcepackagename=sourcepackagename, name=name, |
519 | + builder_recipe=builder_recipe) |
520 | + |
521 | + def test_creation(self): |
522 | + # The metadata supplied when a SourcePackageRecipe is created is |
523 | + # present on the new object. |
524 | + registrant = self.factory.makePerson() |
525 | + owner = self.factory.makeTeam(owner=registrant) |
526 | + distroseries = self.factory.makeDistroSeries() |
527 | + sourcepackagename = self.factory.makeSourcePackageName() |
528 | + name = self.factory.getUniqueString(u'recipe-name') |
529 | + builder_recipe = self.makeBuilderRecipe() |
530 | + recipe = getUtility(ISourcePackageRecipeSource).new( |
531 | + registrant=registrant, owner=owner, distroseries=distroseries, |
532 | + sourcepackagename=sourcepackagename, name=name, |
533 | + builder_recipe=builder_recipe) |
534 | + self.assertEquals( |
535 | + (registrant, owner, distroseries, sourcepackagename, name), |
536 | + (recipe.registrant, recipe.owner, recipe.distroseries, |
537 | + recipe.sourcepackagename, recipe.name)) |
538 | + |
539 | + def test_source_implements_interface(self): |
540 | + # The SourcePackageRecipe class implements ISourcePackageRecipeSource. |
541 | + self.assertProvides( |
542 | + getUtility(ISourcePackageRecipeSource), |
543 | + ISourcePackageRecipeSource) |
544 | + |
545 | + def test_recipe_implements_interface(self): |
546 | + # SourcePackageRecipe objects implement ISourcePackageRecipe. |
547 | + recipe = self.makeSourcePackageRecipeFromBuilderRecipe( |
548 | + self.makeBuilderRecipe()) |
549 | + self.assertProvides(recipe, ISourcePackageRecipe) |
550 | + |
551 | + def test_branch_links_created(self): |
552 | + # When a recipe is created, we can query it for links to the branch |
553 | + # it references. |
554 | + branch = self.factory.makeAnyBranch() |
555 | + builder_recipe = self.makeBuilderRecipe(branch) |
556 | + sp_recipe = self.makeSourcePackageRecipeFromBuilderRecipe( |
557 | + builder_recipe) |
558 | + self.assertEquals([branch], list(sp_recipe.getReferencedBranches())) |
559 | + |
560 | + def test_multiple_branch_links_created(self): |
561 | + # If a recipe links to more than one branch, getReferencedBranches() |
562 | + # returns all of them. |
563 | + branch1 = self.factory.makeAnyBranch() |
564 | + branch2 = self.factory.makeAnyBranch() |
565 | + builder_recipe = self.makeBuilderRecipe(branch1, branch2) |
566 | + sp_recipe = self.makeSourcePackageRecipeFromBuilderRecipe( |
567 | + builder_recipe) |
568 | + self.assertEquals( |
569 | + sorted([branch1, branch2]), |
570 | + sorted(sp_recipe.getReferencedBranches())) |
571 | + |
572 | + def test_random_user_cant_edit(self): |
573 | + # An arbitrary user can't set attributes. |
574 | + branch1 = self.factory.makeAnyBranch() |
575 | + builder_recipe1 = self.makeBuilderRecipe(branch1) |
576 | + sp_recipe = self.makeSourcePackageRecipeFromBuilderRecipe( |
577 | + builder_recipe1) |
578 | + branch2 = self.factory.makeAnyBranch() |
579 | + builder_recipe2 = self.makeBuilderRecipe(branch2) |
580 | + login_person(self.factory.makePerson()) |
581 | + self.assertRaises( |
582 | + Unauthorized, setattr, sp_recipe, 'builder_recipe', |
583 | + builder_recipe2) |
584 | + |
585 | + def test_set_recipe_text_resets_branch_references(self): |
586 | + # When the recipe_text is replaced, getReferencedBranches returns |
587 | + # (only) the branches referenced by the new recipe. |
588 | + branch1 = self.factory.makeAnyBranch() |
589 | + builder_recipe1 = self.makeBuilderRecipe(branch1) |
590 | + sp_recipe = self.makeSourcePackageRecipeFromBuilderRecipe( |
591 | + builder_recipe1) |
592 | + branch2 = self.factory.makeAnyBranch() |
593 | + builder_recipe2 = self.makeBuilderRecipe(branch2) |
594 | + login_person(sp_recipe.owner.teamowner) |
595 | + #import pdb; pdb.set_trace() |
596 | + sp_recipe.builder_recipe = builder_recipe2 |
597 | + self.assertEquals([branch2], list(sp_recipe.getReferencedBranches())) |
598 | + |
599 | + def test_rejects_run_command(self): |
600 | + recipe_text = '''\ |
601 | + # bzr-builder format 0.2 deb-version 0.1-{revno} |
602 | + %(base)s |
603 | + run touch test |
604 | + ''' % dict(base=self.factory.makeAnyBranch().bzr_identity) |
605 | + parser = RecipeParser(textwrap.dedent(recipe_text)) |
606 | + builder_recipe = parser.parse() |
607 | + self.assertRaises( |
608 | + ForbiddenInstruction, |
609 | + self.makeSourcePackageRecipeFromBuilderRecipe, builder_recipe) |
610 | + |
611 | + def test_run_rejected_without_mangling_recipe(self): |
612 | + branch1 = self.factory.makeAnyBranch() |
613 | + builder_recipe1 = self.makeBuilderRecipe(branch1) |
614 | + sp_recipe = self.makeSourcePackageRecipeFromBuilderRecipe( |
615 | + builder_recipe1) |
616 | + recipe_text = '''\ |
617 | + # bzr-builder format 0.2 deb-version 0.1-{revno} |
618 | + %(base)s |
619 | + run touch test |
620 | + ''' % dict(base=self.factory.makeAnyBranch().bzr_identity) |
621 | + parser = RecipeParser(textwrap.dedent(recipe_text)) |
622 | + builder_recipe2 = parser.parse() |
623 | + login_person(sp_recipe.owner.teamowner) |
624 | + self.assertRaises( |
625 | + ForbiddenInstruction, setattr, sp_recipe, 'builder_recipe', |
626 | + builder_recipe2) |
627 | + self.assertEquals([branch1], list(sp_recipe.getReferencedBranches())) |
628 | + |
629 | + def test_reject_newer_formats(self): |
630 | + builder_recipe = self.makeBuilderRecipe() |
631 | + builder_recipe.format = 0.3 |
632 | + self.assertRaises( |
633 | + TooNewRecipeFormat, |
634 | + self.makeSourcePackageRecipeFromBuilderRecipe, builder_recipe) |
635 | + |
636 | +class TestRecipeBranchRoundTripping(TestCaseWithFactory): |
637 | + |
638 | + layer = DatabaseFunctionalLayer |
639 | + |
640 | + def setUp(self): |
641 | + super(TestRecipeBranchRoundTripping, self).setUp() |
642 | + self.base_branch = self.factory.makeAnyBranch() |
643 | + self.nested_branch = self.factory.makeAnyBranch() |
644 | + self.merged_branch = self.factory.makeAnyBranch() |
645 | + self.branch_identities = { |
646 | + 'base': self.base_branch.bzr_identity, |
647 | + 'nested': self.nested_branch.bzr_identity, |
648 | + 'merged': self.merged_branch.bzr_identity, |
649 | + } |
650 | + |
651 | + def get_recipe(self, recipe_text): |
652 | + builder_recipe = RecipeParser(textwrap.dedent(recipe_text)).parse() |
653 | + registrant = self.factory.makePerson() |
654 | + owner = self.factory.makeTeam(owner=registrant) |
655 | + distroseries = self.factory.makeDistroSeries() |
656 | + sourcepackagename = self.factory.makeSourcePackageName() |
657 | + name = self.factory.getUniqueString(u'recipe-name') |
658 | + recipe = getUtility(ISourcePackageRecipeSource).new( |
659 | + registrant=registrant, owner=owner, distroseries=distroseries, |
660 | + sourcepackagename=sourcepackagename, name=name, |
661 | + builder_recipe=builder_recipe) |
662 | + return recipe.builder_recipe |
663 | + |
664 | + def check_base_recipe_branch(self, branch, url, revspec=None, |
665 | + num_child_branches=0, revid=None, deb_version=None): |
666 | + self.check_recipe_branch(branch, None, url, revspec=revspec, |
667 | + num_child_branches=num_child_branches, revid=revid) |
668 | + self.assertEqual(deb_version, branch.deb_version) |
669 | + |
670 | + def check_recipe_branch(self, branch, name, url, revspec=None, |
671 | + num_child_branches=0, revid=None): |
672 | + self.assertEqual(name, branch.name) |
673 | + self.assertEqual(url, branch.url) |
674 | + self.assertEqual(revspec, branch.revspec) |
675 | + self.assertEqual(revid, branch.revid) |
676 | + self.assertEqual(num_child_branches, len(branch.child_branches)) |
677 | + |
678 | + def test_builds_simplest_recipe(self): |
679 | + recipe_text = '''\ |
680 | + # bzr-builder format 0.2 deb-version 0.1-{revno} |
681 | + %(base)s |
682 | + ''' % self.branch_identities |
683 | + base_branch = self.get_recipe(recipe_text) |
684 | + self.check_base_recipe_branch( |
685 | + base_branch, self.base_branch.bzr_identity, |
686 | + deb_version='0.1-{revno}') |
687 | + |
688 | + def test_builds_recipe_with_merge(self): |
689 | + recipe_text = '''\ |
690 | + # bzr-builder format 0.2 deb-version 0.1-{revno} |
691 | + %(base)s |
692 | + merge bar %(merged)s |
693 | + ''' % self.branch_identities |
694 | + base_branch = self.get_recipe(recipe_text) |
695 | + self.check_base_recipe_branch( |
696 | + base_branch, self.base_branch.bzr_identity, num_child_branches=1, |
697 | + deb_version='0.1-{revno}') |
698 | + child_branch, location = base_branch.child_branches[0].as_tuple() |
699 | + self.assertEqual(None, location) |
700 | + self.check_recipe_branch( |
701 | + child_branch, "bar", self.merged_branch.bzr_identity) |
702 | + |
703 | + def test_builds_recipe_with_nest(self): |
704 | + recipe_text = '''\ |
705 | + # bzr-builder format 0.2 deb-version 0.1-{revno} |
706 | + %(base)s |
707 | + nest bar %(nested)s baz |
708 | + ''' % self.branch_identities |
709 | + base_branch = self.get_recipe(recipe_text) |
710 | + self.check_base_recipe_branch( |
711 | + base_branch, self.base_branch.bzr_identity, num_child_branches=1, |
712 | + deb_version='0.1-{revno}') |
713 | + child_branch, location = base_branch.child_branches[0].as_tuple() |
714 | + self.assertEqual("baz", location) |
715 | + self.check_recipe_branch( |
716 | + child_branch, "bar", self.nested_branch.bzr_identity) |
717 | + |
718 | + def test_builds_recipe_with_nest_then_merge(self): |
719 | + recipe_text = '''\ |
720 | + # bzr-builder format 0.2 deb-version 0.1-{revno} |
721 | + %(base)s |
722 | + nest bar %(nested)s baz |
723 | + merge zam %(merged)s |
724 | + ''' % self.branch_identities |
725 | + base_branch = self.get_recipe(recipe_text) |
726 | + self.check_base_recipe_branch( |
727 | + base_branch, self.base_branch.bzr_identity, num_child_branches=2, |
728 | + deb_version='0.1-{revno}') |
729 | + child_branch, location = base_branch.child_branches[0].as_tuple() |
730 | + self.assertEqual("baz", location) |
731 | + self.check_recipe_branch( |
732 | + child_branch, "bar", self.nested_branch.bzr_identity) |
733 | + child_branch, location = base_branch.child_branches[1].as_tuple() |
734 | + self.assertEqual(None, location) |
735 | + self.check_recipe_branch( |
736 | + child_branch, "zam", self.merged_branch.bzr_identity) |
737 | + |
738 | + def test_builds_recipe_with_merge_then_nest(self): |
739 | + recipe_text = '''\ |
740 | + # bzr-builder format 0.2 deb-version 0.1-{revno} |
741 | + %(base)s |
742 | + merge zam %(merged)s |
743 | + nest bar %(nested)s baz |
744 | + ''' % self.branch_identities |
745 | + base_branch = self.get_recipe(recipe_text) |
746 | + self.check_base_recipe_branch( |
747 | + base_branch, self.base_branch.bzr_identity, num_child_branches=2, |
748 | + deb_version='0.1-{revno}') |
749 | + child_branch, location = base_branch.child_branches[0].as_tuple() |
750 | + self.assertEqual(None, location) |
751 | + self.check_recipe_branch( |
752 | + child_branch, "zam", self.merged_branch.bzr_identity) |
753 | + child_branch, location = base_branch.child_branches[1].as_tuple() |
754 | + self.assertEqual("baz", location) |
755 | + self.check_recipe_branch( |
756 | + child_branch, "bar", self.nested_branch.bzr_identity) |
757 | + |
758 | + def test_builds_a_merge_in_to_a_nest(self): |
759 | + recipe_text = '''\ |
760 | + # bzr-builder format 0.2 deb-version 0.1-{revno} |
761 | + %(base)s |
762 | + nest bar %(nested)s baz |
763 | + merge zam %(merged)s |
764 | + ''' % self.branch_identities |
765 | + base_branch = self.get_recipe(recipe_text) |
766 | + self.check_base_recipe_branch( |
767 | + base_branch, self.base_branch.bzr_identity, num_child_branches=1, |
768 | + deb_version='0.1-{revno}') |
769 | + child_branch, location = base_branch.child_branches[0].as_tuple() |
770 | + self.assertEqual("baz", location) |
771 | + self.check_recipe_branch( |
772 | + child_branch, "bar", self.nested_branch.bzr_identity, |
773 | + num_child_branches=1) |
774 | + child_branch, location = child_branch.child_branches[0].as_tuple() |
775 | + self.assertEqual(None, location) |
776 | + self.check_recipe_branch( |
777 | + child_branch, "zam", self.merged_branch.bzr_identity) |
778 | + |
779 | + def tests_builds_nest_into_a_nest(self): |
780 | + nested2 = self.factory.makeAnyBranch() |
781 | + self.branch_identities['nested2'] = nested2.bzr_identity |
782 | + recipe_text = '''\ |
783 | + # bzr-builder format 0.2 deb-version 0.1-{revno} |
784 | + %(base)s |
785 | + nest bar %(nested)s baz |
786 | + nest zam %(nested2)s zoo |
787 | + ''' % self.branch_identities |
788 | + base_branch = self.get_recipe(recipe_text) |
789 | + self.check_base_recipe_branch( |
790 | + base_branch, self.base_branch.bzr_identity, num_child_branches=1, |
791 | + deb_version='0.1-{revno}') |
792 | + child_branch, location = base_branch.child_branches[0].as_tuple() |
793 | + self.assertEqual("baz", location) |
794 | + self.check_recipe_branch( |
795 | + child_branch, "bar", self.nested_branch.bzr_identity, |
796 | + num_child_branches=1) |
797 | + child_branch, location = child_branch.child_branches[0].as_tuple() |
798 | + self.assertEqual("zoo", location) |
799 | + self.check_recipe_branch(child_branch, "zam", nested2.bzr_identity) |
800 | + |
801 | + def tests_builds_recipe_with_revspecs(self): |
802 | + recipe_text = '''\ |
803 | + # bzr-builder format 0.2 deb-version 0.1-{revno} |
804 | + %(base)s revid:a |
805 | + nest bar %(nested)s baz tag:b |
806 | + merge zam %(merged)s 2 |
807 | + ''' % self.branch_identities |
808 | + base_branch = self.get_recipe(recipe_text) |
809 | + self.check_base_recipe_branch( |
810 | + base_branch, self.base_branch.bzr_identity, num_child_branches=2, |
811 | + revspec="revid:a", deb_version='0.1-{revno}') |
812 | + instruction = base_branch.child_branches[0] |
813 | + child_branch = instruction.recipe_branch |
814 | + location = instruction.nest_path |
815 | + self.assertEqual("baz", location) |
816 | + self.check_recipe_branch( |
817 | + child_branch, "bar", self.nested_branch.bzr_identity, |
818 | + revspec="tag:b") |
819 | + child_branch, location = base_branch.child_branches[1].as_tuple() |
820 | + self.assertEqual(None, location) |
821 | + self.check_recipe_branch( |
822 | + child_branch, "zam", self.merged_branch.bzr_identity, revspec="2") |
823 | + |
824 | + |
825 | + |
826 | +def test_suite(): |
827 | + return unittest.TestLoader().loadTestsFromName(__name__) |
828 | + |
829 | |
830 | === modified file 'utilities/sourcedeps.conf' |
831 | --- utilities/sourcedeps.conf 2010-01-13 05:18:26 +0000 |
832 | +++ utilities/sourcedeps.conf 2010-01-13 20:46:22 +0000 |
833 | @@ -1,3 +1,4 @@ |
834 | +bzr-builder lp:~launchpad-pqm/bzr-builder/trunk;revno=62 |
835 | bzr-git lp:~launchpad-pqm/bzr-git/devel;revno=248 |
836 | bzr-hg lp:~launchpad-pqm/bzr-hg/devel;revno=281 |
837 | bzr-loom lp:~launchpad-pqm/bzr-loom/trunk;revno=47 |
Hi there,
This branch adds the first content class for the sourcepackagerecipe stuff -- the SourcePacakgeRecipe class.
It doesn't do anything fancy at all wrt storing the recipe -- that can come later.
This can't be landed until a LOSA creates the bzr-builder sourcecode branch -- "bzr co lp:bzr-builder sourcecode/" until then.
Cheers,
mwh