Merge lp:~jelmer/launchpad/bfbia into lp:launchpad

Proposed by Jelmer Vernooij
Status: Rejected
Rejected by: Jelmer Vernooij
Proposed branch: lp:~jelmer/launchpad/bfbia
Merge into: lp:launchpad
Diff against target: 3214 lines (+2770/-12)
32 files modified
database/schema/comments.sql (+15/-0)
database/schema/patch-2208-66-0.sql (+45/-0)
database/schema/security.cfg (+12/-0)
lib/canonical/buildd/buildbranch (+197/-0)
lib/canonical/buildd/sourcepackagebranch.py (+131/-0)
lib/canonical/buildd/test_buildd_branch (+49/-0)
lib/canonical/launchpad/emailtemplates/branch-build-request.txt (+8/-0)
lib/canonical/launchpad/interfaces/_schema_circular_imports.py (+4/-0)
lib/lp/app/browser/configure.zcml (+8/-0)
lib/lp/code/browser/configure.zcml (+46/-3)
lib/lp/code/browser/sourcepackagebranchbuild.py (+198/-0)
lib/lp/code/browser/tests/test_sourcepackagebranchbuild.py (+264/-0)
lib/lp/code/configure.zcml (+44/-0)
lib/lp/code/doc/branch.txt (+1/-0)
lib/lp/code/interfaces/branch.py (+21/-0)
lib/lp/code/interfaces/sourcepackagebranchbuild.py (+131/-0)
lib/lp/code/interfaces/sourcepackagerecipebuild.py (+1/-1)
lib/lp/code/mail/sourcepackagebranchbuild.py (+86/-0)
lib/lp/code/mail/sourcepackagerecipebuild.py (+1/-1)
lib/lp/code/mail/tests/test_sourcepackagebranchbuild.py (+132/-0)
lib/lp/code/model/branch.py (+81/-1)
lib/lp/code/model/branchbuilder.py (+202/-0)
lib/lp/code/model/sourcepackagebranchbuild.py (+362/-0)
lib/lp/code/model/sourcepackagerecipedata.py (+1/-1)
lib/lp/code/model/tests/test_recipebuilder.py (+1/-1)
lib/lp/code/model/tests/test_sourcepackagebranchbuild.py (+450/-0)
lib/lp/code/templates/sourcepackagebranchbuild-index.pt (+199/-0)
lib/lp/registry/interfaces/distroseries.py (+3/-2)
lib/lp/registry/model/distroseries.py (+3/-1)
lib/lp/soyuz/interfaces/sourcepackagerelease.py (+10/-1)
lib/lp/soyuz/model/sourcepackagerelease.py (+4/-0)
lib/lp/testing/factory.py (+60/-0)
To merge this branch: bzr merge lp:~jelmer/launchpad/bfbia
Reviewer Review Type Date Requested Status
Launchpad code reviewers Pending
Review via email: mp+64036@code.launchpad.net
To post a comment you must log in.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'database/schema/comments.sql'
2--- database/schema/comments.sql 2011-06-03 12:16:19 +0000
3+++ database/schema/comments.sql 2011-06-09 15:37:32 +0000
4@@ -1527,6 +1527,21 @@
5 COMMENT ON COLUMN SourcePackageRecipeDistroSeries.distroseries IS 'The primary key of the DistroSeries.';
6 COMMENT ON COLUMN SourcePackageRecipeDistroSeries.sourcepackagerecipe IS 'The primary key of the SourcePackageRecipe.';
7
8+-- SourcePackageBranchBuild
9+
10+COMMENT ON TABLE sourcepackagebranchbuild IS 'The build record for the process of building a source package as described by a branch.';
11+COMMENT ON COLUMN sourcepackagebranchbuild.distroseries IS 'The distroseries the build was for.';
12+COMMENT ON COLUMN sourcepackagebranchbuild.requester IS 'Who requested the build.';
13+COMMENT ON COLUMN sourcepackagebranchbuild.branch IS 'The branch being processed.';
14+COMMENT ON COLUMN sourcepackagebranchbuild.revision IS 'The revision being processed.';
15+--COMMENT ON COLUMN sourcepackagebranchbuild.manifest IS 'The evaluated branch that was built.';
16+COMMENT ON COLUMN sourcepackagebranchbuild.package_build IS 'The package_build with the base information about this build.';
17+
18+-- SourcePackageBranchBuildJob
19+COMMENT ON TABLE sourcepackagebranchbuildjob IS 'The link between a SourcePackagebranchBuild row and a Job row to schedule a build of a source package branch.';
20+
21+COMMENT ON COLUMN sourcepackagebranchbuildjob.sourcepackage_branch_build IS 'The build record describing the package being built.';
22+
23 -- SourcePackageRecipeBuild
24
25 COMMENT ON TABLE SourcePackageRecipeBuild IS 'The build record for the process of building a source package as described by a recipe.';
26
27=== added file 'database/schema/patch-2208-66-0.sql'
28--- database/schema/patch-2208-66-0.sql 1970-01-01 00:00:00 +0000
29+++ database/schema/patch-2208-66-0.sql 2011-06-09 15:37:32 +0000
30@@ -0,0 +1,45 @@
31+-- Copyright 2011 Canonical Ltd. This software is licensed under the
32+-- GNU Affero General Public License version 3 (see the file LICENSE).
33+
34+SET client_min_messages=ERROR;
35+
36+CREATE TABLE sourcepackagebranchbuild (
37+ id serial PRIMARY KEY,
38+ distroseries integer NOT NULL
39+ CONSTRAINT sourcepackagebranchbuild__distroseries__fk REFERENCES distroseries,
40+ requester integer NOT NULL
41+ CONSTRAINT sourcepackagebranchbuild_requester_fkey REFERENCES person,
42+ branch integer
43+ CONSTRAINT sourcepackagebranchbuild__branch__fk REFERENCES branch,
44+-- manifest integer NOT NULL
45+-- CONSTRAINT sourcepackagebranchbuild_manifest_fkey REFERENCES sourcepackagerecipedata,
46+ package_build integer NOT NULL
47+ CONSTRAINT sourcepackagebranchbuild_package_build_fkey REFERENCES packagebuild,
48+ revision integer
49+ CONSTRAINT sourcepackagebranchbuild_revision REFERENCES revision
50+);
51+
52+CREATE INDEX sourcepackagebranchbuild__distroseries__idx ON sourcepackagebranchbuild USING btree (distroseries);
53+--CREATE INDEX sourcepackagebranchbuild__manifest__idx ON sourcepackagebranchbuild USING btree (manifest);
54+CREATE INDEX sourcepackagebranchbuild__branch__idx ON sourcepackagebranchbuild USING btree (branch);
55+CREATE INDEX sourcepackagebranchbuild__revision__idx ON sourcepackagebranchbuild USING btree (revision);
56+CREATE INDEX sourcepackagebranchbuild__requester__idx ON sourcepackagebranchbuild USING btree (requester);
57+
58+CREATE TABLE sourcepackagebranchbuildjob (
59+ id serial PRIMARY KEY,
60+ job integer NOT NULL
61+ CONSTRAINT sourcepackagebranchbuildjob__job__key UNIQUE
62+ CONSTRAINT sourcepackagebranchbuildjob_job_fkey REFERENCES job,
63+ sourcepackage_branch_build integer NOT NULL
64+ CONSTRAINT sourcepackagebranchbuildjob__sourcepackage_branch_build__key UNIQUE
65+ CONSTRAINT sourcepackagebranchbuildjob_sourcepackage_branch_build_fkey REFERENCES sourcepackagebranchbuild
66+);
67+
68+ALTER TABLE sourcepackagerelease
69+ ADD COLUMN sourcepackage_branch_build integer
70+ CONSTRAINT sourcepackagerelease_sourcepackage_branch_build_fkey REFERENCES sourcepackagebranchbuild
71+;
72+
73+CREATE INDEX sourcepackagerelease__sourcepackage_branch_build__idx ON sourcepackagerelease USING btree (sourcepackage_branch_build);
74+
75+INSERT INTO LaunchpadDatabaseRevision VALUES (2208, 66, 0);
76
77=== modified file 'database/schema/security.cfg'
78--- database/schema/security.cfg 2011-06-06 15:19:37 +0000
79+++ database/schema/security.cfg 2011-06-09 15:37:32 +0000
80@@ -280,6 +280,8 @@
81 public.shippingrun = SELECT, INSERT, UPDATE
82 public.sourcepackageformatselection = SELECT
83 public.sourcepackagepublishinghistory = SELECT
84+public.sourcepackagebranchbuild = SELECT, INSERT, UPDATE, DELETE
85+public.sourcepackagebranchbuildjob = SELECT, INSERT, UPDATE, DELETE
86 public.sourcepackagerecipe = SELECT, INSERT, UPDATE, DELETE
87 public.sourcepackagerecipebuild = SELECT, INSERT, UPDATE, DELETE
88 public.sourcepackagerecipebuildjob = SELECT, INSERT, UPDATE, DELETE
89@@ -417,6 +419,7 @@
90 public.project = SELECT
91 public.shipitreport = SELECT
92 public.shippingrun = SELECT
93+public.sourcepackagebranchbuild = SELECT
94 public.sourcepackagerecipebuild = SELECT
95 public.sourcepackagerelease = SELECT
96 public.sourcepackagereleasefile = SELECT
97@@ -769,6 +772,8 @@
98 public.processor = SELECT
99 public.processorfamily = SELECT
100 public.sourcepackagename = SELECT
101+public.sourcepackagebranchbuild = SELECT, INSERT
102+public.sourcepackagebranchbuildjob = SELECT, INSERT
103 public.sourcepackagerecipe = SELECT, UPDATE
104 public.sourcepackagerecipebuild = SELECT, INSERT
105 public.sourcepackagerecipebuildjob = SELECT, INSERT
106@@ -925,6 +930,8 @@
107 public.seriessourcepackagebranch = SELECT
108 public.sourcepackagename = SELECT
109 public.sourcepackagepublishinghistory = SELECT
110+public.sourcepackagebranchbuild = SELECT, UPDATE
111+public.sourcepackagebranchbuildjob = SELECT, INSERT, UPDATE, DELETE
112 public.sourcepackagerecipe = SELECT
113 public.sourcepackagerecipebuild = SELECT, UPDATE
114 public.sourcepackagerecipebuildjob = SELECT, INSERT, UPDATE, DELETE
115@@ -1333,6 +1340,8 @@
116 public.sourcepackageformatselection = SELECT
117 public.sourcepackagename = SELECT, INSERT
118 public.sourcepackagepublishinghistory = SELECT, INSERT
119+public.sourcepackagebranchbuild = SELECT, UPDATE
120+public.sourcepackagebranchbuildjob = SELECT, UPDATE
121 public.sourcepackagerecipe = SELECT, UPDATE
122 public.sourcepackagerecipebuild = SELECT, UPDATE
123 public.sourcepackagerecipebuildjob = SELECT, UPDATE
124@@ -1439,6 +1448,8 @@
125 public.sourcepackagefilepublishing = SELECT
126 public.sourcepackagename = SELECT
127 public.sourcepackagepublishinghistory = SELECT, INSERT, UPDATE
128+public.sourcepackagebranchbuild = SELECT
129+public.sourcepackagebranchbuildjob = SELECT, INSERT, UPDATE
130 public.sourcepackagerecipebuild = SELECT
131 public.sourcepackagerecipebuildjob = SELECT, INSERT, UPDATE
132 public.sourcepackagerelease = SELECT, UPDATE
133@@ -2090,6 +2101,7 @@
134 public.signedcodeofconduct = SELECT, UPDATE
135 public.sourcepackagename = SELECT
136 public.sourcepackagepublishinghistory = SELECT, UPDATE
137+public.sourcepackagebranchbuild = SELECT, UPDATE
138 public.sourcepackagerecipe = SELECT, UPDATE
139 public.sourcepackagerecipebuild = SELECT, UPDATE
140 public.sourcepackagerecipedata = SELECT, UPDATE
141
142=== added file 'lib/canonical/buildd/buildbranch'
143--- lib/canonical/buildd/buildbranch 1970-01-01 00:00:00 +0000
144+++ lib/canonical/buildd/buildbranch 2011-06-09 15:37:32 +0000
145@@ -0,0 +1,197 @@
146+#!/usr/bin/env python
147+# Copyright 2010 Canonical Ltd. This software is licensed under the
148+# GNU Affero General Public License version 3 (see the file LICENSE).
149+
150+"""A script that builds a package from a branch and a chroot."""
151+
152+__metaclass__ = type
153+
154+
155+import os
156+import os.path
157+import pwd
158+from resource import RLIMIT_AS, setrlimit
159+import socket
160+from subprocess import call
161+import sys
162+
163+
164+RETCODE_SUCCESS = 0
165+RETCODE_FAILURE_INSTALL = 200
166+RETCODE_FAILURE_BUILD_TREE = 201
167+RETCODE_FAILURE_INSTALL_BUILD_DEPS = 202
168+RETCODE_FAILURE_BUILD_SOURCE_PACKAGE = 203
169+
170+
171+class NotVirtualized(Exception):
172+ """Exception raised when not running in a virtualized environment."""
173+
174+ def __init__(self):
175+ Exception.__init__(self, 'Not running under Xen.')
176+
177+
178+class BranchBuilder:
179+ """Builds a package from a branch."""
180+
181+ def __init__(self, branch_url, revision_id, build_id, author_name,
182+ author_email, suite, distroseries_name, component,
183+ archive_purpose):
184+ """Constructor.
185+
186+ :param branch_url: The URL of the branch to build
187+ :param revision_id: The revision id of the revision to build
188+ :param build_id: The id of the build (a str).
189+ :param author_name: The name of the author (a str).
190+ :param author_email: The email address of the author (a str).
191+ :param suite: The suite the package should be built for (a str).
192+ """
193+ self.branch_url = branch_url
194+ self.revision_id = revision_id
195+ self.build_id = build_id
196+ self.author_name = author_name.decode('utf-8')
197+ self.author_email = author_email
198+ self.archive_purpose = archive_purpose
199+ self.component = component
200+ self.distroseries_name = distroseries_name
201+ self.suite = suite
202+ self.base_branch = None
203+ self.chroot_path = get_build_path(build_id, 'chroot-autobuild')
204+ self.work_dir_relative = os.environ['HOME'] + '/work'
205+ self.work_dir = os.path.join(self.chroot_path,
206+ self.work_dir_relative[1:])
207+ self.tree_path = os.path.join(self.work_dir, 'tree')
208+ self.branch_url = branch_url
209+ self.username = pwd.getpwuid(os.getuid())[0]
210+
211+ def install(self):
212+ """Install all the requirements for building branches.
213+
214+ :return: A retcode from apt.
215+ """
216+ # XXX: AaronBentley 2010-07-07 bug=602463: pbuilder uses aptitude but
217+ # does not depend on it.
218+ return self.chroot([
219+ 'apt-get', 'install', '-y', 'pbuilder', 'aptitude'])
220+
221+ def buildTree(self):
222+ """Build the branch into a source tree.
223+
224+ As a side-effect, sets self.source_dir_relative.
225+ :return: a retcode from `bzr dailydeb`.
226+ """
227+ try:
228+ ensure_virtualized()
229+ except NotVirtualized, e:
230+ sys.stderr.write('Aborting on failed virtualization check:\n')
231+ sys.stderr.write(str(e))
232+ return 1
233+ assert not os.path.exists(self.tree_path)
234+ manifest_path = os.path.join(self.tree_path, 'manifest')
235+ # As of bzr 2.2, a defined identity is needed. In this case, we're
236+ # using buildd@<hostname>.
237+ hostname = socket.gethostname()
238+ bzr_email = 'buildd@%s' % hostname
239+
240+ print 'Building branch:'
241+ sys.stdout.flush()
242+ env = {
243+ 'DEBEMAIL': self.author_email,
244+ 'DEBFULLNAME': self.author_name.encode('utf-8'),
245+ 'BZR_EMAIL': bzr_email}
246+ retcode = call([
247+ 'bzr', 'dailydeb', '--safe', '--no-build', self.branch_url,
248+ self.tree_path, '--revision=revid:%s' % self.revision_id,
249+ '--manifest', manifest_path], env=env)
250+ if retcode != 0:
251+ return retcode
252+ (source,) = [name for name in os.listdir(self.tree_path)
253+ if name != 'manifest']
254+ self.source_dir_relative = os.path.join(
255+ self.work_dir_relative, 'tree', source)
256+ return retcode
257+
258+ def getPackageName(self):
259+ source_dir = os.path.join(
260+ self.chroot_path, self.source_dir_relative.lstrip('/'))
261+ changelog = os.path.join(source_dir, 'debian/changelog')
262+ return open(changelog, 'r').readline().split(' ')[0]
263+
264+ def installBuildDeps(self):
265+ """Install the build-depends of the source tree."""
266+ package = self.getPackageName()
267+ currently_building_path = os.path.join(
268+ self.chroot_path, 'CurrentlyBuilding')
269+ currently_building_contents = (
270+ 'Package: %s\n'
271+ 'Suite: %s\n'
272+ 'Component: %s\n'
273+ 'Purpose: %s\n'
274+ 'Build-Debug-Symbols: no\n' %
275+ (package, self.suite, self.component, self.archive_purpose))
276+ currently_building = open(currently_building_path, 'w')
277+ currently_building.write(currently_building_contents)
278+ currently_building.close()
279+ return self.chroot(['sh', '-c', 'cd %s &&'
280+ '/usr/lib/pbuilder/pbuilder-satisfydepends'
281+ % self.source_dir_relative])
282+
283+ def chroot(self, args, echo=False):
284+ """Run a command in the chroot.
285+
286+ :param args: the command and arguments to run.
287+ :return: the status code.
288+ """
289+ if echo:
290+ print "Running in chroot: %s" % ' '.join(
291+ "'%s'" % arg for arg in args)
292+ sys.stdout.flush()
293+ return call([
294+ '/usr/bin/sudo', '/usr/sbin/chroot', self.chroot_path] + args)
295+
296+ def buildSourcePackage(self):
297+ """Build the source package.
298+
299+ :return: a retcode from dpkg-buildpackage.
300+ """
301+ retcode = self.chroot([
302+ 'su', '-c', 'cd %s && /usr/bin/dpkg-buildpackage -i -I -us -uc -S'
303+ % self.source_dir_relative, self.username])
304+ for filename in os.listdir(self.tree_path):
305+ path = os.path.join(self.tree_path, filename)
306+ if os.path.isfile(path):
307+ os.rename(path, get_build_path(self.build_id, filename))
308+ return retcode
309+
310+
311+def get_build_path(build_id, *extra):
312+ """Generate a path within the build directory.
313+
314+ :param build_id: the build id to use.
315+ :param extra: the extra path segments within the build directory.
316+ :return: the generated path.
317+ """
318+ return os.path.join(
319+ os.environ["HOME"], "build-" + build_id, *extra)
320+
321+
322+def ensure_virtualized():
323+ """Raise an exception if not running in a virtualized environment.
324+
325+ Raises if not running under Xen.
326+ """
327+ if not os.path.isdir('/proc/xen') or os.path.exists('/proc/xen/xsd_kva'):
328+ raise NotVirtualized()
329+
330+
331+if __name__ == '__main__':
332+ setrlimit(RLIMIT_AS, (1000000000, -1))
333+ builder = BranchBuilder(*sys.argv[1:])
334+ if builder.buildTree() != 0:
335+ sys.exit(RETCODE_FAILURE_BUILD_TREE)
336+ if builder.install() != 0:
337+ sys.exit(RETCODE_FAILURE_INSTALL)
338+ if builder.installBuildDeps() != 0:
339+ sys.exit(RETCODE_FAILURE_INSTALL_BUILD_DEPS)
340+ if builder.buildSourcePackage() != 0:
341+ sys.exit(RETCODE_FAILURE_BUILD_SOURCE_PACKAGE)
342+ sys.exit(RETCODE_SUCCESS)
343
344=== added file 'lib/canonical/buildd/sourcepackagebranch.py'
345--- lib/canonical/buildd/sourcepackagebranch.py 1970-01-01 00:00:00 +0000
346+++ lib/canonical/buildd/sourcepackagebranch.py 2011-06-09 15:37:32 +0000
347@@ -0,0 +1,131 @@
348+# Copyright 2010 Canonical Ltd. This software is licensed under the
349+# GNU Affero General Public License version 3 (see the file LICENSE).
350+# pylint: disable-msg=E1002
351+
352+"""The manager class for building packages from branches."""
353+
354+import os
355+import re
356+
357+from canonical.buildd.debian import (
358+ DebianBuildManager,
359+ DebianBuildState,
360+ get_build_path,
361+)
362+RETCODE_SUCCESS = 0
363+RETCODE_FAILURE_INSTALL = 200
364+RETCODE_FAILURE_BUILD_TREE = 201
365+RETCODE_FAILURE_INSTALL_BUILD_DEPS = 202
366+RETCODE_FAILURE_BUILD_SOURCE_PACKAGE = 203
367+
368+
369+def get_chroot_path(build_id, *extra):
370+ """Return a path within the chroot.
371+
372+ :param build_id: The build_id of the build.
373+ :param extra: Additional path elements.
374+ """
375+ return get_build_path(
376+ build_id, 'chroot-autobuild', os.environ['HOME'][1:], *extra)
377+
378+
379+class SourcePackageBranchBuildState(DebianBuildState):
380+ """The set of states that a branch build can be in."""
381+ BUILD_BRANCH = "BUILD_BRANCH"
382+
383+
384+class SourcePackageBranchBuildManager(DebianBuildManager):
385+ """Build a source package from a bzr-builder branch."""
386+
387+ initial_build_state = SourcePackageBranchBuildState.BUILD_BRANCH
388+
389+ def __init__(self, slave, buildid):
390+ """Constructor.
391+
392+ :param slave: A build slave device.
393+ :param buildid: The id of the build (a str).
394+ """
395+ DebianBuildManager.__init__(self, slave, buildid)
396+ self.build_branch_path = slave._config.get(
397+ "sourcepackagebranchmanager", "buildbranchpath")
398+
399+ def initiate(self, files, chroot, extra_args):
400+ """Initiate a build with a given set of files and chroot.
401+
402+ :param files: The files sent by the manager with the request.
403+ :param chroot: The sha1sum of the chroot to use.
404+ :param extra_args: A dict of extra arguments.
405+ """
406+ self.branch_url = extra_args['branch_url']
407+ self.revision_id = extra_args['revision_id']
408+ self.suite = extra_args['suite']
409+ self.component = extra_args['ogrecomponent']
410+ self.author_name = extra_args['author_name']
411+ self.author_email = extra_args['author_email']
412+ self.archive_purpose = extra_args['archive_purpose']
413+ self.distroseries_name = extra_args['distroseries_name']
414+
415+ super(SourcePackageBranchBuildManager, self).initiate(
416+ files, chroot, extra_args)
417+
418+ def doRunBuild(self):
419+ """Run the build process to build the source package."""
420+ os.makedirs(get_chroot_path(self._buildid, 'work'))
421+ args = [
422+ "buildbranch.py", self.branch_url, self.revision_id,
423+ self._buildid, self.author_name.encode('utf-8'),
424+ self.author_email, self.suite, self.distroseries_name,
425+ self.component, self.archive_purpose]
426+ self.runSubProcess(self.build_branch_path, args)
427+
428+ def iterate_BUILD_BRANCH(self, retcode):
429+ """Move from BUILD_BRANCH to the next logical state."""
430+ if retcode == RETCODE_SUCCESS:
431+ self.gatherResults()
432+ print("Returning build status: OK")
433+ elif retcode == RETCODE_FAILURE_INSTALL_BUILD_DEPS:
434+ if not self.alreadyfailed:
435+ tmpLog = self.getTmpLogContents()
436+ rx = (
437+ 'The following packages have unmet dependencies:\n'
438+ '.*: Depends: ([^ ]*( \([^)]*\))?)')
439+ mo = re.search(rx, tmpLog, re.M)
440+ if mo:
441+ self._slave.depFail(mo.group(1))
442+ print("Returning build status: DEPFAIL")
443+ print("Dependencies: " + mo.group(1))
444+ else:
445+ print("Returning build status: Build failed")
446+ self._slave.buildFail()
447+ self.alreadyfailed = True
448+ elif (
449+ retcode >= RETCODE_FAILURE_INSTALL and
450+ retcode <= RETCODE_FAILURE_BUILD_SOURCE_PACKAGE):
451+ # XXX AaronBentley 2009-01-13: We should handle depwait separately
452+ if not self.alreadyfailed:
453+ self._slave.buildFail()
454+ print("Returning build status: Build failed.")
455+ self.alreadyfailed = True
456+ else:
457+ if not self.alreadyfailed:
458+ self._slave.builderFail()
459+ print("Returning build status: Builder failed.")
460+ self.alreadyfailed = True
461+ self._state = DebianBuildState.REAP
462+ self.doReapProcesses()
463+
464+ def getChangesFilename(self):
465+ """Return the path to the changes file."""
466+ work_path = get_build_path(self._buildid)
467+ for name in os.listdir(work_path):
468+ if name.endswith('_source.changes'):
469+ return os.path.join(work_path, name)
470+
471+ def gatherResults(self):
472+ """Gather the results of the build and add them to the file cache.
473+
474+ The primary file we care about is the .changes file.
475+ The manifest is also a useful record.
476+ """
477+ DebianBuildManager.gatherResults(self)
478+ self._slave.addWaitingFile(get_build_path(self._buildid, 'manifest'))
479
480=== added file 'lib/canonical/buildd/test_buildd_branch'
481--- lib/canonical/buildd/test_buildd_branch 1970-01-01 00:00:00 +0000
482+++ lib/canonical/buildd/test_buildd_branch 2011-06-09 15:37:32 +0000
483@@ -0,0 +1,49 @@
484+#!/usr/bin/env python
485+# Copyright 2010 Canonical Ltd. This software is licensed under the
486+# GNU Affero General Public License version 3 (see the file LICENSE).
487+#
488+# This is a script to do end-to-end testing of the buildd with a bzr-builder
489+# branch, without involving the BuilderBehaviour.
490+
491+country_code = 'us'
492+apt_cacher_ng_host = 'stumpy'
493+distroseries_name = 'maverick'
494+branch_url = 'http://bazaar.launchpad.dev/~ppa-user/+junk/wakeonlan'
495+revision_id = 'someemail@host-with-randomness'
496+
497+def deb_line(host, suites):
498+ prefix = 'deb http://'
499+ if apt_cacher_ng_host != None:
500+ prefix += '%s:3142/' % apt_cacher_ng_host
501+ return '%s%s %s %s' % (prefix, host, distroseries_name, suites)
502+
503+import sys
504+from xmlrpclib import ServerProxy
505+
506+proxy = ServerProxy('http://localhost:8221/rpc')
507+print proxy.echo('Hello World')
508+print proxy.info()
509+status = proxy.status()
510+print status
511+if status[0] != 'BuilderStatus.IDLE':
512+ print "Aborting due to non-IDLE builder."
513+ sys.exit(1)
514+print proxy.build(
515+ '1-2', 'sourcepackagebranch', '1ef177161c3cb073e66bf1550931c6fbaa0a94b0',
516+ {}, {'author_name': u'Steve\u1234',
517+ 'author_email': 'stevea@example.org',
518+ 'suite': distroseries_name,
519+ 'distroseries_name': distroseries_name,
520+ 'ogrecomponent': 'universe',
521+ 'archive_purpose': 'puppies',
522+ 'branch_url': branch_url,
523+ 'revision_id': revision_id,
524+ 'archives': [
525+ deb_line('%s.archive.ubuntu.com/ubuntu' % country_code,
526+ 'main universe'),
527+ deb_line('ppa.launchpad.net/launchpad/bzr-builder-dev/ubuntu',
528+ 'main'),]})
529+#status = proxy.status()
530+#for filename, sha1 in status[3].iteritems():
531+# print filename
532+#proxy.clean()
533
534=== added file 'lib/canonical/launchpad/emailtemplates/branch-build-request.txt'
535--- lib/canonical/launchpad/emailtemplates/branch-build-request.txt 1970-01-01 00:00:00 +0000
536+++ lib/canonical/launchpad/emailtemplates/branch-build-request.txt 2011-06-09 15:37:32 +0000
537@@ -0,0 +1,8 @@
538+ * State: %(status)s
539+ * Branch: %(branch_owner)s/%(branch)s
540+ * Archive: %(archive_owner)s/%(archive)s
541+ * Distroseries: %(distroseries)s
542+ * Duration: %(duration)s
543+ * Build Log: %(log_url)s
544+ * Upload Log: %(upload_log_url)s
545+ * Builder: %(builder_url)s
546
547=== renamed file 'lib/canonical/launchpad/emailtemplates/build-request.txt' => 'lib/canonical/launchpad/emailtemplates/recipe-build-request.txt'
548=== modified file 'lib/canonical/launchpad/interfaces/_schema_circular_imports.py'
549--- lib/canonical/launchpad/interfaces/_schema_circular_imports.py 2011-06-09 08:07:52 +0000
550+++ lib/canonical/launchpad/interfaces/_schema_circular_imports.py 2011-06-09 15:37:32 +0000
551@@ -262,6 +262,10 @@
552 IBranch, '_createMergeProposal', 'prerequisite_branch', IBranch)
553 patch_collection_return_type(
554 IBranch, 'getMergeProposals', IBranchMergeProposal)
555+patch_plain_parameter_type(
556+ IBranch, 'requestBuild', 'archive', IArchive)
557+patch_plain_parameter_type(
558+ IBranch, 'requestBuild', 'distroseries', IDistroSeries)
559
560 IBranchMergeProposal['getComment'].queryTaggedValue(
561 LAZR_WEBSERVICE_EXPORTED)['return_type'].schema = ICodeReviewComment
562
563=== modified file 'lib/lp/app/browser/configure.zcml'
564--- lib/lp/app/browser/configure.zcml 2011-05-28 15:08:55 +0000
565+++ lib/lp/app/browser/configure.zcml 2011-06-09 15:37:32 +0000
566@@ -504,6 +504,14 @@
567 factory="lp.app.browser.tales.BuildImageDisplayAPI"
568 name="image"
569 />
570+
571+ <adapter
572+ for="lp.code.interfaces.sourcepackagebranchbuild.ISourcePackageBranchBuild"
573+ provides="zope.traversing.interfaces.IPathAdapter"
574+ factory="lp.app.browser.tales.BuildImageDisplayAPI"
575+ name="image"
576+ />
577+
578 <adapter
579 for="lp.code.interfaces.sourcepackagerecipebuild.ISourcePackageRecipeBuild"
580 provides="zope.traversing.interfaces.IPathAdapter"
581
582=== modified file 'lib/lp/code/browser/configure.zcml'
583--- lib/lp/code/browser/configure.zcml 2011-05-27 21:03:22 +0000
584+++ lib/lp/code/browser/configure.zcml 2011-06-09 15:37:32 +0000
585@@ -1354,9 +1354,52 @@
586 name="+bmq-macros"
587 permission="zope.Public"
588 template="../templates/branchmergequeue-macros.pt"/>
589-
590-
591- </facet>
592+ </facet>
593+
594+ <facet facet="branches">
595+ <browser:defaultView
596+ for="lp.code.interfaces.sourcepackagebranchbuild.ISourcePackageBranchBuild"
597+ name="+index"
598+ />
599+ <browser:page
600+ for="lp.code.interfaces.sourcepackagebranchbuild.ISourcePackageBranchBuild"
601+ class="lp.code.browser.sourcepackagebranchbuild.SourcePackageBranchBuildView"
602+ name="+index"
603+ template="../templates/sourcepackagebranchbuild-index.pt"
604+ permission="launchpad.View"/>
605+ <browser:page
606+ for="lp.code.interfaces.sourcepackagebranchbuild.ISourcePackageBranchBuild"
607+ class="lp.code.browser.sourcepackagebranchbuild.SourcePackageBranchBuildCancelView"
608+ name="+cancel"
609+ template="../../app/templates/generic-edit.pt"
610+ permission="launchpad.Admin"/>
611+ <browser:page
612+ for="lp.code.interfaces.sourcepackagebranchbuild.ISourcePackageBranchBuild"
613+ class="lp.code.browser.sourcepackagebranchbuild.SourcePackageBranchBuildRescoreView"
614+ name="+rescore"
615+ template="../../app/templates/generic-edit.pt"
616+ permission="launchpad.Admin"/>
617+ </facet>
618+
619+ <browser:url
620+ for="lp.code.interfaces.sourcepackagebranchbuild.ISourcePackageBranchBuild"
621+ attribute_to_parent="archive"
622+ path_expression="string:+branchbuild/${id}"
623+ />
624+ <adapter
625+ for="lp.code.interfaces.sourcepackagebranchbuild.ISourcePackageBranchBuild"
626+ provides="zope.traversing.interfaces.IPathAdapter"
627+ factory="lp.code.browser.sourcepackagebranchbuild.SourcePackageBranchBuildFormatterAPI"
628+ name="fmt"
629+ />
630+
631+ <browser:menus
632+ classes="SourcePackageBranchBuildContextMenu"
633+ module="lp.code.browser.sourcepackagebranchbuild"/>
634+
635+ <browser:navigation
636+ module="lp.code.browser.sourcepackagebranchbuild"
637+ classes="SourcePackageBranchBuildNavigation" />
638
639 <browser:url
640 for="lp.code.interfaces.branchmergequeue.IBranchMergeQueue"
641
642=== added file 'lib/lp/code/browser/sourcepackagebranchbuild.py'
643--- lib/lp/code/browser/sourcepackagebranchbuild.py 1970-01-01 00:00:00 +0000
644+++ lib/lp/code/browser/sourcepackagebranchbuild.py 2011-06-09 15:37:32 +0000
645@@ -0,0 +1,198 @@
646+# Copyright 2010 Canonical Ltd. This software is licensed under the
647+# GNU Affero General Public License version 3 (see the file LICENSE).
648+
649+"""SourcePackageBranchBuild views."""
650+
651+__metaclass__ = type
652+
653+__all__ = [
654+ 'SourcePackageBranchBuildContextMenu',
655+ 'SourcePackageBranchBuildNavigation',
656+ 'SourcePackageBranchBuildView',
657+ 'SourcePackageBranchBuildCancelView',
658+ 'SourcePackageBranchBuildRescoreView',
659+ ]
660+
661+from zope.interface import Interface
662+from zope.schema import Int
663+
664+from canonical.launchpad.browser.librarian import FileNavigationMixin
665+from canonical.launchpad.webapp import (
666+ canonical_url,
667+ ContextMenu,
668+ enabled_with_permission,
669+ LaunchpadView,
670+ Link,
671+ Navigation,
672+ )
673+from lp.app.browser.launchpadform import (
674+ action,
675+ LaunchpadFormView,
676+ )
677+from lp.app.browser.tales import CustomizableFormatter
678+from lp.buildmaster.enums import BuildStatus
679+from lp.code.interfaces.sourcepackagebranchbuild import (
680+ ISourcePackageBranchBuild,
681+ )
682+from lp.services.job.interfaces.job import JobStatus
683+from lp.services.propertycache import cachedproperty
684+
685+
686+UNEDITABLE_BUILD_STATES = (
687+ BuildStatus.FULLYBUILT,
688+ BuildStatus.FAILEDTOBUILD,
689+ BuildStatus.SUPERSEDED,
690+ BuildStatus.FAILEDTOUPLOAD,)
691+
692+
693+class SourcePackageBranchBuildFormatterAPI(CustomizableFormatter):
694+ """Adapter providing fmt support for ISourcePackageBranchBuild objects."""
695+
696+ _link_summary_template = '%(title)s [%(owner)s/%(archive)s]'
697+
698+ def _link_summary_values(self):
699+ return {'title': self._context.title,
700+ 'owner': self._context.archive.owner.name,
701+ 'archive': self._context.archive.name}
702+
703+
704+class SourcePackageBranchBuildNavigation(Navigation, FileNavigationMixin):
705+
706+ usedfor = ISourcePackageBranchBuild
707+
708+
709+class SourcePackageBranchBuildContextMenu(ContextMenu):
710+ """Navigation menu for sourcepackagebranch build."""
711+
712+ usedfor = ISourcePackageBranchBuild
713+
714+ facet = 'branches'
715+
716+ links = ('cancel', 'rescore')
717+
718+ @enabled_with_permission('launchpad.Admin')
719+ def cancel(self):
720+ if self.context.status in UNEDITABLE_BUILD_STATES:
721+ enabled = False
722+ else:
723+ enabled = True
724+ return Link('+cancel', 'Cancel build', icon='remove', enabled=enabled)
725+
726+ @enabled_with_permission('launchpad.Admin')
727+ def rescore(self):
728+ if self.context.status in UNEDITABLE_BUILD_STATES:
729+ enabled = False
730+ else:
731+ enabled = True
732+ return Link('+rescore', 'Rescore build', icon='edit', enabled=enabled)
733+
734+
735+class SourcePackageBranchBuildView(LaunchpadView):
736+ """Default view of a SourcePackageBranchBuild."""
737+
738+ @property
739+ def status(self):
740+ """A human-friendly status string."""
741+ if (self.context.status == BuildStatus.NEEDSBUILD
742+ and self.eta is None):
743+ return 'No suitable builders'
744+ return {
745+ BuildStatus.NEEDSBUILD: 'Pending build',
746+ BuildStatus.UPLOADING: 'Build uploading',
747+ BuildStatus.FULLYBUILT: 'Successful build',
748+ BuildStatus.MANUALDEPWAIT: (
749+ 'Could not build because of missing dependencies'),
750+ BuildStatus.CHROOTWAIT: (
751+ 'Could not build because of chroot problem'),
752+ BuildStatus.SUPERSEDED: (
753+ 'Could not build because source package was superseded'),
754+ BuildStatus.FAILEDTOUPLOAD: 'Could not be uploaded correctly',
755+ }.get(self.context.status, self.context.status.title)
756+
757+ @cachedproperty
758+ def eta(self):
759+ """The datetime when the build job is estimated to complete.
760+
761+ This is the BuildQueue.estimated_duration plus the
762+ Job.date_started or BuildQueue.getEstimatedJobStartTime.
763+ """
764+ if self.context.buildqueue_record is None:
765+ return None
766+ queue_record = self.context.buildqueue_record
767+ if queue_record.job.status == JobStatus.WAITING:
768+ start_time = queue_record.getEstimatedJobStartTime()
769+ if start_time is None:
770+ return None
771+ else:
772+ start_time = queue_record.job.date_started
773+ duration = queue_record.estimated_duration
774+ return start_time + duration
775+
776+ @cachedproperty
777+ def date(self):
778+ """The date when the build completed or is estimated to complete."""
779+ if self.estimate:
780+ return self.eta
781+ return self.context.date_finished
782+
783+ @cachedproperty
784+ def estimate(self):
785+ """If true, the date value is an estimate."""
786+ if self.context.date_finished is not None:
787+ return False
788+ return self.eta is not None
789+
790+ def binary_builds(self):
791+ return list(self.context.binary_builds)
792+
793+
794+class SourcePackageBranchBuildCancelView(LaunchpadFormView):
795+ """View for cancelling a build."""
796+
797+ class schema(Interface):
798+ """Schema for cancelling a build."""
799+
800+ page_title = label = "Cancel build"
801+
802+ @property
803+ def cancel_url(self):
804+ return canonical_url(self.context)
805+ next_url = cancel_url
806+
807+ @action('Cancel build', name='cancel')
808+ def request_action(self, action, data):
809+ """Cancel the build."""
810+ self.context.cancelBuild()
811+
812+
813+class SourcePackageBranchBuildRescoreView(LaunchpadFormView):
814+ """View for rescoring a build."""
815+
816+ class schema(Interface):
817+ """Schema for deleting a build."""
818+ score = Int(
819+ title=u'Score', required=True,
820+ description=u'The score of the branch.')
821+
822+ page_title = label = "Rescore build"
823+
824+ def __call__(self):
825+ if self.context.buildqueue_record is not None:
826+ return super(SourcePackageBranchBuildRescoreView, self).__call__()
827+ self.request.response.addWarningNotification(
828+ 'Cannot rescore this build because it is not queued.')
829+ self.request.response.redirect(canonical_url(self.context))
830+
831+ @property
832+ def cancel_url(self):
833+ return canonical_url(self.context)
834+ next_url = cancel_url
835+
836+ @action('Rescore build', name='rescore')
837+ def request_action(self, action, data):
838+ """Rescore the build."""
839+ self.context.buildqueue_record.lastscore = int(data['score'])
840+
841+ @property
842+ def initial_values(self):
843+ return {'score': str(self.context.buildqueue_record.lastscore)}
844
845=== added file 'lib/lp/code/browser/tests/test_sourcepackagebranchbuild.py'
846--- lib/lp/code/browser/tests/test_sourcepackagebranchbuild.py 1970-01-01 00:00:00 +0000
847+++ lib/lp/code/browser/tests/test_sourcepackagebranchbuild.py 2011-06-09 15:37:32 +0000
848@@ -0,0 +1,264 @@
849+# Copyright 2010 Canonical Ltd. This software is licensed under the
850+# GNU Affero General Public License version 3 (see the file LICENSE).
851+# pylint: disable-msg=F0401,E1002
852+
853+"""Tests for the source package branch view classes and templates."""
854+
855+__metaclass__ = type
856+
857+from mechanize import LinkNotFoundError
858+from storm.locals import Store
859+from testtools.matchers import StartsWith
860+import transaction
861+from zope.component import getUtility
862+from zope.security.interfaces import Unauthorized
863+from zope.security.proxy import removeSecurityProxy
864+
865+from canonical.launchpad.testing.pages import (
866+ extract_text,
867+ find_main_content,
868+ find_tags_by_class,
869+ )
870+from canonical.launchpad.webapp import canonical_url
871+from canonical.testing.layers import DatabaseFunctionalLayer
872+from lp.app.interfaces.launchpad import ILaunchpadCelebrities
873+from lp.buildmaster.enums import BuildStatus
874+from lp.soyuz.model.processor import ProcessorFamily
875+from lp.testing import (
876+ ANONYMOUS,
877+ BrowserTestCase,
878+ login,
879+ logout,
880+ person_logged_in,
881+ TestCaseWithFactory,
882+ )
883+
884+
885+class TestCanonicalUrlForBranchBuild(TestCaseWithFactory):
886+
887+ layer = DatabaseFunctionalLayer
888+
889+ def test_canonical_url(self):
890+ owner = self.factory.makePerson(name='ppa-owner')
891+ ppa = self.factory.makeArchive(owner=owner, name='ppa')
892+ build = self.factory.makeSourcePackageBranchBuild(archive=ppa)
893+ self.assertThat(
894+ canonical_url(build),
895+ StartsWith(
896+ 'http://launchpad.dev/~ppa-owner/+archive/ppa/+branchbuild/'))
897+
898+
899+class TestSourcePackageBranchBuild(BrowserTestCase):
900+ """Create some sample data for branch tests."""
901+
902+ layer = DatabaseFunctionalLayer
903+
904+ def setUp(self):
905+ """Provide useful defaults."""
906+ super(TestSourcePackageBranchBuild, self).setUp()
907+ self.chef = self.factory.makePerson(
908+ displayname='Master Chef', name='chef', password='test')
909+ self.user = self.chef
910+ self.ppa = self.factory.makeArchive(
911+ displayname='Secret PPA', owner=self.chef, name='ppa')
912+ self.squirrel = self.factory.makeDistroSeries(
913+ displayname='Secret Squirrel', name='secret', version='100.04',
914+ distribution=self.ppa.distribution)
915+ naked_squirrel = removeSecurityProxy(self.squirrel)
916+ naked_squirrel.nominatedarchindep = self.squirrel.newArch(
917+ 'i386', ProcessorFamily.get(1), False, self.chef,
918+ supports_virtualized=True)
919+
920+ def makeBranchBuild(self):
921+ """Create and return a specific bazaar revision."""
922+ chocolate = self.factory.makeProduct(name='chocolate')
923+ cake_branch = self.factory.makeProductBranch(
924+ owner=self.chef, name='cake', product=chocolate)
925+ return self.factory.makeSourcePackageBranchBuild(
926+ branch=cake_branch, archive=self.ppa, distroseries=self.squirrel)
927+
928+ def test_cancel_build(self):
929+ """An admin can cancel a build."""
930+ experts = getUtility(ILaunchpadCelebrities).bazaar_experts.teamowner
931+ queue = self.factory.makeSourcePackageBranchBuildJob()
932+ build = queue.specific_job.build
933+ transaction.commit()
934+ build_url = canonical_url(build)
935+ logout()
936+
937+ browser = self.getUserBrowser(build_url, user=experts)
938+ browser.getLink('Cancel build').click()
939+
940+ self.assertEqual(
941+ browser.getLink('Cancel').url,
942+ build_url)
943+
944+ browser.getControl('Cancel build').click()
945+
946+ self.assertEqual(
947+ browser.url,
948+ build_url)
949+
950+ login(ANONYMOUS)
951+ self.assertEqual(
952+ BuildStatus.SUPERSEDED,
953+ build.status)
954+
955+ def test_cancel_build_not_admin(self):
956+ """No one but admins can cancel a build."""
957+ queue = self.factory.makeSourcePackageBranchBuildJob()
958+ build = queue.specific_job.build
959+ transaction.commit()
960+ build_url = canonical_url(build)
961+ logout()
962+
963+ browser = self.getUserBrowser(build_url, user=self.chef)
964+ self.assertRaises(
965+ LinkNotFoundError,
966+ browser.getLink, 'Cancel build')
967+
968+ self.assertRaises(
969+ Unauthorized,
970+ self.getUserBrowser, build_url + '/+cancel', user=self.chef)
971+
972+ def test_cancel_build_wrong_state(self):
973+ """If the build isn't queued, you can't cancel it."""
974+ experts = getUtility(ILaunchpadCelebrities).bazaar_experts.teamowner
975+ build = self.makeBranchBuild()
976+ build.cancelBuild()
977+ transaction.commit()
978+ build_url = canonical_url(build)
979+ logout()
980+
981+ browser = self.getUserBrowser(build_url, user=experts)
982+ self.assertRaises(
983+ LinkNotFoundError,
984+ browser.getLink, 'Cancel build')
985+
986+ def test_rescore_build(self):
987+ """An admin can rescore a build."""
988+ experts = getUtility(ILaunchpadCelebrities).bazaar_experts.teamowner
989+ queue = self.factory.makeSourcePackageBranchBuildJob()
990+ build = queue.specific_job.build
991+ transaction.commit()
992+ build_url = canonical_url(build)
993+ logout()
994+
995+ browser = self.getUserBrowser(build_url, user=experts)
996+ browser.getLink('Rescore build').click()
997+
998+ self.assertEqual(
999+ browser.getLink('Cancel').url,
1000+ build_url)
1001+
1002+ browser.getControl('Score').value = '1024'
1003+
1004+ browser.getControl('Rescore build').click()
1005+
1006+ self.assertEqual(
1007+ browser.url,
1008+ build_url)
1009+
1010+ login(ANONYMOUS)
1011+ self.assertEqual(
1012+ build.buildqueue_record.lastscore,
1013+ 1024)
1014+
1015+ def test_rescore_build_invalid_score(self):
1016+ """Build scores can only take numbers."""
1017+ experts = getUtility(ILaunchpadCelebrities).bazaar_experts.teamowner
1018+ queue = self.factory.makeSourcePackageBranchBuildJob()
1019+ build = queue.specific_job.build
1020+ transaction.commit()
1021+ build_url = canonical_url(build)
1022+ logout()
1023+
1024+ browser = self.getUserBrowser(build_url, user=experts)
1025+ browser.getLink('Rescore build').click()
1026+
1027+ self.assertEqual(
1028+ browser.getLink('Cancel').url,
1029+ build_url)
1030+
1031+ browser.getControl('Score').value = 'tentwentyfour'
1032+
1033+ browser.getControl('Rescore build').click()
1034+
1035+ self.assertEqual(
1036+ extract_text(find_tags_by_class(browser.contents, 'message')[1]),
1037+ 'Invalid integer data')
1038+
1039+ def test_rescore_build_not_admin(self):
1040+ """No one but admins can rescore a build."""
1041+ queue = self.factory.makeSourcePackageBranchBuildJob()
1042+ build = queue.specific_job.build
1043+ transaction.commit()
1044+ build_url = canonical_url(build)
1045+ logout()
1046+
1047+ browser = self.getUserBrowser(build_url, user=self.chef)
1048+ self.assertRaises(
1049+ LinkNotFoundError,
1050+ browser.getLink, 'Rescore build')
1051+
1052+ self.assertRaises(
1053+ Unauthorized,
1054+ self.getUserBrowser, build_url + '/+rescore', user=self.chef)
1055+
1056+ def test_rescore_build_wrong_state(self):
1057+ """If the build isn't queued, you can't rescore it."""
1058+ experts = getUtility(ILaunchpadCelebrities).bazaar_experts.teamowner
1059+ build = self.makeBranchBuild()
1060+ build.cancelBuild()
1061+ transaction.commit()
1062+ build_url = canonical_url(build)
1063+ logout()
1064+
1065+ browser = self.getUserBrowser(build_url, user=experts)
1066+ self.assertRaises(
1067+ LinkNotFoundError,
1068+ browser.getLink, 'Rescore build')
1069+
1070+ def test_rescore_build_wrong_state_stale_link(self):
1071+ """Show sane error if you attempt to rescore a non-queued build.
1072+
1073+ This is the case where the user has a stale link that they click on.
1074+ """
1075+ build = self.factory.makeSourcePackageBranchBuild()
1076+ build.cancelBuild()
1077+ experts = getUtility(ILaunchpadCelebrities).bazaar_experts.teamowner
1078+ index_url = canonical_url(build)
1079+ browser = self.getViewBrowser(build, '+rescore', user=experts)
1080+ self.assertEqual(index_url, browser.url)
1081+ self.assertIn(
1082+ 'Cannot rescore this build because it is not queued.',
1083+ browser.contents)
1084+
1085+ def test_rescore_build_wrong_state_stale_page(self):
1086+ """Show sane error if you attempt to rescore a non-queued build.
1087+
1088+ This is the case where the user is on the rescore page and submits.
1089+ """
1090+ build = self.factory.makeSourcePackageBranchBuild()
1091+ experts = getUtility(ILaunchpadCelebrities).bazaar_experts.teamowner
1092+ index_url = canonical_url(build)
1093+ browser = self.getViewBrowser(build, '+rescore', user=experts)
1094+ with person_logged_in(experts):
1095+ build.cancelBuild()
1096+ browser.getLink('Rescore build').click()
1097+ self.assertEqual(index_url, browser.url)
1098+ self.assertIn(
1099+ 'Cannot rescore this build because it is not queued.',
1100+ browser.contents)
1101+
1102+ def test_builder_history(self):
1103+ build = self.makeBranchBuild()
1104+ Store.of(build).flush()
1105+ build_url = canonical_url(build)
1106+ removeSecurityProxy(build).builder = self.factory.makeBuilder()
1107+ browser = self.getViewBrowser(build.builder, '+history')
1108+ self.assertTextMatchesExpressionIgnoreWhitespace(
1109+ 'Build history.*~chef/chocolate/cake branch build',
1110+ extract_text(find_main_content(browser.contents)))
1111+ self.assertEqual(build_url,
1112+ browser.getLink('~chef/chocolate/cake branch build').url)
1113
1114=== modified file 'lib/lp/code/configure.zcml'
1115--- lib/lp/code/configure.zcml 2011-06-02 20:30:39 +0000
1116+++ lib/lp/code/configure.zcml 2011-06-09 15:37:32 +0000
1117@@ -903,6 +903,49 @@
1118 interface="lp.code.interfaces.diff.IStaticDiffSource" />
1119 </securedutility>
1120
1121+ <!-- SourcePackageBranchBuild -->
1122+
1123+ <class
1124+ class="lp.code.model.sourcepackagebranchbuild.SourcePackageBranchBuild">
1125+ <require permission="launchpad.View" interface="lp.code.interfaces.sourcepackagebranchbuild.ISourcePackageBranchBuild"/>
1126+ <!-- This is needed for UploadProcessor to run. The permission isn't
1127+ important; launchpad.Edit isn't actually held by anybody. -->
1128+ <require permission="launchpad.Edit" set_attributes="status upload_log date_finished requester" />
1129+ </class>
1130+
1131+ <securedutility
1132+ component="lp.code.model.sourcepackagebranchbuild.SourcePackageBranchBuild"
1133+ provides="lp.code.interfaces.sourcepackagebranchbuild.ISourcePackageBranchBuildSource">
1134+ <allow interface="lp.code.interfaces.sourcepackagebranchbuild.ISourcePackageBranchBuildSource"/>
1135+ </securedutility>
1136+
1137+ <securedutility
1138+ component="lp.code.model.sourcepackagebranchbuild.SourcePackageBranchBuild"
1139+ provides="lp.buildmaster.interfaces.buildfarmjob.ISpecificBuildFarmJobSource"
1140+ name="BRANCHBUILD">
1141+ <allow interface="lp.buildmaster.interfaces.buildfarmjob.ISpecificBuildFarmJobSource"/>
1142+ </securedutility>
1143+
1144+ <class
1145+ class="lp.code.model.sourcepackagebranchbuild.SourcePackageBranchBuildJob">
1146+ <require
1147+ permission="launchpad.View"
1148+ interface="lp.code.interfaces.sourcepackagebranchbuild.ISourcePackageBranchBuildJob"/>
1149+ </class>
1150+
1151+ <securedutility
1152+ component="lp.code.model.sourcepackagebranchbuild.SourcePackageBranchBuildJob"
1153+ provides="lp.code.interfaces.sourcepackagebranchbuild.ISourcePackageBranchBuildJobSource">
1154+ <allow interface="lp.code.interfaces.sourcepackagebranchbuild.ISourcePackageBranchBuildJobSource"/>
1155+ </securedutility>
1156+
1157+ <adapter factory="lp.code.model.branchbuilder.BranchBuildBehavior"
1158+ permission="zope.Public" />
1159+
1160+ <utility component="lp.code.model.sourcepackagebranchbuild.SourcePackageBranchBuildJob"
1161+ name="BRANCHBUILD"
1162+ provides="lp.buildmaster.interfaces.buildfarmjob.IBuildFarmJob"/>
1163+
1164 <!-- SourcePackageRecipe -->
1165
1166 <securedutility
1167@@ -953,6 +996,7 @@
1168 <require permission="launchpad.View"
1169 interface="lp.code.interfaces.sourcepackagerecipe.ISourcePackageRecipeData"/>
1170 </class>
1171+
1172 <!-- SourcePackageRecipe -->
1173 <class
1174 class="lp.code.model.sourcepackagerecipe.SourcePackageRecipe">
1175
1176=== modified file 'lib/lp/code/doc/branch.txt'
1177--- lib/lp/code/doc/branch.txt 2011-05-27 19:53:20 +0000
1178+++ lib/lp/code/doc/branch.txt 2011-06-09 15:37:32 +0000
1179@@ -401,6 +401,7 @@
1180 productseries.branch
1181 productseries.translations_branch
1182 seriessourcepackagebranch.branch
1183+ sourcepackagebranchbuild.branch
1184 sourcepackagerecipedata.base_branch
1185 sourcepackagerecipedatainstruction.branch
1186 specificationbranch.branch
1187
1188=== modified file 'lib/lp/code/interfaces/branch.py'
1189--- lib/lp/code/interfaces/branch.py 2011-05-28 04:09:11 +0000
1190+++ lib/lp/code/interfaces/branch.py 2011-06-09 15:37:32 +0000
1191@@ -1026,6 +1026,27 @@
1192 required=False, readonly=True,
1193 vocabulary=ControlFormat))
1194
1195+ @call_with(requester=REQUEST_USER)
1196+ @operation_parameters(
1197+ archive=Reference(schema=Interface), # Really IArchive
1198+ distroseries=Reference(schema=Interface), # Really IDistroSeries
1199+ pocket=Choice(vocabulary=PackagePublishingPocket,),
1200+ revision_id=TextLine(title=_("Revision id to build"), required=False))
1201+ @operation_returns_entry(Interface)
1202+ @export_write_operation()
1203+ @operation_for_version("beta")
1204+ def requestBuild(archive, distroseries, requester, pocket, revision_id):
1205+ """Request that the branch be built in to the specified archive.
1206+
1207+ :param archive: The IArchive which you want the build to end up in.
1208+ :param requester: the person requesting the build.
1209+ :param pocket: the pocket that should be targeted.
1210+ :param revision_id: optional revision_id of the revision to build
1211+ (defaults to tip)
1212+ :raises: various specific upload errors if the requestor is not
1213+ able to upload to the archive.
1214+ """
1215+
1216
1217 class IBranchEdit(Interface):
1218 """IBranch attributes that require launchpad.Edit permission."""
1219
1220=== added file 'lib/lp/code/interfaces/sourcepackagebranchbuild.py'
1221--- lib/lp/code/interfaces/sourcepackagebranchbuild.py 1970-01-01 00:00:00 +0000
1222+++ lib/lp/code/interfaces/sourcepackagebranchbuild.py 2011-06-09 15:37:32 +0000
1223@@ -0,0 +1,131 @@
1224+# Copyright 2010 Canonical Ltd. This software is licensed under the
1225+# GNU Affero General Public License version 3 (see the file LICENSE).
1226+
1227+# pylint: disable-msg=E0213,E0211
1228+
1229+"""Interfaces for source package branch builds."""
1230+
1231+__metaclass__ = type
1232+__all__ = [
1233+ 'ISourcePackageBranchBuild',
1234+ 'ISourcePackageBranchBuildSource',
1235+ 'ISourcePackageBranchBuildJob',
1236+ 'ISourcePackageBranchBuildJobSource',
1237+ ]
1238+
1239+from lazr.restful.declarations import export_as_webservice_entry
1240+from lazr.restful.fields import (
1241+ CollectionField,
1242+ Reference,
1243+ )
1244+from zope.interface import Interface
1245+from zope.schema import (
1246+ Bool,
1247+ Int,
1248+ Object,
1249+ )
1250+
1251+from canonical.launchpad import _
1252+from lp.buildmaster.interfaces.buildfarmjob import ISpecificBuildFarmJobSource
1253+from lp.buildmaster.interfaces.packagebuild import IPackageBuild
1254+from lp.code.interfaces.branch import IBranch
1255+from lp.code.interfaces.revision import IRevision
1256+#from lp.code.interfaces.sourcepackagerecipe import (
1257+# ISourcePackageRecipeData,
1258+# )
1259+from lp.registry.interfaces.distroseries import IDistroSeries
1260+from lp.registry.interfaces.person import IPerson
1261+from lp.services.job.interfaces.job import IJob
1262+from lp.soyuz.interfaces.binarypackagebuild import IBinaryPackageBuild
1263+from lp.soyuz.interfaces.buildfarmbuildjob import IBuildFarmBuildJob
1264+from lp.soyuz.interfaces.sourcepackagerelease import ISourcePackageRelease
1265+
1266+
1267+class ISourcePackageBranchBuild(IPackageBuild):
1268+ """A build of a source package."""
1269+ export_as_webservice_entry()
1270+
1271+ id = Int(title=_("Identifier for this build."))
1272+
1273+ binary_builds = CollectionField(
1274+ Reference(IBinaryPackageBuild),
1275+ title=_("The binary builds that resulted from this."), readonly=True)
1276+
1277+ distroseries = Reference(
1278+ IDistroSeries, title=_("The distroseries being built for"),
1279+ readonly=True)
1280+
1281+ requester = Object(
1282+ schema=IPerson, required=False,
1283+ title=_("The person who wants this to be done."))
1284+
1285+ branch = Object(
1286+ schema=IBranch, title=_("The branch being built."), required=False)
1287+
1288+ revision = Object(schema=IRevision,
1289+ title=_("The revision in the branch being built."),
1290+ required=False)
1291+
1292+# manifest = Object(
1293+# schema=ISourcePackageRecipeData, title=_(
1294+# 'A snapshot of the recipe for this build.'))
1295+
1296+# def getManifestText():
1297+# """The text of the manifest for this build."""
1298+
1299+ source_package_release = Reference(
1300+ ISourcePackageRelease, title=_("The produced source package release"),
1301+ readonly=True)
1302+
1303+ is_virtualized = Bool(title=_('If True, this build is virtualized.'))
1304+
1305+ def getFileByName(filename):
1306+ """Return the file under +files with specified name."""
1307+
1308+ def cancelBuild():
1309+ """Cancel the build."""
1310+
1311+ def destroySelf():
1312+ """Delete the build itself."""
1313+
1314+
1315+class ISourcePackageBranchBuildSource(ISpecificBuildFarmJobSource):
1316+ """A utility of this interface be used to create source package builds."""
1317+
1318+ def new(distroseries, branch, requester, archive, date_created=None):
1319+ """Create an `ISourcePackageBranchBuild`.
1320+
1321+ :param distroseries: The `IDistroSeries` that this is building
1322+ against.
1323+ :param branch: The `IBranch` that this is building.
1324+ :param requester: The `IPerson` who wants to build it.
1325+ :param date_created: The date this build record was created. If not
1326+ provided, defaults to now.
1327+ :return: `ISourcePackageBranchBuild`.
1328+ """
1329+
1330+ def makeDailyBuilds(logger=None):
1331+ """Create and return builds for stale ISourcePackageBranches.
1332+
1333+ :param logger: An optional logger to write debug info to.
1334+ """
1335+
1336+
1337+class ISourcePackageBranchBuildJob(IBuildFarmBuildJob):
1338+ """A read-only interface for branch build jobs."""
1339+
1340+ job = Reference(
1341+ IJob, title=_("Job"), required=True, readonly=True,
1342+ description=_("Data common to all job types."))
1343+
1344+
1345+class ISourcePackageBranchBuildJobSource(Interface):
1346+ """A utility of this interface used to create _things_."""
1347+
1348+ def new(build, job):
1349+ """Create a new `ISourcePackageBranchBuildJob`.
1350+
1351+ :param build: An `ISourcePackageBranchBuild`.
1352+ :param job: An `IJob`.
1353+ :return: `ISourcePackageBranchBuildJob`.
1354+ """
1355
1356=== modified file 'lib/lp/code/interfaces/sourcepackagerecipebuild.py'
1357--- lib/lp/code/interfaces/sourcepackagerecipebuild.py 2011-05-04 03:49:28 +0000
1358+++ lib/lp/code/interfaces/sourcepackagerecipebuild.py 2011-06-09 15:37:32 +0000
1359@@ -3,7 +3,7 @@
1360
1361 # pylint: disable-msg=E0213,E0211
1362
1363-"""Interfaces for source package builds."""
1364+"""Interfaces for source package recipe builds."""
1365
1366 __metaclass__ = type
1367 __all__ = [
1368
1369=== added file 'lib/lp/code/mail/sourcepackagebranchbuild.py'
1370--- lib/lp/code/mail/sourcepackagebranchbuild.py 1970-01-01 00:00:00 +0000
1371+++ lib/lp/code/mail/sourcepackagebranchbuild.py 2011-06-09 15:37:32 +0000
1372@@ -0,0 +1,86 @@
1373+# Copyright 2010 Canonical Ltd. This software is licensed under the
1374+# GNU Affero General Public License version 3 (see the file LICENSE).
1375+
1376+__metaclass__ = type
1377+
1378+__all__ = [
1379+ 'SourcePackageBranchBuildMailer',
1380+ ]
1381+
1382+
1383+from canonical.config import config
1384+from canonical.launchpad.webapp import canonical_url
1385+from lp.app.browser.tales import DurationFormatterAPI
1386+from lp.services.mail.basemailer import (
1387+ BaseMailer,
1388+ RecipientReason,
1389+ )
1390+
1391+
1392+class SourcePackageBranchBuildMailer(BaseMailer):
1393+
1394+ @classmethod
1395+ def forStatus(cls, build):
1396+ """Create a mailer for notifying about build status.
1397+
1398+ :param build: The build to notify about the state of.
1399+ """
1400+ requester = build.requester
1401+ recipients = {requester: RecipientReason.forBuildRequester(requester)}
1402+ return cls(
1403+ '[branch build #%(build_id)d] of ~%(branch_owner)s %(branch)s in'
1404+ ' %(distroseries)s: %(status)s',
1405+ 'branch-build-request.txt', recipients,
1406+ config.canonical.noreply_from_address, build)
1407+
1408+ def __init__(self, subject, body_template, recipients, from_address,
1409+ build):
1410+ BaseMailer.__init__(
1411+ self, subject, body_template, recipients, from_address,
1412+ notification_type='branch-build-status')
1413+ self.build = build
1414+
1415+ def _getHeaders(self, email):
1416+ """See `BaseMailer`"""
1417+ headers = super(
1418+ SourcePackageBranchBuildMailer, self)._getHeaders(email)
1419+ headers.update({
1420+ 'X-Launchpad-Build-State': self.build.status.name,
1421+ })
1422+ return headers
1423+
1424+ def _getTemplateParams(self, email, recipient):
1425+ """See `BaseMailer`"""
1426+ params = super(
1427+ SourcePackageBranchBuildMailer, self)._getTemplateParams(
1428+ email, recipient)
1429+ params.update({
1430+ 'status': self.build.status.title,
1431+ 'build_id': self.build.id,
1432+ 'distroseries': self.build.distroseries.name,
1433+ 'branch': self.build.branch.name,
1434+ 'branch_owner': self.build.branch.owner.name,
1435+ 'archive': self.build.archive.name,
1436+ 'archive_owner': self.build.archive.owner.name,
1437+ 'log_url': '',
1438+ 'component': self.build.current_component.name,
1439+ 'duration': '',
1440+ 'builder_url': '',
1441+ 'build_url': canonical_url(self.build),
1442+ 'upload_log_url': '',
1443+ })
1444+ if self.build.builder is not None:
1445+ params['builder_url'] = canonical_url(self.build.builder)
1446+ if self.build.duration is not None:
1447+ duration_formatter = DurationFormatterAPI(self.build.duration)
1448+ params['duration'] = duration_formatter.approximateduration()
1449+ if self.build.log is not None:
1450+ params['log_url'] = self.build.log.getURL()
1451+ if self.build.upload_log is not None:
1452+ params['upload_log_url'] = self.build.upload_log_url
1453+ return params
1454+
1455+ def _getFooter(self, params):
1456+ """See `BaseMailer`"""
1457+ return ('%(build_url)s\n'
1458+ '%(reason)s\n' % params)
1459
1460=== modified file 'lib/lp/code/mail/sourcepackagerecipebuild.py'
1461--- lib/lp/code/mail/sourcepackagerecipebuild.py 2011-03-04 01:04:06 +0000
1462+++ lib/lp/code/mail/sourcepackagerecipebuild.py 2011-06-09 15:37:32 +0000
1463@@ -30,7 +30,7 @@
1464 return cls(
1465 '[recipe build #%(build_id)d] of ~%(recipe_owner)s %(recipe)s in'
1466 ' %(distroseries)s: %(status)s',
1467- 'build-request.txt', recipients,
1468+ 'recipe-build-request.txt', recipients,
1469 config.canonical.noreply_from_address, build)
1470
1471 def __init__(self, subject, body_template, recipients, from_address,
1472
1473=== added file 'lib/lp/code/mail/tests/test_sourcepackagebranchbuild.py'
1474--- lib/lp/code/mail/tests/test_sourcepackagebranchbuild.py 1970-01-01 00:00:00 +0000
1475+++ lib/lp/code/mail/tests/test_sourcepackagebranchbuild.py 2011-06-09 15:37:32 +0000
1476@@ -0,0 +1,132 @@
1477+# Copyright 2010 Canonical Ltd. This software is licensed under the
1478+# GNU Affero General Public License version 3 (see the file LICENSE).
1479+
1480+
1481+__metaclass__ = type
1482+
1483+
1484+from datetime import timedelta
1485+from unittest import TestLoader
1486+
1487+from storm.locals import Store
1488+from zope.security.proxy import removeSecurityProxy
1489+
1490+from canonical.config import config
1491+from canonical.launchpad.webapp import canonical_url
1492+from canonical.testing.layers import LaunchpadFunctionalLayer
1493+from lp.buildmaster.enums import BuildStatus
1494+from lp.code.mail.sourcepackagerecipebuild import (
1495+ SourcePackageRecipeBuildMailer,
1496+ )
1497+from lp.testing import TestCaseWithFactory
1498+
1499+
1500+expected_body = u"""\
1501+ * State: Successfully built
1502+ * Recipe: person/recipe
1503+ * Archive: archiveowner/ppa
1504+ * Distroseries: distroseries
1505+ * Duration: five minutes
1506+ * Build Log: %s
1507+ * Upload Log:
1508+ * Builder: http://launchpad.dev/builders/bob
1509+"""
1510+
1511+superseded_body = u"""\
1512+ * State: Build for superseded Source
1513+ * Recipe: person/recipe
1514+ * Archive: archiveowner/ppa
1515+ * Distroseries: distroseries
1516+ * Duration:
1517+ * Build Log:
1518+ * Upload Log:
1519+ * Builder:
1520+"""
1521+
1522+class TestSourcePackageRecipeBuildMailer(TestCaseWithFactory):
1523+
1524+ layer = LaunchpadFunctionalLayer
1525+
1526+ def makeStatusEmail(self, build):
1527+ mailer = SourcePackageRecipeBuildMailer.forStatus(build)
1528+ email = removeSecurityProxy(build.requester).preferredemail.email
1529+ return mailer.generateEmail(email, build.requester)
1530+
1531+ def test_generateEmail(self):
1532+ """GenerateEmail produces the right headers and body."""
1533+ person = self.factory.makePerson(name='person')
1534+ cake = self.factory.makeSourcePackageRecipe(
1535+ name=u'recipe', owner=person)
1536+ pantry_owner = self.factory.makePerson(name='archiveowner')
1537+ pantry = self.factory.makeArchive(name='ppa', owner=pantry_owner)
1538+ secret = self.factory.makeDistroSeries(name=u'distroseries')
1539+ build = self.factory.makeSourcePackageRecipeBuild(
1540+ recipe=cake, distroseries=secret, archive=pantry,
1541+ status=BuildStatus.FULLYBUILT, duration=timedelta(minutes=5))
1542+ naked_build = removeSecurityProxy(build)
1543+ naked_build.builder = self.factory.makeBuilder(name='bob')
1544+ naked_build.log = self.factory.makeLibraryFileAlias()
1545+ Store.of(build).flush()
1546+ ctrl = self.makeStatusEmail(build)
1547+ self.assertEqual(
1548+ u'[recipe build #%d] of ~person recipe in distroseries: '
1549+ 'Successfully built' % (build.id), ctrl.subject)
1550+ body, footer = ctrl.body.split('\n-- \n')
1551+ self.assertEqual(
1552+ expected_body % build.log.getURL(), body)
1553+ build_url = canonical_url(build)
1554+ self.assertEqual(
1555+ '%s\nYou are the requester of the build.\n' % build_url, footer)
1556+ self.assertEqual(
1557+ config.canonical.noreply_from_address, ctrl.from_addr)
1558+ self.assertEqual(
1559+ 'Requester', ctrl.headers['X-Launchpad-Message-Rationale'])
1560+ self.assertEqual(
1561+ 'recipe-build-status',
1562+ ctrl.headers['X-Launchpad-Notification-Type'])
1563+ self.assertEqual(
1564+ 'FULLYBUILT', ctrl.headers['X-Launchpad-Build-State'])
1565+
1566+ def test_generateEmail_with_null_fields(self):
1567+ """GenerateEmail works when many fields are NULL."""
1568+ person = self.factory.makePerson(name='person')
1569+ cake = self.factory.makeSourcePackageRecipe(
1570+ name=u'recipe', owner=person)
1571+ pantry_owner = self.factory.makePerson(name='archiveowner')
1572+ pantry = self.factory.makeArchive(name='ppa', owner=pantry_owner)
1573+ secret = self.factory.makeDistroSeries(name=u'distroseries')
1574+ build = self.factory.makeSourcePackageRecipeBuild(
1575+ recipe=cake, distroseries=secret, archive=pantry,
1576+ status=BuildStatus.SUPERSEDED)
1577+ Store.of(build).flush()
1578+ ctrl = self.makeStatusEmail(build)
1579+ self.assertEqual(
1580+ u'[recipe build #%d] of ~person recipe in distroseries: '
1581+ 'Build for superseded Source' % (build.id), ctrl.subject)
1582+ body, footer = ctrl.body.split('\n-- \n')
1583+ self.assertEqual(superseded_body, body)
1584+ build_url = canonical_url(build)
1585+ self.assertEqual(
1586+ '%s\nYou are the requester of the build.\n' % build_url, footer)
1587+ self.assertEqual(
1588+ config.canonical.noreply_from_address, ctrl.from_addr)
1589+ self.assertEqual(
1590+ 'Requester', ctrl.headers['X-Launchpad-Message-Rationale'])
1591+ self.assertEqual(
1592+ 'recipe-build-status',
1593+ ctrl.headers['X-Launchpad-Notification-Type'])
1594+ self.assertEqual(
1595+ 'SUPERSEDED', ctrl.headers['X-Launchpad-Build-State'])
1596+
1597+ def test_generateEmail_upload_failure(self):
1598+ """GenerateEmail works when many fields are NULL."""
1599+ build = self.factory.makeSourcePackageRecipeBuild()
1600+ removeSecurityProxy(build).upload_log = (
1601+ self.factory.makeLibraryFileAlias())
1602+ upload_log_fragment = 'Upload Log: %s' % build.upload_log_url
1603+ ctrl = self.makeStatusEmail(build)
1604+ self.assertTrue(upload_log_fragment in ctrl.body)
1605+
1606+
1607+def test_suite():
1608+ return TestLoader().loadTestsFromName(__name__)
1609
1610=== modified file 'lib/lp/code/model/branch.py'
1611--- lib/lp/code/model/branch.py 2011-06-06 20:56:43 +0000
1612+++ lib/lp/code/model/branch.py 2011-06-09 15:37:32 +0000
1613@@ -64,7 +64,10 @@
1614 )
1615 from canonical.launchpad.helpers import shortlist
1616 from canonical.launchpad.interfaces.launchpad import IPrivacy
1617-from canonical.launchpad.interfaces.lpstorm import IMasterStore
1618+from canonical.launchpad.interfaces.lpstorm import (
1619+ IMasterStore,
1620+ IStore,
1621+ )
1622 from canonical.launchpad.webapp import urlappend
1623 from lp.app.errors import UserCannotUnsubscribePerson
1624 from lp.app.interfaces.launchpad import ILaunchpadCelebrities
1625@@ -73,7 +76,10 @@
1626 IBugTaskSet,
1627 )
1628 from lp.bugs.interfaces.bugtaskfilter import filter_bugtasks_by_context
1629+from lp.buildmaster.enums import BuildStatus
1630 from lp.buildmaster.model.buildqueue import BuildQueue
1631+from lp.buildmaster.model.buildfarmjob import BuildFarmJob
1632+from lp.buildmaster.model.packagebuild import PackageBuild
1633 from lp.code.bzr import (
1634 BranchFormat,
1635 ControlFormat,
1636@@ -92,9 +98,12 @@
1637 BranchMergeProposalExists,
1638 BranchTargetError,
1639 BranchTypeError,
1640+ BuildAlreadyPending,
1641+ BuildNotAllowedForDistro,
1642 CannotDeleteBranch,
1643 InvalidBranchMergeProposal,
1644 InvalidMergeQueueConfig,
1645+ TooManyBuilds,
1646 )
1647 from lp.code.event.branchmergeproposal import (
1648 BranchMergeProposalNeedsReviewEvent,
1649@@ -124,6 +133,9 @@
1650 from lp.code.interfaces.seriessourcepackagebranch import (
1651 IFindOfficialBranchLinks,
1652 )
1653+from lp.code.interfaces.sourcepackagebranchbuild import (
1654+ ISourcePackageBranchBuildSource,
1655+ )
1656 from lp.code.mail.branch import send_branch_modified_notifications
1657 from lp.code.model.branchmergeproposal import (
1658 BranchMergeProposal,
1659@@ -135,17 +147,40 @@
1660 Revision,
1661 RevisionAuthor,
1662 )
1663+from lp.code.model.sourcepackagebranchbuild import SourcePackageBranchBuild
1664 from lp.code.model.seriessourcepackagebranch import SeriesSourcePackageBranch
1665 from lp.codehosting.bzrutils import safe_open
1666 from lp.registry.interfaces.person import (
1667 validate_person,
1668 validate_public_person,
1669 )
1670+from lp.registry.interfaces.pocket import PackagePublishingPocket
1671 from lp.services.database.bulk import load_related
1672 from lp.services.job.interfaces.job import JobStatus
1673 from lp.services.job.model.job import Job
1674 from lp.services.mail.notificationrecipientset import NotificationRecipientSet
1675 from lp.services.propertycache import cachedproperty
1676+from lp.soyuz.interfaces.archive import IArchiveSet
1677+from lp.registry.interfaces.distroseries import IDistroSeriesSet
1678+
1679+
1680+class NonPPABuildRequest(Exception):
1681+ """A build was requested to a non-PPA and this is currently
1682+ unsupported."""
1683+
1684+
1685+def get_buildable_distroseries_set(user):
1686+ ppas = getUtility(IArchiveSet).getPPAsForUser(user)
1687+ supported_distros = set([ppa.distribution for ppa in ppas])
1688+ # Now add in Ubuntu.
1689+ supported_distros.add(getUtility(ILaunchpadCelebrities).ubuntu)
1690+ distros = getUtility(IDistroSeriesSet).search()
1691+
1692+ buildables = []
1693+ for distro in distros:
1694+ if distro.active and distro.distribution in supported_distros:
1695+ buildables.append(distro)
1696+ return buildables
1697
1698
1699 class Branch(SQLBase, BzrIdentityMixin):
1700@@ -1101,6 +1136,12 @@
1701 TranslationTemplatesBuild,
1702 TranslationTemplatesBuild.branch == self).remove()
1703
1704+ def _deleteBuildReferencess(self):
1705+ store = Store.of(self)
1706+ builds = store.find(
1707+ SourcePackageBranchBuild, SourcePackageBranchBuild.branch==self)
1708+ builds.set(branch_id=None)
1709+
1710 def destroySelf(self, break_references=False):
1711 """See `IBranch`."""
1712 from lp.code.interfaces.branchjob import IReclaimBranchSpaceJobSource
1713@@ -1112,6 +1153,7 @@
1714
1715 self._deleteBranchSubscriptions()
1716 self._deleteJobs()
1717+ self._deleteBuildReferencess()
1718
1719 # Now destroy the branch.
1720 branch_id = self.id
1721@@ -1217,6 +1259,44 @@
1722 except ValueError: # The json string is invalid
1723 raise InvalidMergeQueueConfig
1724
1725+ def requestBuild(self, archive, requester, distroseries,
1726+ pocket=PackagePublishingPocket.RELEASE,
1727+ manual=False, revision_id=None):
1728+ """See `IBranchView`."""
1729+ if not archive.is_ppa:
1730+ raise NonPPABuildRequest
1731+
1732+ buildable_distros = get_buildable_distroseries_set(archive.owner)
1733+ if distroseries not in buildable_distros:
1734+ raise BuildNotAllowedForDistro(self, distroseries)
1735+
1736+ reject_reason = archive.checkUpload(
1737+ requester, self.distroseries, None, archive.default_component,
1738+ pocket)
1739+ if reject_reason is not None:
1740+ raise reject_reason
1741+ if SourcePackageBranchBuild.getRecentBuilds(
1742+ requester, self, distroseries).count() >= 5:
1743+ raise TooManyBuilds(self, distroseries)
1744+ pending = IStore(self).find(SourcePackageBranchBuild,
1745+ SourcePackageBranchBuild.branch_id == self.id,
1746+ SourcePackageBranchBuild.distroseries_id == distroseries.id,
1747+ PackageBuild.archive_id == archive.id,
1748+ PackageBuild.id == SourcePackageBranchBuild.package_build_id,
1749+ BuildFarmJob.id == PackageBuild.build_farm_job_id,
1750+ BuildFarmJob.status == BuildStatus.NEEDSBUILD)
1751+ if pending.any() is not None:
1752+ raise BuildAlreadyPending(self, distroseries)
1753+
1754+ build = getUtility(ISourcePackageBranchBuildSource).new(distroseries,
1755+ self, requester, archive)
1756+ build.queueBuild()
1757+ queue_record = build.buildqueue_record
1758+ if manual:
1759+ queue_record.manualScore(queue_record.lastscore + 100)
1760+ return build
1761+
1762+
1763
1764 class DeletionOperation:
1765 """Represent an operation to perform as part of branch deletion."""
1766
1767=== added file 'lib/lp/code/model/branchbuilder.py'
1768--- lib/lp/code/model/branchbuilder.py 1970-01-01 00:00:00 +0000
1769+++ lib/lp/code/model/branchbuilder.py 2011-06-09 15:37:32 +0000
1770@@ -0,0 +1,202 @@
1771+# Copyright 2010 Canonical Ltd. This software is licensed under the
1772+# GNU Affero General Public License version 3 (see the file LICENSE).
1773+
1774+"""Code to build branches on the buildfarm."""
1775+
1776+__metaclass__ = type
1777+__all__ = [
1778+ 'BranchBuildBehavior',
1779+ ]
1780+
1781+import traceback
1782+
1783+from zope.component import adapts
1784+from zope.interface import implements
1785+from zope.security.proxy import removeSecurityProxy
1786+
1787+from canonical.config import config
1788+from lp.buildmaster.interfaces.builder import CannotBuild
1789+from lp.buildmaster.interfaces.buildfarmjobbehavior import (
1790+ IBuildFarmJobBehavior,
1791+ )
1792+from lp.buildmaster.model.buildfarmjobbehavior import BuildFarmJobBehaviorBase
1793+from lp.code.interfaces.sourcepackagebranchbuild import (
1794+ ISourcePackageBranchBuildJob,
1795+ )
1796+from lp.registry.interfaces.pocket import PackagePublishingPocket
1797+from lp.soyuz.adapters.archivedependencies import (
1798+ get_primary_current_component,
1799+ get_sources_list_for_building,
1800+ )
1801+
1802+
1803+class BranchBuildBehavior(BuildFarmJobBehaviorBase):
1804+ """How to build a branch on the build farm."""
1805+
1806+ adapts(ISourcePackageBranchBuildJob)
1807+ implements(IBuildFarmJobBehavior)
1808+
1809+ status = None
1810+
1811+ @property
1812+ def build(self):
1813+ return self.buildfarmjob.build
1814+
1815+ @property
1816+ def display_name(self):
1817+ ret = "%s, %s" % (
1818+ self.build.distroseries.displayname, self.build.branch.unique_name)
1819+ if self._builder is not None:
1820+ ret += " (on %s)" % self._builder.url
1821+ return ret
1822+
1823+ def logStartBuild(self, logger):
1824+ """See `IBuildFarmJobBehavior`."""
1825+ logger.info("startBuild(%s)", self.display_name)
1826+
1827+ def _extraBuildArgs(self, distroarchseries, logger=None):
1828+ """
1829+ Return the extra arguments required by the slave for the given build.
1830+ """
1831+ # Build extra arguments.
1832+ args = {}
1833+ suite = self.build.distroseries.name
1834+ if self.build.pocket != PackagePublishingPocket.RELEASE:
1835+ suite += "-%s" % (self.build.pocket.name.lower())
1836+ args['suite'] = suite
1837+ args['arch_tag'] = distroarchseries.architecturetag
1838+ requester = self.build.requester
1839+ if requester.preferredemail is None:
1840+ # Use a constant, known, name and email.
1841+ args["author_name"] = 'Launchpad Package Builder'
1842+ args["author_email"] = config.canonical.noreply_from_address
1843+ else:
1844+ args["author_name"] = requester.displayname
1845+ # We have to remove the security proxy here b/c there's not a
1846+ # logged in entity, and anonymous email lookups aren't allowed.
1847+ # Don't keep the naked requester around though.
1848+ args["author_email"] = removeSecurityProxy(
1849+ requester).preferredemail.email
1850+ args["branch_url"] = str(self.build.branch.bzr_identity)
1851+ args["revision_id"] = self.build.revision_id
1852+ args['archive_purpose'] = self.build.archive.purpose.name
1853+ args["ogrecomponent"] = get_primary_current_component(
1854+ self.build.archive, self.build.distroseries,
1855+ None)
1856+ args['archives'] = get_sources_list_for_building(self.build,
1857+ distroarchseries, None)
1858+
1859+ # config.builddmaster.bzr_builder_sources_list can contain a
1860+ # sources.list entry for an archive that will contain a
1861+ # bzr-builder package that needs to be used to build this
1862+ # branch.
1863+ try:
1864+ extra_archive = config.builddmaster.bzr_builder_sources_list
1865+ except AttributeError:
1866+ extra_archive = None
1867+
1868+ if extra_archive is not None:
1869+ try:
1870+ sources_line = extra_archive % (
1871+ {'series': self.build.distroseries.name})
1872+ args['archives'].append(sources_line)
1873+ except StandardError:
1874+ # Someone messed up the config, don't add it.
1875+ if logger:
1876+ logger.error(
1877+ "Exception processing bzr_builder_sources_list:\n%s"
1878+ % traceback.format_exc())
1879+
1880+ args['distroseries_name'] = self.build.distroseries.name
1881+ return args
1882+
1883+ def dispatchBuildToSlave(self, build_queue_id, logger):
1884+ """See `IBuildFarmJobBehavior`."""
1885+
1886+ distroseries = self.build.distroseries
1887+ # Start the binary package build on the slave builder. First
1888+ # we send the chroot.
1889+ distroarchseries = distroseries.getDistroArchSeriesByProcessor(
1890+ self._builder.processor)
1891+ if distroarchseries is None:
1892+ raise CannotBuild("Unable to find distroarchseries for %s in %s" %
1893+ (self._builder.processor.name,
1894+ self.build.distroseries.displayname))
1895+ args = self._extraBuildArgs(distroarchseries, logger)
1896+ chroot = distroarchseries.getChroot()
1897+ if chroot is None:
1898+ raise CannotBuild("Unable to find a chroot for %s" %
1899+ distroarchseries.displayname)
1900+ logger.info(
1901+ "Sending chroot file for branch build to %s" % self._builder.name)
1902+ d = self._builder.slave.cacheFile(logger, chroot)
1903+
1904+ def got_cache_file(ignored):
1905+ # Generate a string which can be used to cross-check when obtaining
1906+ # results so we know we are referring to the right database object in
1907+ # subsequent runs.
1908+ buildid = "%s-%s" % (self.build.id, build_queue_id)
1909+ cookie = self.buildfarmjob.generateSlaveBuildCookie()
1910+ chroot_sha1 = chroot.content.sha1
1911+ logger.info(
1912+ "Initiating build %s on %s" % (buildid, self._builder.url))
1913+
1914+ return self._builder.slave.build(
1915+ cookie, "sourcepackagebranch", chroot_sha1, {}, args)
1916+
1917+ def log_build_result((status, info)):
1918+ message = """%s (%s):
1919+ ***** RESULT *****
1920+ %s
1921+ %s: %s
1922+ ******************
1923+ """ % (
1924+ self._builder.name,
1925+ self._builder.url,
1926+ args,
1927+ status,
1928+ info,
1929+ )
1930+ logger.info(message)
1931+
1932+ return d.addCallback(got_cache_file).addCallback(log_build_result)
1933+
1934+ def verifyBuildRequest(self, logger):
1935+ """Assert some pre-build checks.
1936+
1937+ The build request is checked:
1938+ * Virtualized builds can't build on a non-virtual builder
1939+ * Ensure that we have a chroot
1940+ * Ensure that the build pocket allows builds for the current
1941+ distroseries state.
1942+ """
1943+ build = self.build
1944+ assert not (not self._builder.virtualized and build.is_virtualized), (
1945+ "Attempt to build virtual item on a non-virtual builder.")
1946+
1947+ # This should already have been checked earlier, but just check again
1948+ # here in case of programmer errors.
1949+ reason = build.archive.checkUploadToPocket(
1950+ build.distroseries, build.pocket)
1951+ assert reason is None, (
1952+ "%s (%s) can not be built for pocket %s: invalid pocket due "
1953+ "to the series status of %s." %
1954+ (build.title, build.id, build.pocket.name,
1955+ build.distroseries.name))
1956+
1957+ def updateSlaveStatus(self, raw_slave_status, status):
1958+ """Parse the branch build specific status info into the status dict.
1959+
1960+ This includes:
1961+ * filemap => dictionary or None
1962+ * dependencies => string or None
1963+ """
1964+ build_status_with_files = (
1965+ 'BuildStatus.OK',
1966+ 'BuildStatus.PACKAGEFAIL',
1967+ 'BuildStatus.DEPFAIL',
1968+ )
1969+ if (status['builder_status'] == 'BuilderStatus.WAITING' and
1970+ status['build_status'] in build_status_with_files):
1971+ status['filemap'] = raw_slave_status[3]
1972+ status['dependencies'] = raw_slave_status[4]
1973
1974=== added file 'lib/lp/code/model/sourcepackagebranchbuild.py'
1975--- lib/lp/code/model/sourcepackagebranchbuild.py 1970-01-01 00:00:00 +0000
1976+++ lib/lp/code/model/sourcepackagebranchbuild.py 2011-06-09 15:37:32 +0000
1977@@ -0,0 +1,362 @@
1978+# Copyright 2010 Canonical Ltd. This software is licensed under the
1979+# GNU Affero General Public License version 3 (see the file LICENSE).
1980+
1981+# pylint: disable-msg=F0401,E1002
1982+
1983+"""Implementation code for source package builds."""
1984+
1985+__metaclass__ = type
1986+__all__ = [
1987+ 'SourcePackageBranchBuild',
1988+ ]
1989+
1990+from datetime import (
1991+ datetime,
1992+ timedelta,
1993+ )
1994+
1995+from pytz import utc
1996+from storm.locals import (
1997+ Int,
1998+ Reference,
1999+ Storm,
2000+ )
2001+from storm.store import Store
2002+from zope.component import (
2003+ getUtility,
2004+ )
2005+from zope.interface import (
2006+ classProvides,
2007+ implements,
2008+ )
2009+
2010+from canonical.database.constants import UTC_NOW
2011+from canonical.launchpad.browser.librarian import ProxiedLibraryFileAlias
2012+from canonical.launchpad.interfaces.lpstorm import (
2013+ IMasterStore,
2014+ )
2015+from lp.app.errors import NotFoundError
2016+from lp.buildmaster.enums import (
2017+ BuildFarmJobType,
2018+ BuildStatus,
2019+ )
2020+from lp.buildmaster.model.buildfarmjob import BuildFarmJobOldDerived
2021+from lp.buildmaster.model.buildqueue import BuildQueue
2022+from lp.buildmaster.model.packagebuild import (
2023+ PackageBuild,
2024+ PackageBuildDerived,
2025+ )
2026+from lp.code.interfaces.sourcepackagebranchbuild import (
2027+ ISourcePackageBranchBuild,
2028+ ISourcePackageBranchBuildJob,
2029+ ISourcePackageBranchBuildJobSource,
2030+ ISourcePackageBranchBuildSource,
2031+ )
2032+from lp.code.mail.sourcepackagebranchbuild import (
2033+ SourcePackageBranchBuildMailer,
2034+ )
2035+#from lp.code.model.sourcepackagerecipedata import SourcePackageRecipeData
2036+from lp.registry.interfaces.pocket import PackagePublishingPocket
2037+from lp.services.job.model.job import Job
2038+from lp.soyuz.model.binarypackagebuild import BinaryPackageBuild
2039+from lp.soyuz.model.buildfarmbuildjob import BuildFarmBuildJob
2040+from lp.soyuz.model.sourcepackagerelease import SourcePackageRelease
2041+
2042+
2043+class SourcePackageBranchBuild(PackageBuildDerived, Storm):
2044+
2045+ __storm_table__ = 'SourcePackageBranchBuild'
2046+
2047+ implements(ISourcePackageBranchBuild)
2048+ classProvides(ISourcePackageBranchBuildSource)
2049+
2050+ package_build_id = Int(name='package_build', allow_none=False)
2051+ package_build = Reference(package_build_id, 'PackageBuild.id')
2052+
2053+ build_farm_job_type = BuildFarmJobType.BRANCHBUILD
2054+
2055+ id = Int(primary=True)
2056+
2057+ is_private = False
2058+
2059+ # The list of build status values for which email notifications are
2060+ # allowed to be sent. It is up to each callback as to whether it will
2061+ # consider sending a notification but it won't do so if the status is not
2062+ # in this list.
2063+ ALLOWED_STATUS_NOTIFICATIONS = [
2064+ 'OK', 'PACKAGEFAIL', 'DEPFAIL', 'CHROOTFAIL']
2065+
2066+ @property
2067+ def binary_builds(self):
2068+ """See `ISourcePackageBranchBuild`."""
2069+ return Store.of(self).find(BinaryPackageBuild,
2070+ BinaryPackageBuild.source_package_release ==
2071+ SourcePackageRelease.id,
2072+ SourcePackageRelease.source_package_branch_build == self.id)
2073+
2074+ @property
2075+ def current_component(self):
2076+ # Only PPAs currently have a sane default component at the
2077+ # moment, but we only support branch builds for PPAs.
2078+ component = self.archive.default_component
2079+ assert component is not None
2080+ return component
2081+
2082+ distroseries_id = Int(name='distroseries', allow_none=True)
2083+ distroseries = Reference(distroseries_id, 'DistroSeries.id')
2084+ distro_series = distroseries
2085+
2086+ @property
2087+ def distribution(self):
2088+ """See `IPackageBuild`."""
2089+ return self.distroseries.distribution
2090+
2091+ is_virtualized = True
2092+
2093+ branch_id = Int(name='branch', allow_none=True)
2094+ branch = Reference(branch_id, 'Branch.id')
2095+
2096+ revision_id = Int(name='revision', allow_none=True)
2097+ revision = Reference(revision_id, 'Revision.id')
2098+
2099+# manifest = Reference(
2100+# id, 'SourcePackageRecipeData.sourcepackage_recipe_build_id',
2101+# on_remote=True)
2102+#
2103+# def setManifestText(self, text):
2104+# if text is None:
2105+# if self.manifest is not None:
2106+# IStore(self.manifest).remove(self.manifest)
2107+# elif self.manifest is None:
2108+# SourcePackageRecipeData.createManifestFromText(text, self)
2109+# else:
2110+# from bzrlib.plugins.builder.recipe import RecipeParser
2111+# self.manifest.setRecipe(RecipeParser(text).parse())
2112+#
2113+# def getManifestText(self):
2114+# if self.manifest is None:
2115+# return None
2116+# return str(self.manifest.getRecipe())
2117+
2118+ requester_id = Int(name='requester', allow_none=False)
2119+ requester = Reference(requester_id, 'Person.id')
2120+
2121+ @property
2122+ def buildqueue_record(self):
2123+ """See `IBuildFarmJob`."""
2124+ store = Store.of(self)
2125+ results = store.find(
2126+ BuildQueue,
2127+ SourcePackageBranchBuildJob.job == BuildQueue.jobID,
2128+ SourcePackageBranchBuildJob.build == self.id)
2129+ return results.one()
2130+
2131+ @property
2132+ def source_package_release(self):
2133+ """See `ISourcePackageBranchBuild`."""
2134+ return Store.of(self).find(
2135+ SourcePackageRelease, source_package_branch_build=self).one()
2136+
2137+ @property
2138+ def title(self):
2139+ if self.branch is None:
2140+ return 'build for deleted branch'
2141+ else:
2142+ return '%s build' % self.branch.unique_name
2143+
2144+ def __init__(self, package_build, distroseries, branch, requester,
2145+ revision):
2146+ """Construct a SourcePackageBranchBuild."""
2147+ super(SourcePackageBranchBuild, self).__init__()
2148+ self.package_build = package_build
2149+ self.distroseries = distroseries
2150+ self.branch = branch
2151+ self.requester = requester
2152+ self.revision = revision
2153+
2154+ @classmethod
2155+ def new(cls, distroseries, branch, requester, archive, pocket=None,
2156+ date_created=None, duration=None, revision=None):
2157+ """See `ISourcePackageBranchBuildSource`."""
2158+ store = IMasterStore(SourcePackageBranchBuild)
2159+ if pocket is None:
2160+ pocket = PackagePublishingPocket.RELEASE
2161+ if date_created is None:
2162+ date_created = UTC_NOW
2163+ packagebuild = PackageBuild.new(cls.build_farm_job_type,
2164+ True, archive, pocket, date_created=date_created)
2165+ spbuild = cls(packagebuild, distroseries, branch, requester, revision)
2166+ store.add(spbuild)
2167+ return spbuild
2168+
2169+ def _unqueueBuild(self):
2170+ """Remove the build's queue and job."""
2171+ store = Store.of(self)
2172+ if self.buildqueue_record is not None:
2173+ job = self.buildqueue_record.job
2174+ store.remove(self.buildqueue_record)
2175+ store.find(
2176+ SourcePackageBranchBuildJob,
2177+ SourcePackageBranchBuildJob.build == self.id).remove()
2178+ store.remove(job)
2179+
2180+ def cancelBuild(self):
2181+ """See `ISourcePackageBranchBuild.`"""
2182+ self._unqueueBuild()
2183+ self.status = BuildStatus.SUPERSEDED
2184+
2185+ def destroySelf(self):
2186+ self._unqueueBuild()
2187+ store = Store.of(self)
2188+ releases = store.find(
2189+ SourcePackageRelease,
2190+ SourcePackageRelease.source_package_branch_build == self.id)
2191+ for release in releases:
2192+ release.source_package_branch_build = None
2193+ package_build = self.package_build
2194+ store.remove(self)
2195+ package_build.destroySelf()
2196+
2197+ @classmethod
2198+ def getByID(cls, build_id):
2199+ """See `ISourcePackageBranchBuildSource`."""
2200+ store = IMasterStore(SourcePackageBranchBuild)
2201+ return store.find(cls, cls.id == build_id).one()
2202+
2203+ @classmethod
2204+ def getByBuildFarmJob(cls, build_farm_job):
2205+ """See `ISpecificBuildFarmJobSource`."""
2206+ return Store.of(build_farm_job).find(cls,
2207+ cls.package_build_id == PackageBuild.id,
2208+ PackageBuild.build_farm_job_id == build_farm_job.id).one()
2209+
2210+ @classmethod
2211+ def getRecentBuilds(cls, requester, branch, distroseries, _now=None):
2212+ from lp.buildmaster.model.buildfarmjob import BuildFarmJob
2213+ if _now is None:
2214+ _now = datetime.now(utc)
2215+ store = IMasterStore(SourcePackageBranchBuild)
2216+ old_threshold = _now - timedelta(days=1)
2217+ return store.find(cls, cls.distroseries_id == distroseries.id,
2218+ cls.requester_id == requester.id, cls.branch_id == branch.id,
2219+ BuildFarmJob.date_created > old_threshold,
2220+ BuildFarmJob.id == PackageBuild.build_farm_job_id,
2221+ PackageBuild.id == cls.package_build_id)
2222+
2223+ def makeJob(self):
2224+ """See `ISourcePackageBranchBuildJob`."""
2225+ store = Store.of(self)
2226+ job = Job()
2227+ store.add(job)
2228+ specific_job = getUtility(
2229+ ISourcePackageBranchBuildJobSource).new(self, job)
2230+ return specific_job
2231+
2232+ def estimateDuration(self):
2233+ """See `IPackageBuild`."""
2234+ # FIXME
2235+ return timedelta(minutes=10)
2236+
2237+ def verifySuccessfulUpload(self):
2238+ return self.source_package_release is not None
2239+
2240+ def notify(self, extra_info=None):
2241+ """See `IPackageBuild`."""
2242+ # If our branch has been deleted, any notification will fail.
2243+ if self.branch is None:
2244+ return
2245+ if self.status == BuildStatus.FULLYBUILT:
2246+ # Don't send mail for successful builds; it can be just
2247+ # too much.
2248+ return
2249+ mailer = SourcePackageBranchBuildMailer.forStatus(self)
2250+ mailer.sendAll()
2251+
2252+ def lfaUrl(self, lfa):
2253+ """Return the URL for a LibraryFileAlias, in the context of self.
2254+ """
2255+ if lfa is None:
2256+ return None
2257+ return ProxiedLibraryFileAlias(lfa, self).http_url
2258+
2259+ @property
2260+ def log_url(self):
2261+ """See `IPackageBuild`.
2262+
2263+ Overridden here so that it uses the SourcePackageBranchBuild as
2264+ context.
2265+ """
2266+ return self.lfaUrl(self.log)
2267+
2268+ @property
2269+ def upload_log_url(self):
2270+ """See `IPackageBuild`.
2271+
2272+ Overridden here so that it uses the SourcePackageBranchBuild as
2273+ context.
2274+ """
2275+ return self.lfaUrl(self.upload_log)
2276+
2277+ def getFileByName(self, filename):
2278+ """See `ISourcePackageBranchBuild`."""
2279+ files = dict((lfa.filename, lfa)
2280+ for lfa in [self.log, self.upload_log]
2281+ if lfa is not None)
2282+ try:
2283+ return files[filename]
2284+ except KeyError:
2285+ raise NotFoundError(filename)
2286+
2287+ def getUploader(self, changes):
2288+ """See `IPackageBuild`."""
2289+ return self.requester
2290+
2291+
2292+class SourcePackageBranchBuildJob(BuildFarmJobOldDerived, Storm):
2293+ classProvides(ISourcePackageBranchBuildJobSource)
2294+ implements(ISourcePackageBranchBuildJob)
2295+
2296+ __storm_table__ = 'sourcepackagebranchbuildjob'
2297+
2298+ id = Int(primary=True)
2299+
2300+ job_id = Int(name='job', allow_none=False)
2301+ job = Reference(job_id, 'Job.id')
2302+
2303+ build_id = Int(name='sourcepackage_branch_build', allow_none=False)
2304+ build = Reference(
2305+ build_id, 'SourcePackageBranchBuild.id')
2306+
2307+ @property
2308+ def processor(self):
2309+ return self.build.distroseries.nominatedarchindep.default_processor
2310+
2311+ @property
2312+ def virtualized(self):
2313+ """See `IBuildFarmJob`."""
2314+ return self.build.is_virtualized
2315+
2316+ def __init__(self, build, job):
2317+ self.build = build
2318+ self.job = job
2319+ super(SourcePackageBranchBuildJob, self).__init__()
2320+
2321+ def _set_build_farm_job(self):
2322+ """Setup the IBuildFarmJob delegate.
2323+
2324+ We override this to provide a delegate specific to package builds."""
2325+ self.build_farm_job = BuildFarmBuildJob(self.build)
2326+
2327+ @classmethod
2328+ def new(cls, build, job):
2329+ """See `ISourcePackageBranchBuildJobSource`."""
2330+ specific_job = cls(build, job)
2331+ store = IMasterStore(cls)
2332+ store.add(specific_job)
2333+ return specific_job
2334+
2335+ def getName(self):
2336+ return "%s-%s" % (self.id, self.build_id)
2337+
2338+ def score(self):
2339+ return 2505 + self.build.archive.relative_build_score
2340
2341=== modified file 'lib/lp/code/model/sourcepackagerecipedata.py'
2342--- lib/lp/code/model/sourcepackagerecipedata.py 2010-10-28 15:59:13 +0000
2343+++ lib/lp/code/model/sourcepackagerecipedata.py 2011-06-09 15:37:32 +0000
2344@@ -47,7 +47,6 @@
2345 TooNewRecipeFormat,
2346 )
2347 from lp.code.interfaces.branchlookup import IBranchLookup
2348-from lp.code.model.branch import Branch
2349
2350
2351 class InstructionType(DBEnumeratedType):
2352@@ -309,6 +308,7 @@
2353 def getReferencedBranches(self):
2354 """Return an iterator of the Branch objects referenced by this recipe.
2355 """
2356+ from lp.code.model.branch import Branch
2357 yield self.base_branch
2358 sub_branches = IStore(self).find(
2359 Branch,
2360
2361=== modified file 'lib/lp/code/model/tests/test_recipebuilder.py'
2362--- lib/lp/code/model/tests/test_recipebuilder.py 2011-05-04 02:41:23 +0000
2363+++ lib/lp/code/model/tests/test_recipebuilder.py 2011-06-09 15:37:32 +0000
2364@@ -243,7 +243,7 @@
2365 "Exception processing bzr_builder_sources_list:",
2366 logger.getLogBuffer())
2367
2368- def test_extraBuildArgs_withNoBZrBuilderConfigSet(self):
2369+ def test_extraBuildArgs_withNoBzrBuilderConfigSet(self):
2370 # Ensure _extraBuildArgs doesn't blow up when
2371 # bzr_builder_sources_list isn't set.
2372 job = self.makeJob()
2373
2374=== added file 'lib/lp/code/model/tests/test_sourcepackagebranchbuild.py'
2375--- lib/lp/code/model/tests/test_sourcepackagebranchbuild.py 1970-01-01 00:00:00 +0000
2376+++ lib/lp/code/model/tests/test_sourcepackagebranchbuild.py 2011-06-09 15:37:32 +0000
2377@@ -0,0 +1,450 @@
2378+# Copyright 2010 Canonical Ltd. This software is licensed under the
2379+# GNU Affero General Public License version 3 (see the file LICENSE).
2380+
2381+"""Tests for source package builds."""
2382+
2383+__metaclass__ = type
2384+
2385+from datetime import (
2386+ timedelta,
2387+ )
2388+import re
2389+
2390+from storm.locals import Store
2391+import transaction
2392+from zope.component import getUtility
2393+from zope.security.proxy import removeSecurityProxy
2394+
2395+from twisted.trial.unittest import TestCase as TrialTestCase
2396+
2397+from canonical.launchpad.interfaces.lpstorm import IStore
2398+from canonical.launchpad.webapp.authorization import check_permission
2399+from canonical.launchpad.webapp.testing import verifyObject
2400+from canonical.testing.layers import (
2401+ LaunchpadFunctionalLayer,
2402+ LaunchpadZopelessLayer,
2403+ )
2404+from lp.app.errors import NotFoundError
2405+from lp.buildmaster.enums import BuildStatus
2406+from lp.buildmaster.interfaces.buildqueue import IBuildQueue
2407+from lp.buildmaster.model.buildfarmjob import BuildFarmJob
2408+from lp.buildmaster.model.packagebuild import PackageBuild
2409+from lp.buildmaster.tests.mock_slaves import WaitingSlave
2410+from lp.buildmaster.tests.test_packagebuild import (
2411+ TestGetUploadMethodsMixin,
2412+ TestHandleStatusMixin,
2413+ )
2414+from lp.code.interfaces.sourcepackagebranchbuild import (
2415+ ISourcePackageBranchBuild,
2416+ ISourcePackageBranchBuildJob,
2417+ ISourcePackageBranchBuildSource,
2418+ )
2419+from lp.code.mail.sourcepackagebranchbuild import (
2420+ SourcePackageBranchBuildMailer,
2421+ )
2422+from lp.code.model.sourcepackagebranchbuild import SourcePackageBranchBuild
2423+from lp.registry.interfaces.pocket import PackagePublishingPocket
2424+from lp.services.mail.sendmail import format_address
2425+from lp.soyuz.interfaces.processor import IProcessorFamilySet
2426+from lp.soyuz.model.processor import ProcessorFamily
2427+from lp.testing import (
2428+ ANONYMOUS,
2429+ login,
2430+ person_logged_in,
2431+ TestCaseWithFactory,
2432+ )
2433+from lp.testing.fakemethod import FakeMethod
2434+from lp.testing.mail_helpers import pop_notifications
2435+
2436+
2437+class TestSourcePackageBranchBuild(TestCaseWithFactory):
2438+ """Test the source package build object."""
2439+
2440+ layer = LaunchpadFunctionalLayer
2441+
2442+ def makeSourcePackageBranchBuild(self):
2443+ """Create a `SourcePackageBranchBuild` for testing."""
2444+ person = self.factory.makePerson()
2445+ distroseries = self.factory.makeDistroSeries()
2446+ distroseries_i386 = distroseries.newArch(
2447+ 'i386', ProcessorFamily.get(1), False, person,
2448+ supports_virtualized=True)
2449+ removeSecurityProxy(distroseries).nominatedarchindep = (
2450+ distroseries_i386)
2451+
2452+ return getUtility(ISourcePackageBranchBuildSource).new(
2453+ distroseries=distroseries,
2454+ branch=self.factory.makeBranch(),
2455+ archive=self.factory.makeArchive(),
2456+ requester=person)
2457+
2458+ def test_providesInterfaces(self):
2459+ # SourcePackageBranchBuild provides IPackageBuild and
2460+ # ISourcePackageBranchBuild.
2461+ spb = self.makeSourcePackageBranchBuild()
2462+ self.assertProvides(spb, ISourcePackageBranchBuild)
2463+
2464+ def test_implements_interface(self):
2465+ build = self.makeSourcePackageBranchBuild()
2466+ verifyObject(ISourcePackageBranchBuild, build)
2467+
2468+ def test_saves_record(self):
2469+ # A source package branch build can be stored in the database
2470+ spb = self.makeSourcePackageBranchBuild()
2471+ transaction.commit()
2472+ self.assertProvides(spb, ISourcePackageBranchBuild)
2473+
2474+ def test_makeJob(self):
2475+ # A build farm job can be obtained from a SourcePackageBranchBuild
2476+ spb = self.makeSourcePackageBranchBuild()
2477+ job = spb.makeJob()
2478+ self.assertProvides(job, ISourcePackageBranchBuildJob)
2479+
2480+ def test_queueBuild(self):
2481+ spb = self.makeSourcePackageBranchBuild()
2482+ bq = spb.queueBuild(spb)
2483+
2484+ self.assertProvides(bq, IBuildQueue)
2485+ self.assertProvides(bq.specific_job, ISourcePackageBranchBuildJob)
2486+ self.assertEqual(True, bq.virtualized)
2487+
2488+ # The processor for SourcePackageBranchBuilds should not be None.
2489+ # They do require specific environments.
2490+ self.assertNotEqual(None, bq.processor)
2491+ self.assertEqual(
2492+ spb.distroseries.nominatedarchindep.default_processor,
2493+ bq.processor)
2494+ self.assertEqual(bq, spb.buildqueue_record)
2495+
2496+ def test_getBuildCookie(self):
2497+ # A build cookie is made up of the job type and record id.
2498+ # The uploadprocessor relies on this format.
2499+ sprb = self.makeSourcePackageBranchBuild()
2500+ Store.of(sprb).flush()
2501+ cookie = sprb.getBuildCookie()
2502+ expected_cookie = "BRANCHBUILD-%d" % sprb.id
2503+ self.assertEquals(expected_cookie, cookie)
2504+
2505+ def test_title(self):
2506+ # A branche build's title currently consists of the branch's
2507+ # unique name
2508+ spb = self.makeSourcePackageBranchBuild()
2509+ title = "%s build" % spb.branch.unique_name
2510+ self.assertEqual(spb.title, title)
2511+
2512+ def test_getTitle(self):
2513+ # A branch build job's title is the same as its build's title.
2514+ spb = self.makeSourcePackageBranchBuild()
2515+ job = spb.makeJob()
2516+ self.assertEqual(job.getTitle(), spb.title)
2517+
2518+ def test_distribution(self):
2519+ # A source package branch build has a distribution derived from
2520+ # its series.
2521+ spb = self.makeSourcePackageBranchBuild()
2522+ self.assertEqual(spb.distroseries.distribution, spb.distribution)
2523+
2524+ def test_current_component(self):
2525+ # Since branches build only into PPAs, they always build in main.
2526+ # PPAs lack indices for other components.
2527+ spb = self.makeSourcePackageBranchBuild()
2528+ self.assertEqual('main', spb.current_component.name)
2529+
2530+ def test_is_private(self):
2531+ # A source package branch build is currently always public.
2532+ spb = self.makeSourcePackageBranchBuild()
2533+ self.assertEqual(False, spb.is_private)
2534+
2535+ def test_view_private_branch(self):
2536+ """Branchbuilds with private branches are restricted."""
2537+ owner = self.factory.makePerson()
2538+ branch = self.factory.makeAnyBranch(owner=owner)
2539+ with person_logged_in(owner):
2540+ build = self.factory.makeSourcePackageBranchBuild(branch=branch)
2541+ self.assertTrue(check_permission('launchpad.View', build))
2542+ removeSecurityProxy(branch).private = True
2543+ with person_logged_in(self.factory.makePerson()):
2544+ self.assertFalse(check_permission('launchpad.View', build))
2545+ login(ANONYMOUS)
2546+ self.assertFalse(check_permission('launchpad.View', build))
2547+
2548+ def test_view_private_archive(self):
2549+ """branchbuilds with private branches are restricted."""
2550+ owner = self.factory.makePerson()
2551+ archive = self.factory.makeArchive(owner=owner, private=True)
2552+ build = self.factory.makeSourcePackageBranchBuild(archive=archive)
2553+ with person_logged_in(owner):
2554+ self.assertTrue(check_permission('launchpad.View', build))
2555+ with person_logged_in(self.factory.makePerson()):
2556+ self.assertFalse(check_permission('launchpad.View', build))
2557+ login(ANONYMOUS)
2558+ self.assertFalse(check_permission('launchpad.View', build))
2559+
2560+ def test_estimateDuration(self):
2561+ # The duration is currently hardcoded to 10 minutes.
2562+ spb = self.makeSourcePackageBranchBuild()
2563+ cur_date = self.factory.getUniqueDate()
2564+ self.assertEqual(timedelta(minutes=10), spb.estimateDuration())
2565+
2566+ def test_getFileByName(self):
2567+ """getFileByName returns the logs when requested by name."""
2568+ spb = self.factory.makeSourcePackageBranchBuild()
2569+ removeSecurityProxy(spb).log = (
2570+ self.factory.makeLibraryFileAlias(filename='buildlog.txt.gz'))
2571+ self.assertEqual(spb.log, spb.getFileByName('buildlog.txt.gz'))
2572+ self.assertRaises(NotFoundError, spb.getFileByName, 'foo')
2573+ removeSecurityProxy(spb).log = (
2574+ self.factory.makeLibraryFileAlias(filename='foo'))
2575+ self.assertEqual(spb.log, spb.getFileByName('foo'))
2576+ self.assertRaises(NotFoundError, spb.getFileByName, 'buildlog.txt.gz')
2577+ removeSecurityProxy(spb).upload_log = (
2578+ self.factory.makeLibraryFileAlias(filename='upload.txt.gz'))
2579+ self.assertEqual(spb.upload_log, spb.getFileByName('upload.txt.gz'))
2580+
2581+ def test_binary_builds(self):
2582+ """The binary_builds property should be populated automatically."""
2583+ spb = self.factory.makeSourcePackageBranchBuild()
2584+ multiverse = self.factory.makeComponent(name='multiverse')
2585+ spr = self.factory.makeSourcePackageRelease(
2586+ source_package_branch_build=spb, component=multiverse)
2587+ self.assertEqual([], list(spb.binary_builds))
2588+ binary = self.factory.makeBinaryPackageBuild(spr)
2589+ self.factory.makeBinaryPackageBuild()
2590+ Store.of(binary).flush()
2591+ self.assertEqual([binary], list(spb.binary_builds))
2592+
2593+ def test_requestBuild(self):
2594+ """Manifest should start empty, but accept SourcePackageBranchData."""
2595+ branch = self.factory.makeBranch()
2596+ archive = self.factory.makeArchive(owner=branch.owner)
2597+ distroseries = self.factory.makeUbuntuDistroSeries()
2598+ distroseries_i386 = distroseries.newArch(
2599+ 'i386', ProcessorFamily.get(1), False, branch.owner,
2600+ supports_virtualized=True)
2601+ removeSecurityProxy(distroseries).nominatedarchindep = (
2602+ distroseries_i386)
2603+ build = branch.requestBuild(archive, branch.owner,
2604+ distroseries)
2605+ self.assertProvides(build, ISourcePackageBranchBuild)
2606+
2607+ def test_getRecentBuilds(self):
2608+ """Recent builds match the same person, series and receipe.
2609+
2610+ Builds do not match if they are older than 24 hours, or have a
2611+ different requester, series or branch.
2612+ """
2613+ requester = self.factory.makePerson()
2614+ branch = self.factory.makeBranch()
2615+ series = self.factory.makeDistroSeries()
2616+ now = self.factory.getUniqueDate()
2617+ build = self.factory.makeSourcePackageBranchBuild(branch=branch,
2618+ requester=requester)
2619+ self.factory.makeSourcePackageBranchBuild(
2620+ branch=branch, distroseries=series)
2621+ self.factory.makeSourcePackageBranchBuild(
2622+ requester=requester, distroseries=series)
2623+
2624+ def get_recent():
2625+ Store.of(build).flush()
2626+ return SourcePackageBranchBuild.getRecentBuilds(
2627+ requester, branch, series, _now=now)
2628+ self.assertContentEqual([], get_recent())
2629+ yesterday = now - timedelta(days=1)
2630+ recent_build = self.factory.makeSourcePackageBranchBuild(
2631+ branch=branch, distroseries=series, requester=requester,
2632+ date_created=yesterday)
2633+ self.assertContentEqual([], get_recent())
2634+ a_second = timedelta(seconds=1)
2635+ removeSecurityProxy(recent_build).date_created += a_second
2636+ self.assertContentEqual([recent_build], get_recent())
2637+
2638+ def test_destroySelf(self):
2639+ # ISourcePackageBranchBuild should make sure to remove jobs and build
2640+ # queue entries and then invalidate itself.
2641+ build = self.factory.makeSourcePackageBranchBuild()
2642+ build.destroySelf()
2643+
2644+ def test_destroySelf_clears_release(self):
2645+ # Destroying a sourcepackagebranchbuild removes references to it from
2646+ # its releases.
2647+ build = self.factory.makeSourcePackageBranchBuild()
2648+ release = self.factory.makeSourcePackageRelease(
2649+ source_package_branch_build=build)
2650+ self.assertEqual(build, release.source_package_branch_build)
2651+ build.destroySelf()
2652+ self.assertIs(None, release.source_package_branch_build)
2653+ transaction.commit()
2654+
2655+ def test_destroySelf_destroys_referenced(self):
2656+ # Destroying a sourcepackagebranchbuild also destroys the
2657+ # PackageBuild and BuildFarmJob it references.
2658+ build = self.factory.makeSourcePackageBranchBuild()
2659+ store = Store.of(build)
2660+ naked_build = removeSecurityProxy(build)
2661+ # Ensure database ids are set.
2662+ store.flush()
2663+ package_build_id = naked_build.package_build_id
2664+ build_farm_job_id = naked_build.package_build.build_farm_job_id
2665+ build.destroySelf()
2666+ result = store.find(PackageBuild, PackageBuild.id == package_build_id)
2667+ self.assertIs(None, result.one())
2668+ result = store.find(
2669+ BuildFarmJob, BuildFarmJob.id == build_farm_job_id)
2670+ self.assertIs(None, result.one())
2671+
2672+ def test_cancelBuild(self):
2673+ # ISourcePackageBranchBuild should make sure to remove jobs and build
2674+ # queue entries and then invalidate itself.
2675+ build = self.factory.makeSourcePackageBranchBuild()
2676+ build.cancelBuild()
2677+
2678+ self.assertEqual(
2679+ BuildStatus.SUPERSEDED,
2680+ build.status)
2681+
2682+ def test_getSpecificJob(self):
2683+ # getSpecificJob returns the SourcePackageBranchBuild
2684+ sprb = self.makeSourcePackageBranchBuild()
2685+ Store.of(sprb).flush()
2686+ job = sprb.build_farm_job.getSpecificJob()
2687+ self.assertEqual(sprb, job)
2688+
2689+ def test_getUploader(self):
2690+ # For ACL purposes the uploader is the build requester.
2691+ build = self.makeSourcePackageBranchBuild()
2692+ self.assertEquals(build.requester,
2693+ build.getUploader(None))
2694+
2695+
2696+class TestAsBuildmaster(TestCaseWithFactory):
2697+
2698+ layer = LaunchpadZopelessLayer
2699+
2700+ def test_notify(self):
2701+ """We do not send mail on completion of source package branch builds.
2702+
2703+ See bug 778437.
2704+ """
2705+ person = self.factory.makePerson(name='person')
2706+ cake = self.factory.makeBranch(
2707+ name=u'branch', owner=person)
2708+ pantry = self.factory.makeArchive(name='ppa')
2709+ secret = self.factory.makeDistroSeries(name=u'distroseries')
2710+ build = self.factory.makeSourcePackageBranchBuild(
2711+ branch=cake, distroseries=secret, archive=pantry)
2712+ removeSecurityProxy(build).status = BuildStatus.FULLYBUILT
2713+ IStore(build).flush()
2714+ build.notify()
2715+ self.assertEquals(0, len(pop_notifications()))
2716+
2717+ def assertBuildMessageValid(self, build, message):
2718+ # Not currently used; can be used if we do want to check about any
2719+ # notifications sent in other cases.
2720+ requester = build.requester
2721+ requester_address = format_address(
2722+ requester.displayname, requester.preferredemail.email)
2723+ mailer = SourcePackageBranchBuildMailer.forStatus(build)
2724+ expected = mailer.generateEmail(
2725+ requester.preferredemail.email, requester)
2726+ self.assertEqual(
2727+ requester_address, re.sub(r'\n\t+', ' ', message['To']))
2728+ self.assertEqual(expected.subject, message['Subject'].replace(
2729+ '\n\t', ' '))
2730+ self.assertEqual(
2731+ expected.body, message.get_payload(decode=True))
2732+
2733+ def test_notify_when_branch_deleted(self):
2734+ """Notify does nothing if branch has been deleted."""
2735+ person = self.factory.makePerson(name='person')
2736+ cake = self.factory.makeBranch(
2737+ name=u'branch', owner=person)
2738+ pantry = self.factory.makeArchive(name='ppa')
2739+ secret = self.factory.makeDistroSeries(name=u'distroseries')
2740+ build = self.factory.makeSourcePackageBranchBuild(
2741+ branch=cake, distroseries=secret, archive=pantry)
2742+ removeSecurityProxy(build).status = BuildStatus.FULLYBUILT
2743+ cake.destroySelf()
2744+ IStore(build).flush()
2745+ build.notify()
2746+ notifications = pop_notifications()
2747+ self.assertEquals(0, len(notifications))
2748+
2749+
2750+class TestBuildNotifications(TrialTestCase):
2751+
2752+ layer = LaunchpadZopelessLayer
2753+
2754+ def setUp(self):
2755+ super(TestBuildNotifications, self).setUp()
2756+ from lp.testing.factory import LaunchpadObjectFactory
2757+ self.factory = LaunchpadObjectFactory()
2758+
2759+ def prepare_build(self, fake_successful_upload=False):
2760+ queue_record = self.factory.makeSourcePackageBranchBuildJob()
2761+ build = queue_record.specific_job.build
2762+ naked_build = removeSecurityProxy(build)
2763+ naked_build.status = BuildStatus.FULLYBUILT
2764+ naked_build.date_started = self.factory.getUniqueDate()
2765+ if fake_successful_upload:
2766+ naked_build.verifySuccessfulUpload = FakeMethod(
2767+ result=True)
2768+ queue_record.builder = self.factory.makeBuilder()
2769+ slave = WaitingSlave('BuildStatus.OK')
2770+ queue_record.builder.setSlaveForTesting(slave)
2771+ return build
2772+
2773+ def assertDeferredNotifyCount(self, status, build, expected_count):
2774+ d = build.handleStatus(status, None, {'filemap': {}})
2775+ def cb(result):
2776+ self.assertEqual(expected_count, len(pop_notifications()))
2777+ d.addCallback(cb)
2778+ return d
2779+
2780+ def test_handleStatus_PACKAGEFAIL(self):
2781+ """Failing to build the package immediately sends a notification."""
2782+ return self.assertDeferredNotifyCount(
2783+ "PACKAGEFAIL", self.prepare_build(), 1)
2784+
2785+ def test_handleStatus_OK(self):
2786+ """Building the source package does _not_ immediately send mail.
2787+
2788+ (The archive uploader mail send one later.
2789+ """
2790+ return self.assertDeferredNotifyCount(
2791+ "OK", self.prepare_build(), 0)
2792+
2793+#XXX 2011-05-20 gmb bug=785679
2794+# This test has been disabled since it broke intermittently in
2795+# buildbot (but does not fail in isolation locally).
2796+## def test_handleStatus_OK_successful_upload(self):
2797+## return self.assertDeferredNotifyCount(
2798+## "OK", self.prepare_build(True), 0)
2799+
2800+
2801+class MakeSPBranchBuildMixin:
2802+ """Provide the common makeBuild method returning a queued build."""
2803+
2804+ def makeBuild(self):
2805+ person = self.factory.makePerson()
2806+ distroseries = self.factory.makeDistroSeries()
2807+ processor_fam = getUtility(IProcessorFamilySet).getByName('x86')
2808+ distroseries_i386 = distroseries.newArch(
2809+ 'i386', processor_fam, False, person,
2810+ supports_virtualized=True)
2811+ distroseries.nominatedarchindep = distroseries_i386
2812+ build = self.factory.makeSourcePackageBranchBuild(
2813+ distroseries=distroseries,
2814+ status=BuildStatus.FULLYBUILT,
2815+ duration=timedelta(minutes=5))
2816+ build.queueBuild(build)
2817+ return build
2818+
2819+
2820+class TestGetUploadMethodsForSPBranchBuild(
2821+ MakeSPBranchBuildMixin, TestGetUploadMethodsMixin, TestCaseWithFactory):
2822+ """IPackageBuild.getUpload-related methods work with SPBranch builds."""
2823+
2824+
2825+class TestHandleStatusForSPRBuild(
2826+ MakeSPBranchBuildMixin, TestHandleStatusMixin, TrialTestCase):
2827+ """IPackageBuild.handleStatus works with SPBranch builds."""
2828
2829=== added file 'lib/lp/code/templates/sourcepackagebranchbuild-index.pt'
2830--- lib/lp/code/templates/sourcepackagebranchbuild-index.pt 1970-01-01 00:00:00 +0000
2831+++ lib/lp/code/templates/sourcepackagebranchbuild-index.pt 2011-06-09 15:37:32 +0000
2832@@ -0,0 +1,199 @@
2833+<html
2834+ xmlns="http://www.w3.org/1999/xhtml"
2835+ xmlns:tal="http://xml.zope.org/namespaces/tal"
2836+ xmlns:metal="http://xml.zope.org/namespaces/metal"
2837+ xmlns:i18n="http://xml.zope.org/namespaces/i18n"
2838+ metal:use-macro="view/macro:page/main_only"
2839+ i18n:domain="launchpad"
2840+>
2841+
2842+ <body>
2843+
2844+ <tal:registering metal:fill-slot="registering">
2845+ created
2846+ <span tal:content="context/date_created/fmt:displaydate"
2847+ tal:attributes="title context/date_created/fmt:datetime"
2848+ >on 2005-01-01</span>
2849+ </tal:registering>
2850+
2851+ <div metal:fill-slot="main">
2852+
2853+ <div class="yui-g">
2854+
2855+ <div id="status" class="yui-u first">
2856+ <div class="portlet">
2857+ <div metal:use-macro="template/macros/status" />
2858+ </div>
2859+ </div>
2860+
2861+ <div id="details" class="yui-u">
2862+ <div class="portlet">
2863+ <div metal:use-macro="template/macros/details" />
2864+ </div>
2865+ </div>
2866+
2867+ </div> <!-- yui-g -->
2868+
2869+ <div id="buildlog" class="portlet"
2870+ tal:condition="context/status/enumvalue:BUILDING">
2871+ <div metal:use-macro="template/macros/buildlog" />
2872+ </div>
2873+
2874+ </div> <!-- main -->
2875+
2876+
2877+<metal:macros fill-slot="bogus">
2878+
2879+ <metal:macro define-macro="details">
2880+ <tal:comment replace="nothing">
2881+ Details section.
2882+ </tal:comment>
2883+ <h2>Build details</h2>
2884+ <div class="two-column-list">
2885+ <dl>
2886+ <dt>Branch:</dt>
2887+ <dd>
2888+ <tal:branch replace="structure context/branch/fmt:link" />
2889+ </dd>
2890+ </dl>
2891+ <dl>
2892+ <dt>Archive:</dt>
2893+ <dd>
2894+ <span tal:replace="structure context/archive/fmt:link"
2895+ >Celso PPA</span>
2896+ </dd>
2897+ </dl>
2898+ <dl>
2899+ <dt>Series:</dt>
2900+ <dd><a class="sprite distribution"
2901+ tal:define="series context/distroseries"
2902+ tal:attributes="href series/fmt:url"
2903+ tal:content="series/displayname">Breezy Badger</a>
2904+ </dd>
2905+ </dl>
2906+ <dl>
2907+ <dt>Pocket:</dt>
2908+ <dd><span tal:replace="context/pocket/title">Release</span></dd>
2909+ </dl>
2910+ <dl>
2911+ <dt>Binary builds:</dt>
2912+ <dd tal:repeat="binary view/binary_builds"
2913+ tal:content="structure binary/fmt:link">
2914+ </dd>
2915+ <dd tal:condition="not: view/binary_builds">None</dd>
2916+ </dl>
2917+ </div>
2918+ </metal:macro>
2919+
2920+ <metal:macro define-macro="status">
2921+ <tal:comment replace="nothing">
2922+ Status section.
2923+ </tal:comment>
2924+ <h2>Build status</h2>
2925+ <p>
2926+ <span tal:replace="structure context/image:icon" />
2927+ <span tal:attributes="
2928+ class string:buildstatus${context/status/name};"
2929+ tal:content="context/status/title">Fully built</span>
2930+ <tal:building condition="context/status/enumvalue:BUILDING">
2931+ on <a tal:content="context/buildqueue_record/builder/title"
2932+ tal:attributes="href context/buildqueue_record/builder/fmt:url"/>
2933+ </tal:building>
2934+ <tal:built condition="context/builder">
2935+ on <a tal:content="context/builder/title"
2936+ tal:attributes="href context/builder/fmt:url"/>
2937+ </tal:built>
2938+ </p>
2939+
2940+ <ul>
2941+ <li tal:condition="context/dependencies">
2942+ Missing build dependencies: <em
2943+ tal:content="context/dependencies">x, y, z</em>
2944+ </li>
2945+ <tal:reallypending condition="context/buildqueue_record">
2946+ <tal:pending condition="context/buildqueue_record/job/status/enumvalue:WAITING">
2947+ <li tal:define="eta context/buildqueue_record/getEstimatedJobStartTime">
2948+ Start <tal:eta
2949+ replace="eta/fmt:approximatedate">in 3 hours</tal:eta>
2950+ (<span tal:replace="context/buildqueue_record/lastscore"/>)
2951+ <a href="https://help.launchpad.net/Packaging/BuildScores"
2952+ target="_blank">What's this?</a>
2953+ </li>
2954+ </tal:pending>
2955+ </tal:reallypending>
2956+ <tal:started condition="context/date_started">
2957+ <li tal:condition="context/date_started">
2958+ Started <span
2959+ tal:define="start context/date_started"
2960+ tal:attributes="title start/fmt:datetime"
2961+ tal:content="start/fmt:displaydate">2008-01-01</span>
2962+ </li>
2963+ </tal:started>
2964+ <tal:finish condition="not: context/date_finished">
2965+ <li tal:define="eta view/eta" tal:condition="view/eta">
2966+ Estimated finish <tal:eta
2967+ replace="eta/fmt:approximatedate">in 3 hours</tal:eta>
2968+ </li>
2969+ </tal:finish>
2970+
2971+ <li tal:condition="context/date_finished">
2972+ Finished <span
2973+ tal:attributes="title context/date_finished/fmt:datetime"
2974+ tal:content="context/date_finished/fmt:displaydate">2008-01-01</span>
2975+ <tal:duration condition="context/duration">
2976+ (took <span tal:replace="context/duration/fmt:exactduration"/>)
2977+ </tal:duration>
2978+ </li>
2979+ <li tal:define="file context/log"
2980+ tal:condition="file">
2981+ <a class="sprite download"
2982+ tal:attributes="href context/log_url">buildlog</a>
2983+ (<span tal:replace="file/content/filesize/fmt:bytes" />)
2984+ </li>
2985+ <li tal:define="file context/upload_log"
2986+ tal:condition="file">
2987+ <a class="sprite download"
2988+ tal:attributes="href context/upload_log_url">uploadlog</a>
2989+ (<span tal:replace="file/content/filesize/fmt:bytes" />)
2990+ </li>
2991+ </ul>
2992+
2993+ <div
2994+ style="margin-top: 1.5em"
2995+ tal:define="context_menu view/context/menu:context;
2996+ link context_menu/cancel"
2997+ tal:condition="link/enabled"
2998+ >
2999+ <a tal:replace="structure link/fmt:link" />
3000+ </div>
3001+ <div
3002+ style="margin-top: 1.5em"
3003+ tal:define="context_menu view/context/menu:context;
3004+ link context_menu/rescore"
3005+ tal:condition="link/enabled"
3006+ >
3007+ <a tal:replace="structure link/fmt:link" />
3008+ </div>
3009+
3010+
3011+ </metal:macro>
3012+
3013+ <metal:macro define-macro="buildlog">
3014+ <tal:comment replace="nothing">
3015+ Buildlog section.
3016+ </tal:comment>
3017+ <h2>Buildlog</h2>
3018+ <div id="buildlog-tail" class="logtail"
3019+ tal:define="logtail context/buildqueue_record/logtail"
3020+ tal:content="structure logtail/fmt:text-to-html">
3021+ <p>Things are crashing and burning all over the place.</p>
3022+ </div>
3023+ <p class="discreet" tal:condition="view/user">
3024+ Updated on <span tal:replace="structure view/user/fmt:local-time"/>
3025+ </p>
3026+ </metal:macro>
3027+
3028+</metal:macros>
3029+
3030+ </body>
3031+</html>
3032
3033=== modified file 'lib/lp/registry/interfaces/distroseries.py'
3034--- lib/lp/registry/interfaces/distroseries.py 2011-06-08 11:06:04 +0000
3035+++ lib/lp/registry/interfaces/distroseries.py 2011-06-09 15:37:32 +0000
3036@@ -648,8 +648,8 @@
3037 dsc_maintainer_rfc822, dsc_standards_version, dsc_format,
3038 dsc_binaries, archive, copyright, build_conflicts,
3039 build_conflicts_indep, dateuploaded=None,
3040- source_package_recipe_build=None, user_defined_fields=None,
3041- homepage=None):
3042+ source_package_recipe_build=None, source_package_branch_build=None,
3043+ user_defined_fields=None, homepage=None):
3044 """Create an uploads `SourcePackageRelease`.
3045
3046 Set this distroseries set to be the uploadeddistroseries.
3047@@ -685,6 +685,7 @@
3048 :param archive: IArchive to where the upload was targeted
3049 :param dateuploaded: optional datetime, if omitted assumed nowUTC
3050 :param source_package_recipe_build: optional SourcePackageRecipeBuild
3051+ :param source_package_branch_build: optional SourcePackageBranchBuild
3052 :param user_defined_fields: optional sequence of key-value pairs with
3053 user defined fields.
3054 :param homepage: optional string with (unchecked) upstream homepage
3055
3056=== modified file 'lib/lp/registry/model/distroseries.py'
3057--- lib/lp/registry/model/distroseries.py 2011-06-08 15:27:40 +0000
3058+++ lib/lp/registry/model/distroseries.py 2011-06-09 15:37:32 +0000
3059@@ -1324,7 +1324,8 @@
3060 dsc_maintainer_rfc822, dsc_standards_version, dsc_format,
3061 dsc_binaries, archive, copyright, build_conflicts,
3062 build_conflicts_indep, dateuploaded=DEFAULT,
3063- source_package_recipe_build=None, user_defined_fields=None,
3064+ source_package_recipe_build=None,
3065+ source_package_branch_build=None, user_defined_fields=None,
3066 homepage=None):
3067 """See `IDistroSeries`."""
3068 return SourcePackageRelease(
3069@@ -1342,6 +1343,7 @@
3070 build_conflicts=build_conflicts,
3071 build_conflicts_indep=build_conflicts_indep,
3072 source_package_recipe_build=source_package_recipe_build,
3073+ source_package_branch_build=source_package_branch_build,
3074 user_defined_fields=user_defined_fields, homepage=homepage)
3075
3076 def getComponentByName(self, name):
3077
3078=== modified file 'lib/lp/soyuz/interfaces/sourcepackagerelease.py'
3079--- lib/lp/soyuz/interfaces/sourcepackagerelease.py 2011-03-06 06:26:38 +0000
3080+++ lib/lp/soyuz/interfaces/sourcepackagerelease.py 2011-06-09 15:37:32 +0000
3081@@ -163,10 +163,19 @@
3082 schema=Interface,
3083 description=_("The `SourcePackageRecipeBuild` which produced this "
3084 "source package release, or None if it was created from a "
3085- "traditional upload."),
3086+ "traditional upload or a direct branch build."),
3087 title=_("Source package recipe build"),
3088 required=False, readonly=True)
3089
3090+ # Really ISourcePackageBranchBuild -- see _schema_circular_imports.
3091+ source_package_branch_build = Reference(
3092+ schema=Interface,
3093+ description=_("The `SourcePackageBranchBuild` which produced this "
3094+ "source package release, or None if it was created from a "
3095+ "traditional upload or a recipe build."),
3096+ title=_("Source package branch build"),
3097+ required=False, readonly=True)
3098+
3099 def addFile(file):
3100 """Add the provided library file alias (file) to the list of files
3101 in this package.
3102
3103=== modified file 'lib/lp/soyuz/model/sourcepackagerelease.py'
3104--- lib/lp/soyuz/model/sourcepackagerelease.py 2011-05-27 21:12:25 +0000
3105+++ lib/lp/soyuz/model/sourcepackagerelease.py 2011-06-09 15:37:32 +0000
3106@@ -154,6 +154,10 @@
3107 source_package_recipe_build = Reference(
3108 source_package_recipe_build_id, 'SourcePackageRecipeBuild.id')
3109
3110+ source_package_branch_build_id = Int(name='sourcepackage_branch_build')
3111+ source_package_branch_build = Reference(
3112+ source_package_branch_build_id, 'SourcePackageBranchBuild.id')
3113+
3114 # XXX cprov 2006-09-26: Those fields are set as notNull and required in
3115 # ISourcePackageRelease, however they can't be not NULL in DB since old
3116 # records doesn't satisfy this condition. We will sort it before using
3117
3118=== modified file 'lib/lp/testing/factory.py'
3119--- lib/lp/testing/factory.py 2011-06-09 08:07:52 +0000
3120+++ lib/lp/testing/factory.py 2011-06-09 15:37:32 +0000
3121@@ -148,6 +148,9 @@
3122 from lp.code.interfaces.codeimportresult import ICodeImportResultSet
3123 from lp.code.interfaces.linkedbranch import ICanHasLinkedBranch
3124 from lp.code.interfaces.revision import IRevisionSet
3125+from lp.code.interfaces.sourcepackagebranchbuild import (
3126+ ISourcePackageBranchBuildSource
3127+ )
3128 from lp.code.interfaces.sourcepackagerecipe import (
3129 ISourcePackageRecipeSource,
3130 MINIMAL_RECIPE_TEXT,
3131@@ -2684,6 +2687,59 @@
3132 IStore(source_package_recipe).flush()
3133 return source_package_recipe
3134
3135+ def makeSourcePackageBranchBuild(self, sourcepackage=None, branch=None,
3136+ requester=None, archive=None,
3137+ sourcename=None, distroseries=None,
3138+ pocket=None, date_created=None,
3139+ status=BuildStatus.NEEDSBUILD,
3140+ duration=None):
3141+ """Make a new SourcePackageBranchBuild."""
3142+ if branch is None:
3143+ branch = self.makeBranch(name=sourcename)
3144+ if archive is None:
3145+ archive = self.makeArchive()
3146+ if distroseries is None:
3147+ distroseries = self.makeDistroSeries(
3148+ distribution=archive.distribution)
3149+ arch = self.makeDistroArchSeries(distroseries=distroseries)
3150+ removeSecurityProxy(distroseries).nominatedarchindep = arch
3151+ if requester is None:
3152+ requester = self.makePerson()
3153+ spb_build = getUtility(ISourcePackageBranchBuildSource).new(
3154+ distroseries=distroseries,
3155+ branch=branch,
3156+ archive=archive,
3157+ requester=requester,
3158+ pocket=pocket,
3159+ date_created=date_created)
3160+ removeSecurityProxy(spb_build).status = status
3161+ if duration is not None:
3162+ naked_spbb = removeSecurityProxy(spb_build)
3163+ if naked_spbb.date_started is None:
3164+ naked_spbb.date_started = spb_build.date_created
3165+ naked_spbb.date_finished = naked_spbb.date_started + duration
3166+ IStore(spb_build).flush()
3167+ return spb_build
3168+
3169+ def makeSourcePackageBranchBuildJob(
3170+ self, score=9876, virtualized=True, estimated_duration=64,
3171+ sourcename=None, branch_build=None):
3172+ """Create a `SourcePackageBranchBuildJob` and a `BuildQueue` for
3173+ testing."""
3174+ if branch_build is None:
3175+ branch_build = self.makeSourcePackageBranchBuild(
3176+ sourcename=sourcename)
3177+ branch_build_job = branch_build.makeJob()
3178+
3179+ store = getUtility(IStoreSelector).get(MAIN_STORE, DEFAULT_FLAVOR)
3180+ bq = BuildQueue(
3181+ job=branch_build_job.job, lastscore=score,
3182+ job_type=BuildFarmJobType.BRANCHBUILD,
3183+ estimated_duration=timedelta(seconds=estimated_duration),
3184+ virtualized=virtualized)
3185+ store.add(bq)
3186+ return bq
3187+
3188 def makeSourcePackageRecipeBuild(self, sourcepackage=None, recipe=None,
3189 requester=None, archive=None,
3190 sourcename=None, distroseries=None,
3191@@ -3398,6 +3454,7 @@
3192 dsc_format='1.0', dsc_binaries='foo-bin',
3193 date_uploaded=UTC_NOW,
3194 source_package_recipe_build=None,
3195+ source_package_branch_build=None,
3196 dscsigningkey=None,
3197 user_defined_fields=None,
3198 changelog_entry=None,
3199@@ -3407,6 +3464,8 @@
3200 if distroseries is None:
3201 if source_package_recipe_build is not None:
3202 distroseries = source_package_recipe_build.distroseries
3203+ elif source_package_branch_build is not None:
3204+ distroseries = source_package_branch_build.distroseries
3205 else:
3206 if archive is None:
3207 distribution = None
3208@@ -3470,6 +3529,7 @@
3209 archive=archive,
3210 dateuploaded=date_uploaded,
3211 source_package_recipe_build=source_package_recipe_build,
3212+ source_package_branch_build=source_package_branch_build,
3213 user_defined_fields=user_defined_fields,
3214 homepage=homepage)
3215