Merge lp:~jtv/launchpad/bug-537866 into lp:launchpad

Proposed by Jeroen T. Vermeulen
Status: Merged
Approved by: Jeroen T. Vermeulen
Approved revision: no longer in the source branch.
Merged at revision: not available
Proposed branch: lp:~jtv/launchpad/bug-537866
Merge into: lp:launchpad
Diff against target: 695 lines (+372/-77)
9 files modified
lib/lp/buildmaster/interfaces/buildfarmjobbehavior.py (+1/-1)
lib/lp/buildmaster/model/buildfarmjobbehavior.py (+94/-10)
lib/lp/buildmaster/tests/test_buildfarmjobbehavior.py (+80/-8)
lib/lp/code/model/recipebuilder.py (+11/-2)
lib/lp/soyuz/doc/buildd-slavescanner.txt (+4/-3)
lib/lp/testing/factory.py (+87/-40)
lib/lp/translations/doc/translationtemplatesbuildbehavior.txt (+41/-12)
lib/lp/translations/model/translationtemplatesbuildbehavior.py (+17/-1)
lib/lp/translations/tests/test_translationtemplatesbuildbehavior.py (+37/-0)
To merge this branch: bzr merge lp:~jtv/launchpad/bug-537866
Reviewer Review Type Date Requested Status
Guilherme Salgado (community) Approve
Review via email: mp+21238@code.launchpad.net

Commit message

Fix branch_url passed to the slave; fix slave build id verification for TranslationTemplatesBuildJob.

Description of the change

= Bug 537866 =

Two bugs with generating translation templates in the build farm based on bzr branches are fixed here:

 * Despite the TranslationTemplatesBuildJob having the right XMLRPC parameters for the slave in its metadata (as serialized in json_data), there was still a bit of old code that overrode it in TranslationTemplatesBuildBehavior.dispatchBuildToSlave—and with the wrong branch URL. An existing test proved easily extended to cover this.

 * BuildFarmJobBehavior.verifySlaveBuildID did not accept the format of TranslationTemplatesBuildJob names. I had to override it.

The first of these is a simple matter of using the stored parameters instead of recomputing them. Technically it may be nicer to compute the branch URL in TranslationTemplatesBuildBehavior.dispatchBuildToSlave, i.e. at the last moment. But on the other hand TranslationTemplatesBuildJob inherits the handling of a parameters dict from BranchJob anyway, and seeing the URL there makes these objects a lot easier to recognize and deal with during debugging. Something doesn't work? Snip the URL and try it out in a shell. At the stage we're in, that is invaluable. It may remain so if we have to debug problems with this highly complex infrastructure in the future.

The second happens because the base BuildFarmJobBehavior's verifySlaveBuildID assumed a slave build id composed of a build id and a buildqueue id, separated by a dash. TranslationTemplatesBuildJob does not have a build, so it substitutes the branch name instead. (One test runs a nasty case through verifySlaveBuildID where the branch name has dashes wherever a branch name can have them).

In order to avoid code duplication in doing so, I extracted two helpers from the base verifySlaveBuildID: one verifies the Build id contained in the name, and the other the BuildQueue id. The main method just extracts those two parts, calls the helpers, and finally checks that the objects are properly matched. The two helpers ended up so similar that I couldn't help creating yet a third helper that implements either, depending on some customizing parameters.

Implementing the tests required me to produce a realistic Build object. This hurts, but a _relatively_ easy way to do it was to create a SourcePackageRelease first. The factory did not have a makeSPR, but it had another method that created an SPR in the process of creating something else, so I extracted makeSPR from that.

To test:
{{{
./bin/test -vv -t test_translationtemplatesbuildbehavior -t buildfarmjobbehavior
}}}

For Q/A, go through the instructions at https://dev.launchpad.net/Translations/BuildTemplatesOnLocalBuildFarm (which is a bit of work, but at least it's documented in detail).

No lint.

Jeroen

To post a comment you must log in.
Revision history for this message
Guilherme Salgado (salgado) wrote :
Download full text (8.6 KiB)

Hi Jeroen,

I found a few issues that I'd like you to address before I do a proper
review of this branch.

 review needsfixing

> === modified file 'lib/lp/buildmaster/interfaces/buildfarmjobbehavior.py'
> --- lib/lp/buildmaster/interfaces/buildfarmjobbehavior.py 2010-01-21 05:03:16 +0000
> +++ lib/lp/buildmaster/interfaces/buildfarmjobbehavior.py 2010-03-12 13:56:29 +0000
> @@ -1,4 +1,4 @@
> -# Copyright 2009 Canonical Ltd. This software is licensed under the
> +# Copyright 2009-2010 Canonical Ltd. This software is licensed under the
> # GNU Affero General Public License version 3 (see the file LICENSE).
>
> # pylint: disable-msg=E0211,E0213
> @@ -57,6 +57,28 @@
> of IBuilder.slaveStatus().
> """
>
> + def getVerifiedBuild(raw_id):
> + """Verify the `Build` id component of a slave build id.

I find it a bit odd that the method's name implies it's used to get
something but its docstring says it just verifies. How about

    """Return the `Build` object with the given id.

    ...
    """

I'd also like the docstring to state what sort of verification is done.

Oh, and does this need to be part of the public interface? ISTM that
it's only used internally, so it could be left out of the interface, no?

> +
> + By default, a `BuildFarmJob` has an identifying name of the form
> + "b-q", where b is the id of its `Build` and q is the id of its
> + `BuildQueue` record.
> +
> + Use `getVerifiedBuild` to verify the "b" part, and retrieve the
> + associated `Build`.
> + """
> +
> + def getVerifiedBuildQueue(raw_id):
> + """Verify the `BuildQueue` id component of a slave build id.

Same here.

> +
> + By default, a `BuildFarmJob` has an identifying name of the form
> + "b-q", where b is the id of its `Build` and q is the id of its
> + `BuildQueue` record.
> +
> + Use `getVerifiedBuildQueue` to verify the "q" part, and retrieve
> + the associated `BuildQueue` object.
> + """
> +
> def verifySlaveBuildID(slave_build_id):
> """Verify that a slave's build ID shows no signs of corruption.
>
>
> === modified file 'lib/lp/buildmaster/model/buildfarmjobbehavior.py'
> --- lib/lp/buildmaster/model/buildfarmjobbehavior.py 2010-03-05 13:52:32 +0000
> +++ lib/lp/buildmaster/model/buildfarmjobbehavior.py 2010-03-12 13:56:29 +0000
> @@ -1,4 +1,4 @@
> -# Copyright 2009 Canonical Ltd. This software is licensed under the
> +# Copyright 2009-2010 Canonical Ltd. This software is licensed under the
> # GNU Affero General Public License version 3 (see the file LICENSE).
>
> # pylint: disable-msg=E0211,E0213
> @@ -17,17 +17,22 @@
> import xmlrpclib
>
> from sqlobject import SQLObjectNotFound
> +
> from zope.component import getUtility
> from zope.interface import implements
> -from zope.security.proxy import removeSecurityProxy
> +from zope.security.proxy import removeSecurityProxy, isinstance as zisinstance
>
> from canonical import encoding
> from canonical.librarian.interfaces import ILibrarianClient
> +
> +from canonical.launchpad.webapp.interfaces import NotFoundError
> from lp.buildmaster.interfaces.bui...

Read more...

review: Needs Fixing
Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :

This is scary. Some arbitrary characters seem to have changed in the code after I last ran "make lint" and the tests. The duplicate test method names (where the word "Queue" was missing in two places) may have been an omission on my part as I continued adding tests at a late stage, though I think these were from an earlier state that I also ran through lint. The "utem" and missing spaces definitely passed lint since I last worked on them.

I hope it's simply because of the mouse cursor moving and activating a different window while I'm typing (as it likes to do for some reason—I know there's a setting for that but I guess my touchpad driver doesn't obey it) but in the past few hours I've also had unexpected characters appearing when copy/pasting. I'll reboot as soon as possible.

Anyway, I've removed the two helper methods from the interface and updated the doc strings.

Revision history for this message
Guilherme Salgado (salgado) wrote :
Download full text (14.6 KiB)

I was doing a second pass on this but then you told me you've got some
more things you need to change here, so I'm sending what I have so far.

> === modified file 'lib/lp/buildmaster/tests/test_buildfarmjobbehavior.py'
> --- lib/lp/buildmaster/tests/test_buildfarmjobbehavior.py 2010-03-06 04:57:40 +0000
> +++ lib/lp/buildmaster/tests/test_buildfarmjobbehavior.py 2010-03-12 16:54:28 +0000
> @@ -3,10 +3,18 @@
>
> """Unit tests for BuildFarmJobBehaviorBase."""
>
> -from unittest import TestCase, TestLoader
> +from unittest import TestLoader
> +
> +from zope.component import getUtility
> +
> +from canonical.testing.layers import ZopelessDatabaseLayer
> +from lp.testing import TestCaseWithFactory
>
> from lp.buildmaster.interfaces.buildbase import BuildStatus
> +from lp.buildmaster.interfaces.builder import CorruptBuildID
> from lp.buildmaster.model.buildfarmjobbehavior import BuildFarmJobBehaviorBase
> +from lp.registry.interfaces.pocket import PackagePublishingPocket
> +from lp.soyuz.interfaces.processor import IProcessorFamilySet
>
>
> class FakeBuildFarmJob:
> @@ -14,27 +22,90 @@
> pass
>
>
> -class TestBuildFarmJobBehaviorBase(TestCase):
> +class TestBuildFarmJobBehaviorBase(TestCaseWithFactory):
> """Test very small, basic bits of BuildFarmJobBehaviorBase."""
>
> - def setUp(self):
> - self.behavior = BuildFarmJobBehaviorBase(FakeBuildFarmJob())
> + layer = ZopelessDatabaseLayer
> +
> + def _makeBehavior(self, buildfarmjob=None):
> + """Create a `BuildFarmJobBehaviorBase`."""
> + if buildfarmjob is None:
> + buildfarmjob = FakeBuildFarmJob()
> + return BuildFarmJobBehaviorBase(buildfarmjob)
> +
> + def _makeBuild(self):
> + """Create a `Build` object."""
> + x86 = getUtility(IProcessorFamilySet).getByName('x86')
> + distroarchseries = self.factory.makeDistroArchSeries(
> + architecturetag='x86', processorfamily=x86)
> + distroseries = distroarchseries.distroseries
> + archive = self.factory.makeArchive(
> + distribution=distroseries.distribution)
> + pocket = PackagePublishingPocket.RELEASE
> + spr = self.factory.makeSPR(distroseries=distroseries, archive=archive)
> +
> + return spr.createBuild(
> + distroarchseries=distroarchseries, pocket=pocket, archive=archive)
> +
> + def _makeBuildQueue(self):
> + """Create a `BuildQueue` object."""
> + return self.factory.makeSourcePackageRecipeBuildJob()
>
> def test_extractBuildStatus_baseline(self):
> # extractBuildStatus picks the name of the build status out of a
> # dict describing the slave's status.
> slave_status = {'build_status': 'BuildStatus.BUILDING'}
> + behavior = self._makeBehavior()
> self.assertEqual(
> BuildStatus.BUILDING.name,
> - self.behavior.extractBuildStatus(slave_status))
> + behavior.extractBuildStatus(slave_status))
>
> def test_extractBuildStatus_malformed(self):
> # extractBuildStatus errors out when the status string is not
> # of the form it expects.
> slave_status = ...

Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :
Download full text (9.1 KiB)

> I was doing a second pass on this but then you told me you've got some
> more things you need to change here, so I'm sending what I have so far.

That's right. The latest change was in the translationtemplatesbuildbehavior doctest and, with a bit of extra work, allowed me to retire an XXX.

> > === modified file 'lib/lp/testing/factory.py'
> > --- lib/lp/testing/factory.py 2010-03-08 12:35:15 +0000
> > +++ lib/lp/testing/factory.py 2010-03-12 16:54:28 +0000
> > @@ -2007,46 +2007,35 @@
> > def getAnySourcePackageUrgency(self):
> > return SourcePackageUrgency.MEDIUM
> >
> > - def makeSourcePackagePublishingHistory(self, sourcepackagename=None,
> > - distroseries=None,
> maintainer=None,
> > - creator=None, component=None,
> > - section=None, urgency=None,
> > - version=None, archive=None,
> > - builddepends=None,
> > - builddependsindep=None,
> > - build_conflicts=None,
> > - build_conflicts_indep=None,
> > - architecturehintlist='all',
> > - dateremoved=None,
> > - date_uploaded=UTC_NOW,
> > - pocket=None,
> > - status=None,
> > - scheduleddeletiondate=None,
> > - dsc_standards_version='3.6.2',
> > - dsc_format='1.0',
> > - dsc_binaries='foo-bin',
> > - ):
> > - if sourcepackagename is None:
> > - sourcepackagename = self.makeSourcePackageName()
> > - spn = sourcepackagename
> > + def makeSPR(self, archive=None, sourcepackagename=None,
> distroseries=None,
>
> Why not makeSourcePackageRelease()? That seems to be consistent with the
> existing ones and avoids the abbreviation, which are not good anyway.

Sure. It's only 5 lines extra; I thought it would be worse. I admit it: I abbreviated to avoid having to line up the parameters on all the following lines behind the opening parenthesis.

> > + if sourcepackagename is None:
> > + sourcepackagename = self.makeSourcePackageName()
> > + spn = sourcepackagename
>
> Would you mind dropping the line above and using 'sourcepackagename' in the
> single place that uses it below?

Not at all. I was just copying over the original code, but now that it's broken down into more manageable pieces, might as well clean up a bit more.

> > @@ -2064,12 +2053,13 @@
> > if creator is None:
> > creator = self.makePerson()
> >
> > + if dscsigningkey is None:
> > + gpg_key = self.makeGPGKey(creator)
>
> This is broken now -- gpg_key is no longer used, so y...

Read more...

Revision history for this message
Guilherme Salgado (salgado) wrote :
Download full text (5.8 KiB)

On Fri, 2010-03-12 at 20:24 +0000, Jeroen T. Vermeulen wrote:
> > > + def makeSourcePackagePublishingHistory(self, sourcepackagename=None,
> > > + distroseries=None,
> > maintainer=None,
> > > + creator=None, component=None,
> > > + section=None, urgency=None,
> > > + version=None, archive=None,
> > > + builddepends=None,
> > > + builddependsindep=None,
> > > + build_conflicts=None,
> > > + build_conflicts_indep=None,
> > > + architecturehintlist='all',
> > > + dateremoved=None,
> > > + date_uploaded=UTC_NOW,
> > > + pocket=None,
> > > + status=None,
> > > + scheduleddeletiondate=None,
> > > + dsc_standards_version='3.6.2',
> > > + dsc_format='1.0',
> > > + dsc_binaries='foo-bin',
> > > + ):
> > > + if pocket is None:
> > > + pocket = self.getAnyPocket()
> > > +
> > > + if status is None:
> > > + status = PackagePublishingStatus.PENDING
> > > +
> > > + spr = self.makeSPR(
> > > + archive=archive,
> > > + sourcepackagename=sourcepackagename,
> > > + distroseries=distroseries,
> > > + maintainer=maintainer,
> > > + creator=creator, component=component,
> > > + section=section,
> > > + urgency=urgency,
> > > + version=version,
> > > + builddepends=builddepends,
> > > + builddependsindep=builddependsindep,
> > > + build_conflicts=build_conflicts,
> > > + build_conflicts_indep=build_conflicts_indep,
> > > + architecturehintlist=architecturehintlist,
> > > + dsc_standards_version=dsc_standards_version,
> > > + dsc_format=dsc_format,
> > > + dsc_binaries=dsc_binaries,
> > > + date_uploaded=date_uploaded)
> > >
> > > sspph = SourcePackagePublishingHistory(
> > > - distroseries=distroseries,
> > > + distroseries=spr.distroseries,
> >
> > Is this change needed? Couldn't you keep using the distroseries passed as
> > argument?
>
> Again, you're right. I just need to create a distroseries here if I don't already have one, so it adds a tiny bit of duplication.
>

Oh, it wasn't clear to me that you were doing that because makeSPR would
create the distro when None was given to it. If there was a comment I
wouldn't have complained here.

Anyway, I'm happy if you want to leave this tiny bit of duplication but
I'd also be happy with what you had before, a...

Read more...

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/buildmaster/interfaces/buildfarmjobbehavior.py'
2--- lib/lp/buildmaster/interfaces/buildfarmjobbehavior.py 2010-01-21 05:03:16 +0000
3+++ lib/lp/buildmaster/interfaces/buildfarmjobbehavior.py 2010-03-13 10:43:28 +0000
4@@ -1,4 +1,4 @@
5-# Copyright 2009 Canonical Ltd. This software is licensed under the
6+# Copyright 2009-2010 Canonical Ltd. This software is licensed under the
7 # GNU Affero General Public License version 3 (see the file LICENSE).
8
9 # pylint: disable-msg=E0211,E0213
10
11=== modified file 'lib/lp/buildmaster/model/buildfarmjobbehavior.py'
12--- lib/lp/buildmaster/model/buildfarmjobbehavior.py 2010-03-05 13:52:32 +0000
13+++ lib/lp/buildmaster/model/buildfarmjobbehavior.py 2010-03-13 10:43:28 +0000
14@@ -1,4 +1,4 @@
15-# Copyright 2009 Canonical Ltd. This software is licensed under the
16+# Copyright 2009-2010 Canonical Ltd. This software is licensed under the
17 # GNU Affero General Public License version 3 (see the file LICENSE).
18
19 # pylint: disable-msg=E0211,E0213
20@@ -17,17 +17,22 @@
21 import xmlrpclib
22
23 from sqlobject import SQLObjectNotFound
24+
25 from zope.component import getUtility
26 from zope.interface import implements
27-from zope.security.proxy import removeSecurityProxy
28+from zope.security.proxy import removeSecurityProxy, isinstance as zisinstance
29
30 from canonical import encoding
31 from canonical.librarian.interfaces import ILibrarianClient
32+
33+from canonical.launchpad.webapp.interfaces import NotFoundError
34 from lp.buildmaster.interfaces.builder import CorruptBuildID
35 from lp.buildmaster.interfaces.buildfarmjobbehavior import (
36 BuildBehaviorMismatch, IBuildFarmJobBehavior)
37 from lp.buildmaster.interfaces.buildqueue import IBuildQueueSet
38+from lp.buildmaster.model.buildqueue import BuildQueue
39 from lp.services.job.interfaces.job import JobStatus
40+from lp.soyuz.interfaces.build import IBuildSet
41
42
43 class BuildFarmJobBehaviorBase:
44@@ -59,22 +64,101 @@
45 The default behavior is that we don't add any extra values."""
46 return {}
47
48+ def _helpVerifyBuildIDComponent(self, raw_id, item_type, finder):
49+ """Helper for verifying parts of a `BuildFarmJob` name.
50+
51+ Different `IBuildFarmJob` implementations can have different
52+ ways of constructing their identifying names. The names are
53+ produced by `IBuildFarmJob.getName` and verified by
54+ `IBuildFarmJobBehavior.verifySlaveBuildID`.
55+
56+ This little helper makes it easier to verify an object id
57+ embedded in that name, check that it's a valid number, and
58+ retrieve the associated database object.
59+
60+ :param raw_id: An unverified id string as extracted from the
61+ build name. The method will verify that it is a number, and
62+ try to retrieve the associated object.
63+ :param item_type: The type of object this id represents. Should
64+ be a class.
65+ :param finder: A function that, given an integral id, finds the
66+ associated object of type `item_type`.
67+ :raise CorruptBuildID: If `raw_id` is malformed in some way or
68+ the associated `item_type` object is not found.
69+ :return: An object that is an instance of `item_type`.
70+ """
71+ type_name = item_type.__name__
72+ try:
73+ numeric_id = int(raw_id)
74+ except ValueError:
75+ raise CorruptBuildID(
76+ "%s ID is not a number: '%s'" % (type_name, raw_id))
77+
78+ try:
79+ item = finder(numeric_id)
80+ except (NotFoundError, SQLObjectNotFound), reason:
81+ raise CorruptBuildID(
82+ "%s %d is not available: %s" % (
83+ type_name, numeric_id, reason))
84+ except Exception, reason:
85+ raise CorruptBuildID(
86+ "Error while looking up %s %d: %s" % (
87+ type_name, numeric_id, reason))
88+
89+ if item is None:
90+ raise CorruptBuildID("There is no %s with id %d." % (
91+ type_name, numeric_id))
92+
93+ assert zisinstance(item, item_type), (
94+ "Looked for %s, but found %s." % (type_name, repr(item)))
95+
96+ return item
97+
98+ def getVerifiedBuild(self, raw_id):
99+ """Verify and retrieve the `Build` component of a slave build id.
100+
101+ This does part of the verification for `verifySlaveBuildID`.
102+
103+ By default, a `BuildFarmJob` has an identifying name of the form
104+ "b-q", where b is the id of its `Build` and q is the id of its
105+ `BuildQueue` record.
106+
107+ Use `getVerifiedBuild` to verify the "b" part, and retrieve the
108+ associated `Build`.
109+ """
110+ # Avoid circular import.
111+ from lp.soyuz.model.build import Build
112+
113+ return self._helpVerifyBuildIDComponent(
114+ raw_id, Build, getUtility(IBuildSet).getByBuildID)
115+
116+ def getVerifiedBuildQueue(self, raw_id):
117+ """Verify and retrieve the `BuildQueue` component of a slave build id.
118+
119+ This does part of the verification for `verifySlaveBuildID`.
120+
121+ By default, a `BuildFarmJob` has an identifying name of the form
122+ "b-q", where b is the id of its `Build` and q is the id of its
123+ `BuildQueue` record.
124+
125+ Use `getVerifiedBuildQueue` to verify the "q" part, and retrieve
126+ the associated `BuildQueue` object.
127+ """
128+ return self._helpVerifyBuildIDComponent(
129+ raw_id, BuildQueue, getUtility(IBuildQueueSet).get)
130+
131 def verifySlaveBuildID(self, slave_build_id):
132 """See `IBuildFarmJobBehavior`."""
133 # Extract information from the identifier.
134 try:
135 build_id, queue_item_id = slave_build_id.split('-')
136- build_id = int(build_id)
137- queue_item_id = int(queue_item_id)
138 except ValueError:
139 raise CorruptBuildID('Malformed build ID')
140+
141+ build = self.getVerifiedBuild(build_id)
142+ queue_item = self.getVerifiedBuildQueue(queue_item_id)
143
144- try:
145- queue_item = getUtility(IBuildQueueSet).get(queue_item_id)
146- # Check whether build and buildqueue are properly related.
147- except SQLObjectNotFound, reason:
148- raise CorruptBuildID(str(reason))
149- if queue_item.specific_job.build.id != build_id:
150+ if build != queue_item.specific_job.build:
151 raise CorruptBuildID('Job build entry mismatch')
152
153 def updateBuild(self, queueItem):
154
155=== modified file 'lib/lp/buildmaster/tests/test_buildfarmjobbehavior.py'
156--- lib/lp/buildmaster/tests/test_buildfarmjobbehavior.py 2010-03-06 04:57:40 +0000
157+++ lib/lp/buildmaster/tests/test_buildfarmjobbehavior.py 2010-03-13 10:43:28 +0000
158@@ -3,10 +3,18 @@
159
160 """Unit tests for BuildFarmJobBehaviorBase."""
161
162-from unittest import TestCase, TestLoader
163+from unittest import TestLoader
164+
165+from zope.component import getUtility
166+
167+from canonical.testing.layers import ZopelessDatabaseLayer
168+from lp.testing import TestCaseWithFactory
169
170 from lp.buildmaster.interfaces.buildbase import BuildStatus
171+from lp.buildmaster.interfaces.builder import CorruptBuildID
172 from lp.buildmaster.model.buildfarmjobbehavior import BuildFarmJobBehaviorBase
173+from lp.registry.interfaces.pocket import PackagePublishingPocket
174+from lp.soyuz.interfaces.processor import IProcessorFamilySet
175
176
177 class FakeBuildFarmJob:
178@@ -14,27 +22,91 @@
179 pass
180
181
182-class TestBuildFarmJobBehaviorBase(TestCase):
183+class TestBuildFarmJobBehaviorBase(TestCaseWithFactory):
184 """Test very small, basic bits of BuildFarmJobBehaviorBase."""
185
186- def setUp(self):
187- self.behavior = BuildFarmJobBehaviorBase(FakeBuildFarmJob())
188+ layer = ZopelessDatabaseLayer
189+
190+ def _makeBehavior(self, buildfarmjob=None):
191+ """Create a `BuildFarmJobBehaviorBase`."""
192+ if buildfarmjob is None:
193+ buildfarmjob = FakeBuildFarmJob()
194+ return BuildFarmJobBehaviorBase(buildfarmjob)
195+
196+ def _makeBuild(self):
197+ """Create a `Build` object."""
198+ x86 = getUtility(IProcessorFamilySet).getByName('x86')
199+ distroarchseries = self.factory.makeDistroArchSeries(
200+ architecturetag='x86', processorfamily=x86)
201+ distroseries = distroarchseries.distroseries
202+ archive = self.factory.makeArchive(
203+ distribution=distroseries.distribution)
204+ pocket = PackagePublishingPocket.RELEASE
205+ spr = self.factory.makeSourcePackageRelease(
206+ distroseries=distroseries, archive=archive)
207+
208+ return spr.createBuild(
209+ distroarchseries=distroarchseries, pocket=pocket, archive=archive)
210+
211+ def _makeBuildQueue(self):
212+ """Create a `BuildQueue` object."""
213+ return self.factory.makeSourcePackageRecipeBuildJob()
214
215 def test_extractBuildStatus_baseline(self):
216 # extractBuildStatus picks the name of the build status out of a
217 # dict describing the slave's status.
218 slave_status = {'build_status': 'BuildStatus.BUILDING'}
219+ behavior = self._makeBehavior()
220 self.assertEqual(
221 BuildStatus.BUILDING.name,
222- self.behavior.extractBuildStatus(slave_status))
223+ behavior.extractBuildStatus(slave_status))
224
225 def test_extractBuildStatus_malformed(self):
226 # extractBuildStatus errors out when the status string is not
227 # of the form it expects.
228 slave_status = {'build_status': 'BUILDING'}
229- self.assertRaises(
230- AssertionError,
231- self.behavior.extractBuildStatus, slave_status)
232+ behavior = self._makeBehavior()
233+ self.assertRaises(
234+ AssertionError, behavior.extractBuildStatus, slave_status)
235+
236+ def test_getVerifiedBuild_success(self):
237+ build = self._makeBuild()
238+ behavior = self._makeBehavior()
239+ raw_id = str(build.id)
240+
241+ self.assertEqual(build, behavior.getVerifiedBuild(raw_id))
242+
243+ def test_getVerifiedBuild_malformed(self):
244+ behavior = self._makeBehavior()
245+ self.assertRaises(CorruptBuildID, behavior.getVerifiedBuild, 'hi!')
246+
247+ def test_getVerifiedBuild_notfound(self):
248+ build = self._makeBuild()
249+ behavior = self._makeBehavior()
250+ nonexistent_id = str(build.id + 99)
251+
252+ self.assertRaises(
253+ CorruptBuildID, behavior.getVerifiedBuild, nonexistent_id)
254+
255+ def test_getVerifiedBuildQueue_success(self):
256+ buildqueue = self._makeBuildQueue()
257+ behavior = self._makeBehavior()
258+ raw_id = str(buildqueue.id)
259+
260+ self.assertEqual(buildqueue, behavior.getVerifiedBuildQueue(raw_id))
261+
262+ def test_getVerifiedBuildQueue_malformed(self):
263+ behavior = self._makeBehavior()
264+ self.assertRaises(
265+ CorruptBuildID, behavior.getVerifiedBuildQueue, 'bye!')
266+
267+ def test_getVerifiedBuildQueue_notfound(self):
268+ buildqueue = self._makeBuildQueue()
269+ behavior = self._makeBehavior()
270+ nonexistent_id = str(buildqueue.id + 99)
271+
272+ self.assertRaises(
273+ CorruptBuildID, behavior.getVerifiedBuildQueue, nonexistent_id)
274
275
276 def test_suite():
277
278=== modified file 'lib/lp/code/model/recipebuilder.py'
279--- lib/lp/code/model/recipebuilder.py 2010-02-05 15:06:28 +0000
280+++ lib/lp/code/model/recipebuilder.py 2010-03-13 10:43:28 +0000
281@@ -8,7 +8,7 @@
282 'RecipeBuildBehavior',
283 ]
284
285-from zope.component import adapts
286+from zope.component import adapts, getUtility
287 from zope.interface import implements
288
289 from lp.archiveuploader.permission import check_upload_to_pocket
290@@ -18,7 +18,8 @@
291 from lp.buildmaster.model.buildfarmjobbehavior import (
292 BuildFarmJobBehaviorBase)
293 from lp.code.interfaces.sourcepackagerecipebuild import (
294- ISourcePackageRecipeBuildJob)
295+ ISourcePackageRecipeBuildJob, ISourcePackageRecipeBuildSource)
296+from lp.code.model.sourcepackagerecipebuild import SourcePackageRecipeBuild
297 from lp.registry.interfaces.pocket import PackagePublishingPocket
298 from lp.soyuz.adapters.archivedependencies import (
299 get_primary_current_component, get_sources_list_for_building)
300@@ -168,3 +169,11 @@
301 extra_info['logtail'] = raw_slave_status[2]
302
303 return extra_info
304+
305+ def getVerifiedBuild(self, raw_id):
306+ """See `IBuildFarmJobBehavior`."""
307+ # This type of job has a build that is of type BuildBase but not
308+ # actually a Build.
309+ return self._helpVerifyBuildIDComponent(
310+ raw_id, SourcePackageRecipeBuild,
311+ getUtility(ISourcePackageRecipeBuildSource).getById)
312
313=== modified file 'lib/lp/soyuz/doc/buildd-slavescanner.txt'
314--- lib/lp/soyuz/doc/buildd-slavescanner.txt 2010-03-10 12:50:18 +0000
315+++ lib/lp/soyuz/doc/buildd-slavescanner.txt 2010-03-13 10:43:28 +0000
316@@ -95,15 +95,16 @@
317 >>> lostbuilding_builder = MockBuilder(
318 ... 'Lost Building Slave', LostBuildingSlave())
319 >>> buildergroup.rescueBuilderIfLost(lostbuilding_builder)
320- WARNING:root:Builder 'Lost Building Slave' rescued from
321- '1000-10000': 'Object not found'
322+ WARNING:root:Builder 'Lost Building Slave' rescued from '1000-10000':
323+ 'Build 1000 is not available: 'Object not found''
324
325 Then a lost slave in status 'WAITING':
326
327 >>> lostwaiting_builder = MockBuilder(
328 ... 'Lost Waiting Slave', LostWaitingSlave())
329 >>> buildergroup.rescueBuilderIfLost(lostwaiting_builder)
330- WARNING:root:Builder 'Lost Waiting Slave' rescued from '1000-10000': 'Object not found'
331+ WARNING:root:Builder 'Lost Waiting Slave' rescued from '1000-10000':
332+ 'Build 1000 is not available: 'Object not found''
333
334 Both got rescued, as expected.
335
336
337=== modified file 'lib/lp/testing/factory.py'
338--- lib/lp/testing/factory.py 2010-03-11 20:54:36 +0000
339+++ lib/lp/testing/factory.py 2010-03-13 10:43:28 +0000
340@@ -2008,6 +2008,81 @@
341 def getAnySourcePackageUrgency(self):
342 return SourcePackageUrgency.MEDIUM
343
344+ def makeSourcePackageRelease(self, archive=None, sourcepackagename=None,
345+ distroseries=None, maintainer=None,
346+ creator=None, component=None, section=None,
347+ urgency=None, version=None,
348+ builddepends=None, builddependsindep=None,
349+ build_conflicts=None,
350+ build_conflicts_indep=None,
351+ architecturehintlist='all',
352+ dsc_maintainer_rfc822=None,
353+ dsc_standards_version='3.6.2',
354+ dsc_format='1.0', dsc_binaries='foo-bin',
355+ date_uploaded=UTC_NOW):
356+ """Make a `SourcePackageRelease`."""
357+ if distroseries is None:
358+ if archive is None:
359+ distribution = None
360+ else:
361+ distribution = archive.distribution
362+ distroseries = self.makeDistroRelease(distribution=distribution)
363+
364+ if archive is None:
365+ archive = self.makeArchive(
366+ distribution=distroseries.distribution,
367+ purpose=ArchivePurpose.PRIMARY)
368+
369+ if sourcepackagename is None:
370+ sourcepackagename = self.makeSourcePackageName()
371+
372+ if component is None:
373+ component = self.makeComponent()
374+
375+ if urgency is None:
376+ urgency = self.getAnySourcePackageUrgency()
377+
378+ if section is None:
379+ section = self.getUniqueString('section')
380+ section = getUtility(ISectionSet).ensure(section)
381+
382+ if maintainer is None:
383+ maintainer = self.makePerson()
384+
385+ maintainer_email = '%s <%s>' % (
386+ maintainer.displayname,
387+ maintainer.preferredemail.email)
388+
389+ if creator is None:
390+ creator = self.makePerson()
391+
392+ if version is None:
393+ version = self.getUniqueString('version')
394+
395+ return distroseries.createUploadedSourcePackageRelease(
396+ sourcepackagename=sourcepackagename,
397+ maintainer=maintainer,
398+ creator=creator,
399+ component=component,
400+ section=section,
401+ urgency=urgency,
402+ version=version,
403+ builddepends=builddepends,
404+ builddependsindep=builddependsindep,
405+ build_conflicts=build_conflicts,
406+ build_conflicts_indep=build_conflicts_indep,
407+ architecturehintlist=architecturehintlist,
408+ changelog_entry=None,
409+ dsc=None,
410+ copyright=self.getUniqueString(),
411+ dscsigningkey=None,
412+ dsc_maintainer_rfc822=maintainer_email,
413+ dsc_standards_version=dsc_standards_version,
414+ dsc_format=dsc_format,
415+ dsc_binaries=dsc_binaries,
416+ archive=archive,
417+ dateuploaded=date_uploaded)
418+
419 def makeSourcePackagePublishingHistory(self, sourcepackagename=None,
420 distroseries=None, maintainer=None,
421 creator=None, component=None,
422@@ -2027,54 +2102,31 @@
423 dsc_format='1.0',
424 dsc_binaries='foo-bin',
425 ):
426- if sourcepackagename is None:
427- sourcepackagename = self.makeSourcePackageName()
428- spn = sourcepackagename
429-
430+ """Make a `SourcePackagePublishingHistory`."""
431 if distroseries is None:
432- distroseries = self.makeDistroRelease()
433+ if archive is None:
434+ distribution = None
435+ else:
436+ distribution = archive.distribution
437+ distroseries = self.makeDistroRelease(distribution=distribution)
438
439 if archive is None:
440 archive = self.makeArchive(
441 distribution=distroseries.distribution,
442 purpose=ArchivePurpose.PRIMARY)
443
444- if component is None:
445- component = self.makeComponent()
446-
447 if pocket is None:
448 pocket = self.getAnyPocket()
449
450 if status is None:
451 status = PackagePublishingStatus.PENDING
452
453- if urgency is None:
454- urgency = self.getAnySourcePackageUrgency()
455-
456- if section is None:
457- section = self.getUniqueString('section')
458- section = getUtility(ISectionSet).ensure(section)
459-
460- if maintainer is None:
461- maintainer = self.makePerson()
462-
463- maintainer_email = '%s <%s>' % (
464- maintainer.displayname,
465- maintainer.preferredemail.email)
466-
467- if creator is None:
468- creator = self.makePerson()
469-
470- if version is None:
471- version = self.getUniqueString('version')
472-
473- gpg_key = self.makeGPGKey(creator)
474-
475- spr = distroseries.createUploadedSourcePackageRelease(
476- sourcepackagename=spn,
477+ spr = self.makeSourcePackageRelease(
478+ archive=archive,
479+ sourcepackagename=sourcepackagename,
480+ distroseries=distroseries,
481 maintainer=maintainer,
482- creator=creator,
483- component=component,
484+ creator=creator, component=component,
485 section=section,
486 urgency=urgency,
487 version=version,
488@@ -2083,15 +2135,10 @@
489 build_conflicts=build_conflicts,
490 build_conflicts_indep=build_conflicts_indep,
491 architecturehintlist=architecturehintlist,
492- changelog_entry=None,
493- dsc=None,
494- copyright=self.getUniqueString(),
495- dscsigningkey=gpg_key,
496- dsc_maintainer_rfc822=maintainer_email,
497 dsc_standards_version=dsc_standards_version,
498 dsc_format=dsc_format,
499 dsc_binaries=dsc_binaries,
500- archive=archive, dateuploaded=date_uploaded)
501+ date_uploaded=date_uploaded)
502
503 sspph = SourcePackagePublishingHistory(
504 distroseries=distroseries,
505
506=== modified file 'lib/lp/translations/doc/translationtemplatesbuildbehavior.txt'
507--- lib/lp/translations/doc/translationtemplatesbuildbehavior.txt 2010-03-05 13:52:32 +0000
508+++ lib/lp/translations/doc/translationtemplatesbuildbehavior.txt 2010-03-13 10:43:28 +0000
509@@ -2,13 +2,21 @@
510
511 == Setup ==
512
513-Set up build environment.
514+Set up build environment. Clear out the build queue.
515
516 >>> import transaction
517 >>> import logging
518 >>> logger = logging.getLogger()
519 >>> logger.setLevel(logging.CRITICAL)
520
521+ >>> from canonical.database.sqlbase import quote
522+ >>> from canonical.launchpad.interfaces.lpstorm import IMasterStore
523+ >>> from lp.services.job.interfaces.job import JobStatus
524+ >>> from lp.services.job.model.job import Job
525+ >>> store = IMasterStore(Job)
526+ >>> query = store.execute("UPDATE Job SET status = %s" % quote(
527+ ... JobStatus.FAILED))
528+
529 >>> from lp.buildmaster.master import BuilddMaster
530 >>> from canonical.buildd.tests import BuilddSlaveTestSetup
531 >>> bm = BuilddMaster(logger, transaction)
532@@ -39,7 +47,21 @@
533
534 Make a builder to process our build request.
535
536- >>> builder = factory.makeBuilder(virtualized=False, processor=processor)
537+ >>> builder = factory.makeBuilder(
538+ ... virtualized=True, processor=processor, vm_host='hostname')
539+
540+The builder doesn't talk to a real slave. We don't have those in our
541+test suite. But we give it a fake one.
542+
543+ >>> from lp.testing.fakemethod import FakeMethod
544+ >>> class FakeSlave:
545+ ... build = FakeMethod()
546+ ... cacheFile = FakeMethod()
547+ ... resume = FakeMethod(result=('Output here', 'Errors here', 0))
548+
549+ >>> from zope.security.proxy import removeSecurityProxy
550+ >>> slave = FakeSlave()
551+ >>> removeSecurityProxy(builder).slave = slave
552
553
554 == Get a job! ==
555@@ -60,20 +82,27 @@
556 >>> print buildqueue.date_started
557 None
558
559-Dispatch the job to the build slave. The proper method to call here
560-would have been Builder.findAndStartJob, but it has not been generalised
561-to handle new job types yet.
562-
563-# XXX JeroenVermeulen bug=506617: call findAndStartJob instead.
564-
565- >>> from zope.security.proxy import removeSecurityProxy
566- >>> removeSecurityProxy(builder)._dispatchBuildCandidate(buildqueue)
567-
568-The build is now marked as started.
569+Our job is now first in line to be executed.
570+
571+ >>> removeSecurityProxy(builder)._findBuildCandidate() == buildqueue
572+ True
573+
574+Dispatch the first job in line (ours!) to the build slave.
575+
576+ >>> activated_job = builder.findAndStartJob()
577+ >>> activated_job == buildqueue
578+ True
579+
580+Our build is now marked as started.
581
582 >>> buildqueue.date_started is None
583 False
584
585+The slave's build method has been called.
586+
587+ >>> slave.build.call_count
588+ 1
589+
590
591 == Teardown ==
592
593
594=== modified file 'lib/lp/translations/model/translationtemplatesbuildbehavior.py'
595--- lib/lp/translations/model/translationtemplatesbuildbehavior.py 2010-02-11 19:11:11 +0000
596+++ lib/lp/translations/model/translationtemplatesbuildbehavior.py 2010-03-13 10:43:28 +0000
597@@ -17,6 +17,7 @@
598
599 from canonical.launchpad.interfaces import ILaunchpadCelebrities
600
601+from lp.buildmaster.interfaces.builder import CorruptBuildID
602 from lp.buildmaster.interfaces.buildfarmjobbehavior import (
603 IBuildFarmJobBehavior)
604 from lp.buildmaster.model.buildfarmjobbehavior import (
605@@ -40,12 +41,27 @@
606 self._builder.slave.cacheFile(logger, chroot)
607 buildid = self.buildfarmjob.getName()
608
609- args = { 'branch_url': self.buildfarmjob.branch.url }
610+ args = self.buildfarmjob.metadata
611 filemap = {}
612
613 self._builder.slave.build(
614 buildid, self.build_type, chroot_sha1, filemap, args)
615
616+ def verifySlaveBuildID(self, slave_build_id):
617+ """See `IBuildFarmJobBehavior`."""
618+ try:
619+ branch_name, queue_item_id = slave_build_id.rsplit('-', 1)
620+ except ValueError:
621+ raise CorruptBuildID(
622+ "Malformed translation templates build id: '%s'" % (
623+ slave_build_id))
624+
625+ buildqueue = self.getVerifiedBuildQueue(queue_item_id)
626+ if buildqueue.job != self.buildfarmjob.job:
627+ raise CorruptBuildID(
628+ "ID mismatch for translation templates build '%s'" % (
629+ slave_build_id))
630+
631 def _getChroot(self):
632 ubuntu = getUtility(ILaunchpadCelebrities).ubuntu
633 return ubuntu.currentseries.nominatedarchindep.getChroot()
634
635=== modified file 'lib/lp/translations/tests/test_translationtemplatesbuildbehavior.py'
636--- lib/lp/translations/tests/test_translationtemplatesbuildbehavior.py 2010-03-12 08:31:43 +0000
637+++ lib/lp/translations/tests/test_translationtemplatesbuildbehavior.py 2010-03-13 10:43:28 +0000
638@@ -19,6 +19,7 @@
639 from lp.buildmaster.interfaces.buildbase import BuildStatus
640 from lp.buildmaster.interfaces.buildfarmjobbehavior import (
641 IBuildFarmJobBehavior)
642+from lp.buildmaster.interfaces.builder import CorruptBuildID
643 from lp.buildmaster.interfaces.buildqueue import IBuildQueueSet
644 from lp.testing import TestCaseWithFactory
645 from lp.testing.fakemethod import FakeMethod
646@@ -123,6 +124,10 @@
647 self.assertEqual(
648 'translation-templates', slave_status['test_build_type'])
649 self.assertIn('branch_url', slave_status['test_build_args'])
650+ # The slave receives the public http URL for the branch.
651+ self.assertEqual(
652+ behavior.buildfarmjob.branch.composePublicURL(),
653+ slave_status['test_build_args']['branch_url'])
654
655 def test_getChroot(self):
656 # _getChroot produces the current chroot for the current Ubuntu
657@@ -170,6 +175,38 @@
658 self.assertEqual(1, builder.cleanSlave.call_count)
659 self.assertEqual(0, behavior._uploadTarball.call_count)
660
661+ def test_verifySlaveBuildID_success(self):
662+ # TranslationTemplatesBuildJob.getName generates slave build ids
663+ # that TranslationTemplatesBuildBehavior.verifySlaveBuildID
664+ # accepts.
665+ behavior = self._makeBehavior()
666+ buildfarmjob = behavior.buildfarmjob
667+ job = buildfarmjob.job
668+
669+ # The test is that this not raise CorruptBuildID (or anything
670+ # else, for that matter).
671+ behavior.verifySlaveBuildID(behavior.buildfarmjob.getName())
672+
673+ def test_verifySlaveBuildID_handles_dashes(self):
674+ # TranslationTemplatesBuildBehavior.verifySlaveBuildID can deal
675+ # with dashes in branch names.
676+ behavior = self._makeBehavior()
677+ buildfarmjob = behavior.buildfarmjob
678+ job = buildfarmjob.job
679+ buildfarmjob.branch.name = 'x-y-z--'
680+
681+ # The test is that this not raise CorruptBuildID (or anything
682+ # else, for that matter).
683+ behavior.verifySlaveBuildID(behavior.buildfarmjob.getName())
684+
685+ def test_verifySlaveBuildID_malformed(self):
686+ behavior = self._makeBehavior()
687+ self.assertRaises(CorruptBuildID, behavior.verifySlaveBuildID, 'huh?')
688+
689+ def test_verifySlaveBuildID_notfound(self):
690+ behavior = self._makeBehavior()
691+ self.assertRaises(CorruptBuildID, behavior.verifySlaveBuildID, '1-1')
692+
693
694 def test_suite():
695 return TestLoader().loadTestsFromName(__name__)