← Back to team overview

launchpad-reviewers team mailing list archive

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

 

Curtis Hovey has proposed merging lp:~sinzui/launchpad/nomination-investigation-1 into lp:launchpad.

Commit message:
Replace doc/bug-nomination.txt with unittests.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~sinzui/launchpad/nomination-investigation-1/+merge/129489

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
-- 
https://code.launchpad.net/~sinzui/launchpad/nomination-investigation-1/+merge/129489
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~sinzui/launchpad/nomination-investigation-1 into lp:launchpad.
=== removed file 'lib/lp/bugs/doc/bug-nomination.txt'
--- lib/lp/bugs/doc/bug-nomination.txt	2012-08-07 02:31:56 +0000
+++ lib/lp/bugs/doc/bug-nomination.txt	1970-01-01 00:00:00 +0000
@@ -1,570 +0,0 @@
-Bug Nomination
-==============
-
-A bug supervisor can nominate a bug to be fixed in a specific
-distribution or product series. Nominations are created by
-calling IBug.addNomination.
-
-    >>> from zope.component import getUtility
-    >>> from zope.interface.verify import verifyClass
-    >>> from zope.security.proxy import removeSecurityProxy
-    >>> from lp.testing import login_person
-    >>> from lp.bugs.interfaces.bug import IBugSet
-    >>> from lp.bugs.interfaces.bugnomination import IBugNomination
-    >>> from lp.bugs.model.bugnomination import BugNomination
-    >>> from lp.registry.interfaces.distribution import IDistributionSet
-    >>> from lp.registry.interfaces.person import IPersonSet
-    >>> from lp.registry.interfaces.product import IProductSet
-    >>> from lp.testing.sampledata import (ADMIN_EMAIL)
-    >>> login(ADMIN_EMAIL)
-    >>> nominator = factory.makePerson(name='nominator')
-    >>> ubuntu = getUtility(IDistributionSet).getByName("ubuntu")
-    >>> ubuntu = removeSecurityProxy(ubuntu)
-    >>> ubuntu.bug_supervisor = nominator
-    >>> firefox = getUtility(IProductSet).getByName("firefox")
-    >>> firefox = removeSecurityProxy(firefox)
-    >>> firefox.bug_supervisor = nominator
-    >>> ignored = login_person(nominator)
-
-The BugNomination class implements IBugNomination.
-
-    >>> verifyClass(IBugNomination, BugNomination)
-    True
-
-    >>> bugset = getUtility(IBugSet)
-    >>> bug_one = bugset.get(1)
-
-    >>> ubuntu_grumpy = ubuntu.getSeries("grumpy")
-    >>> personset = getUtility(IPersonSet)
-    >>> nominator = personset.getByName("nominator")
-
-    >>> grumpy_nomination = bug_one.addNomination(
-    ...     target=ubuntu_grumpy, owner=nominator)
-
-The nomination records the distro series or series for which the bug
-was nominated and the user that submitted the nomination (the "owner").
-
-    >>> print grumpy_nomination.owner.name
-    nominator
-
-    >>> print grumpy_nomination.distroseries.fullseriesname
-    Ubuntu Grumpy
-
-Let's create another nomination, this time on a product series.
-
-    >>> from lp.registry.interfaces.product import IProductSet
-
-    >>> firefox = getUtility(IProductSet).getByName("firefox")
-
-    >>> firefox_trunk = firefox.getSeries("trunk")
-
-    >>> nominator = personset.getByName("nominator")
-
-    >>> firefox_ms_nomination = bug_one.addNomination(
-    ...     target=firefox_trunk, owner=nominator)
-
-    >>> print firefox_ms_nomination.owner.name
-    nominator
-
-    >>> print firefox_ms_nomination.productseries.title
-    Mozilla Firefox trunk series
-
-The target of a nomination can also be accessed through its target
-attribute.
-
-    >>> print grumpy_nomination.target.bugtargetdisplayname
-    Ubuntu Grumpy
-
-    >>> print firefox_ms_nomination.target.bugtargetdisplayname
-    Mozilla Firefox trunk
-
-Use IBug.canBeNominatedFor to see if a bug can be nominated for a
-particular distroseries or productseries. This will consider whether
-the bug has already been nominated for that series, or even already
-targeted to that series without a nomination, which can happen for bugs
-that were reported prior to the release management/nomination
-functionality existing.
-
-    >>> ubuntu_breezy_autotest = ubuntu.getSeries("breezy-autotest")
-
-    >>> bug_one.canBeNominatedFor(firefox_trunk)
-    False
-
-    >>> bug_one.canBeNominatedFor(ubuntu_grumpy)
-    False
-
-    >>> bug_one.canBeNominatedFor(ubuntu_breezy_autotest)
-    True
-
-Bug five is already targeted to Ubuntu Warty, so even though it has no
-Warty nominations, it cannot be targeted to Warty.
-
-    >>> bug_five = bugset.get(5)
-
-    >>> def by_bugtargetdisplayname(bugtask):
-    ...     return bugtask.target.bugtargetdisplayname.lower()
-
-    >>> tasks = sorted(bug_five.bugtasks, key=by_bugtargetdisplayname)
-
-    >>> for task in tasks:
-    ...     print task.target.bugtargetdisplayname
-    Mozilla Firefox
-    Mozilla Firefox 1.0
-    mozilla-firefox (Ubuntu Warty)
-
-    >>> ubuntu_warty = ubuntu.getSeries("warty")
-    >>> bug_five.canBeNominatedFor(ubuntu_warty)
-    False
-
-The getNominationFor() method returns a nomination for a specific
-productseries or distroseries. If there is no nomination for the target
-provided, a NotFoundError is raised.
-
-    >>> bug_one.getNominationFor(firefox_trunk)
-    <BugNomination ...>
-
-    >>> bug_one.getNominationFor(ubuntu_grumpy)
-    <BugNomination ...>
-
-    >>> bug_one.getNominationFor(ubuntu_breezy_autotest)
-    Traceback (most recent call last):
-      ...
-    NotFoundError: ...
-
-IBug.getNominations() returns a list of all IBugNominations for a bug,
-ordered by IBugTarget.bugtargetdisplayname.
-
-    >>> nominations = bug_one.getNominations()
-
-    >>> [nomination.target.bugtargetdisplayname for nomination in nominations]
-    [u'Mozilla Firefox 1.0', u'Mozilla Firefox trunk',
-     u'Ubuntu Grumpy', u'Ubuntu Hoary']
-
-This method also accepts a target argument, for further filtering.
-
-    >>> nominations = bug_one.getNominations(firefox)
-
-    >>> [nomination.target.bugtargetdisplayname for nomination in nominations]
-    [u'Mozilla Firefox 1.0', u'Mozilla Firefox trunk']
-
-    >>> nominations = bug_one.getNominations(ubuntu)
-
-    >>> [nomination.target.bugtargetdisplayname for nomination in nominations]
-    [u'Ubuntu Grumpy', u'Ubuntu Hoary']
-
-
-Nomination Status
------------------
-
-A nomination is created with an initial status of "Nominated".
-Internally this state is called PROPOSED, but in the UI we display it
-as "Nominated".
-
-    >>> ubuntu_breezy_autotest_nomination = bug_one.addNomination(
-    ...     target=ubuntu_breezy_autotest, owner=nominator)
-
-    >>> print ubuntu_breezy_autotest_nomination.status.title
-    Nominated
-    >>> ubuntu_breezy_autotest_nomination.isProposed()
-    True
-    >>> ubuntu_breezy_autotest_nomination.isApproved()
-    False
-    >>> ubuntu_breezy_autotest_nomination.isDeclined()
-    False
-
-Nomination status changes have an associated workflow. For this reason,
-setting status directly is not possible.
-
-    >>> from lp.bugs.interfaces.bugnomination import BugNominationStatus
-
-    >>> nomination.status = BugNominationStatus.APPROVED
-    Traceback (most recent call last):
-      ...
-    ForbiddenAttribute: ...
-
-The status of a nomination is changed by calling either the approve() or
-decline() method. Only users with launchpad.Driver permission on the
-nomination can approve or decline it.
-
-    >>> from lp.services.webapp.authorization import check_permission
-    >>> from lp.services.webapp.interfaces import ILaunchBag
-
-    >>> current_user = getUtility(ILaunchBag).user
-
-    >>> current_user == nominator
-    True
-    >>> check_permission("launchpad.Driver", firefox_ms_nomination)
-    False
-
-    >>> firefox_ms_nomination.approve(nominator)
-    Traceback (most recent call last):
-      ..
-    Unauthorized: ...
-
-    >>> firefox_ms_nomination.decline(nominator)
-    Traceback (most recent call last):
-      ..
-    Unauthorized: ...
-
-(Log in as an admin to set the driver.)
-
-    >>> login("foo.bar@xxxxxxxxxxxxx")
-
-    >>> no_privs = personset.getByName("no-priv")
-    >>> firefox_ms_nomination.target.driver = no_privs
-
-    >>> login("no-priv@xxxxxxxxxxxxx")
-
-
-Approving a nomination
-----------------------
-
-When a nomination is approved, the appropriate bugtask(s) are created on
-the target of the nomination and the status is set to APPROVED.
-
-For example, there are currently no bugtasks on the firefox_trunk
-productseries.
-
-    >>> from lp.bugs.interfaces.bugtasksearch import BugTaskSearchParams
-
-    >>> params = BugTaskSearchParams(user=no_privs, bug=bug_one)
-    >>> found_tasks = firefox_trunk.searchTasks(params)
-    >>> found_tasks.count()
-    0
-
-When a nomination is approved, one task is created, targeted at
-firefox_trunk.
-
-    >>> firefox_ms_nomination.approve(no_privs)
-
-    >>> firefox_ms_nomination.isApproved()
-    True
-    >>> firefox_ms_nomination.isProposed()
-    False
-    >>> firefox_ms_nomination.isDeclined()
-    False
-
-    >>> found_tasks.count()
-    1
-    >>> bugtask = found_tasks[0]
-    >>> bugtask.target == firefox_trunk
-    True
-    >>> print bugtask.owner.name
-    no-priv
-
-When a distribution bug nomination is approved, a task is created for
-each package the bug affects in that distro. For example, let's ensure
-bug #1 affects more than one Ubuntu package.
-
-    >>> from lp.bugs.interfaces.bugtask import IBugTaskSet
-
-    >>> ubuntu_tbird = ubuntu.getSourcePackage("thunderbird")
-    >>> ignore = factory.makeSourcePackagePublishingHistory(
-    ...     distroseries=ubuntu.currentseries,
-    ...     sourcepackagename=ubuntu_tbird.sourcepackagename)
-
-    >>> getUtility(IBugTaskSet).createTask(bug_one, no_privs, ubuntu_tbird)
-    <BugTask ...>
-
-    >>> tasks = sorted(
-    ...     bug_one.bugtasks, key=by_bugtargetdisplayname)
-
-    >>> for task in tasks:
-    ...     print task.target.bugtargetdisplayname
-    Mozilla Firefox
-    Mozilla Firefox trunk
-    mozilla-firefox (Debian)
-    mozilla-firefox (Ubuntu)
-    thunderbird (Ubuntu)
-
-When we approve the nomination, two more Ubuntu tasks are added for the
-Grumpy series. The user that made the decision is stored in the decider
-attribute. The date on which the decision was made is stored in the
-date_decided attribute.
-
-(Again, first we'll set the driver with an admin user, to ensure
-no_privs can actually approve the nomination.)
-
-    >>> login("foo.bar@xxxxxxxxxxxxx")
-    >>> grumpy_nomination.target.driver = no_privs
-    >>> login("no-priv@xxxxxxxxxxxxx")
-
-    >>> grumpy_nomination.date_decided is None
-    True
-    >>> grumpy_nomination.approve(no_privs)
-    >>> print grumpy_nomination.status.title
-    Approved
-    >>> print grumpy_nomination.decider.name
-    no-priv
-    >>> grumpy_nomination.date_decided
-    datetime...
-
-    >>> tasks = sorted(
-    ...     bug_one.bugtasks, key=by_bugtargetdisplayname)
-
-    >>> for task in tasks:
-    ...     print task.target.bugtargetdisplayname
-    Mozilla Firefox
-    Mozilla Firefox trunk
-    mozilla-firefox (Debian)
-    mozilla-firefox (Ubuntu Grumpy)
-    mozilla-firefox (Ubuntu)
-    thunderbird (Ubuntu Grumpy)
-    thunderbird (Ubuntu)
-
-Let's now nominate for Warty. no_privs is the driver, so will have
-no problems.
-
-    >>> ubuntu_warty = ubuntu.getSeries("warty")
-    >>> login("foo.bar@xxxxxxxxxxxxx")
-    >>> ubuntu_warty.driver = no_privs
-    >>> login("no-priv@xxxxxxxxxxxxx")
-
-    >>> warty_nomination = bug_one.addNomination(
-    ...     target=ubuntu_warty, owner=no_privs)
-    >>> warty_nomination.approve(no_privs)
-
-    >>> print warty_nomination.status.title
-    Approved
-    >>> print warty_nomination.decider.name
-    no-priv
-    >>> warty_nomination.date_decided
-    datetime...
-
-    >>> tasks = sorted(
-    ...     bug_one.bugtasks, key=by_bugtargetdisplayname)
-
-    >>> for task in tasks:
-    ...     print task.target.bugtargetdisplayname
-    Mozilla Firefox
-    Mozilla Firefox trunk
-    mozilla-firefox (Debian)
-    mozilla-firefox (Ubuntu Grumpy)
-    mozilla-firefox (Ubuntu Warty)
-    mozilla-firefox (Ubuntu)
-    thunderbird (Ubuntu Grumpy)
-    thunderbird (Ubuntu Warty)
-    thunderbird (Ubuntu)
-
-    >>> login("foo.bar@xxxxxxxxxxxxx")
-    >>> ubuntu_warty.driver = None
-
-
-Declining a nomination
-----------------------
-
-Declining a nomination simply sets its status to DECLINED. No tasks are
-created.
-
-    >>> login("foo.bar@xxxxxxxxxxxxx")
-    >>> ubuntu_breezy_autotest_nomination.target.driver = no_privs
-    >>> login("no-priv@xxxxxxxxxxxxx")
-
-    >>> ubuntu_breezy_autotest_nomination.date_decided is None
-    True
-    >>> print ubuntu_breezy_autotest_nomination.status.title
-    Nominated
-
-    >>> ubuntu_breezy_autotest_nomination.decline(no_privs)
-
-    >>> print ubuntu_breezy_autotest_nomination.status.title
-    Declined
-
-    >>> ubuntu_breezy_autotest_nomination.isDeclined()
-    True
-    >>> ubuntu_breezy_autotest_nomination.isApproved()
-    False
-    >>> ubuntu_breezy_autotest_nomination.isProposed()
-    False
-    >>> print ubuntu_breezy_autotest_nomination.decider.name
-    no-priv
-    >>> ubuntu_breezy_autotest_nomination.date_decided
-    datetime...
-
-If a nomination is declined, the bug can be re-nominated for the same target.
-The decider and date declined are reset to None.
-
-    >>> bug_one.canBeNominatedFor(ubuntu_breezy_autotest)
-    True
-    >>> breezy_nomination = bug_one.addNomination(
-    ...     target=ubuntu_breezy_autotest, owner=no_privs)
-    >>> ubuntu_breezy_autotest_nomination.isApproved()
-    False
-    >>> breezy_nomination.isDeclined()
-    False
-    >>> breezy_nomination.isProposed()
-    True
-    >>> print breezy_nomination.decider
-    None
-    >>> print breezy_nomination.date_decided
-    None
-
-
-Automatic targeting of new source packages
-------------------------------------------
-
-If a another distribution task is added, and nomination for that
-distribution's series already exists, the nominations will be valid
-for the new task as well, and bugtasks will be created for all accepted
-ones.
-
-The nominations are per distroseries, they are not source package
-specific, so they are automatically valid for new bugtasks. What's
-important are the accepted nominations. Bug one has an accepted
-nomination for Grumpy and Warty:
-
-    >>> accepted_nominations = [
-    ...     nomination for nomination in bug_one.getNominations(ubuntu)
-    ...     if nomination.isApproved()]
-    >>> for nomination in accepted_nominations:
-    ...     print nomination.distroseries.displayname
-    Grumpy
-    Warty
-
-So if we create a new bugtask on evolution (Ubuntu), a task for
-evolution (Ubuntu Grumpy) and evolution (Ubuntu Warty) will be created
-automatically.
-
-    >>> ubuntu_evolution = ubuntu.getSourcePackage('evolution')
-    >>> getUtility(IBugTaskSet).createTask(
-    ...     bug_one, no_privs, ubuntu_evolution)
-    <BugTask ...>
-
-    >>> tasks = sorted(
-    ...     bug_one.bugtasks, key=by_bugtargetdisplayname)
-
-    >>> for task in tasks:
-    ...     print task.target.bugtargetdisplayname
-    evolution (Ubuntu Grumpy)
-    evolution (Ubuntu Warty)
-    evolution (Ubuntu)
-    ...
-
-
-Changing the Source Package of a Targeted Bugtask
--------------------------------------------------
-
-The nomination model requires that a generic distribution task exists
-for each distroseries task. This causes some problem when renaming the
-source package on an accepted nomination. For example, if we would
-change the thunderbird package on the Grumpy task, it won't have a
-corresponding generic distribution task.
-
-The way we tie nominations to distribution series, and not to source
-packages, makes it hard to solve source package changes in a nice way.
-So what happens when a source package is changed is that we simply
-rename all other bugtasks which points to the same distribution and
-source package name.  This is not ideal, but hopefully package renames
-after the bug has been targeted to a series is rare enough for this to
-be acceptable.
-
-    >>> thunderbird_grumpy = tasks[-3]
-    >>> thunderbird_grumpy.bugtargetname
-    u'thunderbird (Ubuntu Grumpy)'
-
-    >>> thunderbird_grumpy.transitionToTarget(
-    ...     ubuntu.getSeries('grumpy').getSourcePackage('pmount'),
-    ...     getUtility(ILaunchBag).user)
-
-    >>> tasks = sorted(
-    ...     bug_one.bugtasks, key=by_bugtargetdisplayname)
-
-    >>> for task in tasks:
-    ...     print task.target.bugtargetdisplayname
-    evolution (Ubuntu Grumpy)
-    evolution (Ubuntu Warty)
-    evolution (Ubuntu)
-    Mozilla Firefox
-    Mozilla Firefox trunk
-    mozilla-firefox (Debian)
-    mozilla-firefox (Ubuntu Grumpy)
-    mozilla-firefox (Ubuntu Warty)
-    mozilla-firefox (Ubuntu)
-    pmount (Ubuntu Grumpy)
-    pmount (Ubuntu Warty)
-    pmount (Ubuntu)
-
-The same is done if the distribution task's source package is changed.
-
-    >>> pmount_ubuntu = tasks[-1]
-    >>> pmount_ubuntu.bugtargetname
-    u'pmount (Ubuntu)'
-
-    >>> ubuntu_thunderbird = ubuntu.getSourcePackage('thunderbird')
-    >>> pmount_ubuntu.transitionToTarget(
-    ...     ubuntu_thunderbird, getUtility(ILaunchBag).user)
-
-    >>> tasks = sorted(
-    ...     bug_one.bugtasks, key=by_bugtargetdisplayname)
-
-    >>> for task in tasks:
-    ...     print task.target.bugtargetdisplayname
-    evolution (Ubuntu Grumpy)
-    evolution (Ubuntu Warty)
-    evolution (Ubuntu)
-    Mozilla Firefox
-    Mozilla Firefox trunk
-    mozilla-firefox (Debian)
-    mozilla-firefox (Ubuntu Grumpy)
-    mozilla-firefox (Ubuntu Warty)
-    mozilla-firefox (Ubuntu)
-    thunderbird (Ubuntu Grumpy)
-    thunderbird (Ubuntu Warty)
-    thunderbird (Ubuntu)
-
-
-Bug Nomination Set
-------------------
-
-IBugNominationSet is used to fetch bug nominations by ID. This is useful
-mainly in traversal code.
-
-    >>> from lp.bugs.interfaces.bugnomination import IBugNominationSet
-
-    >>> getUtility(IBugNominationSet).get(1)
-    <BugNomination at ...>
-
-If a nomination is not found, a NotFoundError is raised.
-
-    >>> getUtility(IBugNominationSet).get(-1)
-    Traceback (most recent call last):
-      ...
-    NotFoundError: ...
-
-
-Error Handling
---------------
-
-Trying to nominate a bug for a series for which it's already nominated
-or targeted raises a NominationError.
-
-    >>> bug_one.addNomination(
-    ...     target=ubuntu_grumpy, owner=no_privs)
-    Traceback (most recent call last):
-      ..
-    NominationError: ...
-
-    >>> bug_one.addNomination(
-    ...     target=firefox_trunk, owner=no_privs)
-    Traceback (most recent call last):
-      ..
-    NominationError: ...
-
-Nominating a bug for an obsolete distroseries raises a
-NominationSeriesObsoleteError. Let's make a new obsolete distroseries
-to demonstrate.
-
-    >>> from lp.registry.interfaces.series import SeriesStatus
-
-    >>> login("foo.bar@xxxxxxxxxxxxx")
-    >>> ubuntu_edgy = factory.makeDistroSeries(
-    ...     distribution=ubuntu, version='6.10',
-    ...     status=SeriesStatus.OBSOLETE)
-    >>> login("no-priv@xxxxxxxxxxxxx")
-
-    >>> bug_one.addNomination(target=ubuntu_edgy, owner=no_privs)
-    Traceback (most recent call last):
-      ..
-    NominationSeriesObsoleteError: ...
-
-    >>> logout()

=== modified file 'lib/lp/bugs/interfaces/bugnomination.py'
--- lib/lp/bugs/interfaces/bugnomination.py	2012-01-01 02:58:52 +0000
+++ lib/lp/bugs/interfaces/bugnomination.py	2012-10-12 18:08:24 +0000
@@ -47,7 +47,6 @@
     )
 
 from lp import _
-from lp.app.interfaces.launchpad import IHasDateCreated
 from lp.app.validators.validation import can_be_nominated_for_series
 from lp.bugs.interfaces.bug import IBug
 from lp.bugs.interfaces.bugtarget import IBugTarget
@@ -101,7 +100,7 @@
         """)
 
 
-class IBugNomination(IHasBug, IHasOwner, IHasDateCreated):
+class IBugNomination(IHasBug, IHasOwner):
     """A nomination for a bug to be fixed in a specific series.
 
     A nomination can apply to an IDistroSeries or an IProductSeries.

=== modified file 'lib/lp/bugs/model/tests/test_bug.py'
--- lib/lp/bugs/model/tests/test_bug.py	2012-10-08 01:02:13 +0000
+++ lib/lp/bugs/model/tests/test_bug.py	2012-10-12 18:08:24 +0000
@@ -70,6 +70,31 @@
         nomination = bug.getNominationFor(sourcepackage)
         self.assertEqual(series, nomination.target)
 
+    def makeManyNominations(self):
+        target = self.factory.makeSourcePackage()
+        series = target.distroseries
+        with person_logged_in(series.distribution.owner):
+            nomination = self.factory.makeBugNomination(target=target)
+        bug = nomination.bug
+        other_series = self.factory.makeProductSeries()
+        other_target = other_series.product
+        self.factory.makeBugTask(bug=bug, target=other_target)
+        with person_logged_in(other_target.owner):
+            other_nomination = bug.addNomination(
+                other_target.owner, other_series)
+        return bug, [nomination, other_nomination]
+
+    def test_getNominations(self):
+        # The getNominations() method returns all the nominations for the bug.
+        bug, nominations = self.makeManyNominations()
+        self.assertContentEqual(nominations, bug.getNominations())
+
+    def test_getNominations_with_target(self):
+        # The target argument filters the nominations to just one pillar.
+        bug, nominations = self.makeManyNominations()
+        pillar = nominations[0].target.pillar
+        self.assertContentEqual([nominations[0]], bug.getNominations(pillar))
+
     def test_markAsDuplicate_None(self):
         # Calling markAsDuplicate(None) on a bug that is not currently a
         # duplicate works correctly, and does not raise an AttributeError.

=== modified file 'lib/lp/bugs/tests/test_bugnomination.py'
--- lib/lp/bugs/tests/test_bugnomination.py	2012-08-08 07:22:51 +0000
+++ lib/lp/bugs/tests/test_bugnomination.py	2012-10-12 18:08:24 +0000
@@ -5,16 +5,200 @@
 
 __metaclass__ = type
 
+from zope.component import getUtility
+
+from lp.app.errors import NotFoundError
+from lp.bugs.interfaces.bugnomination import (
+    BugNominationStatusError,
+    BugNominationStatus,
+    IBugNomination,
+    IBugNominationSet,
+    )
 from lp.soyuz.interfaces.publishing import PackagePublishingStatus
 from lp.testing import (
     celebrity_logged_in,
     login,
     logout,
+    person_logged_in,
     TestCaseWithFactory,
     )
 from lp.testing.layers import DatabaseFunctionalLayer
 
 
+class BugNominationTestCase(TestCaseWithFactory):
+
+    layer = DatabaseFunctionalLayer
+
+    def test_implementation(self):
+        # BugNomination implements IBugNomination.
+        target = self.factory.makeSourcePackage()
+        series = target.distroseries
+        bug = self.factory.makeBug(target=target.distribution_sourcepackage)
+        with person_logged_in(series.distribution.owner):
+            nomination = bug.addNomination(series.distribution.owner, series)
+            self.assertProvides(nomination, IBugNomination)
+        self.assertEqual(series.distribution.owner, nomination.owner)
+        self.assertEqual(bug, nomination.bug)
+        self.assertEqual(series, nomination.distroseries)
+        self.assertIsNone(nomination.productseries)
+        self.assertEqual(BugNominationStatus.PROPOSED, nomination.status)
+        self.assertIsNone(nomination.date_decided)
+        self.assertEqual('UTC', nomination.date_created.tzname())
+
+    def test_target_distroseries(self):
+        # The target property returns the distroseries if it is not None.
+        target = self.factory.makeSourcePackage()
+        series = target.distroseries
+        with person_logged_in(series.distribution.owner):
+            nomination = self.factory.makeBugNomination(target=target)
+        self.assertEqual(series, nomination.distroseries)
+        self.assertEqual(series, nomination.target)
+
+    def test_target_productseries(self):
+        # The target property returns the productseries if it is not None.
+        series = self.factory.makeProductSeries()
+        with person_logged_in(series.product.owner):
+            nomination = self.factory.makeBugNomination(target=series)
+        self.assertEqual(series, nomination.productseries)
+        self.assertEqual(series, nomination.target)
+
+    def test_status_proposed(self):
+        # isProposed is True when the status is PROPOSED.
+        series = self.factory.makeProductSeries()
+        with person_logged_in(series.product.owner):
+            nomination = self.factory.makeBugNomination(target=series)
+        self.assertEqual(BugNominationStatus.PROPOSED, nomination.status)
+        self.assertIs(True, nomination.isProposed())
+        self.assertIs(False, nomination.isDeclined())
+        self.assertIs(False, nomination.isApproved())
+
+    def test_status_declined(self):
+        # isDeclined is True when the status is DECLINED.
+        series = self.factory.makeProductSeries()
+        with person_logged_in(series.product.owner):
+            nomination = self.factory.makeBugNomination(target=series)
+            nomination.decline(series.product.owner)
+        self.assertEqual(BugNominationStatus.DECLINED, nomination.status)
+        self.assertIs(True, nomination.isDeclined())
+        self.assertIs(False, nomination.isProposed())
+        self.assertIs(False, nomination.isApproved())
+
+    def test_status_approved(self):
+        # isApproved is True when the status is APPROVED.
+        series = self.factory.makeProductSeries()
+        with person_logged_in(series.product.owner):
+            nomination = self.factory.makeBugNomination(target=series)
+            nomination.approve(series.product.owner)
+        self.assertEqual(BugNominationStatus.APPROVED, nomination.status)
+        self.assertIs(True, nomination.isApproved())
+        self.assertIs(False, nomination.isDeclined())
+        self.assertIs(False, nomination.isProposed())
+
+    def test_decline(self):
+        # The decline method updates the status and other data.
+        series = self.factory.makeProductSeries()
+        with person_logged_in(series.product.owner):
+            nomination = self.factory.makeBugNomination(target=series)
+            bug_tasks = nomination.bug.bugtasks
+            nomination.decline(series.product.owner)
+        self.assertEqual(BugNominationStatus.DECLINED, nomination.status)
+        self.assertIsNotNone(nomination.date_decided)
+        self.assertEqual(series.product.owner, nomination.decider)
+        self.assertContentEqual(bug_tasks, nomination.bug.bugtasks)
+
+    def test_decline_error(self):
+        # A nomination cannot be declined if it is approved.
+        series = self.factory.makeProductSeries()
+        with person_logged_in(series.product.owner):
+            nomination = self.factory.makeBugNomination(target=series)
+            nomination.approve(series.product.owner)
+            self.assertRaises(
+                BugNominationStatusError,
+                nomination.decline, series.product.owner)
+
+    def test_approve_productseries(self):
+        # Approving a product nomination creates a productseries bug task.
+        series = self.factory.makeProductSeries()
+        with person_logged_in(series.product.owner):
+            nomination = self.factory.makeBugNomination(target=series)
+            bug_tasks = nomination.bug.bugtasks
+            nomination.approve(series.product.owner)
+        self.assertEqual(BugNominationStatus.APPROVED, nomination.status)
+        self.assertIsNotNone(nomination.date_decided)
+        self.assertEqual(series.product.owner, nomination.decider)
+        expected_targets = [bt.target for bt in bug_tasks] + [series]
+        self.assertContentEqual(
+            expected_targets, [bt.target for bt in nomination.bug.bugtasks])
+
+    def test_approve_distroseries_source_package(self):
+        # Approving a package nomination creates a distroseries
+        # source package bug task.
+        target = self.factory.makeSourcePackage()
+        series = target.distroseries
+        with person_logged_in(series.distribution.owner):
+            nomination = self.factory.makeBugNomination(target=target)
+            bug_tasks = nomination.bug.bugtasks
+            nomination.approve(series.distribution.owner)
+        self.assertEqual(BugNominationStatus.APPROVED, nomination.status)
+        self.assertIsNotNone(nomination.date_decided)
+        self.assertEqual(series.distribution.owner, nomination.decider)
+        expected_targets = [bt.target for bt in bug_tasks] + [target]
+        self.assertContentEqual(
+            expected_targets, [bt.target for bt in nomination.bug.bugtasks])
+
+    def test_approve_distroseries_source_package_many(self):
+        # Approving a package nomination creates a distroseries
+        # source package bug task for each affect package in the same distro.
+        # See bug 110195 which argues this is wrong.
+        target = self.factory.makeSourcePackage()
+        series = target.distroseries
+        target2 = self.factory.makeSourcePackage(distroseries=series)
+        bug = self.factory.makeBug(target=target.distribution_sourcepackage)
+        self.factory.makeBugTask(
+            bug=bug, target=target2.distribution_sourcepackage)
+        bug_tasks = bug.bugtasks
+        with person_logged_in(series.distribution.owner):
+            nomination = self.factory.makeBugNomination(bug=bug, target=target)
+            nomination.approve(series.distribution.owner)
+        expected_targets = [bt.target for bt in bug_tasks] + [target, target2]
+        self.assertContentEqual(
+            expected_targets, [bt.target for bt in bug.bugtasks])
+
+    def test_approve_twice(self):
+        # Approving a nomination twice is a no-op.
+        series = self.factory.makeProductSeries()
+        with person_logged_in(series.product.owner):
+            nomination = self.factory.makeBugNomination(target=series)
+            nomination.approve(series.product.owner)
+        self.assertEqual(BugNominationStatus.APPROVED, nomination.status)
+        date_decided = nomination.date_decided
+        self.assertIsNotNone(date_decided)
+        self.assertEqual(series.product.owner, nomination.decider)
+        with celebrity_logged_in('admin') as admin:
+            nomination.approve(admin)
+        self.assertEqual(date_decided, nomination.date_decided)
+        self.assertEqual(series.product.owner, nomination.decider)
+
+    def test_approve_distroseries_source_package_then_retarget(self):
+        # Retargeting a bugtarget with and approved nomination also
+        # retargets the master bug target.
+        target = self.factory.makeSourcePackage()
+        series = target.distroseries
+        with person_logged_in(series.distribution.owner):
+            nomination = self.factory.makeBugNomination(target=target)
+            nomination.approve(series.distribution.owner)
+        target2 = self.factory.makeSourcePackage(
+            distroseries=series, publish=True)
+        product_target = nomination.bug.bugtasks[0].target
+        expected_targets = [
+            product_target, target2, target2.distribution_sourcepackage]
+        bug_task = nomination.bug.bugtasks[-1]
+        with person_logged_in(series.distribution.owner):
+            bug_task.transitionToTarget(target2, series.distribution.owner)
+        self.assertContentEqual(
+            expected_targets, [bt.target for bt in nomination.bug.bugtasks])
+
+
 class CanBeNominatedForTestMixin:
     """Test case mixin for IBug.canBeNominatedFor."""
 
@@ -218,3 +402,19 @@
         self.assertFalse(nomination.canApprove(self.factory.makePerson()))
         self.assertTrue(nomination.canApprove(package_perm.person))
         self.assertTrue(nomination.canApprove(comp_perm.person))
+
+
+class BugNominationSetTestCase(TestCaseWithFactory):
+
+    layer = DatabaseFunctionalLayer
+
+    def test_get(self):
+        series = self.factory.makeProductSeries()
+        with person_logged_in(series.product.owner):
+            nomination = self.factory.makeBugNomination(target=series)
+        bug_nomination_set = getUtility(IBugNominationSet)
+        self.assertEqual(nomination, bug_nomination_set.get(nomination.id))
+
+    def test_get_none(self):
+        bug_nomination_set = getUtility(IBugNominationSet)
+        self.assertRaises(NotFoundError, bug_nomination_set.get, -1)

=== modified file 'lib/lp/bugs/tests/test_bugsupervisor_bugnomination.py'
--- lib/lp/bugs/tests/test_bugsupervisor_bugnomination.py	2012-08-08 07:22:51 +0000
+++ lib/lp/bugs/tests/test_bugsupervisor_bugnomination.py	2012-10-12 18:08:24 +0000
@@ -5,8 +5,13 @@
 
 __metaclass__ = type
 
-from lp.bugs.interfaces.bugnomination import NominationError
+from lp.bugs.interfaces.bugnomination import (
+    NominationError,
+    NominationSeriesObsoleteError,
+    )
+from lp.registry.interfaces.series import SeriesStatus
 from lp.testing import (
+    celebrity_logged_in,
     login,
     login_person,
     logout,
@@ -47,6 +52,14 @@
         self.bug.addNomination(self.bug_supervisor, self.series)
         self.assertTrue(len(self.bug.getNominations()), 1)
 
+    def test_bugsupervisor_addNominationFor_with_existing_nomination(self):
+        # A bug cannot be nominated twice for the same series.
+        login_person(self.bug_supervisor)
+        self.bug.addNomination(self.bug_supervisor, self.series)
+        self.assertTrue(len(self.bug.getNominations()), 1)
+        self.assertRaises(NominationError,
+            self.bug.addNomination, self.user, self.series)
+
     def test_owner_addNominationFor_series(self):
         # A bug may be nominated for a series of a product with an
         # exisiting task by the product's owner.
@@ -82,3 +95,11 @@
         self.bug.addTask(self.bug_supervisor, self.distro)
         self.milestone = self.factory.makeMilestone(
             distribution=self.distro)
+
+    def test_bugsupervisor_addNominationFor_with_obsolete_distroseries(self):
+        # A bug cannot be nominated for an obsolete series.
+        with celebrity_logged_in('admin'):
+            self.series.status = SeriesStatus.OBSOLETE
+        login_person(self.bug_supervisor)
+        self.assertRaises(NominationSeriesObsoleteError,
+            self.bug.addNomination, self.user, self.series)


Follow ups