Merge lp:~mwhudson/launchpad/recipe-model-code into lp:launchpad/db-devel

Proposed by Michael Hudson-Doyle
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
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

To post a comment you must log in.
Revision history for this message
Michael Hudson-Doyle (mwhudson) wrote :

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

Revision history for this message
Jonathan Lange (jml) wrote :

This is going to have to change as a result of the schema changes, I think.

review: Needs Resubmitting
Revision history for this message
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 :-)

Revision history for this message
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

Revision history for this message
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, ForbiddenInstruction's docstring needs to be updated to swap "unsupported" for "forbidden." Also, there are a few tests in TestSourcePackageRecipe, and all the methods of TestRecipeBranchRoundTripping that are missing comments/docstrings/some sort of "this is what this test is doing."

Cheers,
Paul

review: Approve
Revision history for this message
Paul Hummer (rockstar) wrote :

_SourcePackageRecipeData has two definitions of sourcepackage_recipe_build, one of which is commented out. Other than that, still good.

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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

Subscribers

People subscribed via source and target branches

to status/vote changes: