Merge lp:~intellectronica/launchpad/max-heat-from-context into lp:launchpad/db-devel

Proposed by Eleanor Berger
Status: Merged
Merge reported by: Eleanor Berger
Merged at revision: not available
Proposed branch: lp:~intellectronica/launchpad/max-heat-from-context
Merge into: lp:launchpad/db-devel
Diff against target: 1084 lines (+426/-78)
24 files modified
lib/canonical/launchpad/security.py (+3/-9)
lib/lp/bugs/browser/bugtask.py (+26/-9)
lib/lp/bugs/browser/tests/bug-heat-view.txt (+27/-2)
lib/lp/bugs/browser/tests/test_bugtarget_patches_view.py (+102/-0)
lib/lp/bugs/interfaces/bug.py (+7/-3)
lib/lp/bugs/stories/webservice/xx-bug.txt (+33/-0)
lib/lp/buildmaster/interfaces/buildbase.py (+4/-1)
lib/lp/buildmaster/manager.py (+5/-5)
lib/lp/buildmaster/master.py (+1/-1)
lib/lp/buildmaster/model/buildbase.py (+11/-8)
lib/lp/buildmaster/model/builder.py (+3/-3)
lib/lp/registry/model/distributionsourcepackage.py (+1/-0)
lib/lp/registry/stories/distribution/xx-distribution-packages.txt (+16/-2)
lib/lp/soyuz/browser/archivesubscription.py (+20/-6)
lib/lp/soyuz/doc/buildd-slavescanner.txt (+6/-4)
lib/lp/soyuz/model/buildqueue.py (+10/-4)
lib/lp/soyuz/stories/ppa/xx-private-ppa-subscriptions.txt (+31/-8)
lib/lp/soyuz/stories/soyuz/xx-person-packages.txt (+22/-0)
lib/lp/soyuz/tests/test_buildqueue.py (+24/-8)
lib/lp/translations/browser/tests/language-views.txt (+1/-1)
lib/lp/translations/stories/standalone/xx-language.txt (+1/-1)
lib/lp/translations/utilities/doc/pluralforms.txt (+1/-1)
lib/lp/translations/utilities/pluralforms.py (+12/-2)
lib/lp/translations/utilities/tests/test_pluralforms.py (+59/-0)
To merge this branch: bzr merge lp:~intellectronica/launchpad/max-heat-from-context
Reviewer Review Type Date Requested Status
Francis J. Lacoste (community) release-critical Approve
Abel Deuring (community) code Approve
Review via email: mp+20661@code.launchpad.net

Description of the change

This branch changes the way we compare a bug's heat to max_bug_heat in listings. Instead of always comparing to the max_heat of the bugtask's target, we compare it to the max_heat of the context currently being viewed. This is important when looking at the bug listing for a distribution or a project group, because in those cases the bugtasks being displayed may have a target that is different from the one being viewed (like a product in a project listing, or a package in a distro listing).

To post a comment you must log in.
Revision history for this message
Eleanor Berger (intellectronica) wrote :
Download full text (4.7 KiB)

=== modified file 'lib/lp/bugs/browser/bugtask.py'
--- lib/lp/bugs/browser/bugtask.py 2010-03-01 11:28:05 +0000
+++ lib/lp/bugs/browser/bugtask.py 2010-03-04 11:24:09 +0000
@@ -1103,9 +1103,11 @@
         return min(heat_index, 4)

-def bugtask_heat_html(bugtask):
+def bugtask_heat_html(bugtask, target=None):
     """Render the HTML representing bug heat for a given bugask."""
- max_bug_heat = bugtask.target.max_bug_heat
+ if target is None:
+ target = bugtask.target
+ max_bug_heat = target.max_bug_heat
     if max_bug_heat is None:
         max_bug_heat = 5000
     heat_ratio = calculate_heat_display(bugtask.bug.heat, max_bug_heat)
@@ -1979,7 +1981,8 @@
     delegates(IBugTask, 'bugtask')

     def __init__(self, bugtask, has_mentoring_offer, has_bug_branch,
- has_specification, has_patch, request=None):
+ has_specification, has_patch, request=None,
+ target_context=None):
         self.bugtask = bugtask
         self.review_action_widget = None
         self.has_mentoring_offer = has_mentoring_offer
@@ -1987,6 +1990,7 @@
         self.has_specification = has_specification
         self.has_patch = has_patch
         self.request = request
+ self.target_context = target_context

     @property
     def last_significant_change_date(self):
@@ -1998,7 +2002,7 @@
     @property
     def bug_heat_html(self):
         """Returns the bug heat flames HTML."""
- return bugtask_heat_html(self.bugtask)
+ return bugtask_heat_html(self.bugtask, target=self.target_context)

 class BugListingBatchNavigator(TableBatchNavigator):
@@ -2006,8 +2010,10 @@
     # XXX sinzui 2009-05-29 bug=381672: Extract the BugTaskListingItem rules
     # to a mixin so that MilestoneView and others can use it.

- def __init__(self, tasks, request, columns_to_show, size):
+ def __init__(self, tasks, request, columns_to_show, size,
+ target_context=None):
         self.request = request
+ self.target_context = target_context
         TableBatchNavigator.__init__(
             self, tasks, request, columns_to_show=columns_to_show, size=size)

@@ -2025,7 +2031,8 @@
             badge_property['has_branch'],
             badge_property['has_specification'],
             badge_property['has_patch'],
- request=self.request)
+ request=self.request,
+ target_context=self.target_context)

     def getBugListingItems(self):
         """Return a decorated list of visible bug tasks."""
@@ -2428,7 +2435,8 @@
         """Return the batch navigator to be used to batch the bugtasks."""
         return BugListingBatchNavigator(
             tasks, self.request, columns_to_show=self.columns_to_show,
- size=config.malone.buglist_batch_size)
+ size=config.malone.buglist_batch_size,
+ target_context=self.context)

     def buildBugTaskSearchParams(self, searchtext=None, extra_params=None):
         """Build the parameters to submit to the `searchTasks` method.

=== modified file 'lib/lp/bugs/browser/tests/bug-heat-view.txt'
--- lib/lp/bugs/browser/tests/bug-heat-view.txt 2010-02-26 15:36:58 +0000
+++ lib/lp/bug...

Read more...

Revision history for this message
Abel Deuring (adeuring) :
review: Approve (code)
Revision history for this message
Francis J. Lacoste (flacoste) wrote :

Land it on db-devel

review: Approve (release-critical)
9076. By Eleanor Berger

Don't use target_context for calculating bug heat if the context is IPerson or IMaloneApplication - they don't have a max_bug_heat attribute.

9077. By Eleanor Berger

merge changes from devel

9078. By Eleanor Berger

merge changes from devel

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/canonical/launchpad/security.py'
2--- lib/canonical/launchpad/security.py 2010-02-24 23:02:56 +0000
3+++ lib/canonical/launchpad/security.py 2010-03-08 20:45:41 +0000
4@@ -2106,19 +2106,13 @@
5 return user.in_admin
6
7
8-class ViewSourcePackagePublishingHistory(AuthorizationBase):
9+class ViewSourcePackagePublishingHistory(ViewArchive):
10 """Restrict viewing of source publications."""
11 permission = "launchpad.View"
12 usedfor = ISourcePackagePublishingHistory
13
14- def checkAuthenticated(self, user):
15- view_archive = ViewArchive(self.obj.archive)
16- if view_archive.checkAuthenticated(user):
17- return True
18- return user.in_admin
19-
20- def checkUnauthenticated(self):
21- return not self.obj.archive.private
22+ def __init__(self, obj):
23+ super(ViewSourcePackagePublishingHistory, self).__init__(obj.archive)
24
25
26 class EditPublishing(AuthorizationBase):
27
28=== modified file 'lib/lp/bugs/browser/bugtask.py'
29--- lib/lp/bugs/browser/bugtask.py 2010-03-01 11:28:05 +0000
30+++ lib/lp/bugs/browser/bugtask.py 2010-03-08 20:45:41 +0000
31@@ -114,9 +114,10 @@
32 INominationsReviewTableBatchNavigator, INullBugTask, IPersonBugTaskSearch,
33 IProductSeriesBugTask, IRemoveQuestionFromBugTaskForm, IUpstreamBugTask,
34 IUpstreamProductBugTaskSearch, UNRESOLVED_BUGTASK_STATUSES,
35- RESOLVED_BUGTASK_STATUSES)
36+ UNRESOLVED_PLUS_FIXRELEASED_BUGTASK_STATUSES)
37 from lp.bugs.interfaces.bugtracker import BugTrackerType
38 from lp.bugs.interfaces.cve import ICveSet
39+from lp.bugs.interfaces.malone import IMaloneApplication
40 from lp.registry.interfaces.distribution import IDistribution
41 from lp.registry.interfaces.distributionsourcepackage import (
42 IDistributionSourcePackage)
43@@ -1103,9 +1104,11 @@
44 return min(heat_index, 4)
45
46
47-def bugtask_heat_html(bugtask):
48+def bugtask_heat_html(bugtask, target=None):
49 """Render the HTML representing bug heat for a given bugask."""
50- max_bug_heat = bugtask.target.max_bug_heat
51+ if target is None:
52+ target = bugtask.target
53+ max_bug_heat = target.max_bug_heat
54 if max_bug_heat is None:
55 max_bug_heat = 5000
56 heat_ratio = calculate_heat_display(bugtask.bug.heat, max_bug_heat)
57@@ -1886,7 +1889,7 @@
58 """A count of unresolved bugs with patches."""
59 return self.context.searchTasks(
60 None, user=self.user,
61- status=(UNRESOLVED_BUGTASK_STATUSES + RESOLVED_BUGTASK_STATUSES),
62+ status=UNRESOLVED_PLUS_FIXRELEASED_BUGTASK_STATUSES,
63 omit_duplicates=True, has_patch=True).count()
64
65
66@@ -1979,7 +1982,8 @@
67 delegates(IBugTask, 'bugtask')
68
69 def __init__(self, bugtask, has_mentoring_offer, has_bug_branch,
70- has_specification, has_patch, request=None):
71+ has_specification, has_patch, request=None,
72+ target_context=None):
73 self.bugtask = bugtask
74 self.review_action_widget = None
75 self.has_mentoring_offer = has_mentoring_offer
76@@ -1987,6 +1991,7 @@
77 self.has_specification = has_specification
78 self.has_patch = has_patch
79 self.request = request
80+ self.target_context = target_context
81
82 @property
83 def last_significant_change_date(self):
84@@ -1998,7 +2003,7 @@
85 @property
86 def bug_heat_html(self):
87 """Returns the bug heat flames HTML."""
88- return bugtask_heat_html(self.bugtask)
89+ return bugtask_heat_html(self.bugtask, target=self.target_context)
90
91
92 class BugListingBatchNavigator(TableBatchNavigator):
93@@ -2006,8 +2011,10 @@
94 # XXX sinzui 2009-05-29 bug=381672: Extract the BugTaskListingItem rules
95 # to a mixin so that MilestoneView and others can use it.
96
97- def __init__(self, tasks, request, columns_to_show, size):
98+ def __init__(self, tasks, request, columns_to_show, size,
99+ target_context=None):
100 self.request = request
101+ self.target_context = target_context
102 TableBatchNavigator.__init__(
103 self, tasks, request, columns_to_show=columns_to_show, size=size)
104
105@@ -2019,13 +2026,22 @@
106 def _getListingItem(self, bugtask):
107 """Return a decorated bugtask for the bug listing."""
108 badge_property = self.bug_badge_properties[bugtask]
109+ if (IMaloneApplication.providedBy(self.target_context) or
110+ IPerson.providedBy(self.target_context)):
111+ # XXX Tom Berger bug=529846
112+ # When we have a specific interface for things that have bug heat
113+ # it would be better to use that for the check here instead.
114+ target_context = None
115+ else:
116+ target_context = self.target_context
117 return BugTaskListingItem(
118 bugtask,
119 badge_property['has_mentoring_offer'],
120 badge_property['has_branch'],
121 badge_property['has_specification'],
122 badge_property['has_patch'],
123- request=self.request)
124+ request=self.request,
125+ target_context=target_context)
126
127 def getBugListingItems(self):
128 """Return a decorated list of visible bug tasks."""
129@@ -2428,7 +2444,8 @@
130 """Return the batch navigator to be used to batch the bugtasks."""
131 return BugListingBatchNavigator(
132 tasks, self.request, columns_to_show=self.columns_to_show,
133- size=config.malone.buglist_batch_size)
134+ size=config.malone.buglist_batch_size,
135+ target_context=self.context)
136
137 def buildBugTaskSearchParams(self, searchtext=None, extra_params=None):
138 """Build the parameters to submit to the `searchTasks` method.
139
140=== modified file 'lib/lp/bugs/browser/tests/bug-heat-view.txt'
141--- lib/lp/bugs/browser/tests/bug-heat-view.txt 2010-02-26 15:36:58 +0000
142+++ lib/lp/bugs/browser/tests/bug-heat-view.txt 2010-03-08 20:45:41 +0000
143@@ -9,8 +9,8 @@
144 >>> from zope.security.proxy import removeSecurityProxy
145 >>> from BeautifulSoup import BeautifulSoup
146 >>> from lp.bugs.browser.bugtask import bugtask_heat_html
147- >>> def print_flames(bugtask):
148- ... html = bugtask_heat_html(bugtask)
149+ >>> def print_flames(bugtask, target=None):
150+ ... html = bugtask_heat_html(bugtask, target=target)
151 ... soup = BeautifulSoup(html)
152 ... for img in soup.span.contents:
153 ... print img['src']
154@@ -46,6 +46,31 @@
155 0 out of 4 heat flames
156 Heat: 500
157
158+
159+== Specifying the target ==
160+
161+Some bugs can be viewed in a context different from their task's target. For
162+example, bugs with tasks on packages can be viewed in the context of the entire
163+distribution. In such cases, we want to explicitly specify the target, rather
164+than use the bugtask's. We can do that by passing the target as a keyword
165+parameter.
166+
167+ >>> bug = factory.makeBug()
168+ >>> distro = factory.makeDistribution()
169+ >>> dsp = factory.makeDistributionSourcePackage(distribution=distro)
170+ >>> dsp_task = bug.addTask(bug.owner, dsp)
171+ >>> removeSecurityProxy(distro).max_bug_heat = MAX_HEAT
172+ >>> removeSecurityProxy(dsp).max_bug_heat = MAX_HEAT / 2
173+ >>> removeSecurityProxy(bug).heat = MAX_HEAT / 4
174+ >>> print_flames(dsp_task)
175+ /@@/bug-heat-2.png
176+ 2 out of 4 heat flames
177+ Heat: 1250
178+ >>> print_flames(dsp_task, target=distro)
179+ /@@/bug-heat-0.png
180+ 0 out of 4 heat flames
181+ Heat: 1250
182+
183 >>> logout()
184
185
186
187=== added file 'lib/lp/bugs/browser/tests/test_bugtarget_patches_view.py'
188--- lib/lp/bugs/browser/tests/test_bugtarget_patches_view.py 1970-01-01 00:00:00 +0000
189+++ lib/lp/bugs/browser/tests/test_bugtarget_patches_view.py 2010-03-08 20:45:41 +0000
190@@ -0,0 +1,102 @@
191+# Copyright 2010 Canonical Ltd. This software is licensed under the
192+# GNU Affero General Public License version 3 (see the file LICENSE).
193+
194+__metaclass__ = type
195+
196+
197+import unittest
198+
199+from canonical.launchpad.ftests import login
200+from canonical.launchpad.webapp.servers import LaunchpadTestRequest
201+from canonical.testing import LaunchpadFunctionalLayer
202+
203+from lp.bugs.browser.bugtarget import BugsPatchesView
204+from lp.bugs.browser.bugtask import BugListingPortletStatsView
205+from lp.bugs.interfaces.bugtask import BugTaskStatus
206+from lp.testing import TestCaseWithFactory
207+
208+
209+DISPLAY_BUG_STATUS_FOR_PATCHES = {
210+ BugTaskStatus.NEW: True,
211+ BugTaskStatus.INCOMPLETE: True,
212+ BugTaskStatus.INVALID: False,
213+ BugTaskStatus.WONTFIX: False,
214+ BugTaskStatus.CONFIRMED: True,
215+ BugTaskStatus.TRIAGED: True,
216+ BugTaskStatus.INPROGRESS: True,
217+ BugTaskStatus.FIXCOMMITTED: True,
218+ BugTaskStatus.FIXRELEASED: True,
219+ BugTaskStatus.UNKNOWN: False,
220+ }
221+
222+
223+class TestBugTargetPatchCountBase(TestCaseWithFactory):
224+
225+ layer = LaunchpadFunctionalLayer
226+
227+ def setUp(self):
228+ super(TestBugTargetPatchCountBase, self).setUp()
229+ login('foo.bar@canonical.com')
230+ self.product = self.factory.makeProduct()
231+
232+ def makeBugWithPatch(self, status):
233+ bug = self.factory.makeBug(
234+ product=self.product, owner=self.product.owner)
235+ self.factory.makeBugAttachment(bug=bug, is_patch=True)
236+ bug.default_bugtask.transitionToStatus(status, user=bug.owner)
237+
238+
239+class TestBugTargetPatchView(TestBugTargetPatchCountBase):
240+
241+ def setUp(self):
242+ super(TestBugTargetPatchView, self).setUp()
243+ self.view = BugsPatchesView(self.product, LaunchpadTestRequest())
244+
245+ def test_status_of_bugs_with_patches_shown(self):
246+ # Bugs with patches that have the status INVALID, WONTFIX,
247+ # UNKNOWN are not shown in the +patches view; all other
248+ # bugs are shown.
249+ number_of_bugs_shown = 0
250+ for bugtask_status in DISPLAY_BUG_STATUS_FOR_PATCHES:
251+ if DISPLAY_BUG_STATUS_FOR_PATCHES[bugtask_status]:
252+ number_of_bugs_shown += 1
253+ self.makeBugWithPatch(bugtask_status)
254+ batched_tasks = self.view.batchedPatchTasks()
255+ self.assertEqual(
256+ batched_tasks.batch.listlength, number_of_bugs_shown,
257+ "Unexpected number of bugs with patches displayed for status "
258+ "%s" % bugtask_status)
259+
260+
261+class TestBugListingPortletStatsView(TestBugTargetPatchCountBase):
262+
263+ def setUp(self):
264+ super(TestBugListingPortletStatsView, self).setUp()
265+ self.view = BugListingPortletStatsView(
266+ self.product, LaunchpadTestRequest())
267+
268+ def test_bugs_with_patches_count(self):
269+ # Bugs with patches that have the status INVALID, WONTFIX,
270+ # UNKNOWN are not counted in
271+ # BugListingPortletStatsView.bugs_with_patches_count, bugs
272+ # with all other statuses are counted.
273+ number_of_bugs_shown = 0
274+ for bugtask_status in DISPLAY_BUG_STATUS_FOR_PATCHES:
275+ if DISPLAY_BUG_STATUS_FOR_PATCHES[bugtask_status]:
276+ number_of_bugs_shown += 1
277+ self.makeBugWithPatch(bugtask_status)
278+ self.assertEqual(
279+ self.view.bugs_with_patches_count, number_of_bugs_shown,
280+ "Unexpected number of bugs with patches displayed for status "
281+ "%s" % bugtask_status)
282+
283+
284+def test_suite():
285+ suite = unittest.TestSuite()
286+ suite.addTest(unittest.makeSuite(TestBugTargetPatchView))
287+ suite.addTest(unittest.makeSuite(TestBugListingPortletStatsView))
288+ return suite
289+
290+
291+if __name__ == '__main__':
292+ unittest.TextTestRunner().run(test_suite())
293
294=== modified file 'lib/lp/bugs/interfaces/bug.py'
295--- lib/lp/bugs/interfaces/bug.py 2010-03-04 14:01:27 +0000
296+++ lib/lp/bugs/interfaces/bug.py 2010-03-08 20:45:41 +0000
297@@ -37,6 +37,7 @@
298 from lp.bugs.interfaces.bugtask import (
299 BugTaskImportance, BugTaskStatus, IBugTask)
300 from lp.bugs.interfaces.bugwatch import IBugWatch
301+from lp.bugs.interfaces.bugbranch import IBugBranch
302 from lp.bugs.interfaces.cve import ICve
303 from canonical.launchpad.interfaces.launchpad import IPrivacy, NotFoundError
304 from canonical.launchpad.interfaces.message import IMessage
305@@ -251,9 +252,12 @@
306 readonly=True))
307 questions = Attribute("List of questions related to this bug.")
308 specifications = Attribute("List of related specifications.")
309- linked_branches = Attribute(
310- "Branches associated with this bug, usually "
311- "branches on which this bug is being fixed.")
312+ linked_branches = exported(
313+ CollectionField(
314+ title=_("Branches associated with this bug, usually "
315+ "branches on which this bug is being fixed."),
316+ value_type=Reference(schema=IBugBranch),
317+ readonly=True))
318 tags = exported(
319 List(title=_("Tags"), description=_("Separated by whitespace."),
320 value_type=Tag(), required=False))
321
322=== modified file 'lib/lp/bugs/stories/webservice/xx-bug.txt'
323--- lib/lp/bugs/stories/webservice/xx-bug.txt 2010-02-27 20:20:03 +0000
324+++ lib/lp/bugs/stories/webservice/xx-bug.txt 2010-03-08 20:45:41 +0000
325@@ -33,6 +33,7 @@
326 heat: 0
327 id: 11
328 latest_patch_uploaded: None
329+ linked_branches_collection_link: u'http://.../bugs/11/linked_branches'
330 messages_collection_link: u'http://.../bugs/11/messages'
331 name: None
332 number_of_duplicates: 0
333@@ -1965,3 +1966,35 @@
334 >>> for submission in linked_submissions['entries']:
335 ... print submission['submission_key']
336 private-submission
337+
338+Bug branches
339+------------
340+
341+For every bug we can look at the branches linked to it.
342+
343+ >>> bug_four = webservice.get("/bugs/4").jsonBody()
344+ >>> bug_four_branches_url = bug_four['linked_branches_collection_link']
345+ >>> bug_four_branches = webservice.get(bug_four_branches_url).jsonBody()
346+ >>> pprint_collection(bug_four_branches)
347+ resource_type_link: u'http://.../#bug_branch-page-resource'
348+ start: 0
349+ total_size: 2
350+ ---
351+ branch_link: u'http://.../~mark/firefox/release-0.9.2'
352+ bug_link: u'http://.../bugs/4'
353+ resource_type_link: u'http://.../#bug_branch'
354+ self_link: u'http://.../~mark/firefox/release-0.9.2/+bug/4'
355+ ---
356+ branch_link: u'http://.../~name12/firefox/main'
357+ bug_link: u'http://.../bugs/4'
358+ resource_type_link: u'http://.../beta/#bug_branch'
359+ self_link: u'http://.../~name12/firefox/main/+bug/4'
360+ ---
361+
362+For every branch we can also look at the bugs linked to it.
363+
364+ >>> branch_entry = bug_four_branches['entries'][0]
365+ >>> bug_link = webservice.get(
366+ ... branch_entry['bug_link']).jsonBody()
367+ >>> print bug_link['self_link']
368+ http://.../bugs/4
369
370=== modified file 'lib/lp/buildmaster/interfaces/buildbase.py'
371--- lib/lp/buildmaster/interfaces/buildbase.py 2010-02-16 03:52:28 +0000
372+++ lib/lp/buildmaster/interfaces/buildbase.py 2010-03-08 20:45:41 +0000
373@@ -7,7 +7,7 @@
374
375 __metaclass__ = type
376
377-__all__ = ['IBuildBase']
378+__all__ = ['BUILDD_MANAGER_LOG_NAME', 'IBuildBase']
379
380 from zope.interface import Attribute, Interface
381 from zope.schema import Choice, Datetime, Object, TextLine, Timedelta
382@@ -24,6 +24,9 @@
383 from canonical.launchpad import _
384
385
386+BUILDD_MANAGER_LOG_NAME = "slave-scanner"
387+
388+
389 class IBuildBase(Interface):
390 """Common interface shared by farm jobs that build a package."""
391
392
393=== modified file 'lib/lp/buildmaster/manager.py'
394--- lib/lp/buildmaster/manager.py 2010-02-02 16:39:10 +0000
395+++ lib/lp/buildmaster/manager.py 2010-03-08 20:45:41 +0000
396@@ -31,7 +31,7 @@
397 from canonical.launchpad.webapp import urlappend
398 from canonical.librarian.db import write_transaction
399 from canonical.twistedsupport.processmonitor import run_process_with_timeout
400-
401+from lp.buildmaster.interfaces.buildbase import BUILDD_MANAGER_LOG_NAME
402
403 buildd_success_result_map = {
404 'ensurepresent': True,
405@@ -222,7 +222,7 @@
406 Make it less verbose to avoid messing too much with the old code.
407 """
408 level = logging.INFO
409- logger = logging.getLogger('slave-scanner')
410+ logger = logging.getLogger(BUILDD_MANAGER_LOG_NAME)
411
412 # Redirect the output to the twisted log module.
413 channel = logging.StreamHandler(log.StdioOnnaStick())
414@@ -283,7 +283,7 @@
415
416 Perform the finishing-cycle tasks mentioned above.
417 """
418- self.logger.info('Scanning cycle finished.')
419+ self.logger.debug('Scanning cycle finished.')
420 # We are only interested in returned objects of type
421 # BaseDispatchResults, those are the ones that needs evaluation.
422 # None, resulting from successful chains, are discarded.
423@@ -304,7 +304,7 @@
424 # Return the evaluated events for testing purpose.
425 return deferred_results
426
427- self.logger.info('Finishing scanning cycle.')
428+ self.logger.debug('Finishing scanning cycle.')
429 dl = defer.DeferredList(self._deferreds, consumeErrors=True)
430 dl.addBoth(done)
431 return dl
432@@ -415,7 +415,7 @@
433
434 See `RecordingSlave.resumeSlaveHost` for more details.
435 """
436- self.logger.info('Resuming slaves: %s' % recording_slaves)
437+ self.logger.debug('Resuming slaves: %s' % recording_slaves)
438 self.remaining_slaves = recording_slaves
439 if len(self.remaining_slaves) == 0:
440 self.finishCycle()
441
442=== modified file 'lib/lp/buildmaster/master.py'
443--- lib/lp/buildmaster/master.py 2010-01-20 01:23:38 +0000
444+++ lib/lp/buildmaster/master.py 2010-03-08 20:45:41 +0000
445@@ -138,7 +138,7 @@
446 self._tm = tm
447 self.librarian = getUtility(ILibrarianClient)
448 self._archseries = {}
449- self._logger.info("Buildd Master has been initialised")
450+ self._logger.debug("Buildd Master has been initialised")
451
452 def commit(self):
453 self._tm.commit()
454
455=== modified file 'lib/lp/buildmaster/model/buildbase.py'
456--- lib/lp/buildmaster/model/buildbase.py 2010-03-05 23:29:50 +0000
457+++ lib/lp/buildmaster/model/buildbase.py 2010-03-08 20:45:41 +0000
458@@ -29,6 +29,7 @@
459 from canonical.launchpad.helpers import filenameToContentType
460 from canonical.launchpad.interfaces.librarian import ILibraryFileAliasSet
461 from canonical.librarian.utils import copy_and_close
462+from lp.buildmaster.interfaces.buildbase import BUILDD_MANAGER_LOG_NAME
463 from lp.registry.interfaces.pocket import pocketsuffix
464 from lp.soyuz.interfaces.build import BuildStatus
465 from lp.soyuz.model.buildqueue import BuildQueue
466@@ -129,7 +130,7 @@
467
468 def handleStatus(self, status, librarian, slave_status):
469 """See `IBuildBase`."""
470- logger = logging.getLogger()
471+ logger = logging.getLogger(BUILDD_MANAGER_LOG_NAME)
472
473 method = getattr(self, '_handleStatus_' + status, None)
474
475@@ -151,7 +152,8 @@
476 buildid = slave_status['build_id']
477 filemap = slave_status['filemap']
478
479- logger.debug("Processing successful build %s" % buildid)
480+ logger.info("Processing successful build %s from builder %s" % (
481+ buildid, self.buildqueue_record.builder.name))
482 # Explode before collect a binary that is denied in this
483 # distroseries/pocket
484 if not self.archive.allowUpdatesToReleasePocket():
485@@ -180,6 +182,7 @@
486
487 slave = removeSecurityProxy(self.buildqueue_record.builder.slave)
488 for filename in filemap:
489+ logger.info("Grabbing file: %s" % filename)
490 slave_file = slave.getFile(filemap[filename])
491 out_file_name = os.path.join(upload_path, filename)
492 out_file = open(out_file_name, "wb")
493@@ -190,8 +193,8 @@
494 upload_leaf, uploader_logfilename)
495 logger.debug("Saving uploader log at '%s'" % uploader_logfilename)
496
497- logger.debug("Invoking uploader on %s" % root)
498- logger.debug("%s" % uploader_command)
499+ logger.info("Invoking uploader on %s" % root)
500+ logger.info("%s" % uploader_command)
501
502 uploader_process = subprocess.Popen(
503 uploader_command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
504@@ -204,11 +207,11 @@
505 # when it failed HARD (there is a huge effort in process-upload
506 # to not return error, it only happen when the code is broken).
507 uploader_result_code = uploader_process.returncode
508- logger.debug("Uploader returned %d" % uploader_result_code)
509+ logger.info("Uploader returned %d" % uploader_result_code)
510
511 # Quick and dirty hack to carry on on process-upload failures
512 if os.path.exists(upload_dir):
513- logger.debug("The upload directory did not get moved.")
514+ logger.warning("The upload directory did not get moved.")
515 failed_dir = os.path.join(root, "failed-to-move")
516 if not os.path.exists(failed_dir):
517 os.mkdir(failed_dir)
518@@ -261,7 +264,7 @@
519 # binary upload when it was the case.
520 if (self.buildstate != BuildStatus.FULLYBUILT or
521 not self.verifySuccessfulUpload()):
522- logger.debug("Build %s upload failed." % self.id)
523+ logger.warning("Build %s upload failed." % self.id)
524 self.buildstate = BuildStatus.FAILEDTOUPLOAD
525 uploader_log_content = self.getUploadLogContent(root,
526 upload_leaf)
527@@ -271,7 +274,7 @@
528 # Notify the build failure.
529 self.notify(extra_info=uploader_log_content)
530 else:
531- logger.debug(
532+ logger.info(
533 "Gathered %s %d completely" % (
534 self.__class__.__name__, self.id))
535
536
537=== modified file 'lib/lp/buildmaster/model/builder.py'
538--- lib/lp/buildmaster/model/builder.py 2010-01-22 04:01:17 +0000
539+++ lib/lp/buildmaster/model/builder.py 2010-03-08 20:45:41 +0000
540@@ -636,17 +636,17 @@
541
542 def pollBuilders(self, logger, txn):
543 """See IBuilderSet."""
544- logger.info("Slave Scan Process Initiated.")
545+ logger.debug("Slave Scan Process Initiated.")
546
547 buildMaster = BuilddMaster(logger, txn)
548
549- logger.info("Setting Builders.")
550+ logger.debug("Setting Builders.")
551 # Put every distroarchseries we can find into the build master.
552 for archseries in getUtility(IDistroArchSeriesSet):
553 buildMaster.addDistroArchSeries(archseries)
554 buildMaster.setupBuilders(archseries)
555
556- logger.info("Scanning Builders.")
557+ logger.debug("Scanning Builders.")
558 # Scan all the pending builds, update logtails and retrieve
559 # builds where they are completed
560 buildMaster.scanActiveBuilders()
561
562=== modified file 'lib/lp/registry/model/distributionsourcepackage.py'
563--- lib/lp/registry/model/distributionsourcepackage.py 2010-03-05 20:14:29 +0000
564+++ lib/lp/registry/model/distributionsourcepackage.py 2010-03-08 20:45:41 +0000
565@@ -283,6 +283,7 @@
566 results = store.find(
567 Archive,
568 Archive.distribution == self.distribution,
569+ Archive._enabled == True,
570 Archive.private == False,
571 SourcePackagePublishingHistory.archive == Archive.id,
572 (SourcePackagePublishingHistory.status ==
573
574=== modified file 'lib/lp/registry/stories/distribution/xx-distribution-packages.txt'
575--- lib/lp/registry/stories/distribution/xx-distribution-packages.txt 2010-03-06 21:05:16 +0000
576+++ lib/lp/registry/stories/distribution/xx-distribution-packages.txt 2010-03-08 20:45:41 +0000
577@@ -145,7 +145,18 @@
578 >>> ppa_beta = factory.makeArchive(name="beta",
579 ... distribution=ubuntutest)
580
581- # Then publish netapplet to both PPAs
582+The 'ppa_disabled' archive added below will be disabled at the end of this
583+set-up block.
584+It will thus not be listed in the "...other untrusted versions of..." portlet.
585+
586+ >>> ppa_disabled = factory.makeArchive(name="disabled",
587+ ... distribution=ubuntutest)
588+
589+ # Then publish netapplet to all PPAs
590+ >>> netapplet_disabled_pub_breezy = publisher.getPubSource(
591+ ... sourcename="netapplet", archive=ppa_disabled,
592+ ... creator=ppa_disabled.owner,
593+ ... status=PackagePublishingStatus.PUBLISHED, version='0.8.1d1')
594 >>> netapplet_nightly_pub_breezy = publisher.getPubSource(
595 ... sourcename="netapplet", archive=ppa_nightly,
596 ... creator=ppa_nightly.owner,
597@@ -165,6 +176,8 @@
598 >>> from lp.registry.model.karma import KarmaTotalCache
599 >>> ppa_beta_owner_id = ppa_beta.owner.id
600 >>> ppa_nightly_owner_id = ppa_nightly.owner.id
601+ >>> ppa_disabled_owner_id = ppa_disabled.owner.id
602+ >>> ppa_disabled.disable()
603 >>> transaction.commit()
604
605 # XXX: Michael Nelson 2009-07-07 bug=396419. Currently there is no
606@@ -178,12 +191,13 @@
607 ... person=ppa_beta_owner_id, karma_total=200)
608 >>> cache_entry = KarmaTotalCache(
609 ... person=ppa_nightly_owner_id, karma_total=201)
610+ >>> cache_entry = KarmaTotalCache(
611+ ... person=ppa_disabled_owner_id, karma_total=202)
612 >>> transaction.commit()
613 >>> reconnect_stores('launchpad')
614
615 >>> logout()
616
617-
618 A /$DISTRO/+source/$PACKAGE page shows an overview of a source package in
619 a distribution. There are several sections of information.
620
621
622=== modified file 'lib/lp/soyuz/browser/archivesubscription.py'
623--- lib/lp/soyuz/browser/archivesubscription.py 2009-09-29 07:21:40 +0000
624+++ lib/lp/soyuz/browser/archivesubscription.py 2010-03-08 20:45:41 +0000
625@@ -298,12 +298,26 @@
626 self.context)
627
628 # Turn the result set into a list of dicts so it can be easily
629- # accessed in TAL:
630- return [
631- dict(subscription=PersonalArchiveSubscription(self.context,
632- subscr.archive),
633- token=token)
634- for subscr, token in subs_with_tokens]
635+ # accessed in TAL. Note that we need to ensure that only one
636+ # PersonalArchiveSubscription is included for each archive,
637+ # as the person might have participation in multiple
638+ # subscriptions (via different teams).
639+ unique_archives = set()
640+ personal_subscription_tokens = []
641+ for subscription, token in subs_with_tokens:
642+ if subscription.archive in unique_archives:
643+ continue
644+
645+ unique_archives.add(subscription.archive)
646+
647+ personal_subscription = PersonalArchiveSubscription(
648+ self.context, subscription.archive)
649+ personal_subscription_tokens.append({
650+ 'subscription': personal_subscription,
651+ 'token': token
652+ })
653+
654+ return personal_subscription_tokens
655
656
657 class PersonArchiveSubscriptionView(LaunchpadView):
658
659=== modified file 'lib/lp/soyuz/doc/buildd-slavescanner.txt'
660--- lib/lp/soyuz/doc/buildd-slavescanner.txt 2010-02-27 21:18:10 +0000
661+++ lib/lp/soyuz/doc/buildd-slavescanner.txt 2010-03-08 20:45:41 +0000
662@@ -285,7 +285,7 @@
663
664 >>> build = getUtility(IBuildSet).getByQueueEntry(bqItem4)
665 >>> a_builder.updateBuild(bqItem4)
666- CRITICAL:root:***** bob is MANUALDEPWAIT *****
667+ CRITICAL:slave-scanner:***** bob is MANUALDEPWAIT *****
668 >>> build.builder is not None
669 True
670 >>> build.datebuilt is not None
671@@ -314,7 +314,7 @@
672 ... WaitingSlave('BuildStatus.CHROOTFAIL'))
673 >>> build = getUtility(IBuildSet).getByQueueEntry(bqItem5)
674 >>> a_builder.updateBuild(bqItem5)
675- CRITICAL:root:***** bob is CHROOTWAIT *****
676+ CRITICAL:slave-scanner:***** bob is CHROOTWAIT *****
677 >>> build.builder is not None
678 True
679 >>> build.datebuilt is not None
680@@ -340,7 +340,7 @@
681 ... WaitingSlave('BuildStatus.BUILDERFAIL'))
682
683 >>> a_builder.updateBuild(bqItem6)
684- WARNING:root:***** bob has failed *****
685+ WARNING:slave-scanner:***** bob has failed *****
686
687 >>> from canonical.launchpad.ftests import sync
688 >>> sync(a_builder)
689@@ -460,6 +460,7 @@
690
691 >>> build = getUtility(IBuildSet).getByQueueEntry(bqItem10)
692 >>> a_builder.updateBuild(bqItem10)
693+ WARNING:slave-scanner:Build ... upload failed.
694 >>> build.builder is not None
695 True
696 >>> build.datebuilt is not None
697@@ -583,7 +584,7 @@
698 >>> bqItem11.builder.setSlaveForTesting(
699 ... WaitingSlave('BuildStatus.GIVENBACK'))
700 >>> a_builder.updateBuild(bqItem11)
701- WARNING:root:***** 1-1 is GIVENBACK by bob *****
702+ WARNING:slave-scanner:***** 1-1 is GIVENBACK by bob *****
703
704 Ensure GIVENBACK build preserves the history for future use. (we
705 can't be sure if logtail will contain any information, because it
706@@ -1261,6 +1262,7 @@
707 >>> build.upload_log = None
708 >>> candidate.builder.setSlaveForTesting(WaitingSlave('BuildStatus.OK'))
709 >>> a_builder.updateBuild(candidate)
710+ WARNING:slave-scanner:Build ... upload failed.
711 >>> local_transaction.commit()
712
713 >>> build.archive.private
714
715=== modified file 'lib/lp/soyuz/model/buildqueue.py'
716--- lib/lp/soyuz/model/buildqueue.py 2010-02-22 08:30:06 +0000
717+++ lib/lp/soyuz/model/buildqueue.py 2010-03-08 20:45:41 +0000
718@@ -223,16 +223,17 @@
719
720 head_job_processor, head_job_virtualized = head_job_platform
721
722+ now = self._now()
723 delay_query = """
724 SELECT MIN(
725 CASE WHEN
726 EXTRACT(EPOCH FROM
727 (BuildQueue.estimated_duration -
728- (((now() AT TIME ZONE 'UTC') - Job.date_started)))) >= 0
729+ (((%s AT TIME ZONE 'UTC') - Job.date_started)))) >= 0
730 THEN
731 EXTRACT(EPOCH FROM
732 (BuildQueue.estimated_duration -
733- (((now() AT TIME ZONE 'UTC') - Job.date_started))))
734+ (((%s AT TIME ZONE 'UTC') - Job.date_started))))
735 ELSE
736 -- Assume that jobs that have overdrawn their estimated
737 -- duration time budget will complete within 2 minutes.
738@@ -253,7 +254,7 @@
739 AND Job.status = %s
740 AND Builder.virtualized = %s
741 """ % sqlvalues(
742- JobStatus.RUNNING,
743+ now, now, JobStatus.RUNNING,
744 normalize_virtualization(head_job_virtualized))
745
746 if head_job_processor is not None:
747@@ -460,10 +461,15 @@
748
749 # A job will not get dispatched in less than 5 seconds no matter what.
750 start_time = max(5, min_wait_time + sum_of_delays)
751- result = datetime.utcnow() + timedelta(seconds=start_time)
752+ result = self._now() + timedelta(seconds=start_time)
753
754 return result
755
756+ @staticmethod
757+ def _now():
758+ """Provide utcnow() while allowing test code to monkey-patch this."""
759+ return datetime.utcnow()
760+
761
762 class BuildQueueSet(object):
763 """Utility to deal with BuildQueue content class."""
764
765=== modified file 'lib/lp/soyuz/stories/ppa/xx-private-ppa-subscriptions.txt'
766--- lib/lp/soyuz/stories/ppa/xx-private-ppa-subscriptions.txt 2010-02-24 15:52:53 +0000
767+++ lib/lp/soyuz/stories/ppa/xx-private-ppa-subscriptions.txt 2010-03-08 20:45:41 +0000
768@@ -70,21 +70,23 @@
769 ... 'no-subscribers'))
770 No one has access to install software from this PPA.
771
772-Create two new users that can be subscribed to archives:
773+Create two new users that can be subscribed to archives, and a team:
774
775 >>> login('foo.bar@canonical.com')
776 >>> joesmith = factory.makePerson(name="joesmith", displayname="Joe Smith",
777 ... password="test", email="joe@example.com")
778+ >>> teamjoe = factory.makeTeam(
779+ ... owner=joesmith, displayname="Team Joe", name='teamjoe')
780 >>> bradsmith = factory.makePerson(name="bradsmith", displayname="Brad Smith",
781 ... password="test", email="brad@example.com")
782 >>> logout()
783
784-People can be subscribed by entering their details into the displayed
785-form:
786+People and teams can be subscribed by entering their details into the
787+displayed form:
788
789- >>> cprov_browser.getControl(name='field.subscriber').value = 'joesmith'
790+ >>> cprov_browser.getControl(name='field.subscriber').value = 'teamjoe'
791 >>> cprov_browser.getControl(
792- ... name='field.description').value = "Joe is my friend"
793+ ... name='field.description').value = "Joes friends are my friends"
794 >>> cprov_browser.getControl(name="field.actions.add").click()
795 >>> cprov_browser.getControl(name='field.subscriber').value = 'bradsmith'
796 >>> cprov_browser.getControl(
797@@ -100,7 +102,7 @@
798 ... print extract_text(row)
799 Name Expires Comment
800 Brad Smith 2200-08-01 Brad can access for a while. Edit/Cancel
801- Joe Smith Joe is my friend Edit/Cancel
802+ Team Joe Joes friends are my friends Edit/Cancel
803
804
805 == Managing a persons' Archive subscriptions ==
806@@ -120,8 +122,8 @@
807 >>> print extract_text(explanation)
808 You do not have any current subscriptions to private archives...
809
810-First, create a subscription for Joe Smith to mark's archive also, so that
811-Joe has multiple subscriptions:
812+First, create a subscription for Joe Smith's team to mark's archive
813+so that Joe has multiple subscriptions:
814
815 >>> mark_browser = setupBrowser(
816 ... auth="Basic mark@example.com:test")
817@@ -144,6 +146,7 @@
818 PPA named... (ppa:mark/p3a) Mark Shuttleworth View
819 PPA named... (ppa:cprov/p3a) Celso Providelo View
820
821+
822 == Confirming a subscription ==
823
824 When a person clicks on the view button, the subscription is confirmed
825@@ -187,3 +190,23 @@
826 to PPA named p3a for Mark Shuttleworth
827 This repository is signed ...
828
829+Once a person has activated a subscription, being subscribed again via
830+another team does not lead to duplicate entries on the person's
831+subscriptions page.
832+
833+ >>> mark_browser.open("http://launchpad.dev/~mark/+archive/p3a")
834+ >>> mark_browser.getLink("Manage access").click()
835+ >>> mark_browser.getControl(name='field.subscriber').value = 'teamjoe'
836+ >>> mark_browser.getControl(
837+ ... name='field.description').value = "Joe's friends are my friends."
838+ >>> mark_browser.getControl(name="field.actions.add").click()
839+ >>> joe_browser.open(
840+ ... "http://launchpad.dev/~joesmith/+archivesubscriptions")
841+ >>> rows = find_tags_by_class(
842+ ... joe_browser.contents, 'archive-subscription-row')
843+ >>> for row in rows:
844+ ... print extract_text(row)
845+ Archive Owner
846+ PPA named p3a for Mark Shuttleworth (ppa:mark/p3a) Mark Shuttleworth View
847+ PPA named p3a for Celso Providelo (ppa:cprov/p3a) Celso Providelo View
848+
849
850=== modified file 'lib/lp/soyuz/stories/soyuz/xx-person-packages.txt'
851--- lib/lp/soyuz/stories/soyuz/xx-person-packages.txt 2010-02-28 12:58:52 +0000
852+++ lib/lp/soyuz/stories/soyuz/xx-person-packages.txt 2010-03-08 20:45:41 +0000
853@@ -398,3 +398,25 @@
854 source2 PPA named p3a for No Priv... - Ubuntutest Breezy-autotest 666
855 ...ago None - -
856
857+Please note also that disabled archives are not viewable by anonymous users.
858+
859+ >>> def print_archive_package_rows(contents):
860+ ... package_table = find_tag_by_id(
861+ ... anon_browser.contents, 'packages_list')
862+ ... for ppa_row in package_table.findChildren('tr'):
863+ ... print extract_text(ppa_row)
864+
865+ >>> anon_browser.open("http://launchpad.dev/~cprov/+archive/ppa")
866+ >>> print_archive_package_rows(anon_browser)
867+ Package Version Uploaded by
868+ ...
869+ pmount 0.1-1 no signer (2007-07-09)
870+
871+ >>> login("foo.bar@canonical.com")
872+ >>> cprov.archive.disable()
873+ >>> flush_database_updates()
874+ >>> logout()
875+ >>> anon_browser.open("http://launchpad.dev/~cprov/+archive/ppa")
876+ Traceback (most recent call last):
877+ ...
878+ Unauthorized: (..., 'browserDefault', 'launchpad.View')
879
880=== modified file 'lib/lp/soyuz/tests/test_buildqueue.py'
881--- lib/lp/soyuz/tests/test_buildqueue.py 2010-02-25 16:34:13 +0000
882+++ lib/lp/soyuz/tests/test_buildqueue.py 2010-03-08 20:45:41 +0000
883@@ -96,8 +96,12 @@
884 queue_entry.lastscore)
885
886
887-def check_mintime_to_builder(test, bq, min_time):
888+def check_mintime_to_builder(
889+ test, bq, min_time, time_stamp=datetime.utcnow()):
890 """Test the estimated time until a builder becomes available."""
891+ # Monkey-patch BuildQueueSet._now() so it returns a constant time stamp
892+ # that's not too far in the future. This avoids spurious test failures.
893+ monkey_patch_the_now_property(bq)
894 delay = bq._estimateTimeToNextBuilder()
895 test.assertTrue(
896 delay <= min_time,
897@@ -131,8 +135,24 @@
898 return builder_data[(getattr(job.processor, 'id', None), job.virtualized)]
899
900
901+def monkey_patch_the_now_property(buildqueue):
902+ """Patch BuildQueue._now() so it returns a constant time stamp.
903+
904+ This avoids spurious test failures.
905+ """
906+ # Use the date/time the job started if available.
907+ time_stamp = buildqueue.job.date_started
908+ if not time_stamp:
909+ time_stamp = datetime.utcnow()
910+ buildqueue._now = lambda: time_stamp
911+ return time_stamp
912+
913+
914 def check_estimate(test, job, delay_in_seconds):
915 """Does the dispatch time estimate match the expectation?"""
916+ # Monkey-patch BuildQueueSet._now() so it returns a constant time stamp.
917+ # This avoids spurious test failures.
918+ time_stamp = monkey_patch_the_now_property(job)
919 estimate = job.getEstimatedJobStartTime()
920 if delay_in_seconds is None:
921 test.assertEquals(
922@@ -140,7 +160,7 @@
923 "An estimate should not be possible at present but one was "
924 "returned (%s) nevertheless." % estimate)
925 else:
926- estimate -= datetime.utcnow()
927+ estimate -= time_stamp
928 test.assertTrue(
929 estimate.seconds <= delay_in_seconds,
930 "The estimated delay deviates from the expected one (%s > %s)" %
931@@ -495,9 +515,7 @@
932 class TestMinTimeToNextBuilder(SingleArchBuildsBase):
933 """Test estimated time-to-builder with builds targetting a single
934 processor."""
935- # XXX Michael Nelson 20100223 bug=525329
936- # This is still failing spuriously.
937- def disabled_test_min_time_to_next_builder(self):
938+ def test_min_time_to_next_builder(self):
939 """When is the next builder capable of running the job at the head of
940 the queue becoming available?"""
941 # Test the estimation of the minimum time until a builder becomes
942@@ -684,9 +702,7 @@
943
944 class TestMinTimeToNextBuilderMulti(MultiArchBuildsBase):
945 """Test estimated time-to-builder with builds and multiple processors."""
946- # XXX Michael Nelson 20100223 bug=525329
947- # This is still failing spuriously.
948- def disabled_test_min_time_to_next_builder(self):
949+ def test_min_time_to_next_builder(self):
950 """When is the next builder capable of running the job at the head of
951 the queue becoming available?"""
952 # One of four builders for the 'apg' build is immediately available.
953
954=== modified file 'lib/lp/translations/browser/tests/language-views.txt'
955--- lib/lp/translations/browser/tests/language-views.txt 2009-12-03 15:14:55 +0000
956+++ lib/lp/translations/browser/tests/language-views.txt 2010-03-08 20:45:41 +0000
957@@ -93,7 +93,7 @@
958 ... print form_dict['form'], ':', form_dict['examples']
959 0 : 1, 21, 31, 41, 51, 61...
960 1 : 2, 3, 4, 22, 23, 24...
961- 2 : 5, 6, 7, 8, 9, 10...
962+ 2 : 0, 5, 6, 7, 8, 9...
963
964 View LanguageSet
965 ------------------
966
967=== modified file 'lib/lp/translations/stories/standalone/xx-language.txt'
968--- lib/lp/translations/stories/standalone/xx-language.txt 2009-12-18 20:05:08 +0000
969+++ lib/lp/translations/stories/standalone/xx-language.txt 2010-03-08 20:45:41 +0000
970@@ -117,7 +117,7 @@
971 Plural forms
972 Spanish has 2 plural forms:
973 Form 0 for 1.
974- Form 1 for 2, 3, 4, 5, 6, 7...
975+ Form 1 for 0, 2, 3, 4, 5, 6...
976 When ...
977
978 >>> translationteams_portlet = find_portlet(
979
980=== modified file 'lib/lp/translations/utilities/doc/pluralforms.txt'
981--- lib/lp/translations/utilities/doc/pluralforms.txt 2009-09-16 02:35:32 +0000
982+++ lib/lp/translations/utilities/doc/pluralforms.txt 2010-03-08 20:45:41 +0000
983@@ -31,4 +31,4 @@
984 ... print form_dict['form'], ":", form_dict['examples']
985 0 : [1, 21, 31, 41, 51, 61]
986 1 : [2, 3, 4, 22, 23, 24]
987- 2 : [5, 6, 7, 8, 9, 10]
988+ 2 : [0, 5, 6, 7, 8, 9]
989
990=== modified file 'lib/lp/translations/utilities/pluralforms.py'
991--- lib/lp/translations/utilities/pluralforms.py 2009-09-16 02:35:32 +0000
992+++ lib/lp/translations/utilities/pluralforms.py 2010-03-08 20:45:41 +0000
993@@ -19,8 +19,13 @@
994 # The max length of the examples list per plural form.
995 MAX_EXAMPLES = 6
996
997- for number in range(1, 200):
998- form = expression(number)
999+ for number in range(0, 200):
1000+ try:
1001+ form = expression(number)
1002+ except ZeroDivisionError:
1003+ raise BadPluralExpression(
1004+ "Zero division error in the plural form expression.")
1005+
1006 # Create empty list if this form doesn't have one yet
1007 forms.setdefault(form, [])
1008 # If all the plural forms for this language have examples (max. of 6
1009@@ -29,6 +34,11 @@
1010 continue
1011 forms[form].append(number)
1012
1013+ if pluralforms_count != len(forms):
1014+ raise BadPluralExpression(
1015+ "Number of recognized plural forms doesn't match the "
1016+ "expected number of them.")
1017+
1018 # Each dict has two keys, 'form' and 'examples', that address the form
1019 # number index and a list of its examples.
1020 return [{'form' : form, 'examples' : examples}
1021
1022=== added file 'lib/lp/translations/utilities/tests/test_pluralforms.py'
1023--- lib/lp/translations/utilities/tests/test_pluralforms.py 1970-01-01 00:00:00 +0000
1024+++ lib/lp/translations/utilities/tests/test_pluralforms.py 2010-03-08 20:45:41 +0000
1025@@ -0,0 +1,59 @@
1026+# Copyright 2009 Canonical Ltd. This software is licensed under the
1027+# GNU Affero General Public License version 3 (see the file LICENSE).
1028+
1029+import unittest
1030+
1031+from lp.translations.utilities.pluralforms import (
1032+ BadPluralExpression,
1033+ make_friendly_plural_forms)
1034+
1035+class PluralFormsTest(unittest.TestCase):
1036+ """Test utilities for handling plural forms."""
1037+
1038+ def test_make_friendly_plural_form(self):
1039+ single_form = make_friendly_plural_forms('0', 1)
1040+ self.assertEqual(single_form,
1041+ [{'examples': [0, 1, 2, 3, 4, 5], 'form': 0}])
1042+
1043+ two_forms = make_friendly_plural_forms('n!=1', 2)
1044+ self.assertEqual(two_forms,
1045+ [{'examples': [1], 'form': 0},
1046+ {'examples': [0, 2, 3, 4, 5, 6], 'form': 1}])
1047+
1048+ def test_make_friendly_plural_form_failures(self):
1049+ # 'To the degree of' is not accepted.
1050+ self.assertRaises(BadPluralExpression,
1051+ make_friendly_plural_forms, 'n**2', 1)
1052+
1053+ # Expressions longer than 500 characters are not accepted.
1054+ self.assertRaises(BadPluralExpression,
1055+ make_friendly_plural_forms, '1'*501, 1)
1056+
1057+ # Using arbitrary variable names is not allowed.
1058+ self.assertRaises(BadPluralExpression,
1059+ make_friendly_plural_forms, '(a=1)', 1)
1060+
1061+ # If number of actual forms doesn't match requested number.
1062+ self.assertRaises(BadPluralExpression,
1063+ make_friendly_plural_forms, 'n!=1', 3)
1064+
1065+ # Dividing by zero doesn't work.
1066+ self.assertRaises(BadPluralExpression,
1067+ make_friendly_plural_forms, '(n/0)', 1)
1068+
1069+ def test_make_friendly_plural_form_zero_handling(self):
1070+ zero_forms = make_friendly_plural_forms('n!=0', 2)
1071+ self.assertEqual(zero_forms,
1072+ [{'examples': [0], 'form': 0},
1073+ {'examples': [1, 2, 3, 4, 5, 6], 'form': 1}])
1074+
1075+ # Since 'n' can be zero as well, dividing by it won't work.
1076+ self.assertRaises(BadPluralExpression,
1077+ make_friendly_plural_forms, '(1/n)', 1)
1078+
1079+
1080+def test_suite():
1081+ suite = unittest.TestSuite()
1082+ suite.addTest(unittest.makeSuite(PluralFormsTest))
1083+ return suite
1084+

Subscribers

People subscribed via source and target branches

to status/vote changes: