← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~rharding/launchpad/diedoctests_bugtask into lp:launchpad

 

Richard Harding has proposed merging lp:~rharding/launchpad/diedoctests_bugtask into lp:launchpad.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~rharding/launchpad/diedoctests_bugtask/+merge/105502

= Summary =
In order to get some LoC credits for my bugnomination changes I've ported over
the bugtask.txt doctests to unit tests in test_bugtask.py and
test_bugtaskset.py.

== Pre Implementation ==
The pre-impl was basically just permission to go down this path from deryck.

== Implementation Notes ==
It's not all roses and sunshine. The unit tests pull in information the
factory to setup test cases, the doc tests though usually pulled fixture data
to run the test against.

I gave a try at moving the ported doctests into factory users, but it got
very difficult very fast and made reviewing the changes very difficult because
it was no longer easy to look for a doctest in the new unit test code.

This is most evident in the conjoined tests as this is a combination of old
unit tests and the newly ported doctests.

There was some duplication of tests between the doctests and the unit tests.
This might help explain certain tests that were just removed or not ported
over from the doctests.

== Tests ==
lib/lp/bugs/tests/test_bugtaskset.py
lib/lp/bugs/model/tests/test_bugtask.py

== Lint ==

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/bugs/model/tests/test_bugtask.py
  lib/lp/bugs/tests/test_bugtaskset.py

== LoC Qualification ==
I swung the big sword of doom to doctests and this ends up with a negative LoC
so I'm calling this a bankfest.
-- 
https://code.launchpad.net/~rharding/launchpad/diedoctests_bugtask/+merge/105502
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~rharding/launchpad/diedoctests_bugtask into lp:launchpad.
=== removed file 'lib/lp/bugs/doc/bugtask.txt'
--- lib/lp/bugs/doc/bugtask.txt	2012-05-10 14:50:30 +0000
+++ lib/lp/bugs/doc/bugtask.txt	1970-01-01 00:00:00 +0000
@@ -1,1267 +0,0 @@
-Introduction
-===================================
-
-Bugs are problems in software. When a bug gets assigned to a specific
-upstream or distro/sourcepackagename, a bug /task/ is created. In
-essence, a bug task is a bug that needs to be fixed in a specific
-place. Where a bug has things like a title, comments and subscribers,
-it's the bug task that tracks importance, assignee, etc.
-
-Working with Bug Tasks in Launchpad
-===================================
-
-
-Creating Bug Tasks
-------------------
-
-All BugTask creation and retrieval is done through an IBugTaskSet utility.
-
-    >>> from zope.component import getUtility
-    >>> import transaction
-    >>> from lp.bugs.interfaces.bugtask import IBugTaskSet
-    >>> bugtaskset = getUtility(IBugTaskSet)
-
-To create a bug task, you have to be logged in:
-
-    >>> from lp.testing import login, ANONYMOUS
-    >>> login('foo.bar@xxxxxxxxxxxxx')
-
-There are three kinds of bug tasks. We need to pass the bug task creation
-methods some other objects to create a task, so lets get the utilities we need
-to access those other objects:
-
-    >>> from lp.bugs.interfaces.bug import IBugSet
-    >>> from lp.registry.interfaces.distribution import IDistributionSet
-    >>> from lp.registry.interfaces.distroseries import IDistroSeriesSet
-    >>> from lp.registry.interfaces.person import IPersonSet
-    >>> from lp.registry.interfaces.product import IProductSet
-    >>> from lp.registry.interfaces.sourcepackagename import (
-    ...     ISourcePackageNameSet)
-    >>> productset = getUtility(IProductSet)
-    >>> distroset = getUtility(IDistributionSet)
-    >>> distoseriesset = getUtility(IDistroSeriesSet)
-    >>> sourcepackagenameset = getUtility(ISourcePackageNameSet)
-    >>> bugset = getUtility(IBugSet)
-    >>> personset = getUtility(IPersonSet)
-    >>> bug_one = bugset.get(1)
-    >>> mark = personset.getByEmail('mark@xxxxxxxxxxx')
-
-Next, we need to grab some values to provide for importance and status.
-
-    >>> from lp.bugs.interfaces.bugtask import (
-    ...     BugTaskImportance,
-    ...     BugTaskStatus,
-    ...     )
-    >>> STATUS_NEW = BugTaskStatus.NEW
-    >>> STATUS_CONFIRMED = BugTaskStatus.CONFIRMED
-    >>> STATUS_FIXRELEASED = BugTaskStatus.FIXRELEASED
-    >>> IMPORTANCE_MEDIUM = BugTaskImportance.MEDIUM
-
-  i. Upstream -- a bug that has to be fixed in an upstream product
-
-    >>> evolution = productset.get(5)
-    >>> upstream_task = bugtaskset.createTask(
-    ...     bug_one, mark, evolution,
-    ...     status=STATUS_NEW, importance=IMPORTANCE_MEDIUM)
-    >>> upstream_task.product == evolution
-    True
-
-  ii. Distro -- a bug that has to be fixed in a specific distro
-
-    >>> ubuntu = distroset.get(1)
-    >>> a_distro = factory.makeDistribution(name='tubuntu')
-    >>> distro_task = bugtaskset.createTask(
-    ...     bug_one, mark, a_distro,
-    ...     status=STATUS_NEW, importance=IMPORTANCE_MEDIUM)
-    >>> distro_task.distribution == a_distro
-    True
-
-  ii. Distro Series -- a bug that has to be fixed in a specific distro
-  series. These tasks are used for release management and backporting.
-
-    >>> warty = distoseriesset.get(1)
-    >>> distro_series_task = bugtaskset.createTask(
-    ...     bug_one, mark, warty,
-    ...     status=STATUS_NEW, importance=IMPORTANCE_MEDIUM)
-    >>> distro_series_task.distroseries == warty
-    True
-
-
-Next we verify that we can create many tasks at a time for multiple targets.
-
-    >>> bug_many = getUtility(IBugSet).get(4)
-    >>> taskset = bugtaskset.createManyTasks(bug_many, mark,
-    ...    [evolution, a_distro, warty], status=BugTaskStatus.FIXRELEASED)
-    >>> tasks = [(t.product, t.distribution, t.distroseries) for t in taskset]
-    >>> tasks.sort()
-    >>> tasks[0][2] == warty
-    True
-    >>> tasks[1][1] == a_distro
-    True
-    >>> tasks[2][0] == evolution
-    True
-
-# XXX: Brad Bollenbach 2005-02-24: See the bottom of this file for a chunk of
-# test documentation that is missing from here, due to problems with resetting
-# the connection after a ProgrammingError is raised. ARGH.
-
-
-Bug Task Targets
-----------------
-
-    >>> from lp.registry.interfaces.distributionsourcepackage \
-    ...     import IDistributionSourcePackage
-
-The "target" of an IBugTask can be one of the items in the following
-list.
-
-  * an upstream product
-
-    >>> upstream_task.target == evolution
-    True
-
-  * a product series
-
-
-    >>> firefox = productset['firefox']
-    >>> firefox_1_0 = firefox.getSeries("1.0")
-
-    >>> productseries_task = bugtaskset.createTask(bug_one, mark, firefox_1_0)
-
-    >>> productseries_task.target == firefox_1_0
-    True
-
-  * a distribution
-
-    >>> distro_task.target == a_distro
-    True
-
-  * a distroseries
-
-    >>> distro_series_task.target == warty
-    True
-
-  * a distribution sourcepackage
-
-    >>> def get_expected_target(distro_sp_task):
-    ...      return distro_sp_task.target
-
-    >>> debian_ff_task = bugtaskset.get(4)
-    >>> IDistributionSourcePackage.providedBy(debian_ff_task.target)
-    True
-    >>> target = get_expected_target(debian_ff_task)
-    >>> target.distribution.name, target.sourcepackagename.name
-    (u'debian', u'mozilla-firefox')
-
-    >>> ubuntu_linux_task = bugtaskset.get(25)
-    >>> IDistributionSourcePackage.providedBy(ubuntu_linux_task.target)
-    True
-    >>> target = get_expected_target(ubuntu_linux_task)
-    >>> target.distribution.name, target.sourcepackagename.name
-    (u'ubuntu', u'linux-source-2.6.15')
-
-  * a distroseries sourcepackage
-
-    >>> from lp.registry.interfaces.sourcepackage import ISourcePackage
-    >>> from lp.registry.model.sourcepackage import SourcePackage
-    >>> distro_series_sp_task = bugtaskset.get(16)
-    >>> expected_target = SourcePackage(
-    ...     distroseries=distro_series_sp_task.distroseries,
-    ...     sourcepackagename=distro_series_sp_task.sourcepackagename)
-    >>> got_target = distro_series_sp_task.target
-    >>> ISourcePackage.providedBy(distro_series_sp_task.target)
-    True
-    >>> got_target.distroseries == expected_target.distroseries
-    True
-    >>> got_target.sourcepackagename == expected_target.sourcepackagename
-    True
-
-Each task has a "bugtargetdisplayname" and a "bugtargetname", strings
-describing the site of the task. They concatenate the names of the
-distribution,
-
-    >>> bugtask = bugtaskset.get(17)
-    >>> bugtask.bugtargetdisplayname
-    u'mozilla-firefox (Ubuntu)'
-    >>> bugtask.bugtargetname
-    u'mozilla-firefox (Ubuntu)'
-
-distro series, or product;
-
-    >>> bugtask = bugtaskset.get(2)
-    >>> bugtask.bugtargetdisplayname
-    u'Mozilla Firefox'
-    >>> bugtask.bugtargetname
-    u'firefox'
-
-the name of the source package (if any); and the name of the binary
-package (but only if it's named differently from the source
-package).
-
-
-getPackageComponent
-...................
-
-We offer a convenience method on IBugTask which allows you to look up
-the archive component associated to the bugtask's target. Obviously, it
-only applies to tasks that specify package information:
-
-    >>> print upstream_task.getPackageComponent()
-    None
-    >>> print productseries_task.getPackageComponent()
-    None
-    >>> print distro_task.getPackageComponent()
-    None
-    >>> print distro_series_task.getPackageComponent()
-    None
-
-And it only applies to tasks whose packages which are published in
-IDistribution.currentseries (for bugtasks on IDistributions) or the
-bugtask's series (for bugtasks on IDistroSeries)
-
-    >>> print debian_ff_task.getPackageComponent()
-    None
-    >>> print ubuntu_linux_task.getPackageComponent().name
-    main
-    >>> print distro_series_sp_task.getPackageComponent().name
-    main
-
-
-Editing Bug Tasks
------------------
-
-When changing status we must pass the user making the change. Some
-statuses are restricted to Bug Supervisors only.
-
-
-Upstream Bug Tasks
-..................
-
-To edit an upstream task, you must be logged in. Anonymous users
-cannot edit upstream tasks.
-
-    >>> login(ANONYMOUS)
-    >>> upstream_task.transitionToStatus(
-    ...     STATUS_CONFIRMED, getUtility(ILaunchBag).user)
-    Traceback (most recent call last):
-      ...
-    Unauthorized: (..., 'transitionToStatus', 'launchpad.Edit')
-
-Let's login and try again.
-
-    >>> login('jeff.waugh@xxxxxxxxxxxxxxx')
-    >>> upstream_task.transitionToStatus(
-    ...     STATUS_FIXRELEASED, getUtility(ILaunchBag).user)
-
-
-Distro and Distro Series Bug Tasks
-..................................
-
-Any logged-in user can edit tasks filed on distros as long as the bug
-is not marked private. So, as an anonymous user, we cannot edit
-anything:
-
-    >>> login(ANONYMOUS)
-    >>> distro_task.transitionToStatus(
-    ...     STATUS_FIXRELEASED, getUtility(ILaunchBag).user)
-    Traceback (most recent call last):
-      ...
-    Unauthorized: (..., 'transitionToStatus', 'launchpad.Edit')
-
-    >>> sample_person = personset.getByEmail('test@xxxxxxxxxxxxx')
-    >>> distro_series_task.transitionToAssignee(sample_person)
-    Traceback (most recent call last):
-      ...
-    Unauthorized: (..., 'transitionToAssignee', 'launchpad.Edit')
-
-But once authenticated:
-
-    >>> login('test@xxxxxxxxxxxxx')
-
-We can edit the task:
-
-    >>> distro_task.transitionToStatus(
-    ...     STATUS_FIXRELEASED, getUtility(ILaunchBag).user)
-    >>> distro_series_task.transitionToAssignee(sample_person)
-
-
-Conjoined Bug Tasks
-...................
-
-A bugtask open on the current development series for a distro is kept
-in sync with the "generic" bugtask for that distro, because they
-represent the same piece of work. The same is true for product and
-productseries tasks, when the productseries task is targeted to the
-IProduct.developmentfocus. The following attributes are synced:
-
-    * status
-    * assignee
-    * importance
-    * milestone
-    * sourcepackagename
-    * date_confirmed
-    * date_inprogress
-    * date_assigned
-    * date_closed
-    * date_left_new
-    * date_triaged
-    * date_fix_committed
-    * date_fix_released
-
-We'll open a bug on just the distribution, and also a bug on a specific
-package.
-
-    >>> from lp.services.webapp.interfaces import ILaunchBag
-    >>> from lp.bugs.interfaces.bug import CreateBugParams
-    >>> launchbag = getUtility(ILaunchBag)
-    >>> params = CreateBugParams(
-    ...     owner=launchbag.user,
-    ...     title="a test bug",
-    ...     comment="test bug description")
-    >>> ubuntu_netapplet = ubuntu.getSourcePackage("netapplet")
-    >>> ubuntu_netapplet_bug = ubuntu_netapplet.createBug(params)
-    >>> generic_netapplet_task = ubuntu_netapplet_bug.bugtasks[0]
-
-    >>> ubuntu_bug = ubuntu.createBug(params)
-    >>> generic_ubuntu_task = ubuntu_bug.bugtasks[0]
-
-First, we'll target the bug for the current Ubuntu series, Hoary. Note
-that the synched attributes are copied when the series-specific tasks are
-created. We'll set non-default attribute values for each generic task to
-demonstrate.
-
-    >>> print ubuntu.currentseries.name
-    hoary
-
-    # Only owners, experts, or admins can create a milestone.
-    >>> login('foo.bar@xxxxxxxxxxxxx')
-    >>> ubuntu_edgy_milestone = ubuntu.currentseries.newMilestone("knot1")
-    >>> login('test@xxxxxxxxxxxxx')
-
-    >>> generic_netapplet_task.transitionToStatus(
-    ...     BugTaskStatus.INPROGRESS, getUtility(ILaunchBag).user)
-    >>> generic_netapplet_task.transitionToAssignee(sample_person)
-    >>> generic_netapplet_task.milestone = ubuntu_edgy_milestone
-    >>> generic_netapplet_task.transitionToImportance(
-    ...     BugTaskImportance.CRITICAL, ubuntu.owner)
-
-    >>> current_series_ubuntu_task = bugtaskset.createTask(
-    ...     ubuntu_bug, launchbag.user, ubuntu.currentseries)
-
-    >>> current_series_netapplet_task = bugtaskset.createTask(
-    ...     ubuntu_netapplet_bug, launchbag.user,
-    ...     ubuntu_netapplet.development_version)
-
-(The attributes were synched with the generic task.)
-
-    >>> print current_series_netapplet_task.status.title
-    In Progress
-    >>> print current_series_netapplet_task.assignee.displayname
-    Sample Person
-    >>> print current_series_netapplet_task.milestone.name
-    knot1
-    >>> print current_series_netapplet_task.importance.title
-    Critical
-
-    >>> current_series_netapplet_task.date_assigned == (
-    ...     generic_netapplet_task.date_assigned)
-    True
-
-    >>> current_series_netapplet_task.date_confirmed == (
-    ...     generic_netapplet_task.date_confirmed)
-    True
-
-    >>> current_series_netapplet_task.date_inprogress == (
-    ...     generic_netapplet_task.date_inprogress)
-    True
-
-    >>> current_series_netapplet_task.date_closed == (
-    ...     generic_netapplet_task.date_closed)
-    True
-
-We'll also add some product and productseries tasks.
-
-    >>> alsa_utils = productset['alsa-utils']
-
-    >>> print alsa_utils.development_focus.name
-    trunk
-
-    >>> generic_alsa_utils_task = bugtaskset.createTask(
-    ...     ubuntu_netapplet_bug, launchbag.user, alsa_utils)
-
-    >>> devel_focus_alsa_utils_task = bugtaskset.createTask(
-    ...     ubuntu_netapplet_bug, launchbag.user,
-    ...     alsa_utils.getSeries("trunk"))
-
-A conjoined bugtask involves a master and slave in the conjoined
-relationship. The slave is the generic product or distribution task; the
-master is the series-specific task. These tasks are accessed through the
-conjoined_master and conjoined_slave properties.
-
-    >>> current_series_netapplet_task.conjoined_slave == (
-    ...     generic_netapplet_task)
-    True
-    >>> current_series_netapplet_task.conjoined_master is None
-    True
-
-    >>> generic_netapplet_task.conjoined_slave is None
-    True
-    >>> generic_netapplet_task.conjoined_master == (
-    ...     current_series_netapplet_task)
-    True
-
-    >>> current_series_ubuntu_task.conjoined_slave == (
-    ...     generic_ubuntu_task)
-    True
-    >>> current_series_ubuntu_task.conjoined_master is None
-    True
-    >>> generic_ubuntu_task.conjoined_master == (
-    ...     current_series_ubuntu_task)
-    True
-    >>> generic_ubuntu_task.conjoined_slave is None
-    True
-
-    >>> devel_focus_alsa_utils_task.conjoined_slave == (
-    ...     generic_alsa_utils_task)
-    True
-    >>> devel_focus_alsa_utils_task.conjoined_master is None
-    True
-
-    >>> generic_alsa_utils_task.conjoined_slave is None
-    True
-    >>> generic_alsa_utils_task.conjoined_master == (
-    ...     devel_focus_alsa_utils_task)
-    True
-
-A distroseries/productseries task that isn't the current development
-focus, doesn't have any conjoined masters or slaves.
-
-    >>> from storm.store import Store
-
-    # Only owners, experts, or admins can create a series.
-    >>> login('foo.bar@xxxxxxxxxxxxx')
-    >>> alsa_utils_stable = alsa_utils.newSeries(
-    ...     launchbag.user, 'stable', 'The stable series.')
-    >>> login('test@xxxxxxxxxxxxx')
-    >>> Store.of(alsa_utils_stable).flush()
-    >>> alsa_utils.development_focus == alsa_utils_stable
-    False
-    >>> stable_netapplet_task = bugtaskset.createTask(
-    ...     ubuntu_netapplet_bug, launchbag.user, alsa_utils_stable)
-    >>> stable_netapplet_task.conjoined_master is None
-    True
-    >>> stable_netapplet_task.conjoined_slave is None
-    True
-
-    >>> warty = ubuntu.getSeries('warty')
-    >>> warty == ubuntu.currentseries
-    False
-    >>> warty_netapplet_task = bugtaskset.createTask(
-    ...     ubuntu_netapplet_bug, launchbag.user,
-    ...     warty.getSourcePackage(ubuntu_netapplet.sourcepackagename))
-    >>> warty_netapplet_task.conjoined_master is None
-    True
-    >>> warty_netapplet_task.conjoined_slave is None
-    True
-
-If a distribution doesn't have a current series, its tasks don't have a
-conjoined master or slave.
-
-    >>> gentoo = getUtility(IDistributionSet).getByName('gentoo')
-    >>> gentoo.currentseries is None
-    True
-
-    >>> gentoo_netapplet_task = bugtaskset.createTask(
-    ...     ubuntu_netapplet_bug, launchbag.user,
-    ...     gentoo.getSourcePackage(ubuntu_netapplet.sourcepackagename))
-    >>> gentoo_netapplet_task.conjoined_master is None
-    True
-    >>> gentoo_netapplet_task.conjoined_slave is None
-    True
-
-
-Now the attributes are kept in sync. Here are examples of each:
-
-(Login as Foo Bar, because the milestone and importance examples
-require extra privileges.)
-
-    >>> login("foo.bar@xxxxxxxxxxxxx")
-
-1. Status
-
-    >>> print generic_netapplet_task.status.title
-    In Progress
-    >>> print current_series_netapplet_task.status.title
-    In Progress
-    >>> generic_netapplet_task.date_closed is None
-    True
-    >>> current_series_netapplet_task.date_closed is None
-    True
-
-    >>> current_series_netapplet_task.transitionToStatus(
-    ...     BugTaskStatus.FIXRELEASED, getUtility(ILaunchBag).user)
-
-    >>> generic_netapplet_task.date_left_new
-    datetime.datetime...
-    >>> generic_netapplet_task.date_left_new == (
-    ...     current_series_netapplet_task.date_left_new)
-    True
-
-    >>> generic_netapplet_task.date_triaged
-    datetime.datetime...
-    >>> generic_netapplet_task.date_triaged == (
-    ...     current_series_netapplet_task.date_triaged)
-    True
-
-    >>> generic_netapplet_task.date_fix_committed
-    datetime.datetime...
-    >>> generic_netapplet_task.date_fix_committed == (
-    ...     current_series_netapplet_task.date_fix_committed)
-    True
-
-    >>> print generic_netapplet_task.status.title
-    Fix Released
-    >>> print current_series_netapplet_task.status.title
-    Fix Released
-
-    >>> generic_netapplet_task.date_closed
-    datetime.datetime...
-    >>> generic_netapplet_task.date_closed == (
-    ...     current_series_netapplet_task.date_closed)
-    True
-
-    >>> generic_netapplet_task.date_fix_released
-    datetime.datetime...
-    >>> generic_netapplet_task.date_fix_released == (
-    ...     current_series_netapplet_task.date_fix_released)
-    True
-
-2. Assignee
-
-    >>> no_priv = personset.getByEmail('no-priv@xxxxxxxxxxxxx')
-
-    >>> generic_alsa_utils_task.assignee is None
-    True
-    >>> devel_focus_alsa_utils_task.assignee is None
-    True
-    >>> generic_alsa_utils_task.date_assigned is None
-    True
-    >>> devel_focus_alsa_utils_task.date_assigned is None
-    True
-
-    >>> devel_focus_alsa_utils_task.transitionToAssignee(no_priv)
-
-    >>> print generic_alsa_utils_task.assignee.displayname
-    No Privileges Person
-    >>> print devel_focus_alsa_utils_task.assignee.displayname
-    No Privileges Person
-
-    >>> generic_alsa_utils_task.date_assigned
-    datetime.datetime...
-    >>> generic_alsa_utils_task.date_assigned == (
-    ...     devel_focus_alsa_utils_task.date_assigned)
-    True
-
-3. Importance
-
-    >>> print generic_netapplet_task.importance.title
-    Critical
-    >>> print current_series_netapplet_task.importance.title
-    Critical
-
-    >>> current_series_netapplet_task.transitionToImportance(
-    ...     BugTaskImportance.MEDIUM, ubuntu.owner)
-
-    >>> print generic_netapplet_task.importance.title
-    Medium
-    >>> print current_series_netapplet_task.importance.title
-    Medium
-
-Not everyone can edit the importance, though. If an unauthorised user
-is passed to transitionToImportance an exception is raised.
-
-    >>> current_series_netapplet_task.transitionToImportance(
-    ...     BugTaskImportance.LOW, no_priv)
-    Traceback (most recent call last):
-    ...
-    UserCannotEditBugTaskImportance:
-      User does not have sufficient permissions to edit the
-      bug task importance.
-
-    >>> print generic_netapplet_task.importance.title
-    Medium
-
-4. Milestone
-
-    >>> test_milestone = alsa_utils.development_focus.newMilestone("test")
-    >>> noway_milestone = alsa_utils.development_focus.newMilestone("noway")
-    >>> Store.of(test_milestone).flush()
-
-    >>> generic_alsa_utils_task.milestone is None
-    True
-    >>> devel_focus_alsa_utils_task.milestone is None
-    True
-
-    >>> devel_focus_alsa_utils_task.transitionToMilestone(
-    ...     test_milestone, alsa_utils.owner)
-
-    >>> print generic_alsa_utils_task.milestone.name
-    test
-    >>> print devel_focus_alsa_utils_task.milestone.name
-    test
-
-But a normal unprivileged user can't set the milestone.
-
-    >>> devel_focus_alsa_utils_task.transitionToMilestone(
-    ...     noway_milestone, no_priv)
-    Traceback (most recent call last):
-    ...
-    UserCannotEditBugTaskMilestone:
-      User does not have sufficient permissions to edit the bug
-      task milestone.
-
-    >>> print devel_focus_alsa_utils_task.milestone.name
-    test
-
-5. Source package name
-
-    >>> ubuntu_pmount = ubuntu.getSourcePackage("pmount")
-
-    >>> print generic_netapplet_task.sourcepackagename.name
-    netapplet
-    >>> print current_series_netapplet_task.sourcepackagename.name
-    netapplet
-
-    >>> current_series_netapplet_task.transitionToTarget(
-    ...     ubuntu_pmount.development_version)
-
-    >>> print generic_netapplet_task.sourcepackagename.name
-    pmount
-    >>> print current_series_netapplet_task.sourcepackagename.name
-    pmount
-
-A conjoined relationship can be broken, though. If the development
-task (i.e the conjoined master) is Won't Fix, it means that the bug is
-deferred to the next series. In this case the development task should
-be Won't Fix, while the generic task keeps the value it had before,
-allowing it to stay open.
-
-First let's change the status from Fix Released, since it doesn't make
-sense to reject such a task.
-
-    >>> current_series_netapplet_task.transitionToStatus(
-    ...     BugTaskStatus.CONFIRMED, getUtility(ILaunchBag).user)
-    >>> print generic_netapplet_task.status.title
-    Confirmed
-    >>> print current_series_netapplet_task.status.title
-    Confirmed
-    >>> generic_netapplet_task.date_closed is None
-    True
-    >>> current_series_netapplet_task.date_closed is None
-    True
-
-
-Now, if we set the current series task to Won't Fix, the generic task
-will still be confirmed.
-
-    >>> netapplet_owner = current_series_netapplet_task.pillar.owner
-
-    >>> current_series_netapplet_task.transitionToStatus(
-    ...     BugTaskStatus.WONTFIX, netapplet_owner)
-
-    >>> print generic_netapplet_task.status.title
-    Confirmed
-    >>> print current_series_netapplet_task.status.title
-    Won't Fix
-
-    >>> generic_netapplet_task.date_closed is None
-    True
-    >>> current_series_netapplet_task.date_closed is None
-    False
-
-And the bugtasks are no longer conjoined:
-
-    >>> generic_netapplet_task.conjoined_master is None
-    True
-    >>> current_series_netapplet_task.conjoined_slave is None
-    True
-
-If the current development release is marked as Invalid, then the
-bug is invalid for all future series too, and so the general bugtask
-is therefore Invalid also. In other words, conjoined again.
-
-    >>> current_series_netapplet_task.transitionToStatus(
-    ...     BugTaskStatus.NEW, getUtility(ILaunchBag).user)
-
-    # XXX Gavin Panella 2007-06-06 bug=112746:
-    # We must make two transitions.
-    >>> current_series_netapplet_task.transitionToStatus(
-    ...     BugTaskStatus.INVALID, getUtility(ILaunchBag).user)
-
-    >>> print generic_netapplet_task.status.title
-    Invalid
-    >>> print current_series_netapplet_task.status.title
-    Invalid
-
-    >>> generic_netapplet_task.date_closed is None
-    False
-    >>> current_series_netapplet_task.date_closed is None
-    False
-
-
-Bug Privacy
-===========
-
-A bug is either private or public. Private bugs are only visible
-(e.g. in search listings) to explicit subscribers and Launchpad
-admins. Public bugs are visible to anyone.
-
-    >>> from zope.event import notify
-    >>> from lazr.lifecycle.event import ObjectModifiedEvent
-
-
-Privacy and Unprivileged Users
-------------------------------
-
-Let's log in as the user Foo Bar (to be allowed to edit bugs):
-
-    >>> login('foo.bar@xxxxxxxxxxxxx')
-    >>> foobar = launchbag.user
-
-and mark one of the Firefox bugs private. While we do this, we're also
-going to subscribe the Ubuntu team to the bug report to help demonstrate
-later on the interaction between privacy and teams (see the section
-entitled _Privacy and Team Awareness_):
-
-    >>> from lazr.lifecycle.snapshot import Snapshot
-    >>> from lp.bugs.interfaces.bug import IBug
-
-    >>> bug_upstream_firefox_crashes = bugtaskset.get(15)
-
-    >>> ubuntu_team = personset.getByEmail('support@xxxxxxxxxx')
-    >>> subscription = bug_upstream_firefox_crashes.bug.subscribe(
-    ...     ubuntu_team, ubuntu_team)
-
-    >>> old_state = Snapshot(
-    ...     bug_upstream_firefox_crashes.bug, providing=IBug)
-    >>> bug_upstream_firefox_crashes.bug.setPrivate(
-    ...     True, getUtility(ILaunchBag).user)
-    True
-    >>> bug_set_private = ObjectModifiedEvent(
-    ...     bug_upstream_firefox_crashes.bug, old_state,
-    ...     ["id", "title", "private"])
-    >>> notify(bug_set_private)
-
-    >>> from lp.services.database.sqlbase import flush_database_updates
-    >>> flush_database_updates()
-
-If we now login as someone who was neither implicitly nor explicitly
-subscribed to this bug, e.g. No Privileges Person, they will not be
-able to access or set properties of the bugtask.
-
-    >>> login("no-priv@xxxxxxxxxxxxx")
-    >>> mr_no_privs = launchbag.user
-
-    >>> bug_upstream_firefox_crashes.status
-    Traceback (most recent call last):
-      ...
-    Unauthorized: (..., 'status', 'launchpad.View')
-
-    >>> bug_upstream_firefox_crashes.transitionToStatus(
-    ...     BugTaskStatus.FIXCOMMITTED, getUtility(ILaunchBag).user)
-    Traceback (most recent call last):
-      ...
-    Unauthorized: (..., 'transitionToStatus', 'launchpad.Edit')
-
-The private bugs will be invisible to No Privileges Person in the search
-results:
-
-    >>> from lp.services.searchbuilder import any
-    >>> from lp.bugs.interfaces.bugtask import BugTaskSearchParams
-    >>> params = BugTaskSearchParams(
-    ...     status=any(STATUS_NEW, STATUS_CONFIRMED),
-    ...     orderby="id", user=mr_no_privs)
-    >>> upstream_mozilla = productset.getByName('firefox')
-    >>> bugtasks = upstream_mozilla.searchTasks(params)
-    >>> print bugtasks.count()
-    3
-    >>> bug_ids = [bt.bug.id for bt in bugtasks]
-    >>> print sorted(bug_ids)
-    [1, 4, 5]
-
-We can create an access policy grant on the pillar to which the bug is
-targeted and No Privileges Person will have access to the private bug.
-
-    >>> from lp.registry.enums import InformationType
-    >>> from lp.registry.interfaces.accesspolicy import (
-    ...     IAccessPolicyGrantSource,
-    ...     IAccessPolicySource,
-    ...     )
-    >>> aps = getUtility(IAccessPolicySource)
-    >>> [policy] = aps.find(
-    ...     [(upstream_mozilla, InformationType.USERDATA)])
-    >>> apgs = getUtility(IAccessPolicyGrantSource)
-    >>> grant = apgs.grant([(policy, mr_no_privs, ubuntu_team)])
-    >>> bugtasks = upstream_mozilla.searchTasks(params)
-    >>> print bugtasks.count()
-    4
-    >>> bug_ids = [bt.bug.id for bt in bugtasks]
-    >>> print sorted(bug_ids)
-    [1, 4, 5, 6]
-    >>> apgs.revoke([(policy, mr_no_privs)])
-
-
-Open bugtask count for a given list of projects
------------------------------------------------
-
-IBugTaskSet.getOpenBugTasksPerProduct() will return a dictionary
-of product_id:count entries for bugs in an open status that
-the user given as a parameter is allowed to see. If a product,
-such as id=3 does not have any open bugs, it will not appear
-in the result.
-
-    >>> products = [productset.get(id) for id in (3, 5, 20)]
-    >>> bugtask_counts = bugtaskset.getOpenBugTasksPerProduct(
-    ...     sample_person, products)
-    >>> for product_id, count in sorted(bugtask_counts.items()):
-    ...     print 'product_id=%d count=%d' % (product_id, count)
-    product_id=5 count=1
-    product_id=20 count=2
-
-A Launchpad admin will get a higher count for the product with id=20
-because he can see the private bug.
-
-    >>> bugtask_counts = bugtaskset.getOpenBugTasksPerProduct(
-    ...     foobar, products)
-    >>> for product_id, count in sorted(bugtask_counts.items()):
-    ...     print 'product_id=%d count=%d' % (product_id, count)
-    product_id=5 count=1
-    product_id=20 count=3
-
-Someone subscribed to the private bug on the product with id=20
-will also have it added to the count.
-
-    >>> karl = personset.getByName('karl')
-    >>> bugtask_counts = bugtaskset.getOpenBugTasksPerProduct(
-    ...     karl, products)
-    >>> for product_id, count in sorted(bugtask_counts.items()):
-    ...     print 'product_id=%d count=%d' % (product_id, count)
-    product_id=5 count=1
-    product_id=20 count=3
-
-
-Privacy and Priviledged Users
------------------------------
-
-Now, we'll log in as Mark Shuttleworth, who was assigned to this bug
-when it was marked private:
-
-    >>> login("mark@xxxxxxxxxxx")
-
-And note that he can access and set the bugtask attributes:
-
-    >>> bug_upstream_firefox_crashes.status.title
-    'New'
-
-    >>> bug_upstream_firefox_crashes.transitionToStatus(
-    ...     BugTaskStatus.NEW, getUtility(ILaunchBag).user)
-
-
-Privacy and Team Awareness
---------------------------
-
-No Privileges Person can't see the private bug, because he's not a subscriber:
-
-    >>> login("no-priv@xxxxxxxxxxxxx")
-    >>> params = BugTaskSearchParams(
-    ...     status=any(STATUS_NEW, STATUS_CONFIRMED), user=no_priv)
-    >>> firefox_bugtasks = firefox.searchTasks(params)
-    >>> [bugtask.bug.id for bugtask in firefox_bugtasks]
-    [1, 4, 5]
-
-
-But if we add No Privileges Person to the Ubuntu Team, and because the
-Ubuntu Team *is* subscribed to the bug, No Privileges Person will see
-the private bug.
-
-    >>> login("mark@xxxxxxxxxxx")
-    >>> ignored = ubuntu_team.addMember(
-    ...     no_priv, reviewer=ubuntu_team.teamowner)
-
-    >>> login("no-priv@xxxxxxxxxxxxx")
-    >>> params = BugTaskSearchParams(
-    ...     status=any(STATUS_NEW, STATUS_CONFIRMED), user=foobar)
-    >>> firefox_bugtasks = firefox.searchTasks(params)
-    >>> [bugtask.bug.id for bugtask in firefox_bugtasks]
-    [1, 4, 5, 6]
-
-
-Privacy and Launchpad Admins
-----------------------------
-
-Let's log in as Daniel Henrique Debonzi:
-
-    >>> login("daniel.debonzi@xxxxxxxxxxxxx")
-    >>> debonzi = launchbag.user
-
-The same search as above yields the same result, because Daniel Debonzi is an
-administrator.
-
-    >>> firefox = productset.get(4)
-    >>> params = BugTaskSearchParams(status=any(STATUS_NEW,
-    ...                                         STATUS_CONFIRMED),
-    ...                              user=debonzi)
-    >>> firefox_bugtasks = firefox.searchTasks(params)
-    >>> [bugtask.bug.id for bugtask in firefox_bugtasks]
-    [1, 4, 5, 6]
-
-Trying to retrieve the bug directly will work fine:
-
-    >>> bug_upstream_firefox_crashes = bugtaskset.get(15)
-
-As will attribute access:
-
-    >>> bug_upstream_firefox_crashes.status.title
-    'New'
-
-And attribute setting:
-
-    >>> bug_upstream_firefox_crashes.transitionToStatus(
-    ...     BugTaskStatus.CONFIRMED, getUtility(ILaunchBag).user)
-    >>> bug_upstream_firefox_crashes.transitionToStatus(
-    ...     BugTaskStatus.NEW, getUtility(ILaunchBag).user)
-
-
-
-Sorting Bug Tasks
------------------
-
-Bug tasks need to sort in a very particular order. We want product tasks
-first, then ubuntu tasks, then other distro-related tasks. In the
-distro-related tasks we want a distribution-task first, then
-distroseries-tasks for that same distribution. The distroseries tasks
-should be sorted by distroseries version.
-
-Phew.
-
-Let's just make sure that the tasks on bug_one sort correctly.
-
-    >>> tasks = bug_one.bugtasks
-    >>> for task in tasks:
-    ...     print task.bugtargetdisplayname
-    Evolution
-    Mozilla Firefox
-    Mozilla Firefox 1.0
-    mozilla-firefox (Ubuntu)
-    Ubuntu Warty
-    mozilla-firefox (Debian)
-    Tubuntu
-
-
-BugTask Adaptation
-------------------
-
-An IBugTask can be adapted to an IBug.
-
-    >>> from lp.bugs.interfaces.bug import IBug
-
-    >>> bugtask_four = bugtaskset.get(4)
-    >>> bug = IBug(bugtask_four)
-    >>> bug.title
-    u'Firefox does not support SVG'
-
-
-The targetnamecache attribute of BugTask
-----------------------------------------
-
-The BugTask table has this targetnamecache attribute which stores a
-computed value to allow us to sort and search on that value without
-having to do lots of SQL joins. This cached value gets updated daily
-by the update-bugtask-targetnamecaches cronscript and whenever the
-bugtask is changed.  Of course, it's also computed and set when a
-bugtask is created.
-
-`BugTask.bugtargetdisplayname` simply returns `targetnamecache`, and
-the latter is not exposed in `IBugTask`, so the `bugtargetdisplayname`
-is used here.
-
-    >>> netapplet = productset.get(11)
-    >>> upstream_task = bugtaskset.createTask(
-    ...     bug_one, mark, netapplet,
-    ...     status=STATUS_NEW, importance=IMPORTANCE_MEDIUM)
-    >>> upstream_task.bugtargetdisplayname
-    u'NetApplet'
-
-    >>> thunderbird = productset.get(8)
-    >>> upstream_task_id = upstream_task.id
-    >>> upstream_task.transitionToTarget(thunderbird)
-    >>> upstream_task.bugtargetdisplayname
-    u'Mozilla Thunderbird'
-
-    >>> thunderbird.name = 'thunderbird-ng'
-    >>> thunderbird.displayname = 'Mozilla Thunderbird NG'
-
-    # XXX Guilherme Salgado 2005-11-07 bug=3989:
-    # This flush_database_updates() shouldn't be needed because we
-    # already have the transaction.commit() here, but without it
-    # (flush_database_updates), the cronscript won't see the thunderbird name
-    # change.
-    >>> flush_database_updates()
-    >>> transaction.commit()
-
-    >>> import subprocess
-    >>> process = subprocess.Popen(
-    ...     'cronscripts/update-bugtask-targetnamecaches.py', shell=True,
-    ...     stdin=subprocess.PIPE, stdout=subprocess.PIPE,
-    ...     stderr=subprocess.PIPE)
-    >>> (out, err) = process.communicate()
-
-    >>> print err
-    INFO Creating lockfile:
-        /var/lock/launchpad-launchpad-targetnamecacheupdater.lock
-    INFO Updating targetname cache of bugtasks.
-    INFO Calculating targets.
-    INFO Will check ... targets.
-    ...
-    INFO Updating (u'Mozilla Thunderbird',) to 'Mozilla Thunderbird NG'.
-    ...
-    INFO Updated 1 target names.
-    INFO Finished updating targetname cache of bugtasks.
-
-    >>> process.returncode
-    0
-
-    # XXX Guilherme Salgado 2005-11-07:
-    # If we don't call flush_database_caches() here, we won't see the
-    # changes made by the cronscript in objects we already have cached.
-    >>> from lp.services.database.sqlbase import flush_database_caches
-    >>> flush_database_caches()
-    >>> transaction.commit()
-
-    >>> bugtaskset.get(upstream_task_id).bugtargetdisplayname
-    u'Mozilla Thunderbird NG'
-
-With sourcepackage bugtasks that have accepted nominations to a
-series, additional sourcepackage bugtasks are automatically nominated
-to the same series. The nominations are implicitly accepted and have
-targetnamecache updated.
-
-    >>> new_bug, new_bug_event = bugset.createBugWithoutTarget(
-    ...     CreateBugParams(mark, 'New Bug', comment='New Bug'))
-
-    The first message of a new bug has index 0.
-    >>> new_bug.bug_messages[0].index
-    0
-
-    >>> bugtaskset.createTask(
-    ...     new_bug, mark, ubuntu.getSourcePackage('mozilla-firefox'))
-    <BugTask ...>
-
-    >>> new_bug.addNomination(mark, ubuntu.currentseries).approve(mark)
-
-The first task has been created and successfully nominated to Hoary.
-
-    >>> for task in new_bug.bugtasks:
-    ...     print task.bugtargetdisplayname
-    mozilla-firefox (Ubuntu)
-    mozilla-firefox (Ubuntu Hoary)
-
-    >>> bugtaskset.createTask(
-    ...     new_bug, mark, ubuntu.getSourcePackage('alsa-utils'))
-    <BugTask ...>
-
-The second task has been created and has also been successfully
-nominated to Hoary.
-
-    >>> for task in new_bug.bugtasks:
-    ...     print task.bugtargetdisplayname
-    alsa-utils (Ubuntu)
-    mozilla-firefox (Ubuntu)
-    alsa-utils (Ubuntu Hoary)
-    mozilla-firefox (Ubuntu Hoary)
-
-The updating of targetnamecaches is usually done by the cronjob, however
-it can also be invoked directly.
-
-    >>> thunderbird.name = 'thunderbird'
-    >>> thunderbird.displayname = 'Mozilla Thunderbird'
-    >>> transaction.commit()
-
-    >>> upstream_task.bugtargetdisplayname
-    u'Mozilla Thunderbird NG'
-
-    >>> from lp.bugs.scripts.bugtasktargetnamecaches import (
-    ...     BugTaskTargetNameCacheUpdater)
-    >>> from lp.services.log.logger import FakeLogger
-    >>> logger = FakeLogger()
-    >>> updater = BugTaskTargetNameCacheUpdater(transaction, logger)
-    >>> updater.run()
-    INFO Updating targetname cache of bugtasks.
-    INFO Calculating targets.
-    ...
-    INFO Updating (u'Mozilla Thunderbird NG',) to 'Mozilla Thunderbird'.
-    ...
-    INFO Updated 1 target names.
-    INFO Finished updating targetname cache of bugtasks.
-
-    >>> flush_database_caches()
-    >>> transaction.commit()
-    >>> upstream_task.bugtargetdisplayname
-    u'Mozilla Thunderbird'
-
-
-Target Uses Malone
-------------------
-
-Bug tasks have a flag, target_uses_malone, that says whether the bugtask
-target uses Malone as its official bugtracker.
-
-    >>> for bugtask in bug_one.bugtasks:
-    ...     print "%-30s %s" % (
-    ...         bugtask.bugtargetdisplayname, bugtask.target_uses_malone)
-    Evolution                      True
-    Mozilla Firefox                True
-    Mozilla Firefox 1.0            True
-    Mozilla Thunderbird            False
-    mozilla-firefox (Ubuntu)       True
-    Ubuntu Warty                   True
-    mozilla-firefox (Debian)       False
-    Tubuntu                        False
-
-
-BugTask badges
---------------
-
-A bug can have certain properties, which results in a badge being
-displayed in bug listings. BugTaskSet has a method,
-getBugTaskBadgeProperties(), which calculates these properties for
-multiple bug tasks in one go.
-
-    >>> from operator import attrgetter
-    >>> def print_badge_properties(badge_properties):
-    ...     bugtasks = sorted(badge_properties.keys(), key=attrgetter('id'))
-    ...     for bugtask in bugtasks:
-    ...         print "Properties for bug %s:" % (bugtask.bug.id)
-    ...         for key, value in sorted(badge_properties[bugtask].items()):
-    ...             print " %s: %s" % (key, value)
-
-    >>> bug_two = getUtility(IBugSet).get(2)
-    >>> bug_three = getUtility(IBugSet).get(3)
-    >>> some_bugtask = bug_two.bugtasks[0]
-    >>> another_bugtask = bug_three.bugtasks[0]
-    >>> badge_properties = getUtility(IBugTaskSet).getBugTaskBadgeProperties(
-    ...     [some_bugtask, another_bugtask])
-    >>> print_badge_properties(badge_properties)
-    Properties for bug 2:
-     has_branch: False
-     has_patch: False
-     has_specification: False
-    Properties for bug 3:
-     has_branch: False
-     has_patch: False
-     has_specification: False
-
-..., a specification gets linked...
-
-    >>> from lp.blueprints.interfaces.specification import ISpecificationSet
-    >>> spec = getUtility(ISpecificationSet).all_specifications[0]
-    >>> spec.linkBug(bug_two)
-    <SpecificationBug at ...>
-
-... or a branch gets linked to the bug...
-
-    >>> branch = factory.makeAnyBranch()
-    >>> bug_three.linkBranch(branch, no_priv)
-    <BugBranch at ...>
-
-... the properties for the bugtasks reflect this.
-
-    >>> badge_properties = getUtility(IBugTaskSet).getBugTaskBadgeProperties(
-    ...     [some_bugtask, another_bugtask])
-    >>> print_badge_properties(badge_properties)
-    Properties for bug 2:
-     has_branch: False
-     has_patch: False
-     has_specification: True
-    Properties for bug 3:
-     has_branch: True
-     has_patch: False
-     has_specification: False
-
-
-Bugtask Tags
-------------
-
-List of bugtasks often need to display related tags.  Since tags are
-related to bugtasks via bugs, BugTaskSet has a method getBugTaskTags
-that can calculate the tags in one query.
-
-    >>> some_bugtask.bug.tags = [u'foo', u'bar']
-    >>> another_bugtask.bug.tags = [u'baz', u'bop']
-    >>> tags_by_task = getUtility(IBugTaskSet).getBugTaskTags([
-    ...     some_bugtask, another_bugtask])
-    >>> print tags_by_task
-    {3: [u'bar', u'foo'], 6: [u'baz', u'bop']}
-
-
-Similar bugs
-------------
-
-It's possible to get a list of bugs similar to the current bug by
-accessing the similar_bugs property of its bug tasks.
-
-    >>> new_ff_bug = factory.makeBug(product=firefox, title="Firefox")
-    >>> ff_bugtask = new_ff_bug.bugtasks[0]
-
-    >>> similar_bugs = ff_bugtask.findSimilarBugs(user=sample_person)
-    >>> for similar_bug in sorted(similar_bugs, key=attrgetter('id')):
-    ...     print "%s: %s" % (similar_bug.id, similar_bug.title)
-    1: Firefox does not support SVG
-    5: Firefox install instructions should be complete
-
-This also works for distributions...
-
-    >>> ubuntu_bugtask = factory.makeBugTask(bug=new_ff_bug, target=ubuntu)
-    >>> similar_bugs = ubuntu_bugtask.findSimilarBugs(user=sample_person)
-    >>> for similar_bug in sorted(similar_bugs, key=attrgetter('id')):
-    ...     print "%s: %s" % (similar_bug.id, similar_bug.title)
-    1: Firefox does not support SVG
-
-... and for SourcePackages.
-
-    >>> a_ff_bug = factory.makeBug(product=firefox, title="a Firefox")
-    >>> firefox_package = ubuntu.getSourcePackage('mozilla-firefox')
-    >>> firefox_package_bugtask = factory.makeBugTask(
-    ...     bug=a_ff_bug, target=firefox_package)
-
-    >>> similar_bugs = firefox_package_bugtask.findSimilarBugs(
-    ...     user=sample_person)
-    >>> for similar_bug in sorted(similar_bugs, key=attrgetter('id')):
-    ...     print "%s: %s" % (similar_bug.id, similar_bug.title)
-    1: Firefox does not support SVG
-
-Private bugs won't show up in the list of similar bugs unless the user
-is a direct subscriber. We'll demonstrate this by creating a new bug
-against Firefox.
-
-    >>> second_ff_bug = factory.makeBug(
-    ...     product=firefox, title="Yet another Firefox bug")
-    >>> similar_bugs = ff_bugtask.findSimilarBugs(user=no_priv)
-    >>> for similar_bug in sorted(similar_bugs, key=attrgetter('id')):
-    ...     print "%s: %s" % (similar_bug.id, similar_bug.title)
-    1: Firefox does not support SVG
-    5: Firefox install instructions should be complete
-    ...Yet another Firefox bug
-
-If we mark the new bug as private, it won't appear in the similar bugs
-list for no_priv any more, since they're not a direct subscriber.
-
-    >>> second_ff_bug.setPrivate(True, foobar)
-    True
-
-    >>> similar_bugs = ff_bugtask.findSimilarBugs(user=no_priv)
-    >>> for similar_bug in sorted(similar_bugs, key=attrgetter('id')):
-    ...     print "%s: %s" % (similar_bug.id, similar_bug.title)
-    1: Firefox does not support SVG
-    5: Firefox install instructions should be complete
-    ...: a Firefox

=== modified file 'lib/lp/bugs/model/tests/test_bugtask.py'
--- lib/lp/bugs/model/tests/test_bugtask.py	2012-05-14 07:30:03 +0000
+++ lib/lp/bugs/model/tests/test_bugtask.py	2012-05-14 15:32:20 +0000
@@ -3,33 +3,48 @@
 
 __metaclass__ = type
 
-from datetime import timedelta
+import subprocess
+import transaction
 import unittest
 
+from collections import namedtuple
+from datetime import (
+    datetime,
+    timedelta,
+    )
+from operator import attrgetter
+
 from lazr.lifecycle.event import ObjectModifiedEvent
 from lazr.lifecycle.snapshot import Snapshot
 from lazr.restfulclient.errors import Unauthorized
 from testtools.matchers import Equals
 from testtools.testcase import ExpectedException
-import transaction
 from zope.component import getUtility
 from zope.event import notify
 from zope.interface import providedBy
+from zope.security.interfaces import Unauthorized as zopeUnauthorized
 from zope.security.proxy import removeSecurityProxy
 
 from lp.app.enums import ServiceUsage
 from lp.app.interfaces.launchpad import ILaunchpadCelebrities
-from lp.bugs.interfaces.bug import IBugSet
+from lp.blueprints.interfaces.specification import ISpecificationSet
+from lp.bugs.interfaces.bug import (
+    CreateBugParams,
+    IBug,
+    IBugSet,
+    )
 from lp.bugs.interfaces.bugtarget import IBugTarget
 from lp.bugs.interfaces.bugtask import (
     BugTaskImportance,
     BugTaskSearchParams,
     BugTaskStatus,
+    BugTaskStatusSearch,
     CannotDeleteBugtask,
     DB_UNRESOLVED_BUGTASK_STATUSES,
     IBugTaskSet,
     RESOLVED_BUGTASK_STATUSES,
     UNRESOLVED_BUGTASK_STATUSES,
+    UserCannotEditBugTaskMilestone,
     )
 from lp.bugs.interfaces.bugwatch import IBugWatchSet
 from lp.bugs.model.bug import Bug
@@ -46,13 +61,22 @@
     _build_tag_search_clause,
     get_bug_privacy_filter,
     )
+from lp.bugs.scripts.bugtasktargetnamecaches import (
+    BugTaskTargetNameCacheUpdater)
 from lp.bugs.tests.bug import create_old_bug
 from lp.hardwaredb.interfaces.hwdb import (
     HWBus,
     IHWDeviceSet,
     )
 from lp.registry.enums import InformationType
+from lp.registry.interfaces.accesspolicy import (
+    IAccessPolicyGrantSource,
+    IAccessPolicySource,
+    )
 from lp.registry.interfaces.distribution import IDistributionSet
+from lp.registry.interfaces.distributionsourcepackage \
+    import IDistributionSourcePackage
+from lp.registry.interfaces.distroseries import IDistroSeriesSet
 from lp.registry.interfaces.person import (
     IPerson,
     IPersonSet,
@@ -60,10 +84,14 @@
     )
 from lp.registry.interfaces.product import IProductSet
 from lp.registry.interfaces.projectgroup import IProjectGroupSet
+from lp.registry.interfaces.sourcepackage import ISourcePackage
+from lp.registry.model.sourcepackage import SourcePackage
 from lp.services.database.sqlbase import (
     convert_storm_clause_to_string,
     flush_database_updates,
+    flush_database_caches,
     )
+from lp.services.log.logger import FakeLogger
 from lp.services.searchbuilder import (
     all,
     any,
@@ -100,6 +128,486 @@
     LaunchpadZopelessLayer,
     )
 from lp.testing.matchers import HasQueryCount
+from storm.store import Store
+
+
+BugData = namedtuple("BugData", ['owner', 'distro', 'distro_release',
+'source_package', 'bug', 'generic_task', 'series_task', ])
+
+ConjoinedData = namedtuple("ConjoinedData", ['alsa_utils', 'generic_task',
+    'devel_focus_task'])
+
+
+class TestBugTaskAdaptation(TestCase):
+    """Verify bugtask adaptation."""
+
+    layer = DatabaseFunctionalLayer
+
+    def test_bugtask_adaptation(self):
+        """An IBugTask can be adapted to an IBug"""
+        login('foo.bar@xxxxxxxxxxxxx')
+        bugtask_four = getUtility(IBugTaskSet).get(4)
+        bug = IBug(bugtask_four)
+        self.assertEqual(bug.title,
+            u'Firefox does not support SVG')
+
+
+class TestBugTaskCreation(TestCaseWithFactory):
+    """Test BugTaskSet creation methods."""
+
+    layer = DatabaseFunctionalLayer
+
+    def test_upstream_task(self):
+        """A bug that has to be fixed in an upstream product."""
+        bugtaskset = getUtility(IBugTaskSet)
+        bug_one = getUtility(IBugSet).get(1)
+        mark = getUtility(IPersonSet).getByEmail('mark@xxxxxxxxxxx')
+        evolution = getUtility(IProductSet).get(5)
+
+        upstream_task = bugtaskset.createTask(
+            bug_one, mark, evolution,
+            status=BugTaskStatus.NEW,
+            importance=BugTaskImportance.MEDIUM)
+
+        self.assertEqual(upstream_task.product, evolution)
+        self.assertEqual(upstream_task.target, evolution)
+        # getPackageComponent only applies to tasks that specify package info.
+        self.assertEqual(upstream_task.getPackageComponent(), None)
+
+    def test_distro_specific_bug(self):
+        """A bug that needs to be fixed in a specific distro."""
+        bugtaskset = getUtility(IBugTaskSet)
+        bug_one = getUtility(IBugSet).get(1)
+        mark = getUtility(IPersonSet).getByEmail('mark@xxxxxxxxxxx')
+
+        a_distro = self.factory.makeDistribution(name='tubuntu')
+        distro_task = bugtaskset.createTask(
+            bug_one, mark, a_distro,
+            status=BugTaskStatus.NEW,
+            importance=BugTaskImportance.MEDIUM)
+
+        self.assertEqual(distro_task.distribution, a_distro)
+        self.assertEqual(distro_task.target, a_distro)
+        # getPackageComponent only applies to tasks that specify package info.
+        self.assertEqual(distro_task.getPackageComponent(), None)
+
+    def test_distroseries_specific_bug(self):
+        """A bug that needs to be fixed in a specific distro series
+
+        These tasks are used for release management and backporting
+        """
+        bugtaskset = getUtility(IBugTaskSet)
+        bug_one = getUtility(IBugSet).get(1)
+        mark = getUtility(IPersonSet).getByEmail('mark@xxxxxxxxxxx')
+        warty = getUtility(IDistroSeriesSet).get(1)
+
+        distro_series_task = bugtaskset.createTask(
+            bug_one, mark, warty,
+            status=BugTaskStatus.NEW, importance=BugTaskImportance.MEDIUM)
+
+        self.assertEqual(distro_series_task.distroseries, warty)
+        self.assertEqual(distro_series_task.target, warty)
+        # getPackageComponent only applies to tasks that specify package info.
+        self.assertEqual(distro_series_task.getPackageComponent(), None)
+
+    def test_createmany_bugtasks(self):
+        """We can create a set of bugtasks around different targets"""
+        bugtaskset = getUtility(IBugTaskSet)
+        mark = getUtility(IPersonSet).getByEmail('mark@xxxxxxxxxxx')
+        evolution = getUtility(IProductSet).get(5)
+        warty = getUtility(IDistroSeriesSet).get(1)
+        bug_many = getUtility(IBugSet).get(4)
+
+        a_distro = self.factory.makeDistribution(name='tubuntu')
+        taskset = bugtaskset.createManyTasks(bug_many, mark,
+            [evolution, a_distro, warty], status=BugTaskStatus.FIXRELEASED)
+        tasks = [(t.product, t.distribution, t.distroseries) for t in taskset]
+        tasks.sort()
+
+        self.assertEqual(tasks[0][2], warty)
+        self.assertEqual(tasks[1][1], a_distro)
+        self.assertEqual(tasks[2][0], evolution)
+
+
+class TestBugTaskTargets(TestCase):
+    """Verify we handle various bugtask targets correctly"""
+
+    layer = DatabaseFunctionalLayer
+
+    def test_bugtask_target_productseries(self):
+        """The 'target' of a task can be a product series"""
+        login('foo.bar@xxxxxxxxxxxxx')
+        bugtaskset = getUtility(IBugTaskSet)
+        productset = getUtility(IProductSet)
+        bug_one = getUtility(IBugSet).get(1)
+        mark = getUtility(IPersonSet).getByEmail('mark@xxxxxxxxxxx')
+
+        firefox = productset['firefox']
+        firefox_1_0 = firefox.getSeries("1.0")
+        productseries_task = bugtaskset.createTask(bug_one, mark, firefox_1_0)
+
+        self.assertEqual(productseries_task.target, firefox_1_0)
+        # getPackageComponent only applies to tasks that specify package info.
+        self.assertEqual(productseries_task.getPackageComponent(), None)
+
+    def test_bugtask_target_distro_sourcepackage(self):
+        """The 'target' of a task can be a distro sourcepackage"""
+        login('foo.bar@xxxxxxxxxxxxx')
+        bugtaskset = getUtility(IBugTaskSet)
+
+        debian_ff_task = bugtaskset.get(4)
+        self.assertTrue(
+            IDistributionSourcePackage.providedBy(debian_ff_task.target))
+        self.assertEqual(debian_ff_task.getPackageComponent(), None)
+
+        target = debian_ff_task.target
+        self.assertEqual(target.distribution.name, u'debian')
+        self.assertEqual(target.sourcepackagename.name, u'mozilla-firefox')
+
+        ubuntu_linux_task = bugtaskset.get(25)
+        self.assertTrue(
+            IDistributionSourcePackage.providedBy(ubuntu_linux_task.target))
+        self.assertEqual(ubuntu_linux_task.getPackageComponent().name, 'main')
+
+        target = ubuntu_linux_task.target
+        self.assertEqual(target.distribution.name, u'ubuntu')
+        self.assertEqual(target.sourcepackagename.name, u'linux-source-2.6.15')
+
+    def test_bugtask_target_distroseries_sourcepackage(self):
+        """The 'target' of a task can be a distroseries sourcepackage"""
+        login('foo.bar@xxxxxxxxxxxxx')
+        bugtaskset = getUtility(IBugTaskSet)
+        distro_series_sp_task = bugtaskset.get(16)
+
+        self.assertEqual(distro_series_sp_task.getPackageComponent().name,
+            'main')
+
+        expected_target = SourcePackage(
+            distroseries=distro_series_sp_task.distroseries,
+            sourcepackagename=distro_series_sp_task.sourcepackagename)
+        got_target = distro_series_sp_task.target
+        self.assertTrue(
+            ISourcePackage.providedBy(distro_series_sp_task.target))
+        self.assertEqual(got_target.distroseries,
+            expected_target.distroseries)
+        self.assertEqual(got_target.sourcepackagename,
+            expected_target.sourcepackagename)
+
+
+class TestBugTaskTargetName(TestCase):
+    """Verify our targetdisplayname and targetname are correct."""
+
+    layer = DatabaseFunctionalLayer
+
+    def test_targetname_distribution(self):
+        """The distribution name will be concat'd"""
+        login('foo.bar@xxxxxxxxxxxxx')
+        bugtaskset = getUtility(IBugTaskSet)
+        bugtask = bugtaskset.get(17)
+
+        self.assertEqual(bugtask.bugtargetdisplayname,
+            u'mozilla-firefox (Ubuntu)')
+        self.assertEqual(bugtask.bugtargetname,
+            u'mozilla-firefox (Ubuntu)')
+
+    def test_targetname_series_product(self):
+        """The targetname for distro series/product versions will be name of
+        source package or binary package. """
+        login('foo.bar@xxxxxxxxxxxxx')
+        bugtaskset = getUtility(IBugTaskSet)
+        bugtask = bugtaskset.get(2)
+
+        self.assertEqual(bugtask.bugtargetdisplayname, u'Mozilla Firefox')
+        self.assertEqual(bugtask.bugtargetname, u'firefox')
+
+
+class TestEditingBugTask(TestCase):
+    """Verify out editing functionality of bugtasks."""
+
+    layer = DatabaseFunctionalLayer
+
+    def test_edit_upstream(self):
+        """You cannot edit upstream tasks as ANONYMOUS"""
+        login('foo.bar@xxxxxxxxxxxxx')
+        bugtaskset = getUtility(IBugTaskSet)
+        bug_one = getUtility(IBugSet).get(1)
+        mark = getUtility(IPersonSet).getByEmail('mark@xxxxxxxxxxx')
+
+        evolution = getUtility(IProductSet).get(5)
+        upstream_task = bugtaskset.createTask(
+            bug_one, mark, evolution,
+            status=BugTaskStatus.NEW,
+            importance=BugTaskImportance.MEDIUM)
+
+        # An anonymous user cannot edit the bugtask.
+        login(ANONYMOUS)
+        with ExpectedException(zopeUnauthorized, ''):
+            upstream_task.transitionToStatus(
+                BugTaskStatus.CONFIRMED, getUtility(ILaunchBag.user))
+
+        # A logged in user can edit the upstream bugtask.
+        login('jeff.waugh@xxxxxxxxxxxxxxx')
+        upstream_task.transitionToStatus(
+            BugTaskStatus.FIXRELEASED, getUtility(ILaunchBag).user)
+
+    def test_edit_distro_bugtasks(self):
+        """Any logged-in user can edit tasks filed on distros
+
+        However not if the bug is not marked private.
+        So, as an anonymous user, we cannot edit anything:
+        """
+        login(ANONYMOUS)
+
+        bugtaskset = getUtility(IBugTaskSet)
+        distro_task = bugtaskset.get(25)
+
+        # Anonymous cannot change the status.
+        with ExpectedException(zopeUnauthorized):
+            distro_task.transitionToStatus(BugTaskStatus.FIXRELEASED,
+                getUtility(ILaunchBag).user)
+
+        # Anonymous cannot change the assignee.
+        sample_person = getUtility(IPersonSet).getByEmail('test@xxxxxxxxxxxxx')
+        with ExpectedException(zopeUnauthorized):
+            distro_task.transitionToAssignee(sample_person)
+
+        login('test@xxxxxxxxxxxxx')
+
+        distro_task.transitionToStatus(
+            BugTaskStatus.FIXRELEASED,
+            getUtility(ILaunchBag).user)
+        distro_task.transitionToAssignee(sample_person)
+
+
+class TestBugTaskTags(TestCase):
+    """List of bugtasks often need to display related tasks."""
+
+    layer = DatabaseFunctionalLayer
+
+    def test_getting_tags_from_bugs(self):
+        """Tags are related to bugtasks via bugs.
+
+        BugTaskSet has a method getBugTaskTags that can calculate the tags in
+        one query.
+        """
+        login('foo.bar@xxxxxxxxxxxxx')
+        bug_two = getUtility(IBugSet).get(2)
+        some_bugtask = bug_two.bugtasks[0]
+        bug_three = getUtility(IBugSet).get(3)
+        another_bugtask = bug_three.bugtasks[0]
+
+        some_bugtask.bug.tags = [u'foo', u'bar']
+        another_bugtask.bug.tags = [u'baz', u'bop']
+        tags_by_task = getUtility(IBugTaskSet).getBugTaskTags([
+            some_bugtask, another_bugtask])
+
+        self.assertEqual(tags_by_task,
+            {3: [u'bar', u'foo'], 6: [u'baz', u'bop']})
+
+
+class TestBugTaskBadges(TestCaseWithFactory):
+
+    """Verify getBugTaskBadgeProperties"""
+
+    layer = DatabaseFunctionalLayer
+
+    def test_butask_badges_populated(self):
+        """getBugTaskBadgeProperties(), calcs properties for multiple tasks.
+
+        A bug can have certain properties, which results in a badge being
+        displayed in bug listings.
+        """
+        login('foo.bar@xxxxxxxxxxxxx')
+
+        def get_badge_properties(badge_properties):
+            bugtasks = sorted(badge_properties.keys(), key=attrgetter('id'))
+            res = []
+            for bugtask in bugtasks:
+                res.append("Properties for bug %s:" % (bugtask.bug.id))
+                for key, value in sorted(badge_properties[bugtask].items()):
+                    res.append(" %s: %s" % (key, value))
+            return res
+
+        bug_two = getUtility(IBugSet).get(2)
+        some_bugtask = bug_two.bugtasks[0]
+        bug_three = getUtility(IBugSet).get(3)
+        another_bugtask = bug_three.bugtasks[0]
+        badge_properties = getUtility(IBugTaskSet).getBugTaskBadgeProperties(
+            [some_bugtask, another_bugtask])
+
+        self.assertEqual(get_badge_properties(badge_properties),
+           ['Properties for bug 2:',
+           ' has_branch: False',
+           ' has_patch: False',
+           ' has_specification: False',
+           'Properties for bug 3:',
+           ' has_branch: False',
+           ' has_patch: False',
+           ' has_specification: False'])
+
+        # a specification gets linked...
+        spec = getUtility(ISpecificationSet).all_specifications[0]
+        spec.linkBug(bug_two)
+
+        # or a branch gets linked to the bug...
+        no_priv = getUtility(IPersonSet).getByEmail('no-priv@xxxxxxxxxxxxx')
+        branch = self.factory.makeAnyBranch()
+        bug_three.linkBranch(branch, no_priv)
+
+        # the properties for the bugtasks reflect this.
+        badge_properties = getUtility(IBugTaskSet).getBugTaskBadgeProperties(
+            [some_bugtask, another_bugtask])
+        self.assertEqual(get_badge_properties(badge_properties), [
+        'Properties for bug 2:',
+        ' has_branch: False',
+        ' has_patch: False',
+        ' has_specification: True',
+        'Properties for bug 3:',
+        ' has_branch: True',
+        ' has_patch: False',
+        ' has_specification: False',
+         ])
+
+
+class TestBugTaskPrivacy(TestCase):
+    """Verify that the bug is either private or public."""
+
+    layer = DatabaseFunctionalLayer
+
+    def test_bugtask_privacy(self):
+        # Let's log in as the user Foo Bar (to be allowed to edit bugs):
+        launchbag = getUtility(ILaunchBag)
+        login('foo.bar@xxxxxxxxxxxxx')
+        foobar = launchbag.user
+
+        # Mark one of the Firefox bugs private. While we do this, we're also
+        # going to subscribe the Ubuntu team to the bug report to help
+        # demonstrate later on the interaction between privacy and teams (see
+        # the section entitled _Privacy and Team Awareness_):
+        bug_upstream_firefox_crashes = getUtility(IBugTaskSet).get(15)
+
+        ubuntu_team = getUtility(IPersonSet).getByEmail('support@xxxxxxxxxx')
+        bug_upstream_firefox_crashes.bug.subscribe(ubuntu_team, ubuntu_team)
+
+        old_state = Snapshot(bug_upstream_firefox_crashes.bug,
+            providing=IBug)
+        self.assertTrue(bug_upstream_firefox_crashes.bug.setPrivate(True,
+            foobar))
+
+        bug_set_private = ObjectModifiedEvent(
+            bug_upstream_firefox_crashes.bug, old_state,
+            ["id", "title", "private"])
+        notify(bug_set_private)
+        flush_database_updates()
+
+        # If we now login as someone who was neither implicitly nor explicitly
+        # subscribed to this bug, e.g. No Privileges Person, they will not be
+        # able to access or set properties of the bugtask.
+        launchbag = getUtility(ILaunchBag)
+        login("no-priv@xxxxxxxxxxxxx")
+        mr_no_privs = launchbag.user
+
+        with ExpectedException(zopeUnauthorized):
+            bug_upstream_firefox_crashes.status
+
+        with ExpectedException(zopeUnauthorized):
+            bug_upstream_firefox_crashes.transitionToStatus(
+                BugTaskStatus.FIXCOMMITTED, getUtility(ILaunchBag).user)
+
+        # The private bugs will be invisible to No Privileges Person in the
+        # search results:
+        params = BugTaskSearchParams(
+            status=any(BugTaskStatus.NEW, BugTaskStatus.CONFIRMED),
+            orderby="id", user=mr_no_privs)
+        upstream_mozilla = getUtility(IProductSet).getByName('firefox')
+        bugtasks = upstream_mozilla.searchTasks(params)
+        self.assertEqual(bugtasks.count(), 3)
+
+        bug_ids = [bt.bug.id for bt in bugtasks]
+        self.assertEqual(sorted(bug_ids), [1, 4, 5])
+
+        # We can create an access policy grant on the pillar to which the bug
+        # is targeted and No Privileges Person will have access to the private
+        # bug
+        aps = getUtility(IAccessPolicySource)
+        [policy] = aps.find(
+            [(upstream_mozilla, InformationType.USERDATA)])
+        apgs = getUtility(IAccessPolicyGrantSource)
+        apgs.grant([(policy, mr_no_privs, ubuntu_team)])
+        bugtasks = upstream_mozilla.searchTasks(params)
+        self.assertEqual(bugtasks.count(), 4)
+
+        bug_ids = [bt.bug.id for bt in bugtasks]
+        self.assertEqual(sorted(bug_ids), [1, 4, 5, 6])
+        apgs.revoke([(policy, mr_no_privs)])
+
+        # Privacy and Priviledged Users
+        # Now, we'll log in as Mark Shuttleworth, who was assigned to this bug
+        # when it was marked private:
+        login("mark@xxxxxxxxxxx")
+
+        # And note that he can access and set the bugtask attributes:
+        self.assertEqual(bug_upstream_firefox_crashes.status.title,
+            'New')
+        bug_upstream_firefox_crashes.transitionToStatus(
+            BugTaskStatus.NEW, getUtility(ILaunchBag).user)
+
+        # Privacy and Team Awareness
+        # No Privileges Person can't see the private bug, because he's not a
+        # subscriber:
+        no_priv = getUtility(IPersonSet).getByEmail('no-priv@xxxxxxxxxxxxx')
+        params = BugTaskSearchParams(
+            status=any(BugTaskStatus.NEW, BugTaskStatus.CONFIRMED),
+                user=no_priv)
+
+        firefox = getUtility(IProductSet)['firefox']
+        firefox_bugtasks = firefox.searchTasks(params)
+        self.assertEqual([bugtask.bug.id for bugtask in firefox_bugtasks],
+            [1, 4, 5])
+
+        # But if we add No Privileges Person to the Ubuntu Team, and because
+        # the Ubuntu Team *is* subscribed to the bug, No Privileges Person
+        # will see the private bug.
+
+        login("mark@xxxxxxxxxxx")
+        ubuntu_team.addMember(no_priv, reviewer=ubuntu_team.teamowner)
+
+        login("no-priv@xxxxxxxxxxxxx")
+        params = BugTaskSearchParams(
+            status=any(BugTaskStatus.NEW, BugTaskStatus.CONFIRMED),
+                user=foobar)
+        firefox_bugtasks = firefox.searchTasks(params)
+        self.assertEqual([bugtask.bug.id for bugtask in firefox_bugtasks],
+            [1, 4, 5, 6])
+
+        # Privacy and Launchpad Admins
+        # ----------------------------
+        # Let's log in as Daniel Henrique Debonzi:
+        launchbag = getUtility(ILaunchBag)
+        login("daniel.debonzi@xxxxxxxxxxxxx")
+        debonzi = launchbag.user
+
+        # The same search as above yields the same result, because Daniel
+        # Debonzi is an administrator.
+        firefox = getUtility(IProductSet).get(4)
+        params = BugTaskSearchParams(status=any(BugTaskStatus.NEW,
+                                                BugTaskStatus.CONFIRMED),
+                                     user=debonzi)
+        firefox_bugtasks = firefox.searchTasks(params)
+        self.assertEqual([bugtask.bug.id for bugtask in firefox_bugtasks],
+            [1, 4, 5, 6])
+
+        # Trying to retrieve the bug directly will work fine:
+        bug_upstream_firefox_crashes = getUtility(IBugTaskSet).get(15)
+        # As will attribute access:
+        self.assertEqual(bug_upstream_firefox_crashes.status.title,
+            'New')
+
+        # And attribute setting:
+        bug_upstream_firefox_crashes.transitionToStatus(
+            BugTaskStatus.CONFIRMED, getUtility(ILaunchBag).user)
+        bug_upstream_firefox_crashes.transitionToStatus(
+            BugTaskStatus.NEW, getUtility(ILaunchBag).user)
 
 
 class TestBugTaskDelta(TestCaseWithFactory):
@@ -1512,105 +2020,593 @@
         self.assertEqual(bugtask, bug.default_bugtask)
 
 
+class TestStatusCountsForProductSeries(TestCaseWithFactory):
+    """Test BugTaskSet.getStatusCountsForProductSeries()."""
+
+    layer = DatabaseFunctionalLayer
+
+    def setUp(self):
+        super(TestStatusCountsForProductSeries, self).setUp()
+        self.bugtask_set = getUtility(IBugTaskSet)
+        self.owner = self.factory.makePerson()
+        login_person(self.owner)
+        self.product = self.factory.makeProduct(owner=self.owner)
+        self.series = self.factory.makeProductSeries(product=self.product)
+        self.milestone = self.factory.makeMilestone(productseries=self.series)
+
+    def get_counts(self, user):
+        return self.bugtask_set.getStatusCountsForProductSeries(
+            user, self.series)
+
+    def createBugs(self):
+        self.factory.makeBug(milestone=self.milestone)
+        self.factory.makeBug(
+            milestone=self.milestone,
+            information_type=InformationType.USERDATA)
+        self.factory.makeBug(series=self.series)
+        self.factory.makeBug(
+            series=self.series, information_type=InformationType.USERDATA)
+
+    def test_privacy_and_counts_for_unauthenticated_user(self):
+        # An unauthenticated user should see bug counts for each status
+        # that do not include private bugs.
+        self.createBugs()
+        self.assertEqual(
+            {BugTaskStatus.NEW: 2},
+            self.get_counts(None))
+
+    def test_privacy_and_counts_for_owner(self):
+        # The owner should see bug counts for each status that do
+        # include all private bugs.
+        self.createBugs()
+        self.assertEqual(
+            {BugTaskStatus.NEW: 4},
+            self.get_counts(self.owner))
+
+    def test_privacy_and_counts_for_other_user(self):
+        # A random authenticated user should see bug counts for each
+        # status that do include all private bugs, since it is costly to
+        # query just the private bugs that the user has access to view,
+        # and this query may be run many times on a single page.
+        self.createBugs()
+        other = self.factory.makePerson()
+        self.assertEqual(
+            {BugTaskStatus.NEW: 4},
+            self.get_counts(other))
+
+    def test_multiple_statuses(self):
+        # Test that separate counts are provided for each status that
+        # bugs are found in.
+        statuses = [
+            BugTaskStatus.INVALID,
+            BugTaskStatus.OPINION,
+            ]
+        for status in statuses:
+            self.factory.makeBug(milestone=self.milestone, status=status)
+            self.factory.makeBug(series=self.series, status=status)
+        for i in range(3):
+            self.factory.makeBug(series=self.series)
+        expected = {
+            BugTaskStatus.INVALID: 2,
+            BugTaskStatus.OPINION: 2,
+            BugTaskStatus.NEW: 3,
+            }
+        self.assertEqual(expected, self.get_counts(None))
+
+    def test_incomplete_status(self):
+        # INCOMPLETE is stored as either INCOMPLETE_WITH_RESPONSE or
+        # INCOMPLETE_WITHOUT_RESPONSE so the stats do not include a count of
+        # INCOMPLETE tasks.
+        statuses = [
+            BugTaskStatusSearch.INCOMPLETE_WITH_RESPONSE,
+            BugTaskStatusSearch.INCOMPLETE_WITHOUT_RESPONSE,
+            BugTaskStatus.INCOMPLETE,
+            ]
+        for status in statuses:
+            self.factory.makeBug(series=self.series, status=status)
+        flush_database_updates()
+        expected = {
+            BugTaskStatusSearch.INCOMPLETE_WITH_RESPONSE: 1,
+            BugTaskStatusSearch.INCOMPLETE_WITHOUT_RESPONSE: 2,
+            }
+        self.assertEqual(expected, self.get_counts(None))
+
+
+class TestStatusCountsForProductSeries(TestCaseWithFactory):
+    """Test BugTaskSet.getStatusCountsForProductSeries()."""
+
+    layer = DatabaseFunctionalLayer
+
+    def setUp(self):
+        super(TestStatusCountsForProductSeries, self).setUp()
+        self.bugtask_set = getUtility(IBugTaskSet)
+        self.owner = self.factory.makePerson()
+        login_person(self.owner)
+        self.product = self.factory.makeProduct(owner=self.owner)
+        self.series = self.factory.makeProductSeries(product=self.product)
+        self.milestone = self.factory.makeMilestone(productseries=self.series)
+
+    def get_counts(self, user):
+        return self.bugtask_set.getStatusCountsForProductSeries(
+            user, self.series)
+
+    def createBugs(self):
+        self.factory.makeBug(milestone=self.milestone)
+        self.factory.makeBug(
+            milestone=self.milestone,
+            information_type=InformationType.USERDATA)
+        self.factory.makeBug(series=self.series)
+        self.factory.makeBug(
+            series=self.series, information_type=InformationType.USERDATA)
+
+    def test_privacy_and_counts_for_unauthenticated_user(self):
+        # An unauthenticated user should see bug counts for each status
+        # that do not include private bugs.
+        self.createBugs()
+        self.assertEqual(
+            {BugTaskStatus.NEW: 2},
+            self.get_counts(None))
+
+    def test_privacy_and_counts_for_owner(self):
+        # The owner should see bug counts for each status that do
+        # include all private bugs.
+        self.createBugs()
+        self.assertEqual(
+            {BugTaskStatus.NEW: 4},
+            self.get_counts(self.owner))
+
+    def test_privacy_and_counts_for_other_user(self):
+        # A random authenticated user should see bug counts for each
+        # status that do include all private bugs, since it is costly to
+        # query just the private bugs that the user has access to view,
+        # and this query may be run many times on a single page.
+        self.createBugs()
+        other = self.factory.makePerson()
+        self.assertEqual(
+            {BugTaskStatus.NEW: 4},
+            self.get_counts(other))
+
+    def test_multiple_statuses(self):
+        # Test that separate counts are provided for each status that
+        # bugs are found in.
+        statuses = [
+            BugTaskStatus.INVALID,
+            BugTaskStatus.OPINION,
+            ]
+        for status in statuses:
+            self.factory.makeBug(milestone=self.milestone, status=status)
+            self.factory.makeBug(series=self.series, status=status)
+        for i in range(3):
+            self.factory.makeBug(series=self.series)
+        expected = {
+            BugTaskStatus.INVALID: 2,
+            BugTaskStatus.OPINION: 2,
+            BugTaskStatus.NEW: 3,
+            }
+        self.assertEqual(expected, self.get_counts(None))
+
+    def test_incomplete_status(self):
+        # INCOMPLETE is stored as either INCOMPLETE_WITH_RESPONSE or
+        # INCOMPLETE_WITHOUT_RESPONSE so the stats do not include a count of
+        # INCOMPLETE tasks.
+        statuses = [
+            BugTaskStatusSearch.INCOMPLETE_WITH_RESPONSE,
+            BugTaskStatusSearch.INCOMPLETE_WITHOUT_RESPONSE,
+            BugTaskStatus.INCOMPLETE,
+            ]
+        for status in statuses:
+            self.factory.makeBug(series=self.series, status=status)
+        flush_database_updates()
+        expected = {
+            BugTaskStatusSearch.INCOMPLETE_WITH_RESPONSE: 1,
+            BugTaskStatusSearch.INCOMPLETE_WITHOUT_RESPONSE: 2,
+            }
+        self.assertEqual(expected, self.get_counts(None))
+
+
+class TestBugTaskMilestones(TestCaseWithFactory):
+    """Tests that appropriate milestones are returned for bugtasks."""
+
+    layer = DatabaseFunctionalLayer
+
+    def setUp(self):
+        super(TestBugTaskMilestones, self).setUp()
+        self.product = self.factory.makeProduct()
+        self.product_bug = self.factory.makeBug(product=self.product)
+        self.product_milestone = self.factory.makeMilestone(
+            product=self.product)
+        self.distribution = self.factory.makeDistribution()
+        self.distribution_bug = self.factory.makeBug(
+            distribution=self.distribution)
+        self.distribution_milestone = self.factory.makeMilestone(
+            distribution=self.distribution)
+        self.bugtaskset = getUtility(IBugTaskSet)
+
+    def test_get_target_milestones_with_one_task(self):
+        milestones = list(self.bugtaskset.getBugTaskTargetMilestones(
+            [self.product_bug.default_bugtask]))
+        self.assertEqual(
+            [self.product_milestone],
+            milestones)
+
+    def test_get_target_milestones_multiple_tasks(self):
+        tasks = [
+            self.product_bug.default_bugtask,
+            self.distribution_bug.default_bugtask,
+            ]
+        milestones = sorted(
+            self.bugtaskset.getBugTaskTargetMilestones(tasks))
+        self.assertEqual(
+            sorted([self.product_milestone, self.distribution_milestone]),
+            milestones)
+
+
 class TestConjoinedBugTasks(TestCaseWithFactory):
-    """Tests for conjoined bug task functionality."""
+    """Tests for conjoined bug task functionality.
+
+    They represent the same piece of work. The same is true for product and
+    productseries tasks, when the productseries task is targeted to the
+    IProduct.developmentfocus. The following attributes are synced:
+
+    * status
+    * assignee
+    * importance
+    * milestone
+    * sourcepackagename
+    * date_confirmed
+    * date_inprogress
+    * date_assigned
+    * date_closed
+    * date_left_new
+    * date_triaged
+    * date_fix_committed
+    * date_fix_released
+    """
 
     layer = DatabaseFunctionalLayer
 
-    def setUp(self):
+    def _setupBugData(self):
         super(TestConjoinedBugTasks, self).setUp()
-        self.owner = self.factory.makePerson()
-        self.distro = self.factory.makeDistribution(
-            name="eggs", owner=self.owner, bug_supervisor=self.owner)
-        self.distro_release = self.factory.makeDistroSeries(
-            distribution=self.distro, registrant=self.owner)
-        self.source_package = self.factory.makeSourcePackage(
-            sourcepackagename="spam", distroseries=self.distro_release)
-        self.bug = self.factory.makeBug(
-            distribution=self.distro,
-            sourcepackagename=self.source_package.sourcepackagename,
-            owner=self.owner)
-        with person_logged_in(self.owner):
-            nomination = self.bug.addNomination(
-                self.owner, self.distro_release)
-            nomination.approve(self.owner)
-            self.generic_task, self.series_task = self.bug.bugtasks
+        owner = self.factory.makePerson()
+        distro = self.factory.makeDistribution(
+            name="eggs", owner=owner, bug_supervisor=owner)
+        distro_release = self.factory.makeDistroSeries(
+            distribution=distro, registrant=owner)
+        source_package = self.factory.makeSourcePackage(
+            sourcepackagename="spam", distroseries=distro_release)
+        bug = self.factory.makeBug(
+            distribution=distro,
+            sourcepackagename=source_package.sourcepackagename,
+            owner=owner)
+        with person_logged_in(owner):
+            nomination = bug.addNomination(
+                owner, distro_release)
+            nomination.approve(owner)
+            generic_task, series_task = bug.bugtasks
+        return BugData(owner, distro, distro_release, source_package, bug,
+            generic_task, series_task)
 
     def test_editing_generic_status_reflects_upon_conjoined_master(self):
         # If a change is made to the status of a conjoined slave
         # (generic) task, that change is reflected upon the conjoined
         # master.
-        with person_logged_in(self.owner):
+        data = self._setupBugData()
+        with person_logged_in(data.owner):
             # Both the generic task and the series task start off with the
             # status of NEW.
             self.assertEqual(
-                BugTaskStatus.NEW, self.generic_task.status)
+                BugTaskStatus.NEW, data.generic_task.status)
             self.assertEqual(
-                BugTaskStatus.NEW, self.series_task.status)
+                BugTaskStatus.NEW, data.series_task.status)
             # Transitioning the generic task to CONFIRMED.
-            self.generic_task.transitionToStatus(
-                BugTaskStatus.CONFIRMED, self.owner)
+            data.generic_task.transitionToStatus(
+                BugTaskStatus.CONFIRMED, data.owner)
             # Also transitions the series_task.
             self.assertEqual(
-                BugTaskStatus.CONFIRMED, self.series_task.status)
+                BugTaskStatus.CONFIRMED, data.series_task.status)
 
     def test_editing_generic_importance_reflects_upon_conjoined_master(self):
         # If a change is made to the importance of a conjoined slave
         # (generic) task, that change is reflected upon the conjoined
         # master.
-        with person_logged_in(self.owner):
-            self.generic_task.transitionToImportance(
-                BugTaskImportance.HIGH, self.owner)
+        data = self._setupBugData()
+        with person_logged_in(data.owner):
+            data.generic_task.transitionToImportance(
+                BugTaskImportance.HIGH, data.owner)
             self.assertEqual(
-                BugTaskImportance.HIGH, self.series_task.importance)
+                BugTaskImportance.HIGH, data.series_task.importance)
 
     def test_editing_generic_assignee_reflects_upon_conjoined_master(self):
         # If a change is made to the assignee of a conjoined slave
         # (generic) task, that change is reflected upon the conjoined
         # master.
-        with person_logged_in(self.owner):
-            self.generic_task.transitionToAssignee(self.owner)
+        data = self._setupBugData()
+        with person_logged_in(data.owner):
+            data.generic_task.transitionToAssignee(data.owner)
             self.assertEqual(
-                self.owner, self.series_task.assignee)
+                data.owner, data.series_task.assignee)
 
     def test_editing_generic_package_reflects_upon_conjoined_master(self):
         # If a change is made to the source package of a conjoined slave
         # (generic) task, that change is reflected upon the conjoined
         # master.
+        data = self._setupBugData()
         source_package_name = self.factory.makeSourcePackageName("ham")
         self.factory.makeSourcePackagePublishingHistory(
-            distroseries=self.distro.currentseries,
+            distroseries=data.distro.currentseries,
             sourcepackagename=source_package_name)
-        with person_logged_in(self.owner):
-            self.generic_task.transitionToTarget(
-                self.distro.getSourcePackage(source_package_name))
-            self.assertEqual(
-                source_package_name, self.series_task.sourcepackagename)
-
-    def test_creating_conjoined_task_gets_synced_attributes(self):
-        bug = self.factory.makeBug(
-            distribution=self.distro,
-            sourcepackagename=self.source_package.sourcepackagename,
-            owner=self.owner)
-        generic_task = bug.bugtasks[0]
-        bugtaskset = getUtility(IBugTaskSet)
-        with person_logged_in(self.owner):
-            generic_task.transitionToStatus(
-                BugTaskStatus.CONFIRMED, self.owner)
-            self.assertEqual(
-                BugTaskStatus.CONFIRMED, generic_task.status)
-            slave_bugtask = bugtaskset.createTask(
-                bug, self.owner, generic_task.target.development_version)
-            self.assertEqual(
-                BugTaskStatus.CONFIRMED, generic_task.status)
-            self.assertEqual(
-                BugTaskStatus.CONFIRMED, slave_bugtask.status)
-
+        with person_logged_in(data.owner):
+            data.generic_task.transitionToTarget(
+                data.distro.getSourcePackage(source_package_name))
+            self.assertEqual(
+                source_package_name, data.series_task.sourcepackagename)
+
+    def test_conjoined_milestone(self):
+        """Milestone attribute will sync across conjoined tasks."""
+        data = self._setupBugData()
+        login('foo.bar@xxxxxxxxxxxxx')
+        launchbag = getUtility(ILaunchBag)
+        conjoined = getUtility(IProductSet)['alsa-utils']
+        con_generic_task = getUtility(IBugTaskSet).createTask(
+            data.bug, launchbag.user, conjoined)
+        con_devel_task = getUtility(IBugTaskSet).createTask(
+            data.bug, launchbag.user,
+            conjoined.getSeries("trunk"))
+
+        test_milestone = conjoined.development_focus.newMilestone("test")
+        noway_milestone = conjoined.development_focus.newMilestone("noway")
+
+        Store.of(test_milestone).flush()
+
+        self.assertIsNone(con_generic_task.milestone)
+        self.assertIsNone(con_devel_task.milestone)
+
+        con_devel_task.transitionToMilestone(
+            test_milestone, conjoined.owner)
+
+        self.assertEqual(con_generic_task.milestone.name,
+            'test')
+        self.assertEqual(con_devel_task.milestone.name,
+            'test')
+
+        # But a normal unprivileged user can't set the milestone.
+        no_priv = getUtility(IPersonSet).getByEmail('no-priv@xxxxxxxxxxxxx')
+        with ExpectedException(UserCannotEditBugTaskMilestone, ''):
+            con_devel_task.transitionToMilestone(
+                noway_milestone, no_priv)
+        self.assertEqual(con_devel_task.milestone.name,
+            'test')
+
+        con_devel_task.transitionToMilestone(
+            test_milestone, conjoined.owner)
+
+        self.assertEqual(con_generic_task.milestone.name,
+            'test')
+        self.assertEqual(con_devel_task.milestone.name,
+            'test')
+
+    def test_non_current_dev_lacks_conjoined(self):
+        """Tasks not the current dev focus lacks conjoined masters or slaves.
+        """
+        # Only owners, experts, or admins can create a series.
+        login('foo.bar@xxxxxxxxxxxxx')
+        launchbag = getUtility(ILaunchBag)
+        ubuntu = getUtility(IDistributionSet).get(1)
+        alsa_utils = getUtility(IProductSet)['alsa-utils']
+        ubuntu_netapplet = ubuntu.getSourcePackage("netapplet")
+
+        params = CreateBugParams(
+            owner=launchbag.user,
+            title="a test bug",
+            comment="test bug description")
+        ubuntu_netapplet_bug = ubuntu_netapplet.createBug(params)
+
+        alsa_utils_stable = alsa_utils.newSeries(
+            launchbag.user, 'stable', 'The stable series.')
+
+        login('test@xxxxxxxxxxxxx')
+        Store.of(alsa_utils_stable).flush()
+        self.assertNotEqual(alsa_utils.development_focus, alsa_utils_stable)
+
+        stable_netapplet_task = getUtility(IBugTaskSet).createTask(
+            ubuntu_netapplet_bug, launchbag.user, alsa_utils_stable)
+        self.assertIsNone(stable_netapplet_task.conjoined_master)
+        self.assertIsNone(stable_netapplet_task.conjoined_slave)
+
+        warty = ubuntu.getSeries('warty')
+        self.assertNotEqual(warty, ubuntu.currentseries)
+
+        warty_netapplet_task = getUtility(IBugTaskSet).createTask(
+            ubuntu_netapplet_bug, launchbag.user,
+            warty.getSourcePackage(ubuntu_netapplet.sourcepackagename))
+
+        self.assertIsNone(warty_netapplet_task.conjoined_master)
+        self.assertIsNone(warty_netapplet_task.conjoined_slave)
+
+    def test_no_conjoined_without_current_series(self):
+        """Distributions without current series lack a conjoined master/slave.
+        """
+        login('foo.bar@xxxxxxxxxxxxx')
+        launchbag = getUtility(ILaunchBag)
+        ubuntu = getUtility(IDistributionSet).get(1)
+        ubuntu_netapplet = ubuntu.getSourcePackage("netapplet")
+        params = CreateBugParams(
+            owner=launchbag.user,
+            title="a test bug",
+            comment="test bug description")
+        ubuntu_netapplet_bug = ubuntu_netapplet.createBug(params)
+
+        gentoo = getUtility(IDistributionSet).getByName('gentoo')
+        self.assertIsNone(gentoo.currentseries)
+
+        gentoo_netapplet_task = getUtility(IBugTaskSet).createTask(
+            ubuntu_netapplet_bug, launchbag.user,
+            gentoo.getSourcePackage(ubuntu_netapplet.sourcepackagename))
+        self.assertIsNone(gentoo_netapplet_task.conjoined_master)
+        self.assertIsNone(gentoo_netapplet_task.conjoined_slave)
+
+    def test_conjoined_broken_relationship(self):
+        """A conjoined relationship can be broken, though.
+
+        If the development task (i.e the conjoined master) is Won't Fix, it
+        means that the bug is deferred to the next series. In this case the
+        development task should be Won't Fix, while the generic task keeps the
+        value it had before, allowing it to stay open.
+        """
+        data = self._setupBugData()
+        login('foo.bar@xxxxxxxxxxxxx')
+        generic_netapplet_task = data.generic_task
+        current_series_netapplet_task = data.series_task
+
+        # First let's change the status from Fix Released, since it doesn't
+        # make sense to reject such a task.
+        current_series_netapplet_task.transitionToStatus(
+            BugTaskStatus.CONFIRMED, getUtility(ILaunchBag).user)
+        self.assertEqual(generic_netapplet_task.status.title,
+            'Confirmed')
+        self.assertEqual(current_series_netapplet_task.status.title,
+            'Confirmed')
+        self.assertIsNone(generic_netapplet_task.date_closed)
+        self.assertIsNone(current_series_netapplet_task.date_closed)
+
+        # Now, if we set the current series task to Won't Fix, the generic task
+        # will still be confirmed.
+        netapplet_owner = current_series_netapplet_task.pillar.owner
+        current_series_netapplet_task.transitionToStatus(
+            BugTaskStatus.WONTFIX, netapplet_owner)
+
+        self.assertEqual(generic_netapplet_task.status.title,
+            'Confirmed')
+        self.assertEqual(current_series_netapplet_task.status.title,
+            "Won't Fix")
+
+        self.assertIsNone(generic_netapplet_task.date_closed)
+        self.assertIsNotNone(current_series_netapplet_task.date_closed)
+
+        # And the bugtasks are no longer conjoined:
+        self.assertIsNone(generic_netapplet_task.conjoined_master)
+        self.assertIsNone(current_series_netapplet_task.conjoined_slave)
+
+        # If the current development release is marked as Invalid, then the
+        # bug is invalid for all future series too, and so the general bugtask
+        # is therefore Invalid also. In other words, conjoined again.
+
+        current_series_netapplet_task.transitionToStatus(
+            BugTaskStatus.NEW, getUtility(ILaunchBag).user)
+
+        # XXX Gavin Panella 2007-06-06 bug=112746:
+        # We must make two transitions.
+        current_series_netapplet_task.transitionToStatus(
+            BugTaskStatus.INVALID, getUtility(ILaunchBag).user)
+
+        self.assertEqual(generic_netapplet_task.status.title,
+            'Invalid')
+        self.assertEqual(current_series_netapplet_task.status.title,
+            'Invalid')
+
+        self.assertIsNotNone(generic_netapplet_task.date_closed)
+        self.assertIsNotNone(current_series_netapplet_task.date_closed)
+
+    def test_conjoined_tasks_sync(self):
+        """Conjoined properties are sync'd."""
+        launchbag = getUtility(ILaunchBag)
+        login('foo.bar@xxxxxxxxxxxxx')
+
+        sample_person = getUtility(IPersonSet).getByEmail('test@xxxxxxxxxxxxx')
+
+        ubuntu = getUtility(IDistributionSet).get(1)
+        params = CreateBugParams(
+            owner=launchbag.user,
+            title="a test bug",
+            comment="test bug description")
+        ubuntu_bug = ubuntu.createBug(params)
+
+        ubuntu_netapplet = ubuntu.getSourcePackage("netapplet")
+        ubuntu_netapplet_bug = ubuntu_netapplet.createBug(params)
+        generic_netapplet_task = ubuntu_netapplet_bug.bugtasks[0]
+
+        # First, we'll target the bug for the current Ubuntu series, Hoary.
+        # Note that the synced attributes are copied when the series-specific
+        # tasks are created. We'll set non-default attribute values for each
+        # generic task to demonstrate.
+        self.assertEqual('hoary', ubuntu.currentseries.name)
+
+        # Only owners, experts, or admins can create a milestone.
+        ubuntu_edgy_milestone = ubuntu.currentseries.newMilestone("knot1")
+
+        login('test@xxxxxxxxxxxxx')
+        generic_netapplet_task.transitionToStatus(
+            BugTaskStatus.INPROGRESS, getUtility(ILaunchBag).user)
+        generic_netapplet_task.transitionToAssignee(sample_person)
+        generic_netapplet_task.milestone = ubuntu_edgy_milestone
+        generic_netapplet_task.transitionToImportance(
+            BugTaskImportance.CRITICAL, ubuntu.owner)
+
+        getUtility(IBugTaskSet).createTask(ubuntu_bug, launchbag.user,
+            ubuntu.currentseries)
+        current_series_netapplet_task = getUtility(IBugTaskSet).createTask(
+            ubuntu_netapplet_bug, launchbag.user,
+            ubuntu_netapplet.development_version)
+
+        # The attributes were synced with the generic task.
+        self.assertEqual('In Progress',
+            current_series_netapplet_task.status.title)
+        self.assertEqual('Sample Person',
+            current_series_netapplet_task.assignee.displayname)
+        self.assertEqual('knot1',
+            current_series_netapplet_task.milestone.name)
+        self.assertEqual('Critical',
+            current_series_netapplet_task.importance.title)
+
+        self.assertEqual(current_series_netapplet_task.date_assigned,
+            generic_netapplet_task.date_assigned)
+        self.assertEqual(current_series_netapplet_task.date_confirmed,
+           generic_netapplet_task.date_confirmed)
+        self.assertEqual(current_series_netapplet_task.date_inprogress,
+            generic_netapplet_task.date_inprogress)
+        self.assertEqual(current_series_netapplet_task.date_closed,
+           generic_netapplet_task.date_closed)
+
+        # We'll also add some product and productseries tasks.
+        alsa_utils = getUtility(IProductSet)['alsa-utils']
+        self.assertEqual('trunk', alsa_utils.development_focus.name)
+
+        current_series_netapplet_task.transitionToStatus(
+            BugTaskStatus.FIXRELEASED, getUtility(ILaunchBag).user)
+
+        self.assertIsInstance(generic_netapplet_task.date_left_new,
+            datetime,)
+        self.assertEqual(generic_netapplet_task.date_left_new,
+            current_series_netapplet_task.date_left_new)
+
+        self.assertIsInstance(generic_netapplet_task.date_triaged,
+            datetime)
+        self.assertEqual(generic_netapplet_task.date_triaged,
+            current_series_netapplet_task.date_triaged)
+
+        self.assertIsInstance(generic_netapplet_task.date_fix_committed,
+            datetime)
+        self.assertEqual(generic_netapplet_task.date_fix_committed,
+            current_series_netapplet_task.date_fix_committed)
+
+        self.assertEqual('Fix Released', generic_netapplet_task.status.title)
+        self.assertEqual('Fix Released',
+            current_series_netapplet_task.status.title)
+
+        self.assertIsInstance(generic_netapplet_task.date_closed,
+            datetime)
+        self.assertEqual(generic_netapplet_task.date_closed,
+            current_series_netapplet_task.date_closed)
+        self.assertIsInstance(generic_netapplet_task.date_fix_released,
+            datetime)
+        self.assertEqual(generic_netapplet_task.date_fix_released,
+            current_series_netapplet_task.date_fix_released)
 
 # START TEMPORARY BIT FOR BUGTASK AUTOCONFIRM FEATURE FLAG.
 # When feature flag code is removed, delete these tests (up to "# END
 # TEMPORARY BIT FOR BUGTASK AUTOCONFIRM FEATURE FLAG.")
 
+
 class TestAutoConfirmBugTasksFlagForProduct(TestCaseWithFactory):
     """Tests for auto-confirming bug tasks."""
     # Tests for _checkAutoconfirmFeatureFlag.
@@ -2620,3 +3616,134 @@
     def test_sourcepackage(self):
         source = self.factory.makeSourcePackage()
         self.assert_userHasBugSupervisorPrivilegesContext(source)
+
+
+class TestTargetNameCache(TestCase):
+    """BugTask table has a stored computed attribute.
+
+    This targetnamecache attribute which stores a computed value to allow us
+    to sort and search on that value without having to do lots of SQL joins.
+    This cached value gets updated daily by the
+    update-bugtask-targetnamecaches cronscript and whenever the bugtask is
+    changed.  Of course, it's also computed and set when a bugtask is
+    created.
+
+    `BugTask.bugtargetdisplayname` simply returns `targetnamecache`, and
+    the latter is not exposed in `IBugTask`, so the `bugtargetdisplayname`
+    is used here.
+    """
+
+    layer = DatabaseFunctionalLayer
+
+    def test_cron_updating_targetnamecache(self):
+        """Verify the initial target name cache."""
+        login('foo.bar@xxxxxxxxxxxxx')
+        bug_one = getUtility(IBugSet).get(1)
+        mark = getUtility(IPersonSet).getByEmail('mark@xxxxxxxxxxx')
+        netapplet = getUtility(IProductSet).get(11)
+
+        upstream_task = getUtility(IBugTaskSet).createTask(
+            bug_one, mark, netapplet,
+            status=BugTaskStatus.NEW, importance=BugTaskImportance.MEDIUM)
+        self.assertEqual(upstream_task.bugtargetdisplayname,
+            u'NetApplet')
+
+        thunderbird = getUtility(IProductSet).get(8)
+        upstream_task_id = upstream_task.id
+        upstream_task.transitionToTarget(thunderbird)
+        self.assertEqual(upstream_task.bugtargetdisplayname,
+            u'Mozilla Thunderbird')
+
+        thunderbird.name = 'thunderbird-ng'
+        thunderbird.displayname = 'Mozilla Thunderbird NG'
+
+        # XXX Guilherme Salgado 2005-11-07 bug=3989:
+        # This flush_database_updates() shouldn't be needed because we
+        # already have the transaction.commit() here, but without it
+        # (flush_database_updates), the cronscript won't see the thunderbird
+        # name change.
+        flush_database_updates()
+        transaction.commit()
+
+        process = subprocess.Popen(
+            'cronscripts/update-bugtask-targetnamecaches.py', shell=True,
+            stdin=subprocess.PIPE, stdout=subprocess.PIPE,
+            stderr=subprocess.PIPE)
+        (out, err) = process.communicate()
+
+        self.assertTrue(err.startswith(("INFO    Creating lockfile: "
+            "/var/lock/launchpad-launchpad-targetnamecacheupdater.lock")))
+        self.assertTrue('INFO    Updating targetname cache of bugtasks' in err)
+        self.assertTrue('INFO    Calculating targets.' in err)
+        self.assertTrue('INFO    Will check ', err)
+        self.assertTrue("INFO    Updating (u'Mozilla Thunderbird',)" in err)
+        self.assertTrue('INFO    Updated 1 target names.' in err)
+        self.assertTrue('INFO    Finished updating targetname cache' in err)
+
+        self.assertEqual(process.returncode, 0)
+
+        # XXX Guilherme Salgado 2005-11-07:
+        # If we don't call flush_database_caches() here, we won't see the
+        # changes made by the cronscript in objects we already have cached.
+        flush_database_caches()
+        transaction.commit()
+
+        self.assertEqual(
+            getUtility(IBugTaskSet).get(upstream_task_id).bugtargetdisplayname,
+            u'Mozilla Thunderbird NG')
+
+        # With sourcepackage bugtasks that have accepted nominations to a
+        # series, additional sourcepackage bugtasks are automatically
+        # nominated to the same series. The nominations are implicitly
+        # accepted and have targetnamecache updated.
+        ubuntu = getUtility(IDistributionSet).get(1)
+
+        new_bug, new_bug_event = getUtility(IBugSet).createBugWithoutTarget(
+            CreateBugParams(mark, 'New Bug', comment='New Bug'))
+
+        # The first message of a new bug has index 0.
+        self.assertEqual(new_bug.bug_messages[0].index, 0)
+
+        getUtility(IBugTaskSet).createTask(
+            new_bug, mark, ubuntu.getSourcePackage('mozilla-firefox'))
+
+        # The first task has been created and successfully nominated to Hoary.
+        new_bug.addNomination(mark, ubuntu.currentseries).approve(mark)
+
+        task_set = [task.bugtargetdisplayname for task in new_bug.bugtasks]
+        self.assertEqual(task_set, [
+            'mozilla-firefox (Ubuntu)',
+            'mozilla-firefox (Ubuntu Hoary)',
+        ])
+
+        getUtility(IBugTaskSet).createTask(
+            new_bug, mark, ubuntu.getSourcePackage('alsa-utils'))
+
+        # The second task has been created and has also been successfully
+        # nominated to Hoary.
+
+        task_set = [task.bugtargetdisplayname for task in new_bug.bugtasks]
+        self.assertEqual(task_set, [
+            'alsa-utils (Ubuntu)',
+            'mozilla-firefox (Ubuntu)',
+            'alsa-utils (Ubuntu Hoary)',
+            'mozilla-firefox (Ubuntu Hoary)',
+        ])
+
+        # The updating of targetnamecaches is usually done by the cronjob,
+        # however it can also be invoked directly.
+        thunderbird.name = 'thunderbird'
+        thunderbird.displayname = 'Mozilla Thunderbird'
+        transaction.commit()
+
+        self.assertEqual(upstream_task.bugtargetdisplayname,
+            u'Mozilla Thunderbird NG')
+
+        logger = FakeLogger()
+        updater = BugTaskTargetNameCacheUpdater(transaction, logger)
+        updater.run()
+
+        flush_database_caches()
+        transaction.commit()
+        self.assertEqual(upstream_task.bugtargetdisplayname,
+            u'Mozilla Thunderbird')

=== modified file 'lib/lp/bugs/tests/test_bugtaskset.py'
--- lib/lp/bugs/tests/test_bugtaskset.py	2012-05-02 05:25:11 +0000
+++ lib/lp/bugs/tests/test_bugtaskset.py	2012-05-14 15:32:20 +0000
@@ -6,145 +6,99 @@
 __metaclass__ = type
 
 from zope.component import getUtility
-
 from lp.bugs.interfaces.bugtask import (
-    BugTaskStatus,
-    BugTaskStatusSearch,
     IBugTaskSet,
     )
-from lp.registry.enums import InformationType
-from lp.services.database.sqlbase import flush_database_updates
+from lp.bugs.interfaces.bug import (
+    IBugSet,
+    )
+from lp.registry.interfaces.person import IPersonSet
+from lp.registry.interfaces.product import IProductSet
+from lp.services.webapp.interfaces import ILaunchBag
 from lp.testing import (
-    login_person,
-    TestCaseWithFactory,
+    login,
+    TestCase,
     )
 from lp.testing.layers import DatabaseFunctionalLayer
 
 
-class TestStatusCountsForProductSeries(TestCaseWithFactory):
-    """Test BugTaskSet.getStatusCountsForProductSeries()."""
-
-    layer = DatabaseFunctionalLayer
-
-    def setUp(self):
-        super(TestStatusCountsForProductSeries, self).setUp()
-        self.bugtask_set = getUtility(IBugTaskSet)
-        self.owner = self.factory.makePerson()
-        login_person(self.owner)
-        self.product = self.factory.makeProduct(owner=self.owner)
-        self.series = self.factory.makeProductSeries(product=self.product)
-        self.milestone = self.factory.makeMilestone(productseries=self.series)
-
-    def get_counts(self, user):
-        return self.bugtask_set.getStatusCountsForProductSeries(
-            user, self.series)
-
-    def createBugs(self):
-        self.factory.makeBug(milestone=self.milestone)
-        self.factory.makeBug(
-            milestone=self.milestone,
-            information_type=InformationType.USERDATA)
-        self.factory.makeBug(series=self.series)
-        self.factory.makeBug(
-            series=self.series, information_type=InformationType.USERDATA)
-
-    def test_privacy_and_counts_for_unauthenticated_user(self):
-        # An unauthenticated user should see bug counts for each status
-        # that do not include private bugs.
-        self.createBugs()
-        self.assertEqual(
-            {BugTaskStatus.NEW: 2},
-            self.get_counts(None))
-
-    def test_privacy_and_counts_for_owner(self):
-        # The owner should see bug counts for each status that do
-        # include all private bugs.
-        self.createBugs()
-        self.assertEqual(
-            {BugTaskStatus.NEW: 4},
-            self.get_counts(self.owner))
-
-    def test_privacy_and_counts_for_other_user(self):
-        # A random authenticated user should see bug counts for each
-        # status that do include all private bugs, since it is costly to
-        # query just the private bugs that the user has access to view,
-        # and this query may be run many times on a single page.
-        self.createBugs()
-        other = self.factory.makePerson()
-        self.assertEqual(
-            {BugTaskStatus.NEW: 4},
-            self.get_counts(other))
-
-    def test_multiple_statuses(self):
-        # Test that separate counts are provided for each status that
-        # bugs are found in.
-        statuses = [
-            BugTaskStatus.INVALID,
-            BugTaskStatus.OPINION,
-            ]
-        for status in statuses:
-            self.factory.makeBug(milestone=self.milestone, status=status)
-            self.factory.makeBug(series=self.series, status=status)
-        for i in range(3):
-            self.factory.makeBug(series=self.series)
-        expected = {
-            BugTaskStatus.INVALID: 2,
-            BugTaskStatus.OPINION: 2,
-            BugTaskStatus.NEW: 3,
-            }
-        self.assertEqual(expected, self.get_counts(None))
-
-    def test_incomplete_status(self):
-        # INCOMPLETE is stored as either INCOMPLETE_WITH_RESPONSE or
-        # INCOMPLETE_WITHOUT_RESPONSE so the stats do not include a count of
-        # INCOMPLETE tasks.
-        statuses = [
-            BugTaskStatusSearch.INCOMPLETE_WITH_RESPONSE,
-            BugTaskStatusSearch.INCOMPLETE_WITHOUT_RESPONSE,
-            BugTaskStatus.INCOMPLETE,
-            ]
-        for status in statuses:
-            self.factory.makeBug(series=self.series, status=status)
-        flush_database_updates()
-        expected = {
-            BugTaskStatusSearch.INCOMPLETE_WITH_RESPONSE: 1,
-            BugTaskStatusSearch.INCOMPLETE_WITHOUT_RESPONSE: 2,
-            }
-        self.assertEqual(expected, self.get_counts(None))
-
-
-class TestBugTaskMilestones(TestCaseWithFactory):
-    """Tests that appropriate milestones are returned for bugtasks."""
-
-    layer = DatabaseFunctionalLayer
-
-    def setUp(self):
-        super(TestBugTaskMilestones, self).setUp()
-        self.product = self.factory.makeProduct()
-        self.product_bug = self.factory.makeBug(product=self.product)
-        self.product_milestone = self.factory.makeMilestone(
-            product=self.product)
-        self.distribution = self.factory.makeDistribution()
-        self.distribution_bug = self.factory.makeBug(
-            distribution=self.distribution)
-        self.distribution_milestone = self.factory.makeMilestone(
-            distribution=self.distribution)
-        self.bugtaskset = getUtility(IBugTaskSet)
-
-    def test_get_target_milestones_with_one_task(self):
-        milestones = list(self.bugtaskset.getBugTaskTargetMilestones(
-            [self.product_bug.default_bugtask]))
-        self.assertEqual(
-            [self.product_milestone],
-            milestones)
-
-    def test_get_target_milestones_multiple_tasks(self):
-        tasks = [
-            self.product_bug.default_bugtask,
-            self.distribution_bug.default_bugtask,
-            ]
-        milestones = sorted(
-            self.bugtaskset.getBugTaskTargetMilestones(tasks))
-        self.assertEqual(
-            sorted([self.product_milestone, self.distribution_milestone]),
-            milestones)
+def login_foobar():
+    """Helper to get the foobar logged in user"""
+    launchbag = getUtility(ILaunchBag)
+    login('foo.bar@xxxxxxxxxxxxx')
+    return launchbag.user
+
+
+class TestCountsForProducts(TestCase):
+    """Test BugTaskSet.getOpenBugTasksPerProduct"""
+
+    layer = DatabaseFunctionalLayer
+
+    def test_open_product_counts(self):
+        # IBugTaskSet.getOpenBugTasksPerProduct() will return a dictionary
+        # of product_id:count entries for bugs in an open status that
+        # the user given as a parameter is allowed to see. If a product,
+        # such as id=3 does not have any open bugs, it will not appear
+        # in the result.
+        foobar = login_foobar()
+
+        productset = getUtility(IProductSet)
+        products = [productset.get(id) for id in (3, 5, 20)]
+        sample_person = getUtility(IPersonSet).getByEmail('test@xxxxxxxxxxxxx')
+        bugtask_counts = getUtility(IBugTaskSet).getOpenBugTasksPerProduct(
+            sample_person, products)
+        res = sorted(bugtask_counts.items())
+        self.assertEqual(
+            'product_id=%d count=%d' % tuple(res[0]),
+            'product_id=5 count=1')
+        self.assertEqual(
+            'product_id=%d count=%d' % tuple(res[1]),
+            'product_id=20 count=2')
+
+        # A Launchpad admin will get a higher count for the product with id=20
+        # because he can see the private bug.
+        bugtask_counts = getUtility(IBugTaskSet).getOpenBugTasksPerProduct(
+            foobar, products)
+        res = sorted(bugtask_counts.items())
+        self.assertEqual(
+            'product_id=%d count=%d' % tuple(res[0]),
+            'product_id=5 count=1')
+        self.assertEqual(
+            'product_id=%d count=%d' % tuple(res[1]),
+            'product_id=20 count=3')
+
+        # Someone subscribed to the private bug on the product with id=20
+        # will also have it added to the count.
+        karl = getUtility(IPersonSet).getByName('karl')
+        bugtask_counts = getUtility(IBugTaskSet).getOpenBugTasksPerProduct(
+            karl, products)
+        res = sorted(bugtask_counts.items())
+        self.assertEqual(
+            'product_id=%d count=%d' % tuple(res[0]),
+            'product_id=5 count=1')
+        self.assertEqual(
+            'product_id=%d count=%d' % tuple(res[1]),
+            'product_id=20 count=3')
+
+
+class TestSortingBugTasks(TestCase):
+    """Bug tasks need to sort in a very particular order."""
+
+    layer = DatabaseFunctionalLayer
+
+    def test_sortingorder(self):
+        """We want product tasks, then ubuntu, then distro-related.
+
+        In the distro-related tasks we want a distribution-task first, then
+        distroseries-tasks for that same distribution. The distroseries tasks
+        should be sorted by distroseries version.
+        """
+        login('foo.bar@xxxxxxxxxxxxx')
+        bug_one = getUtility(IBugSet).get(1)
+        tasks = bug_one.bugtasks
+        task_names = [task.bugtargetdisplayname for task in tasks]
+        self.assertEqual(task_names, [
+            u'Mozilla Firefox',
+            'mozilla-firefox (Ubuntu)',
+            'mozilla-firefox (Debian)',
+        ])


Follow ups