Merge lp:~abentley/launchpad/skip-repeat-builds into lp:launchpad/db-devel

Proposed by Aaron Bentley
Status: Merged
Approved by: Brad Crittenden
Approved revision: no longer in the source branch.
Merged at revision: 9481
Proposed branch: lp:~abentley/launchpad/skip-repeat-builds
Merge into: lp:launchpad/db-devel
Diff against target: 362 lines (+176/-24)
8 files modified
lib/lp/code/browser/sourcepackagerecipe.py (+14/-7)
lib/lp/code/browser/tests/test_sourcepackagerecipe.py (+24/-3)
lib/lp/code/errors.py (+27/-6)
lib/lp/code/interfaces/webservice.py (+4/-3)
lib/lp/code/model/sourcepackagerecipe.py (+9/-1)
lib/lp/code/model/sourcepackagerecipebuild.py (+3/-0)
lib/lp/code/model/tests/test_sourcepackagerecipe.py (+76/-2)
lib/lp/code/model/tests/test_sourcepackagerecipebuild.py (+19/-2)
To merge this branch: bzr merge lp:~abentley/launchpad/skip-repeat-builds
Reviewer Review Type Date Requested Status
Brad Crittenden (community) code Approve
Review via email: mp+27954@code.launchpad.net

Commit message

Avoid creating duplicate pending builds.

Description of the change

= Summary =
Fix bug #595686: duplicate pending builds can be created.

== Proposed fix ==
Raise BuildAlreadyPending SourcePackageRecipe.requestBuild is called and the
arguments match a build already pending. Handle this in the UI as a validation
error. Handle this SourcePackageRecipeBuild.makeDailyBuilds by skipping to the
next distroseries.

== Pre-implementation notes ==
Preimplementation was with thumper

== Implementation details ==
Also added tests for TooManyBuilds on the webservice. Some quota tests had to
be updated so that they did not raise BuildAlreadyPending instead of
TooManyBuilds.

== Tests ==
bin/test -t test_request_builds_rejects_duplicate -t test_requestBuildRejectRepeats -t test_requestBuildRejectOverQuota -t test_makeDailyBuilds_skips_pending

== Demo and Q/A ==
Create a build. Attempt to create a duplicate build. You should get an error
that the build is already pending.

= Launchpad lint =

Checking for conflicts. and issues in doctests and templates.
Running jslint, xmllint, pyflakes, and pylint.
Using normal rules.

Linting changed files:
  lib/lp/code/model/tests/test_sourcepackagerecipebuild.py
  lib/lp/code/interfaces/webservice.py
  lib/lp/code/errors.py
  lib/lp/code/model/tests/test_sourcepackagerecipe.py
  lib/lp/code/model/sourcepackagerecipebuild.py
  lib/lp/code/browser/sourcepackagerecipe.py
  lib/lp/code/model/sourcepackagerecipe.py
  lib/lp/code/browser/tests/test_sourcepackagerecipe.py

== Pyflakes notices ==

lib/lp/code/interfaces/webservice.py
    8: 'TooManyBuilds' imported but unused
    8: 'CodeImportAlreadyRunning' imported but unused
    8: 'CodeImportNotInReviewedState' imported but unused
    8: 'BuildAlreadyPending' imported but unused
    8: 'BranchMergeProposalExists' imported but unused
    11: 'IBranchSet' imported but unused
    11: 'BranchCreatorNotOwner' imported but unused
    11: 'IBranch' imported but unused
    11: 'BranchExists' imported but unused
    11: 'BranchCreatorNotMemberOfOwnerTeam' imported but unused
    14: 'IBranchMergeProposal' imported but unused
    15: 'IBranchSubscription' imported but unused
    16: 'ICodeImport' imported but unused
    17: 'ICodeReviewComment' imported but unused
    18: 'ICodeReviewVoteReference' imported but unused
    19: 'IStaticDiff' imported but unused
    19: 'IDiff' imported but unused
    19: 'IPreviewDiff' imported but unused
    20: 'ISourcePackageRecipe' imported but unused
    21: 'ISourcePackageRecipeBuild' imported but unused

== Pylint notices ==

lib/lp/code/interfaces/webservice.py
    19: [W0611] Unused import IDiff
    8: [W0611] Unused import TooManyBuilds
    15: [W0611] Unused import IBranchSubscription
    11: [W0611] Unused import BranchCreatorNotOwner
    8: [W0611] Unused import CodeImportNotInReviewedState
    14: [W0611] Unused import IBranchMergeProposal
    8: [W0611] Unused import CodeImportAlreadyRunning
    11: [W0611] Unused import BranchCreatorNotMemberOfOwnerTeam
    16: [W0611] Unused import ICodeImport
    20: [W0611] Unused import ISourcePackageRecipe
    18: [W0611] Unused import ICodeReviewVoteReference
    11: [W0611] Unused import IBranchSet
    19: [W0611] Unused import IStaticDiff
    8: [W0611] Unused import BuildAlreadyPending
    11: [W0611] Unused import IBranch
    19: [W0611] Unused import IPreviewDiff
    8: [W0611] Unused import BranchMergeProposalExists
    11: [W0611] Unused import BranchExists
    21: [W0611] Unused import ISourcePackageRecipeBuild
    17: [W0611] Unused import ICodeReviewComment

To post a comment you must log in.
Revision history for this message
Brad Crittenden (bac) wrote :

Hi Aaron,

Please add a docstring for test_requestBuildRejectRepeats.

The docstring for test_requestBuildRejectOverQuota is identical to the previous. c-n-p error?

Otherwise this branch looks really straightforward. Thanks.

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/code/browser/sourcepackagerecipe.py'
2--- lib/lp/code/browser/sourcepackagerecipe.py 2010-06-16 18:47:46 +0000
3+++ lib/lp/code/browser/sourcepackagerecipe.py 2010-06-18 20:02:28 +0000
4@@ -40,7 +40,7 @@
5 from canonical.launchpad.webapp.sorting import sorted_dotted_numbers
6 from canonical.widgets.itemswidgets import LabeledMultiCheckBoxWidget
7 from lp.buildmaster.interfaces.buildbase import BuildStatus
8-from lp.code.errors import ForbiddenInstruction
9+from lp.code.errors import BuildAlreadyPending, ForbiddenInstruction
10 from lp.code.interfaces.branch import NoSuchBranch
11 from lp.code.interfaces.sourcepackagerecipe import (
12 ISourcePackageRecipe, ISourcePackageRecipeSource, MINIMAL_RECIPE_TEXT)
13@@ -219,11 +219,9 @@
14 label = title
15
16 @property
17- def next_url(self):
18+ def cancel_url(self):
19 return canonical_url(self.context)
20
21- cancel_url = next_url
22-
23 def validate(self, data):
24 over_quota_distroseries = []
25 for distroseries in data['distros']:
26@@ -239,9 +237,18 @@
27 def request_action(self, action, data):
28 """User action for requesting a number of builds."""
29 for distroseries in data['distros']:
30- self.context.requestBuild(
31- data['archive'], self.user, distroseries,
32- PackagePublishingPocket.RELEASE, manual=True)
33+ try:
34+ self.context.requestBuild(
35+ data['archive'], self.user, distroseries,
36+ PackagePublishingPocket.RELEASE, manual=True)
37+ except BuildAlreadyPending, e:
38+ self.setFieldError(
39+ 'distros',
40+ 'An identical build is already pending for %s.' %
41+ e.distroseries)
42+ return
43+ self.next_url = self.cancel_url
44+
45
46
47 class SourcePackageRecipeBuildNavigation(Navigation, FileNavigationMixin):
48
49=== modified file 'lib/lp/code/browser/tests/test_sourcepackagerecipe.py'
50--- lib/lp/code/browser/tests/test_sourcepackagerecipe.py 2010-06-18 07:54:36 +0000
51+++ lib/lp/code/browser/tests/test_sourcepackagerecipe.py 2010-06-18 20:02:28 +0000
52@@ -592,9 +592,10 @@
53 supports_virtualized=True)
54
55 recipe = self.makeRecipe()
56- [recipe.requestBuild(
57- self.ppa, self.chef, woody, PackagePublishingPocket.RELEASE)
58- for x in range(5)]
59+ for x in range(5):
60+ build = recipe.requestBuild(
61+ self.ppa, self.chef, woody, PackagePublishingPocket.RELEASE)
62+ removeSecurityProxy(build).buildstate = BuildStatus.FULLYBUILT
63
64 browser = self.getViewBrowser(recipe, '+request-builds')
65 browser.getControl('Woody').click()
66@@ -602,6 +603,26 @@
67 self.assertIn("You have exceeded today's quota for ubuntu woody.",
68 extract_text(find_main_content(browser.contents)))
69
70+ def test_request_builds_rejects_duplicate(self):
71+ """Over-quota build requests cause validation failures."""
72+ woody = self.factory.makeDistroSeries(
73+ name='woody', displayname='Woody',
74+ distribution=self.ppa.distribution)
75+ woody.nominatedarchindep = woody.newArch(
76+ 'i386', ProcessorFamily.get(1), False, self.factory.makePerson(),
77+ supports_virtualized=True)
78+
79+ recipe = self.makeRecipe()
80+ recipe.requestBuild(
81+ self.ppa, self.chef, woody, PackagePublishingPocket.RELEASE)
82+
83+ browser = self.getViewBrowser(recipe, '+request-builds')
84+ browser.getControl('Woody').click()
85+ browser.getControl('Request builds').click()
86+ self.assertIn(
87+ "An identical build is already pending for ubuntu woody.",
88+ extract_text(find_main_content(browser.contents)))
89+
90
91 class TestSourcePackageRecipeBuildView(BrowserTestCase):
92 """Test behaviour of SourcePackageReciptBuildView."""
93
94=== modified file 'lib/lp/code/errors.py'
95--- lib/lp/code/errors.py 2010-06-11 05:05:52 +0000
96+++ lib/lp/code/errors.py 2010-06-18 20:02:28 +0000
97@@ -7,6 +7,7 @@
98 __all__ = [
99 'BadBranchMergeProposalSearchContext',
100 'BadStateTransition',
101+ 'BuildAlreadyPending',
102 'BranchMergeProposalExists',
103 'CodeImportAlreadyRequested',
104 'CodeImportAlreadyRunning',
105@@ -113,13 +114,33 @@
106 self.newest_supported = newest_supported
107
108
109-class TooManyBuilds(Exception):
110- """A build was requested that exceeded the quota."""
111+class RecipeBuildException(Exception):
112
113- def __init__(self, recipe, distroseries):
114+ def __init__(self, recipe, distroseries, template):
115 self.recipe = recipe
116 self.distroseries = distroseries
117- msg = (
118- 'You have exceeded your quota for recipe %s for distroseries %s'
119- % (self.recipe, distroseries))
120+ msg = template % {'recipe': recipe, 'distroseries': distroseries}
121 Exception.__init__(self, msg)
122+
123+
124+class TooManyBuilds(RecipeBuildException):
125+ """A build was requested that exceeded the quota."""
126+
127+ webservice_error(400)
128+
129+ def __init__(self, recipe, distroseries):
130+ RecipeBuildException.__init__(
131+ self, recipe, distroseries,
132+ 'You have exceeded your quota for recipe %(recipe)s for'
133+ ' distroseries %(distroseries)s')
134+
135+
136+class BuildAlreadyPending(RecipeBuildException):
137+ """A build was requested when an identical build was already pending."""
138+
139+ webservice_error(400)
140+
141+ def __init__(self, recipe, distroseries):
142+ RecipeBuildException.__init__(
143+ self, recipe, distroseries,
144+ 'An identical build of this recipe is already pending.')
145
146=== modified file 'lib/lp/code/interfaces/webservice.py'
147--- lib/lp/code/interfaces/webservice.py 2010-04-23 02:33:32 +0000
148+++ lib/lp/code/interfaces/webservice.py 2010-06-18 20:02:28 +0000
149@@ -6,8 +6,8 @@
150 # The exceptions are imported so that they can produce the special
151 # status code defined by webservice_error when they are raised.
152 from lp.code.errors import (
153- BranchMergeProposalExists, CodeImportAlreadyRunning,
154- CodeImportNotInReviewedState)
155+ BuildAlreadyPending, BranchMergeProposalExists, CodeImportAlreadyRunning,
156+ CodeImportNotInReviewedState, TooManyBuilds)
157 from lp.code.interfaces.branch import (
158 IBranch, IBranchSet, BranchCreatorNotMemberOfOwnerTeam,
159 BranchCreatorNotOwner, BranchExists)
160@@ -18,4 +18,5 @@
161 from lp.code.interfaces.codereviewvote import ICodeReviewVoteReference
162 from lp.code.interfaces.diff import IDiff, IPreviewDiff, IStaticDiff
163 from lp.code.interfaces.sourcepackagerecipe import ISourcePackageRecipe
164-from lp.code.interfaces.sourcepackagerecipebuild import ISourcePackageRecipeBuild
165+from lp.code.interfaces.sourcepackagerecipebuild import (
166+ ISourcePackageRecipeBuild)
167
168=== modified file 'lib/lp/code/model/sourcepackagerecipe.py'
169--- lib/lp/code/model/sourcepackagerecipe.py 2010-06-16 19:47:27 +0000
170+++ lib/lp/code/model/sourcepackagerecipe.py 2010-06-18 20:02:28 +0000
171@@ -24,7 +24,8 @@
172 from canonical.database.datetimecol import UtcDateTimeCol
173 from canonical.launchpad.interfaces.lpstorm import IMasterStore, IStore
174
175-from lp.code.errors import TooManyBuilds
176+from lp.buildmaster.interfaces.buildbase import BuildStatus
177+from lp.code.errors import BuildAlreadyPending, TooManyBuilds
178 from lp.code.interfaces.sourcepackagerecipe import (
179 ISourcePackageRecipe, ISourcePackageRecipeSource,
180 ISourcePackageRecipeData)
181@@ -193,6 +194,13 @@
182 raise reject_reason
183 if self.isOverQuota(requester, distroseries):
184 raise TooManyBuilds(self, distroseries)
185+ pending = IStore(self).find(SourcePackageRecipeBuild,
186+ SourcePackageRecipeBuild.recipe_id == self.id,
187+ SourcePackageRecipeBuild.distroseries_id == distroseries.id,
188+ SourcePackageRecipeBuild.archive_id == archive.id,
189+ SourcePackageRecipeBuild.buildstate == BuildStatus.NEEDSBUILD)
190+ if pending.any() is not None:
191+ raise BuildAlreadyPending(self, distroseries)
192
193 build = getUtility(ISourcePackageRecipeBuildSource).new(distroseries,
194 self, requester, archive)
195
196=== modified file 'lib/lp/code/model/sourcepackagerecipebuild.py'
197--- lib/lp/code/model/sourcepackagerecipebuild.py 2010-06-17 13:38:27 +0000
198+++ lib/lp/code/model/sourcepackagerecipebuild.py 2010-06-18 20:02:28 +0000
199@@ -33,6 +33,7 @@
200 from lp.buildmaster.model.buildbase import BuildBase
201 from lp.buildmaster.model.buildqueue import BuildQueue
202 from lp.buildmaster.model.buildfarmjob import BuildFarmJobOldDerived
203+from lp.code.errors import BuildAlreadyPending
204 from lp.code.interfaces.sourcepackagerecipebuild import (
205 ISourcePackageRecipeBuildJob, ISourcePackageRecipeBuildJobSource,
206 ISourcePackageRecipeBuild, ISourcePackageRecipeBuildSource)
207@@ -229,6 +230,8 @@
208 build = recipe.requestBuild(
209 recipe.daily_build_archive, recipe.owner,
210 distroseries, PackagePublishingPocket.RELEASE)
211+ except BuildAlreadyPending:
212+ continue
213 except:
214 info = sys.exc_info()
215 errorlog.globalErrorUtility.raising(info)
216
217=== modified file 'lib/lp/code/model/tests/test_sourcepackagerecipe.py'
218--- lib/lp/code/model/tests/test_sourcepackagerecipe.py 2010-06-17 13:38:27 +0000
219+++ lib/lp/code/model/tests/test_sourcepackagerecipe.py 2010-06-18 20:02:28 +0000
220@@ -28,10 +28,12 @@
221 from lp.soyuz.interfaces.archive import (
222 ArchiveDisabled, ArchivePurpose, CannotUploadToArchive,
223 InvalidPocketForPPA)
224+from lp.buildmaster.interfaces.buildbase import BuildStatus
225 from lp.buildmaster.interfaces.buildqueue import IBuildQueue
226 from lp.buildmaster.model.buildqueue import BuildQueue
227 from lp.code.errors import (
228- ForbiddenInstruction, TooManyBuilds, TooNewRecipeFormat)
229+ BuildAlreadyPending, ForbiddenInstruction, TooManyBuilds,
230+ TooNewRecipeFormat)
231 from lp.code.interfaces.sourcepackagerecipe import (
232 ISourcePackageRecipe, ISourcePackageRecipeSource, MINIMAL_RECIPE_TEXT)
233 from lp.code.interfaces.sourcepackagerecipebuild import (
234@@ -296,14 +298,41 @@
235 series = list(recipe.distroseries)[0]
236 archive = self.factory.makeArchive(owner=requester)
237 def request_build():
238- recipe.requestBuild(archive, requester, series,
239+ build = recipe.requestBuild(archive, requester, series,
240 PackagePublishingPocket.RELEASE)
241+ removeSecurityProxy(build).buildstate = BuildStatus.FULLYBUILT
242 [request_build() for num in range(5)]
243 e = self.assertRaises(TooManyBuilds, request_build)
244 self.assertIn(
245 'You have exceeded your quota for recipe requester/myrecipe',
246 str(e))
247
248+ def test_requestBuildRejectRepeats(self):
249+ """Reject build requests that are identical to pending builds."""
250+ recipe = self.factory.makeSourcePackageRecipe()
251+ series = list(recipe.distroseries)[0]
252+ archive = self.factory.makeArchive(owner=recipe.owner)
253+ old_build = recipe.requestBuild(archive, recipe.owner, series,
254+ PackagePublishingPocket.RELEASE)
255+ self.assertRaises(
256+ BuildAlreadyPending, recipe.requestBuild, archive, recipe.owner,
257+ series, PackagePublishingPocket.RELEASE)
258+ # Varying archive allows build.
259+ recipe.requestBuild(
260+ self.factory.makeArchive(owner=recipe.owner), recipe.owner,
261+ series, PackagePublishingPocket.RELEASE)
262+ # Varying distroseries allows build.
263+ new_distroseries = self.factory.makeDistroSeries()
264+ new_distroseries.nominatedarchindep = new_distroseries.newArch(
265+ 'i386', ProcessorFamily.get(1), False, recipe.owner,
266+ supports_virtualized=True)
267+ recipe.requestBuild(archive, recipe.owner,
268+ new_distroseries, PackagePublishingPocket.RELEASE)
269+ # Changing status of old build allows new build.
270+ removeSecurityProxy(old_build).buildstate = BuildStatus.FULLYBUILT
271+ recipe.requestBuild(archive, recipe.owner, series,
272+ PackagePublishingPocket.RELEASE)
273+
274 def test_sourcepackagerecipe_description(self):
275 """Ensure that the SourcePackageRecipe has a proper description."""
276 description = u'The whoozits and whatzits.'
277@@ -675,6 +704,51 @@
278 archive=archive, distroseries=distroseries,
279 pocket=PackagePublishingPocket.RELEASE.title)
280
281+ def test_requestBuildRejectRepeat(self):
282+ """Build requests are rejected if already pending."""
283+ person = self.factory.makePerson()
284+ archive = self.factory.makeArchive(owner=person)
285+ distroseries = self.factory.makeDistroSeries()
286+ distroseries_i386 = distroseries.newArch(
287+ 'i386', ProcessorFamily.get(1), False, person,
288+ supports_virtualized=True)
289+ distroseries.nominatedarchindep = distroseries_i386
290+
291+ recipe, user, launchpad = self.makeRecipe(person)
292+ distroseries = ws_object(launchpad, distroseries)
293+ archive = ws_object(launchpad, archive)
294+ recipe.requestBuild(
295+ archive=archive, distroseries=distroseries,
296+ pocket=PackagePublishingPocket.RELEASE.title)
297+ e = self.assertRaises(Exception, recipe.requestBuild,
298+ archive=archive, distroseries=distroseries,
299+ pocket=PackagePublishingPocket.RELEASE.title)
300+ self.assertIn('BuildAlreadyPending', str(e))
301+
302+ def test_requestBuildRejectOverQuota(self):
303+ """Build requests are rejected if they exceed quota."""
304+ person = self.factory.makePerson()
305+ archives = [self.factory.makeArchive(owner=person) for x in range(6)]
306+ distroseries = self.factory.makeDistroSeries()
307+ distroseries_i386 = distroseries.newArch(
308+ 'i386', ProcessorFamily.get(1), False, person,
309+ supports_virtualized=True)
310+ distroseries.nominatedarchindep = distroseries_i386
311+
312+ recipe, user, launchpad = self.makeRecipe(person)
313+ distroseries = ws_object(launchpad, distroseries)
314+ for archive in archives[:-1]:
315+ archive = ws_object(launchpad, archive)
316+ recipe.requestBuild(
317+ archive=archive, distroseries=distroseries,
318+ pocket=PackagePublishingPocket.RELEASE.title)
319+
320+ archive = ws_object(launchpad, archives[-1])
321+ e = self.assertRaises(Exception, recipe.requestBuild,
322+ archive=archive, distroseries=distroseries,
323+ pocket=PackagePublishingPocket.RELEASE.title)
324+ self.assertIn('TooManyBuilds', str(e))
325+
326
327 def test_suite():
328 return unittest.TestLoader().loadTestsFromName(__name__)
329
330=== modified file 'lib/lp/code/model/tests/test_sourcepackagerecipebuild.py'
331--- lib/lp/code/model/tests/test_sourcepackagerecipebuild.py 2010-06-16 19:47:27 +0000
332+++ lib/lp/code/model/tests/test_sourcepackagerecipebuild.py 2010-06-18 20:02:28 +0000
333@@ -236,11 +236,28 @@
334 self.assertEqual(list(recipe.distroseries), [build.distroseries])
335
336 def test_makeDailyBuilds_clears_is_stale(self):
337- recipe = self.factory.makeSourcePackageRecipe(build_daily=True,
338- is_stale=True)
339+ recipe = self.factory.makeSourcePackageRecipe(
340+ build_daily=True, is_stale=True)
341 SourcePackageRecipeBuild.makeDailyBuilds()[0]
342 self.assertFalse(recipe.is_stale)
343
344+ def test_makeDailyBuilds_skips_pending(self):
345+ """When creating daily builds, skip ones that are already pending."""
346+ recipe = self.factory.makeSourcePackageRecipe(
347+ build_daily=True, is_stale=True)
348+ first_distroseries = list(recipe.distroseries)[0]
349+ recipe.requestBuild(
350+ recipe.daily_build_archive, recipe.owner, first_distroseries,
351+ PackagePublishingPocket.RELEASE)
352+ second_distroseries = self.factory.makeDistroSeries()
353+ second_distroseries.nominatedarchindep = second_distroseries.newArch(
354+ 'i386', ProcessorFamily.get(1), False, self.factory.makePerson(),
355+ supports_virtualized=True)
356+ recipe.distroseries.add(second_distroseries)
357+ builds = SourcePackageRecipeBuild.makeDailyBuilds()
358+ self.assertEqual(
359+ [second_distroseries], [build.distroseries for build in builds])
360+
361 def test_getRecentBuilds(self):
362 """Recent builds match the same person, series and receipe.
363

Subscribers

People subscribed via source and target branches

to status/vote changes: