Merge lp:~sinzui/launchpad/nomination-investigation-1 into lp:launchpad

Proposed by Curtis Hovey
Status: Merged
Approved by: Curtis Hovey
Approved revision: no longer in the source branch.
Merged at revision: 16141
Proposed branch: lp:~sinzui/launchpad/nomination-investigation-1
Merge into: lp:launchpad
Diff against target: 902 lines (+248/-573)
5 files modified
lib/lp/bugs/doc/bug-nomination.txt (+0/-570)
lib/lp/bugs/interfaces/bugnomination.py (+1/-2)
lib/lp/bugs/model/tests/test_bug.py (+25/-0)
lib/lp/bugs/tests/test_bugnomination.py (+200/-0)
lib/lp/bugs/tests/test_bugsupervisor_bugnomination.py (+22/-1)
To merge this branch: bzr merge lp:~sinzui/launchpad/nomination-investigation-1
Reviewer Review Type Date Requested Status
Launchpad code reviewers Pending
Review via email: mp+129489@code.launchpad.net

Commit message

Replace doc/bug-nomination.txt with unittests.

Description of the change

Rewrite a doctest while investigating a fix timeout issue.

--------------------------------------------------------------------

QA

    * None, this is just a test change.

LINT

    lib/lp/bugs/interfaces/bugnomination.py
    lib/lp/bugs/model/tests/test_bug.py
    lib/lp/bugs/tests/test_bugnomination.py
    lib/lp/bugs/tests/test_bugsupervisor_bugnomination.py

TEST

    ./bin/test -vvc -t Nomination lp.bugs.model.tests.test_bug
    ./bin/test -vvc lp.bugs.tests.test_bugnomination
    ./bin/test -vvc lp.bugs.tests.test_bugsupervisor_bugnomination

IMPLEMENTATION

Rewrote doc/bug-nomination.txt as unittest that I can extend to fix a bug.
About half of the doctest was already unit tested. There were three
surprises.
1. I was not aware that approving a nomination for a package approved
all the packages too. I found a bug questioning this behaviour and
mentioned it in the test's comment.
2. The "Automatic targeting of new source packages" section was a lie.
The test did not show what the narrative claimed. I think the rules
changed and the test was also changed, but it should have been deleted.
3. BugNomination does *not* implement IHasDateCreated as the interface
claimed, as revealed by the new unittests.
    lib/lp/bugs/interfaces/bugnomination.py
    lib/lp/bugs/model/tests/test_bug.py
    lib/lp/bugs/tests/test_bugnomination.py
    lib/lp/bugs/tests/test_bugsupervisor_bugnomination.py

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=== removed file 'lib/lp/bugs/doc/bug-nomination.txt'
2--- lib/lp/bugs/doc/bug-nomination.txt 2012-08-07 02:31:56 +0000
3+++ lib/lp/bugs/doc/bug-nomination.txt 1970-01-01 00:00:00 +0000
4@@ -1,570 +0,0 @@
5-Bug Nomination
6-==============
7-
8-A bug supervisor can nominate a bug to be fixed in a specific
9-distribution or product series. Nominations are created by
10-calling IBug.addNomination.
11-
12- >>> from zope.component import getUtility
13- >>> from zope.interface.verify import verifyClass
14- >>> from zope.security.proxy import removeSecurityProxy
15- >>> from lp.testing import login_person
16- >>> from lp.bugs.interfaces.bug import IBugSet
17- >>> from lp.bugs.interfaces.bugnomination import IBugNomination
18- >>> from lp.bugs.model.bugnomination import BugNomination
19- >>> from lp.registry.interfaces.distribution import IDistributionSet
20- >>> from lp.registry.interfaces.person import IPersonSet
21- >>> from lp.registry.interfaces.product import IProductSet
22- >>> from lp.testing.sampledata import (ADMIN_EMAIL)
23- >>> login(ADMIN_EMAIL)
24- >>> nominator = factory.makePerson(name='nominator')
25- >>> ubuntu = getUtility(IDistributionSet).getByName("ubuntu")
26- >>> ubuntu = removeSecurityProxy(ubuntu)
27- >>> ubuntu.bug_supervisor = nominator
28- >>> firefox = getUtility(IProductSet).getByName("firefox")
29- >>> firefox = removeSecurityProxy(firefox)
30- >>> firefox.bug_supervisor = nominator
31- >>> ignored = login_person(nominator)
32-
33-The BugNomination class implements IBugNomination.
34-
35- >>> verifyClass(IBugNomination, BugNomination)
36- True
37-
38- >>> bugset = getUtility(IBugSet)
39- >>> bug_one = bugset.get(1)
40-
41- >>> ubuntu_grumpy = ubuntu.getSeries("grumpy")
42- >>> personset = getUtility(IPersonSet)
43- >>> nominator = personset.getByName("nominator")
44-
45- >>> grumpy_nomination = bug_one.addNomination(
46- ... target=ubuntu_grumpy, owner=nominator)
47-
48-The nomination records the distro series or series for which the bug
49-was nominated and the user that submitted the nomination (the "owner").
50-
51- >>> print grumpy_nomination.owner.name
52- nominator
53-
54- >>> print grumpy_nomination.distroseries.fullseriesname
55- Ubuntu Grumpy
56-
57-Let's create another nomination, this time on a product series.
58-
59- >>> from lp.registry.interfaces.product import IProductSet
60-
61- >>> firefox = getUtility(IProductSet).getByName("firefox")
62-
63- >>> firefox_trunk = firefox.getSeries("trunk")
64-
65- >>> nominator = personset.getByName("nominator")
66-
67- >>> firefox_ms_nomination = bug_one.addNomination(
68- ... target=firefox_trunk, owner=nominator)
69-
70- >>> print firefox_ms_nomination.owner.name
71- nominator
72-
73- >>> print firefox_ms_nomination.productseries.title
74- Mozilla Firefox trunk series
75-
76-The target of a nomination can also be accessed through its target
77-attribute.
78-
79- >>> print grumpy_nomination.target.bugtargetdisplayname
80- Ubuntu Grumpy
81-
82- >>> print firefox_ms_nomination.target.bugtargetdisplayname
83- Mozilla Firefox trunk
84-
85-Use IBug.canBeNominatedFor to see if a bug can be nominated for a
86-particular distroseries or productseries. This will consider whether
87-the bug has already been nominated for that series, or even already
88-targeted to that series without a nomination, which can happen for bugs
89-that were reported prior to the release management/nomination
90-functionality existing.
91-
92- >>> ubuntu_breezy_autotest = ubuntu.getSeries("breezy-autotest")
93-
94- >>> bug_one.canBeNominatedFor(firefox_trunk)
95- False
96-
97- >>> bug_one.canBeNominatedFor(ubuntu_grumpy)
98- False
99-
100- >>> bug_one.canBeNominatedFor(ubuntu_breezy_autotest)
101- True
102-
103-Bug five is already targeted to Ubuntu Warty, so even though it has no
104-Warty nominations, it cannot be targeted to Warty.
105-
106- >>> bug_five = bugset.get(5)
107-
108- >>> def by_bugtargetdisplayname(bugtask):
109- ... return bugtask.target.bugtargetdisplayname.lower()
110-
111- >>> tasks = sorted(bug_five.bugtasks, key=by_bugtargetdisplayname)
112-
113- >>> for task in tasks:
114- ... print task.target.bugtargetdisplayname
115- Mozilla Firefox
116- Mozilla Firefox 1.0
117- mozilla-firefox (Ubuntu Warty)
118-
119- >>> ubuntu_warty = ubuntu.getSeries("warty")
120- >>> bug_five.canBeNominatedFor(ubuntu_warty)
121- False
122-
123-The getNominationFor() method returns a nomination for a specific
124-productseries or distroseries. If there is no nomination for the target
125-provided, a NotFoundError is raised.
126-
127- >>> bug_one.getNominationFor(firefox_trunk)
128- <BugNomination ...>
129-
130- >>> bug_one.getNominationFor(ubuntu_grumpy)
131- <BugNomination ...>
132-
133- >>> bug_one.getNominationFor(ubuntu_breezy_autotest)
134- Traceback (most recent call last):
135- ...
136- NotFoundError: ...
137-
138-IBug.getNominations() returns a list of all IBugNominations for a bug,
139-ordered by IBugTarget.bugtargetdisplayname.
140-
141- >>> nominations = bug_one.getNominations()
142-
143- >>> [nomination.target.bugtargetdisplayname for nomination in nominations]
144- [u'Mozilla Firefox 1.0', u'Mozilla Firefox trunk',
145- u'Ubuntu Grumpy', u'Ubuntu Hoary']
146-
147-This method also accepts a target argument, for further filtering.
148-
149- >>> nominations = bug_one.getNominations(firefox)
150-
151- >>> [nomination.target.bugtargetdisplayname for nomination in nominations]
152- [u'Mozilla Firefox 1.0', u'Mozilla Firefox trunk']
153-
154- >>> nominations = bug_one.getNominations(ubuntu)
155-
156- >>> [nomination.target.bugtargetdisplayname for nomination in nominations]
157- [u'Ubuntu Grumpy', u'Ubuntu Hoary']
158-
159-
160-Nomination Status
161------------------
162-
163-A nomination is created with an initial status of "Nominated".
164-Internally this state is called PROPOSED, but in the UI we display it
165-as "Nominated".
166-
167- >>> ubuntu_breezy_autotest_nomination = bug_one.addNomination(
168- ... target=ubuntu_breezy_autotest, owner=nominator)
169-
170- >>> print ubuntu_breezy_autotest_nomination.status.title
171- Nominated
172- >>> ubuntu_breezy_autotest_nomination.isProposed()
173- True
174- >>> ubuntu_breezy_autotest_nomination.isApproved()
175- False
176- >>> ubuntu_breezy_autotest_nomination.isDeclined()
177- False
178-
179-Nomination status changes have an associated workflow. For this reason,
180-setting status directly is not possible.
181-
182- >>> from lp.bugs.interfaces.bugnomination import BugNominationStatus
183-
184- >>> nomination.status = BugNominationStatus.APPROVED
185- Traceback (most recent call last):
186- ...
187- ForbiddenAttribute: ...
188-
189-The status of a nomination is changed by calling either the approve() or
190-decline() method. Only users with launchpad.Driver permission on the
191-nomination can approve or decline it.
192-
193- >>> from lp.services.webapp.authorization import check_permission
194- >>> from lp.services.webapp.interfaces import ILaunchBag
195-
196- >>> current_user = getUtility(ILaunchBag).user
197-
198- >>> current_user == nominator
199- True
200- >>> check_permission("launchpad.Driver", firefox_ms_nomination)
201- False
202-
203- >>> firefox_ms_nomination.approve(nominator)
204- Traceback (most recent call last):
205- ..
206- Unauthorized: ...
207-
208- >>> firefox_ms_nomination.decline(nominator)
209- Traceback (most recent call last):
210- ..
211- Unauthorized: ...
212-
213-(Log in as an admin to set the driver.)
214-
215- >>> login("foo.bar@canonical.com")
216-
217- >>> no_privs = personset.getByName("no-priv")
218- >>> firefox_ms_nomination.target.driver = no_privs
219-
220- >>> login("no-priv@canonical.com")
221-
222-
223-Approving a nomination
224-----------------------
225-
226-When a nomination is approved, the appropriate bugtask(s) are created on
227-the target of the nomination and the status is set to APPROVED.
228-
229-For example, there are currently no bugtasks on the firefox_trunk
230-productseries.
231-
232- >>> from lp.bugs.interfaces.bugtasksearch import BugTaskSearchParams
233-
234- >>> params = BugTaskSearchParams(user=no_privs, bug=bug_one)
235- >>> found_tasks = firefox_trunk.searchTasks(params)
236- >>> found_tasks.count()
237- 0
238-
239-When a nomination is approved, one task is created, targeted at
240-firefox_trunk.
241-
242- >>> firefox_ms_nomination.approve(no_privs)
243-
244- >>> firefox_ms_nomination.isApproved()
245- True
246- >>> firefox_ms_nomination.isProposed()
247- False
248- >>> firefox_ms_nomination.isDeclined()
249- False
250-
251- >>> found_tasks.count()
252- 1
253- >>> bugtask = found_tasks[0]
254- >>> bugtask.target == firefox_trunk
255- True
256- >>> print bugtask.owner.name
257- no-priv
258-
259-When a distribution bug nomination is approved, a task is created for
260-each package the bug affects in that distro. For example, let's ensure
261-bug #1 affects more than one Ubuntu package.
262-
263- >>> from lp.bugs.interfaces.bugtask import IBugTaskSet
264-
265- >>> ubuntu_tbird = ubuntu.getSourcePackage("thunderbird")
266- >>> ignore = factory.makeSourcePackagePublishingHistory(
267- ... distroseries=ubuntu.currentseries,
268- ... sourcepackagename=ubuntu_tbird.sourcepackagename)
269-
270- >>> getUtility(IBugTaskSet).createTask(bug_one, no_privs, ubuntu_tbird)
271- <BugTask ...>
272-
273- >>> tasks = sorted(
274- ... bug_one.bugtasks, key=by_bugtargetdisplayname)
275-
276- >>> for task in tasks:
277- ... print task.target.bugtargetdisplayname
278- Mozilla Firefox
279- Mozilla Firefox trunk
280- mozilla-firefox (Debian)
281- mozilla-firefox (Ubuntu)
282- thunderbird (Ubuntu)
283-
284-When we approve the nomination, two more Ubuntu tasks are added for the
285-Grumpy series. The user that made the decision is stored in the decider
286-attribute. The date on which the decision was made is stored in the
287-date_decided attribute.
288-
289-(Again, first we'll set the driver with an admin user, to ensure
290-no_privs can actually approve the nomination.)
291-
292- >>> login("foo.bar@canonical.com")
293- >>> grumpy_nomination.target.driver = no_privs
294- >>> login("no-priv@canonical.com")
295-
296- >>> grumpy_nomination.date_decided is None
297- True
298- >>> grumpy_nomination.approve(no_privs)
299- >>> print grumpy_nomination.status.title
300- Approved
301- >>> print grumpy_nomination.decider.name
302- no-priv
303- >>> grumpy_nomination.date_decided
304- datetime...
305-
306- >>> tasks = sorted(
307- ... bug_one.bugtasks, key=by_bugtargetdisplayname)
308-
309- >>> for task in tasks:
310- ... print task.target.bugtargetdisplayname
311- Mozilla Firefox
312- Mozilla Firefox trunk
313- mozilla-firefox (Debian)
314- mozilla-firefox (Ubuntu Grumpy)
315- mozilla-firefox (Ubuntu)
316- thunderbird (Ubuntu Grumpy)
317- thunderbird (Ubuntu)
318-
319-Let's now nominate for Warty. no_privs is the driver, so will have
320-no problems.
321-
322- >>> ubuntu_warty = ubuntu.getSeries("warty")
323- >>> login("foo.bar@canonical.com")
324- >>> ubuntu_warty.driver = no_privs
325- >>> login("no-priv@canonical.com")
326-
327- >>> warty_nomination = bug_one.addNomination(
328- ... target=ubuntu_warty, owner=no_privs)
329- >>> warty_nomination.approve(no_privs)
330-
331- >>> print warty_nomination.status.title
332- Approved
333- >>> print warty_nomination.decider.name
334- no-priv
335- >>> warty_nomination.date_decided
336- datetime...
337-
338- >>> tasks = sorted(
339- ... bug_one.bugtasks, key=by_bugtargetdisplayname)
340-
341- >>> for task in tasks:
342- ... print task.target.bugtargetdisplayname
343- Mozilla Firefox
344- Mozilla Firefox trunk
345- mozilla-firefox (Debian)
346- mozilla-firefox (Ubuntu Grumpy)
347- mozilla-firefox (Ubuntu Warty)
348- mozilla-firefox (Ubuntu)
349- thunderbird (Ubuntu Grumpy)
350- thunderbird (Ubuntu Warty)
351- thunderbird (Ubuntu)
352-
353- >>> login("foo.bar@canonical.com")
354- >>> ubuntu_warty.driver = None
355-
356-
357-Declining a nomination
358-----------------------
359-
360-Declining a nomination simply sets its status to DECLINED. No tasks are
361-created.
362-
363- >>> login("foo.bar@canonical.com")
364- >>> ubuntu_breezy_autotest_nomination.target.driver = no_privs
365- >>> login("no-priv@canonical.com")
366-
367- >>> ubuntu_breezy_autotest_nomination.date_decided is None
368- True
369- >>> print ubuntu_breezy_autotest_nomination.status.title
370- Nominated
371-
372- >>> ubuntu_breezy_autotest_nomination.decline(no_privs)
373-
374- >>> print ubuntu_breezy_autotest_nomination.status.title
375- Declined
376-
377- >>> ubuntu_breezy_autotest_nomination.isDeclined()
378- True
379- >>> ubuntu_breezy_autotest_nomination.isApproved()
380- False
381- >>> ubuntu_breezy_autotest_nomination.isProposed()
382- False
383- >>> print ubuntu_breezy_autotest_nomination.decider.name
384- no-priv
385- >>> ubuntu_breezy_autotest_nomination.date_decided
386- datetime...
387-
388-If a nomination is declined, the bug can be re-nominated for the same target.
389-The decider and date declined are reset to None.
390-
391- >>> bug_one.canBeNominatedFor(ubuntu_breezy_autotest)
392- True
393- >>> breezy_nomination = bug_one.addNomination(
394- ... target=ubuntu_breezy_autotest, owner=no_privs)
395- >>> ubuntu_breezy_autotest_nomination.isApproved()
396- False
397- >>> breezy_nomination.isDeclined()
398- False
399- >>> breezy_nomination.isProposed()
400- True
401- >>> print breezy_nomination.decider
402- None
403- >>> print breezy_nomination.date_decided
404- None
405-
406-
407-Automatic targeting of new source packages
408-------------------------------------------
409-
410-If a another distribution task is added, and nomination for that
411-distribution's series already exists, the nominations will be valid
412-for the new task as well, and bugtasks will be created for all accepted
413-ones.
414-
415-The nominations are per distroseries, they are not source package
416-specific, so they are automatically valid for new bugtasks. What's
417-important are the accepted nominations. Bug one has an accepted
418-nomination for Grumpy and Warty:
419-
420- >>> accepted_nominations = [
421- ... nomination for nomination in bug_one.getNominations(ubuntu)
422- ... if nomination.isApproved()]
423- >>> for nomination in accepted_nominations:
424- ... print nomination.distroseries.displayname
425- Grumpy
426- Warty
427-
428-So if we create a new bugtask on evolution (Ubuntu), a task for
429-evolution (Ubuntu Grumpy) and evolution (Ubuntu Warty) will be created
430-automatically.
431-
432- >>> ubuntu_evolution = ubuntu.getSourcePackage('evolution')
433- >>> getUtility(IBugTaskSet).createTask(
434- ... bug_one, no_privs, ubuntu_evolution)
435- <BugTask ...>
436-
437- >>> tasks = sorted(
438- ... bug_one.bugtasks, key=by_bugtargetdisplayname)
439-
440- >>> for task in tasks:
441- ... print task.target.bugtargetdisplayname
442- evolution (Ubuntu Grumpy)
443- evolution (Ubuntu Warty)
444- evolution (Ubuntu)
445- ...
446-
447-
448-Changing the Source Package of a Targeted Bugtask
449--------------------------------------------------
450-
451-The nomination model requires that a generic distribution task exists
452-for each distroseries task. This causes some problem when renaming the
453-source package on an accepted nomination. For example, if we would
454-change the thunderbird package on the Grumpy task, it won't have a
455-corresponding generic distribution task.
456-
457-The way we tie nominations to distribution series, and not to source
458-packages, makes it hard to solve source package changes in a nice way.
459-So what happens when a source package is changed is that we simply
460-rename all other bugtasks which points to the same distribution and
461-source package name. This is not ideal, but hopefully package renames
462-after the bug has been targeted to a series is rare enough for this to
463-be acceptable.
464-
465- >>> thunderbird_grumpy = tasks[-3]
466- >>> thunderbird_grumpy.bugtargetname
467- u'thunderbird (Ubuntu Grumpy)'
468-
469- >>> thunderbird_grumpy.transitionToTarget(
470- ... ubuntu.getSeries('grumpy').getSourcePackage('pmount'),
471- ... getUtility(ILaunchBag).user)
472-
473- >>> tasks = sorted(
474- ... bug_one.bugtasks, key=by_bugtargetdisplayname)
475-
476- >>> for task in tasks:
477- ... print task.target.bugtargetdisplayname
478- evolution (Ubuntu Grumpy)
479- evolution (Ubuntu Warty)
480- evolution (Ubuntu)
481- Mozilla Firefox
482- Mozilla Firefox trunk
483- mozilla-firefox (Debian)
484- mozilla-firefox (Ubuntu Grumpy)
485- mozilla-firefox (Ubuntu Warty)
486- mozilla-firefox (Ubuntu)
487- pmount (Ubuntu Grumpy)
488- pmount (Ubuntu Warty)
489- pmount (Ubuntu)
490-
491-The same is done if the distribution task's source package is changed.
492-
493- >>> pmount_ubuntu = tasks[-1]
494- >>> pmount_ubuntu.bugtargetname
495- u'pmount (Ubuntu)'
496-
497- >>> ubuntu_thunderbird = ubuntu.getSourcePackage('thunderbird')
498- >>> pmount_ubuntu.transitionToTarget(
499- ... ubuntu_thunderbird, getUtility(ILaunchBag).user)
500-
501- >>> tasks = sorted(
502- ... bug_one.bugtasks, key=by_bugtargetdisplayname)
503-
504- >>> for task in tasks:
505- ... print task.target.bugtargetdisplayname
506- evolution (Ubuntu Grumpy)
507- evolution (Ubuntu Warty)
508- evolution (Ubuntu)
509- Mozilla Firefox
510- Mozilla Firefox trunk
511- mozilla-firefox (Debian)
512- mozilla-firefox (Ubuntu Grumpy)
513- mozilla-firefox (Ubuntu Warty)
514- mozilla-firefox (Ubuntu)
515- thunderbird (Ubuntu Grumpy)
516- thunderbird (Ubuntu Warty)
517- thunderbird (Ubuntu)
518-
519-
520-Bug Nomination Set
521-------------------
522-
523-IBugNominationSet is used to fetch bug nominations by ID. This is useful
524-mainly in traversal code.
525-
526- >>> from lp.bugs.interfaces.bugnomination import IBugNominationSet
527-
528- >>> getUtility(IBugNominationSet).get(1)
529- <BugNomination at ...>
530-
531-If a nomination is not found, a NotFoundError is raised.
532-
533- >>> getUtility(IBugNominationSet).get(-1)
534- Traceback (most recent call last):
535- ...
536- NotFoundError: ...
537-
538-
539-Error Handling
540---------------
541-
542-Trying to nominate a bug for a series for which it's already nominated
543-or targeted raises a NominationError.
544-
545- >>> bug_one.addNomination(
546- ... target=ubuntu_grumpy, owner=no_privs)
547- Traceback (most recent call last):
548- ..
549- NominationError: ...
550-
551- >>> bug_one.addNomination(
552- ... target=firefox_trunk, owner=no_privs)
553- Traceback (most recent call last):
554- ..
555- NominationError: ...
556-
557-Nominating a bug for an obsolete distroseries raises a
558-NominationSeriesObsoleteError. Let's make a new obsolete distroseries
559-to demonstrate.
560-
561- >>> from lp.registry.interfaces.series import SeriesStatus
562-
563- >>> login("foo.bar@canonical.com")
564- >>> ubuntu_edgy = factory.makeDistroSeries(
565- ... distribution=ubuntu, version='6.10',
566- ... status=SeriesStatus.OBSOLETE)
567- >>> login("no-priv@canonical.com")
568-
569- >>> bug_one.addNomination(target=ubuntu_edgy, owner=no_privs)
570- Traceback (most recent call last):
571- ..
572- NominationSeriesObsoleteError: ...
573-
574- >>> logout()
575
576=== modified file 'lib/lp/bugs/interfaces/bugnomination.py'
577--- lib/lp/bugs/interfaces/bugnomination.py 2012-01-01 02:58:52 +0000
578+++ lib/lp/bugs/interfaces/bugnomination.py 2012-10-12 18:48:23 +0000
579@@ -47,7 +47,6 @@
580 )
581
582 from lp import _
583-from lp.app.interfaces.launchpad import IHasDateCreated
584 from lp.app.validators.validation import can_be_nominated_for_series
585 from lp.bugs.interfaces.bug import IBug
586 from lp.bugs.interfaces.bugtarget import IBugTarget
587@@ -101,7 +100,7 @@
588 """)
589
590
591-class IBugNomination(IHasBug, IHasOwner, IHasDateCreated):
592+class IBugNomination(IHasBug, IHasOwner):
593 """A nomination for a bug to be fixed in a specific series.
594
595 A nomination can apply to an IDistroSeries or an IProductSeries.
596
597=== modified file 'lib/lp/bugs/model/tests/test_bug.py'
598--- lib/lp/bugs/model/tests/test_bug.py 2012-10-08 01:02:13 +0000
599+++ lib/lp/bugs/model/tests/test_bug.py 2012-10-12 18:48:23 +0000
600@@ -70,6 +70,31 @@
601 nomination = bug.getNominationFor(sourcepackage)
602 self.assertEqual(series, nomination.target)
603
604+ def makeManyNominations(self):
605+ target = self.factory.makeSourcePackage()
606+ series = target.distroseries
607+ with person_logged_in(series.distribution.owner):
608+ nomination = self.factory.makeBugNomination(target=target)
609+ bug = nomination.bug
610+ other_series = self.factory.makeProductSeries()
611+ other_target = other_series.product
612+ self.factory.makeBugTask(bug=bug, target=other_target)
613+ with person_logged_in(other_target.owner):
614+ other_nomination = bug.addNomination(
615+ other_target.owner, other_series)
616+ return bug, [nomination, other_nomination]
617+
618+ def test_getNominations(self):
619+ # The getNominations() method returns all the nominations for the bug.
620+ bug, nominations = self.makeManyNominations()
621+ self.assertContentEqual(nominations, bug.getNominations())
622+
623+ def test_getNominations_with_target(self):
624+ # The target argument filters the nominations to just one pillar.
625+ bug, nominations = self.makeManyNominations()
626+ pillar = nominations[0].target.pillar
627+ self.assertContentEqual([nominations[0]], bug.getNominations(pillar))
628+
629 def test_markAsDuplicate_None(self):
630 # Calling markAsDuplicate(None) on a bug that is not currently a
631 # duplicate works correctly, and does not raise an AttributeError.
632
633=== modified file 'lib/lp/bugs/tests/test_bugnomination.py'
634--- lib/lp/bugs/tests/test_bugnomination.py 2012-08-08 07:22:51 +0000
635+++ lib/lp/bugs/tests/test_bugnomination.py 2012-10-12 18:48:23 +0000
636@@ -5,16 +5,200 @@
637
638 __metaclass__ = type
639
640+from zope.component import getUtility
641+
642+from lp.app.errors import NotFoundError
643+from lp.bugs.interfaces.bugnomination import (
644+ BugNominationStatusError,
645+ BugNominationStatus,
646+ IBugNomination,
647+ IBugNominationSet,
648+ )
649 from lp.soyuz.interfaces.publishing import PackagePublishingStatus
650 from lp.testing import (
651 celebrity_logged_in,
652 login,
653 logout,
654+ person_logged_in,
655 TestCaseWithFactory,
656 )
657 from lp.testing.layers import DatabaseFunctionalLayer
658
659
660+class BugNominationTestCase(TestCaseWithFactory):
661+
662+ layer = DatabaseFunctionalLayer
663+
664+ def test_implementation(self):
665+ # BugNomination implements IBugNomination.
666+ target = self.factory.makeSourcePackage()
667+ series = target.distroseries
668+ bug = self.factory.makeBug(target=target.distribution_sourcepackage)
669+ with person_logged_in(series.distribution.owner):
670+ nomination = bug.addNomination(series.distribution.owner, series)
671+ self.assertProvides(nomination, IBugNomination)
672+ self.assertEqual(series.distribution.owner, nomination.owner)
673+ self.assertEqual(bug, nomination.bug)
674+ self.assertEqual(series, nomination.distroseries)
675+ self.assertIsNone(nomination.productseries)
676+ self.assertEqual(BugNominationStatus.PROPOSED, nomination.status)
677+ self.assertIsNone(nomination.date_decided)
678+ self.assertEqual('UTC', nomination.date_created.tzname())
679+
680+ def test_target_distroseries(self):
681+ # The target property returns the distroseries if it is not None.
682+ target = self.factory.makeSourcePackage()
683+ series = target.distroseries
684+ with person_logged_in(series.distribution.owner):
685+ nomination = self.factory.makeBugNomination(target=target)
686+ self.assertEqual(series, nomination.distroseries)
687+ self.assertEqual(series, nomination.target)
688+
689+ def test_target_productseries(self):
690+ # The target property returns the productseries if it is not None.
691+ series = self.factory.makeProductSeries()
692+ with person_logged_in(series.product.owner):
693+ nomination = self.factory.makeBugNomination(target=series)
694+ self.assertEqual(series, nomination.productseries)
695+ self.assertEqual(series, nomination.target)
696+
697+ def test_status_proposed(self):
698+ # isProposed is True when the status is PROPOSED.
699+ series = self.factory.makeProductSeries()
700+ with person_logged_in(series.product.owner):
701+ nomination = self.factory.makeBugNomination(target=series)
702+ self.assertEqual(BugNominationStatus.PROPOSED, nomination.status)
703+ self.assertIs(True, nomination.isProposed())
704+ self.assertIs(False, nomination.isDeclined())
705+ self.assertIs(False, nomination.isApproved())
706+
707+ def test_status_declined(self):
708+ # isDeclined is True when the status is DECLINED.
709+ series = self.factory.makeProductSeries()
710+ with person_logged_in(series.product.owner):
711+ nomination = self.factory.makeBugNomination(target=series)
712+ nomination.decline(series.product.owner)
713+ self.assertEqual(BugNominationStatus.DECLINED, nomination.status)
714+ self.assertIs(True, nomination.isDeclined())
715+ self.assertIs(False, nomination.isProposed())
716+ self.assertIs(False, nomination.isApproved())
717+
718+ def test_status_approved(self):
719+ # isApproved is True when the status is APPROVED.
720+ series = self.factory.makeProductSeries()
721+ with person_logged_in(series.product.owner):
722+ nomination = self.factory.makeBugNomination(target=series)
723+ nomination.approve(series.product.owner)
724+ self.assertEqual(BugNominationStatus.APPROVED, nomination.status)
725+ self.assertIs(True, nomination.isApproved())
726+ self.assertIs(False, nomination.isDeclined())
727+ self.assertIs(False, nomination.isProposed())
728+
729+ def test_decline(self):
730+ # The decline method updates the status and other data.
731+ series = self.factory.makeProductSeries()
732+ with person_logged_in(series.product.owner):
733+ nomination = self.factory.makeBugNomination(target=series)
734+ bug_tasks = nomination.bug.bugtasks
735+ nomination.decline(series.product.owner)
736+ self.assertEqual(BugNominationStatus.DECLINED, nomination.status)
737+ self.assertIsNotNone(nomination.date_decided)
738+ self.assertEqual(series.product.owner, nomination.decider)
739+ self.assertContentEqual(bug_tasks, nomination.bug.bugtasks)
740+
741+ def test_decline_error(self):
742+ # A nomination cannot be declined if it is approved.
743+ series = self.factory.makeProductSeries()
744+ with person_logged_in(series.product.owner):
745+ nomination = self.factory.makeBugNomination(target=series)
746+ nomination.approve(series.product.owner)
747+ self.assertRaises(
748+ BugNominationStatusError,
749+ nomination.decline, series.product.owner)
750+
751+ def test_approve_productseries(self):
752+ # Approving a product nomination creates a productseries bug task.
753+ series = self.factory.makeProductSeries()
754+ with person_logged_in(series.product.owner):
755+ nomination = self.factory.makeBugNomination(target=series)
756+ bug_tasks = nomination.bug.bugtasks
757+ nomination.approve(series.product.owner)
758+ self.assertEqual(BugNominationStatus.APPROVED, nomination.status)
759+ self.assertIsNotNone(nomination.date_decided)
760+ self.assertEqual(series.product.owner, nomination.decider)
761+ expected_targets = [bt.target for bt in bug_tasks] + [series]
762+ self.assertContentEqual(
763+ expected_targets, [bt.target for bt in nomination.bug.bugtasks])
764+
765+ def test_approve_distroseries_source_package(self):
766+ # Approving a package nomination creates a distroseries
767+ # source package bug task.
768+ target = self.factory.makeSourcePackage()
769+ series = target.distroseries
770+ with person_logged_in(series.distribution.owner):
771+ nomination = self.factory.makeBugNomination(target=target)
772+ bug_tasks = nomination.bug.bugtasks
773+ nomination.approve(series.distribution.owner)
774+ self.assertEqual(BugNominationStatus.APPROVED, nomination.status)
775+ self.assertIsNotNone(nomination.date_decided)
776+ self.assertEqual(series.distribution.owner, nomination.decider)
777+ expected_targets = [bt.target for bt in bug_tasks] + [target]
778+ self.assertContentEqual(
779+ expected_targets, [bt.target for bt in nomination.bug.bugtasks])
780+
781+ def test_approve_distroseries_source_package_many(self):
782+ # Approving a package nomination creates a distroseries
783+ # source package bug task for each affect package in the same distro.
784+ # See bug 110195 which argues this is wrong.
785+ target = self.factory.makeSourcePackage()
786+ series = target.distroseries
787+ target2 = self.factory.makeSourcePackage(distroseries=series)
788+ bug = self.factory.makeBug(target=target.distribution_sourcepackage)
789+ self.factory.makeBugTask(
790+ bug=bug, target=target2.distribution_sourcepackage)
791+ bug_tasks = bug.bugtasks
792+ with person_logged_in(series.distribution.owner):
793+ nomination = self.factory.makeBugNomination(bug=bug, target=target)
794+ nomination.approve(series.distribution.owner)
795+ expected_targets = [bt.target for bt in bug_tasks] + [target, target2]
796+ self.assertContentEqual(
797+ expected_targets, [bt.target for bt in bug.bugtasks])
798+
799+ def test_approve_twice(self):
800+ # Approving a nomination twice is a no-op.
801+ series = self.factory.makeProductSeries()
802+ with person_logged_in(series.product.owner):
803+ nomination = self.factory.makeBugNomination(target=series)
804+ nomination.approve(series.product.owner)
805+ self.assertEqual(BugNominationStatus.APPROVED, nomination.status)
806+ date_decided = nomination.date_decided
807+ self.assertIsNotNone(date_decided)
808+ self.assertEqual(series.product.owner, nomination.decider)
809+ with celebrity_logged_in('admin') as admin:
810+ nomination.approve(admin)
811+ self.assertEqual(date_decided, nomination.date_decided)
812+ self.assertEqual(series.product.owner, nomination.decider)
813+
814+ def test_approve_distroseries_source_package_then_retarget(self):
815+ # Retargeting a bugtarget with and approved nomination also
816+ # retargets the master bug target.
817+ target = self.factory.makeSourcePackage()
818+ series = target.distroseries
819+ with person_logged_in(series.distribution.owner):
820+ nomination = self.factory.makeBugNomination(target=target)
821+ nomination.approve(series.distribution.owner)
822+ target2 = self.factory.makeSourcePackage(
823+ distroseries=series, publish=True)
824+ product_target = nomination.bug.bugtasks[0].target
825+ expected_targets = [
826+ product_target, target2, target2.distribution_sourcepackage]
827+ bug_task = nomination.bug.bugtasks[-1]
828+ with person_logged_in(series.distribution.owner):
829+ bug_task.transitionToTarget(target2, series.distribution.owner)
830+ self.assertContentEqual(
831+ expected_targets, [bt.target for bt in nomination.bug.bugtasks])
832+
833+
834 class CanBeNominatedForTestMixin:
835 """Test case mixin for IBug.canBeNominatedFor."""
836
837@@ -218,3 +402,19 @@
838 self.assertFalse(nomination.canApprove(self.factory.makePerson()))
839 self.assertTrue(nomination.canApprove(package_perm.person))
840 self.assertTrue(nomination.canApprove(comp_perm.person))
841+
842+
843+class BugNominationSetTestCase(TestCaseWithFactory):
844+
845+ layer = DatabaseFunctionalLayer
846+
847+ def test_get(self):
848+ series = self.factory.makeProductSeries()
849+ with person_logged_in(series.product.owner):
850+ nomination = self.factory.makeBugNomination(target=series)
851+ bug_nomination_set = getUtility(IBugNominationSet)
852+ self.assertEqual(nomination, bug_nomination_set.get(nomination.id))
853+
854+ def test_get_none(self):
855+ bug_nomination_set = getUtility(IBugNominationSet)
856+ self.assertRaises(NotFoundError, bug_nomination_set.get, -1)
857
858=== modified file 'lib/lp/bugs/tests/test_bugsupervisor_bugnomination.py'
859--- lib/lp/bugs/tests/test_bugsupervisor_bugnomination.py 2012-08-08 07:22:51 +0000
860+++ lib/lp/bugs/tests/test_bugsupervisor_bugnomination.py 2012-10-12 18:48:23 +0000
861@@ -5,8 +5,13 @@
862
863 __metaclass__ = type
864
865-from lp.bugs.interfaces.bugnomination import NominationError
866+from lp.bugs.interfaces.bugnomination import (
867+ NominationError,
868+ NominationSeriesObsoleteError,
869+ )
870+from lp.registry.interfaces.series import SeriesStatus
871 from lp.testing import (
872+ celebrity_logged_in,
873 login,
874 login_person,
875 logout,
876@@ -47,6 +52,14 @@
877 self.bug.addNomination(self.bug_supervisor, self.series)
878 self.assertTrue(len(self.bug.getNominations()), 1)
879
880+ def test_bugsupervisor_addNominationFor_with_existing_nomination(self):
881+ # A bug cannot be nominated twice for the same series.
882+ login_person(self.bug_supervisor)
883+ self.bug.addNomination(self.bug_supervisor, self.series)
884+ self.assertTrue(len(self.bug.getNominations()), 1)
885+ self.assertRaises(NominationError,
886+ self.bug.addNomination, self.user, self.series)
887+
888 def test_owner_addNominationFor_series(self):
889 # A bug may be nominated for a series of a product with an
890 # exisiting task by the product's owner.
891@@ -82,3 +95,11 @@
892 self.bug.addTask(self.bug_supervisor, self.distro)
893 self.milestone = self.factory.makeMilestone(
894 distribution=self.distro)
895+
896+ def test_bugsupervisor_addNominationFor_with_obsolete_distroseries(self):
897+ # A bug cannot be nominated for an obsolete series.
898+ with celebrity_logged_in('admin'):
899+ self.series.status = SeriesStatus.OBSOLETE
900+ login_person(self.bug_supervisor)
901+ self.assertRaises(NominationSeriesObsoleteError,
902+ self.bug.addNomination, self.user, self.series)