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

Proposed by Richard Harding
Status: Merged
Approved by: Richard Harding
Approved revision: no longer in the source branch.
Merged at revision: 15260
Proposed branch: lp:~rharding/launchpad/diedoctests_bugtask
Merge into: lp:launchpad
Diff against target: 3085 lines (+1422/-1478)
3 files modified
lib/lp/bugs/doc/bugtask.txt (+0/-1267)
lib/lp/bugs/model/tests/test_bugtask.py (+1339/-77)
lib/lp/bugs/tests/test_bugtaskset.py (+83/-134)
To merge this branch: bzr merge lp:~rharding/launchpad/diedoctests_bugtask
Reviewer Review Type Date Requested Status
Aaron Bentley (community) Approve
Review via email: mp+105502@code.launchpad.net

Commit message

Convert doctests for bugtask into unit tests in model/test_bugtask.py and test_bugtaskset.py.

Description of the change

= 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.

To post a comment you must log in.
Revision history for this message
Aaron Bentley (abentley) wrote :

similar_bugs tests need to be reimplemented as unit tests.

review: Needs Fixing
Revision history for this message
Aaron Bentley (abentley) wrote :

You mean "do too much work", not "do to much work." Other than that, this is landable.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== removed file 'lib/lp/bugs/doc/bugtask.txt'
2--- lib/lp/bugs/doc/bugtask.txt 2012-05-10 14:50:30 +0000
3+++ lib/lp/bugs/doc/bugtask.txt 1970-01-01 00:00:00 +0000
4@@ -1,1267 +0,0 @@
5-Introduction
6-===================================
7-
8-Bugs are problems in software. When a bug gets assigned to a specific
9-upstream or distro/sourcepackagename, a bug /task/ is created. In
10-essence, a bug task is a bug that needs to be fixed in a specific
11-place. Where a bug has things like a title, comments and subscribers,
12-it's the bug task that tracks importance, assignee, etc.
13-
14-Working with Bug Tasks in Launchpad
15-===================================
16-
17-
18-Creating Bug Tasks
19-------------------
20-
21-All BugTask creation and retrieval is done through an IBugTaskSet utility.
22-
23- >>> from zope.component import getUtility
24- >>> import transaction
25- >>> from lp.bugs.interfaces.bugtask import IBugTaskSet
26- >>> bugtaskset = getUtility(IBugTaskSet)
27-
28-To create a bug task, you have to be logged in:
29-
30- >>> from lp.testing import login, ANONYMOUS
31- >>> login('foo.bar@canonical.com')
32-
33-There are three kinds of bug tasks. We need to pass the bug task creation
34-methods some other objects to create a task, so lets get the utilities we need
35-to access those other objects:
36-
37- >>> from lp.bugs.interfaces.bug import IBugSet
38- >>> from lp.registry.interfaces.distribution import IDistributionSet
39- >>> from lp.registry.interfaces.distroseries import IDistroSeriesSet
40- >>> from lp.registry.interfaces.person import IPersonSet
41- >>> from lp.registry.interfaces.product import IProductSet
42- >>> from lp.registry.interfaces.sourcepackagename import (
43- ... ISourcePackageNameSet)
44- >>> productset = getUtility(IProductSet)
45- >>> distroset = getUtility(IDistributionSet)
46- >>> distoseriesset = getUtility(IDistroSeriesSet)
47- >>> sourcepackagenameset = getUtility(ISourcePackageNameSet)
48- >>> bugset = getUtility(IBugSet)
49- >>> personset = getUtility(IPersonSet)
50- >>> bug_one = bugset.get(1)
51- >>> mark = personset.getByEmail('mark@example.com')
52-
53-Next, we need to grab some values to provide for importance and status.
54-
55- >>> from lp.bugs.interfaces.bugtask import (
56- ... BugTaskImportance,
57- ... BugTaskStatus,
58- ... )
59- >>> STATUS_NEW = BugTaskStatus.NEW
60- >>> STATUS_CONFIRMED = BugTaskStatus.CONFIRMED
61- >>> STATUS_FIXRELEASED = BugTaskStatus.FIXRELEASED
62- >>> IMPORTANCE_MEDIUM = BugTaskImportance.MEDIUM
63-
64- i. Upstream -- a bug that has to be fixed in an upstream product
65-
66- >>> evolution = productset.get(5)
67- >>> upstream_task = bugtaskset.createTask(
68- ... bug_one, mark, evolution,
69- ... status=STATUS_NEW, importance=IMPORTANCE_MEDIUM)
70- >>> upstream_task.product == evolution
71- True
72-
73- ii. Distro -- a bug that has to be fixed in a specific distro
74-
75- >>> ubuntu = distroset.get(1)
76- >>> a_distro = factory.makeDistribution(name='tubuntu')
77- >>> distro_task = bugtaskset.createTask(
78- ... bug_one, mark, a_distro,
79- ... status=STATUS_NEW, importance=IMPORTANCE_MEDIUM)
80- >>> distro_task.distribution == a_distro
81- True
82-
83- ii. Distro Series -- a bug that has to be fixed in a specific distro
84- series. These tasks are used for release management and backporting.
85-
86- >>> warty = distoseriesset.get(1)
87- >>> distro_series_task = bugtaskset.createTask(
88- ... bug_one, mark, warty,
89- ... status=STATUS_NEW, importance=IMPORTANCE_MEDIUM)
90- >>> distro_series_task.distroseries == warty
91- True
92-
93-
94-Next we verify that we can create many tasks at a time for multiple targets.
95-
96- >>> bug_many = getUtility(IBugSet).get(4)
97- >>> taskset = bugtaskset.createManyTasks(bug_many, mark,
98- ... [evolution, a_distro, warty], status=BugTaskStatus.FIXRELEASED)
99- >>> tasks = [(t.product, t.distribution, t.distroseries) for t in taskset]
100- >>> tasks.sort()
101- >>> tasks[0][2] == warty
102- True
103- >>> tasks[1][1] == a_distro
104- True
105- >>> tasks[2][0] == evolution
106- True
107-
108-# XXX: Brad Bollenbach 2005-02-24: See the bottom of this file for a chunk of
109-# test documentation that is missing from here, due to problems with resetting
110-# the connection after a ProgrammingError is raised. ARGH.
111-
112-
113-Bug Task Targets
114-----------------
115-
116- >>> from lp.registry.interfaces.distributionsourcepackage \
117- ... import IDistributionSourcePackage
118-
119-The "target" of an IBugTask can be one of the items in the following
120-list.
121-
122- * an upstream product
123-
124- >>> upstream_task.target == evolution
125- True
126-
127- * a product series
128-
129-
130- >>> firefox = productset['firefox']
131- >>> firefox_1_0 = firefox.getSeries("1.0")
132-
133- >>> productseries_task = bugtaskset.createTask(bug_one, mark, firefox_1_0)
134-
135- >>> productseries_task.target == firefox_1_0
136- True
137-
138- * a distribution
139-
140- >>> distro_task.target == a_distro
141- True
142-
143- * a distroseries
144-
145- >>> distro_series_task.target == warty
146- True
147-
148- * a distribution sourcepackage
149-
150- >>> def get_expected_target(distro_sp_task):
151- ... return distro_sp_task.target
152-
153- >>> debian_ff_task = bugtaskset.get(4)
154- >>> IDistributionSourcePackage.providedBy(debian_ff_task.target)
155- True
156- >>> target = get_expected_target(debian_ff_task)
157- >>> target.distribution.name, target.sourcepackagename.name
158- (u'debian', u'mozilla-firefox')
159-
160- >>> ubuntu_linux_task = bugtaskset.get(25)
161- >>> IDistributionSourcePackage.providedBy(ubuntu_linux_task.target)
162- True
163- >>> target = get_expected_target(ubuntu_linux_task)
164- >>> target.distribution.name, target.sourcepackagename.name
165- (u'ubuntu', u'linux-source-2.6.15')
166-
167- * a distroseries sourcepackage
168-
169- >>> from lp.registry.interfaces.sourcepackage import ISourcePackage
170- >>> from lp.registry.model.sourcepackage import SourcePackage
171- >>> distro_series_sp_task = bugtaskset.get(16)
172- >>> expected_target = SourcePackage(
173- ... distroseries=distro_series_sp_task.distroseries,
174- ... sourcepackagename=distro_series_sp_task.sourcepackagename)
175- >>> got_target = distro_series_sp_task.target
176- >>> ISourcePackage.providedBy(distro_series_sp_task.target)
177- True
178- >>> got_target.distroseries == expected_target.distroseries
179- True
180- >>> got_target.sourcepackagename == expected_target.sourcepackagename
181- True
182-
183-Each task has a "bugtargetdisplayname" and a "bugtargetname", strings
184-describing the site of the task. They concatenate the names of the
185-distribution,
186-
187- >>> bugtask = bugtaskset.get(17)
188- >>> bugtask.bugtargetdisplayname
189- u'mozilla-firefox (Ubuntu)'
190- >>> bugtask.bugtargetname
191- u'mozilla-firefox (Ubuntu)'
192-
193-distro series, or product;
194-
195- >>> bugtask = bugtaskset.get(2)
196- >>> bugtask.bugtargetdisplayname
197- u'Mozilla Firefox'
198- >>> bugtask.bugtargetname
199- u'firefox'
200-
201-the name of the source package (if any); and the name of the binary
202-package (but only if it's named differently from the source
203-package).
204-
205-
206-getPackageComponent
207-...................
208-
209-We offer a convenience method on IBugTask which allows you to look up
210-the archive component associated to the bugtask's target. Obviously, it
211-only applies to tasks that specify package information:
212-
213- >>> print upstream_task.getPackageComponent()
214- None
215- >>> print productseries_task.getPackageComponent()
216- None
217- >>> print distro_task.getPackageComponent()
218- None
219- >>> print distro_series_task.getPackageComponent()
220- None
221-
222-And it only applies to tasks whose packages which are published in
223-IDistribution.currentseries (for bugtasks on IDistributions) or the
224-bugtask's series (for bugtasks on IDistroSeries)
225-
226- >>> print debian_ff_task.getPackageComponent()
227- None
228- >>> print ubuntu_linux_task.getPackageComponent().name
229- main
230- >>> print distro_series_sp_task.getPackageComponent().name
231- main
232-
233-
234-Editing Bug Tasks
235------------------
236-
237-When changing status we must pass the user making the change. Some
238-statuses are restricted to Bug Supervisors only.
239-
240-
241-Upstream Bug Tasks
242-..................
243-
244-To edit an upstream task, you must be logged in. Anonymous users
245-cannot edit upstream tasks.
246-
247- >>> login(ANONYMOUS)
248- >>> upstream_task.transitionToStatus(
249- ... STATUS_CONFIRMED, getUtility(ILaunchBag).user)
250- Traceback (most recent call last):
251- ...
252- Unauthorized: (..., 'transitionToStatus', 'launchpad.Edit')
253-
254-Let's login and try again.
255-
256- >>> login('jeff.waugh@ubuntulinux.com')
257- >>> upstream_task.transitionToStatus(
258- ... STATUS_FIXRELEASED, getUtility(ILaunchBag).user)
259-
260-
261-Distro and Distro Series Bug Tasks
262-..................................
263-
264-Any logged-in user can edit tasks filed on distros as long as the bug
265-is not marked private. So, as an anonymous user, we cannot edit
266-anything:
267-
268- >>> login(ANONYMOUS)
269- >>> distro_task.transitionToStatus(
270- ... STATUS_FIXRELEASED, getUtility(ILaunchBag).user)
271- Traceback (most recent call last):
272- ...
273- Unauthorized: (..., 'transitionToStatus', 'launchpad.Edit')
274-
275- >>> sample_person = personset.getByEmail('test@canonical.com')
276- >>> distro_series_task.transitionToAssignee(sample_person)
277- Traceback (most recent call last):
278- ...
279- Unauthorized: (..., 'transitionToAssignee', 'launchpad.Edit')
280-
281-But once authenticated:
282-
283- >>> login('test@canonical.com')
284-
285-We can edit the task:
286-
287- >>> distro_task.transitionToStatus(
288- ... STATUS_FIXRELEASED, getUtility(ILaunchBag).user)
289- >>> distro_series_task.transitionToAssignee(sample_person)
290-
291-
292-Conjoined Bug Tasks
293-...................
294-
295-A bugtask open on the current development series for a distro is kept
296-in sync with the "generic" bugtask for that distro, because they
297-represent the same piece of work. The same is true for product and
298-productseries tasks, when the productseries task is targeted to the
299-IProduct.developmentfocus. The following attributes are synced:
300-
301- * status
302- * assignee
303- * importance
304- * milestone
305- * sourcepackagename
306- * date_confirmed
307- * date_inprogress
308- * date_assigned
309- * date_closed
310- * date_left_new
311- * date_triaged
312- * date_fix_committed
313- * date_fix_released
314-
315-We'll open a bug on just the distribution, and also a bug on a specific
316-package.
317-
318- >>> from lp.services.webapp.interfaces import ILaunchBag
319- >>> from lp.bugs.interfaces.bug import CreateBugParams
320- >>> launchbag = getUtility(ILaunchBag)
321- >>> params = CreateBugParams(
322- ... owner=launchbag.user,
323- ... title="a test bug",
324- ... comment="test bug description")
325- >>> ubuntu_netapplet = ubuntu.getSourcePackage("netapplet")
326- >>> ubuntu_netapplet_bug = ubuntu_netapplet.createBug(params)
327- >>> generic_netapplet_task = ubuntu_netapplet_bug.bugtasks[0]
328-
329- >>> ubuntu_bug = ubuntu.createBug(params)
330- >>> generic_ubuntu_task = ubuntu_bug.bugtasks[0]
331-
332-First, we'll target the bug for the current Ubuntu series, Hoary. Note
333-that the synched attributes are copied when the series-specific tasks are
334-created. We'll set non-default attribute values for each generic task to
335-demonstrate.
336-
337- >>> print ubuntu.currentseries.name
338- hoary
339-
340- # Only owners, experts, or admins can create a milestone.
341- >>> login('foo.bar@canonical.com')
342- >>> ubuntu_edgy_milestone = ubuntu.currentseries.newMilestone("knot1")
343- >>> login('test@canonical.com')
344-
345- >>> generic_netapplet_task.transitionToStatus(
346- ... BugTaskStatus.INPROGRESS, getUtility(ILaunchBag).user)
347- >>> generic_netapplet_task.transitionToAssignee(sample_person)
348- >>> generic_netapplet_task.milestone = ubuntu_edgy_milestone
349- >>> generic_netapplet_task.transitionToImportance(
350- ... BugTaskImportance.CRITICAL, ubuntu.owner)
351-
352- >>> current_series_ubuntu_task = bugtaskset.createTask(
353- ... ubuntu_bug, launchbag.user, ubuntu.currentseries)
354-
355- >>> current_series_netapplet_task = bugtaskset.createTask(
356- ... ubuntu_netapplet_bug, launchbag.user,
357- ... ubuntu_netapplet.development_version)
358-
359-(The attributes were synched with the generic task.)
360-
361- >>> print current_series_netapplet_task.status.title
362- In Progress
363- >>> print current_series_netapplet_task.assignee.displayname
364- Sample Person
365- >>> print current_series_netapplet_task.milestone.name
366- knot1
367- >>> print current_series_netapplet_task.importance.title
368- Critical
369-
370- >>> current_series_netapplet_task.date_assigned == (
371- ... generic_netapplet_task.date_assigned)
372- True
373-
374- >>> current_series_netapplet_task.date_confirmed == (
375- ... generic_netapplet_task.date_confirmed)
376- True
377-
378- >>> current_series_netapplet_task.date_inprogress == (
379- ... generic_netapplet_task.date_inprogress)
380- True
381-
382- >>> current_series_netapplet_task.date_closed == (
383- ... generic_netapplet_task.date_closed)
384- True
385-
386-We'll also add some product and productseries tasks.
387-
388- >>> alsa_utils = productset['alsa-utils']
389-
390- >>> print alsa_utils.development_focus.name
391- trunk
392-
393- >>> generic_alsa_utils_task = bugtaskset.createTask(
394- ... ubuntu_netapplet_bug, launchbag.user, alsa_utils)
395-
396- >>> devel_focus_alsa_utils_task = bugtaskset.createTask(
397- ... ubuntu_netapplet_bug, launchbag.user,
398- ... alsa_utils.getSeries("trunk"))
399-
400-A conjoined bugtask involves a master and slave in the conjoined
401-relationship. The slave is the generic product or distribution task; the
402-master is the series-specific task. These tasks are accessed through the
403-conjoined_master and conjoined_slave properties.
404-
405- >>> current_series_netapplet_task.conjoined_slave == (
406- ... generic_netapplet_task)
407- True
408- >>> current_series_netapplet_task.conjoined_master is None
409- True
410-
411- >>> generic_netapplet_task.conjoined_slave is None
412- True
413- >>> generic_netapplet_task.conjoined_master == (
414- ... current_series_netapplet_task)
415- True
416-
417- >>> current_series_ubuntu_task.conjoined_slave == (
418- ... generic_ubuntu_task)
419- True
420- >>> current_series_ubuntu_task.conjoined_master is None
421- True
422- >>> generic_ubuntu_task.conjoined_master == (
423- ... current_series_ubuntu_task)
424- True
425- >>> generic_ubuntu_task.conjoined_slave is None
426- True
427-
428- >>> devel_focus_alsa_utils_task.conjoined_slave == (
429- ... generic_alsa_utils_task)
430- True
431- >>> devel_focus_alsa_utils_task.conjoined_master is None
432- True
433-
434- >>> generic_alsa_utils_task.conjoined_slave is None
435- True
436- >>> generic_alsa_utils_task.conjoined_master == (
437- ... devel_focus_alsa_utils_task)
438- True
439-
440-A distroseries/productseries task that isn't the current development
441-focus, doesn't have any conjoined masters or slaves.
442-
443- >>> from storm.store import Store
444-
445- # Only owners, experts, or admins can create a series.
446- >>> login('foo.bar@canonical.com')
447- >>> alsa_utils_stable = alsa_utils.newSeries(
448- ... launchbag.user, 'stable', 'The stable series.')
449- >>> login('test@canonical.com')
450- >>> Store.of(alsa_utils_stable).flush()
451- >>> alsa_utils.development_focus == alsa_utils_stable
452- False
453- >>> stable_netapplet_task = bugtaskset.createTask(
454- ... ubuntu_netapplet_bug, launchbag.user, alsa_utils_stable)
455- >>> stable_netapplet_task.conjoined_master is None
456- True
457- >>> stable_netapplet_task.conjoined_slave is None
458- True
459-
460- >>> warty = ubuntu.getSeries('warty')
461- >>> warty == ubuntu.currentseries
462- False
463- >>> warty_netapplet_task = bugtaskset.createTask(
464- ... ubuntu_netapplet_bug, launchbag.user,
465- ... warty.getSourcePackage(ubuntu_netapplet.sourcepackagename))
466- >>> warty_netapplet_task.conjoined_master is None
467- True
468- >>> warty_netapplet_task.conjoined_slave is None
469- True
470-
471-If a distribution doesn't have a current series, its tasks don't have a
472-conjoined master or slave.
473-
474- >>> gentoo = getUtility(IDistributionSet).getByName('gentoo')
475- >>> gentoo.currentseries is None
476- True
477-
478- >>> gentoo_netapplet_task = bugtaskset.createTask(
479- ... ubuntu_netapplet_bug, launchbag.user,
480- ... gentoo.getSourcePackage(ubuntu_netapplet.sourcepackagename))
481- >>> gentoo_netapplet_task.conjoined_master is None
482- True
483- >>> gentoo_netapplet_task.conjoined_slave is None
484- True
485-
486-
487-Now the attributes are kept in sync. Here are examples of each:
488-
489-(Login as Foo Bar, because the milestone and importance examples
490-require extra privileges.)
491-
492- >>> login("foo.bar@canonical.com")
493-
494-1. Status
495-
496- >>> print generic_netapplet_task.status.title
497- In Progress
498- >>> print current_series_netapplet_task.status.title
499- In Progress
500- >>> generic_netapplet_task.date_closed is None
501- True
502- >>> current_series_netapplet_task.date_closed is None
503- True
504-
505- >>> current_series_netapplet_task.transitionToStatus(
506- ... BugTaskStatus.FIXRELEASED, getUtility(ILaunchBag).user)
507-
508- >>> generic_netapplet_task.date_left_new
509- datetime.datetime...
510- >>> generic_netapplet_task.date_left_new == (
511- ... current_series_netapplet_task.date_left_new)
512- True
513-
514- >>> generic_netapplet_task.date_triaged
515- datetime.datetime...
516- >>> generic_netapplet_task.date_triaged == (
517- ... current_series_netapplet_task.date_triaged)
518- True
519-
520- >>> generic_netapplet_task.date_fix_committed
521- datetime.datetime...
522- >>> generic_netapplet_task.date_fix_committed == (
523- ... current_series_netapplet_task.date_fix_committed)
524- True
525-
526- >>> print generic_netapplet_task.status.title
527- Fix Released
528- >>> print current_series_netapplet_task.status.title
529- Fix Released
530-
531- >>> generic_netapplet_task.date_closed
532- datetime.datetime...
533- >>> generic_netapplet_task.date_closed == (
534- ... current_series_netapplet_task.date_closed)
535- True
536-
537- >>> generic_netapplet_task.date_fix_released
538- datetime.datetime...
539- >>> generic_netapplet_task.date_fix_released == (
540- ... current_series_netapplet_task.date_fix_released)
541- True
542-
543-2. Assignee
544-
545- >>> no_priv = personset.getByEmail('no-priv@canonical.com')
546-
547- >>> generic_alsa_utils_task.assignee is None
548- True
549- >>> devel_focus_alsa_utils_task.assignee is None
550- True
551- >>> generic_alsa_utils_task.date_assigned is None
552- True
553- >>> devel_focus_alsa_utils_task.date_assigned is None
554- True
555-
556- >>> devel_focus_alsa_utils_task.transitionToAssignee(no_priv)
557-
558- >>> print generic_alsa_utils_task.assignee.displayname
559- No Privileges Person
560- >>> print devel_focus_alsa_utils_task.assignee.displayname
561- No Privileges Person
562-
563- >>> generic_alsa_utils_task.date_assigned
564- datetime.datetime...
565- >>> generic_alsa_utils_task.date_assigned == (
566- ... devel_focus_alsa_utils_task.date_assigned)
567- True
568-
569-3. Importance
570-
571- >>> print generic_netapplet_task.importance.title
572- Critical
573- >>> print current_series_netapplet_task.importance.title
574- Critical
575-
576- >>> current_series_netapplet_task.transitionToImportance(
577- ... BugTaskImportance.MEDIUM, ubuntu.owner)
578-
579- >>> print generic_netapplet_task.importance.title
580- Medium
581- >>> print current_series_netapplet_task.importance.title
582- Medium
583-
584-Not everyone can edit the importance, though. If an unauthorised user
585-is passed to transitionToImportance an exception is raised.
586-
587- >>> current_series_netapplet_task.transitionToImportance(
588- ... BugTaskImportance.LOW, no_priv)
589- Traceback (most recent call last):
590- ...
591- UserCannotEditBugTaskImportance:
592- User does not have sufficient permissions to edit the
593- bug task importance.
594-
595- >>> print generic_netapplet_task.importance.title
596- Medium
597-
598-4. Milestone
599-
600- >>> test_milestone = alsa_utils.development_focus.newMilestone("test")
601- >>> noway_milestone = alsa_utils.development_focus.newMilestone("noway")
602- >>> Store.of(test_milestone).flush()
603-
604- >>> generic_alsa_utils_task.milestone is None
605- True
606- >>> devel_focus_alsa_utils_task.milestone is None
607- True
608-
609- >>> devel_focus_alsa_utils_task.transitionToMilestone(
610- ... test_milestone, alsa_utils.owner)
611-
612- >>> print generic_alsa_utils_task.milestone.name
613- test
614- >>> print devel_focus_alsa_utils_task.milestone.name
615- test
616-
617-But a normal unprivileged user can't set the milestone.
618-
619- >>> devel_focus_alsa_utils_task.transitionToMilestone(
620- ... noway_milestone, no_priv)
621- Traceback (most recent call last):
622- ...
623- UserCannotEditBugTaskMilestone:
624- User does not have sufficient permissions to edit the bug
625- task milestone.
626-
627- >>> print devel_focus_alsa_utils_task.milestone.name
628- test
629-
630-5. Source package name
631-
632- >>> ubuntu_pmount = ubuntu.getSourcePackage("pmount")
633-
634- >>> print generic_netapplet_task.sourcepackagename.name
635- netapplet
636- >>> print current_series_netapplet_task.sourcepackagename.name
637- netapplet
638-
639- >>> current_series_netapplet_task.transitionToTarget(
640- ... ubuntu_pmount.development_version)
641-
642- >>> print generic_netapplet_task.sourcepackagename.name
643- pmount
644- >>> print current_series_netapplet_task.sourcepackagename.name
645- pmount
646-
647-A conjoined relationship can be broken, though. If the development
648-task (i.e the conjoined master) is Won't Fix, it means that the bug is
649-deferred to the next series. In this case the development task should
650-be Won't Fix, while the generic task keeps the value it had before,
651-allowing it to stay open.
652-
653-First let's change the status from Fix Released, since it doesn't make
654-sense to reject such a task.
655-
656- >>> current_series_netapplet_task.transitionToStatus(
657- ... BugTaskStatus.CONFIRMED, getUtility(ILaunchBag).user)
658- >>> print generic_netapplet_task.status.title
659- Confirmed
660- >>> print current_series_netapplet_task.status.title
661- Confirmed
662- >>> generic_netapplet_task.date_closed is None
663- True
664- >>> current_series_netapplet_task.date_closed is None
665- True
666-
667-
668-Now, if we set the current series task to Won't Fix, the generic task
669-will still be confirmed.
670-
671- >>> netapplet_owner = current_series_netapplet_task.pillar.owner
672-
673- >>> current_series_netapplet_task.transitionToStatus(
674- ... BugTaskStatus.WONTFIX, netapplet_owner)
675-
676- >>> print generic_netapplet_task.status.title
677- Confirmed
678- >>> print current_series_netapplet_task.status.title
679- Won't Fix
680-
681- >>> generic_netapplet_task.date_closed is None
682- True
683- >>> current_series_netapplet_task.date_closed is None
684- False
685-
686-And the bugtasks are no longer conjoined:
687-
688- >>> generic_netapplet_task.conjoined_master is None
689- True
690- >>> current_series_netapplet_task.conjoined_slave is None
691- True
692-
693-If the current development release is marked as Invalid, then the
694-bug is invalid for all future series too, and so the general bugtask
695-is therefore Invalid also. In other words, conjoined again.
696-
697- >>> current_series_netapplet_task.transitionToStatus(
698- ... BugTaskStatus.NEW, getUtility(ILaunchBag).user)
699-
700- # XXX Gavin Panella 2007-06-06 bug=112746:
701- # We must make two transitions.
702- >>> current_series_netapplet_task.transitionToStatus(
703- ... BugTaskStatus.INVALID, getUtility(ILaunchBag).user)
704-
705- >>> print generic_netapplet_task.status.title
706- Invalid
707- >>> print current_series_netapplet_task.status.title
708- Invalid
709-
710- >>> generic_netapplet_task.date_closed is None
711- False
712- >>> current_series_netapplet_task.date_closed is None
713- False
714-
715-
716-Bug Privacy
717-===========
718-
719-A bug is either private or public. Private bugs are only visible
720-(e.g. in search listings) to explicit subscribers and Launchpad
721-admins. Public bugs are visible to anyone.
722-
723- >>> from zope.event import notify
724- >>> from lazr.lifecycle.event import ObjectModifiedEvent
725-
726-
727-Privacy and Unprivileged Users
728-------------------------------
729-
730-Let's log in as the user Foo Bar (to be allowed to edit bugs):
731-
732- >>> login('foo.bar@canonical.com')
733- >>> foobar = launchbag.user
734-
735-and mark one of the Firefox bugs private. While we do this, we're also
736-going to subscribe the Ubuntu team to the bug report to help demonstrate
737-later on the interaction between privacy and teams (see the section
738-entitled _Privacy and Team Awareness_):
739-
740- >>> from lazr.lifecycle.snapshot import Snapshot
741- >>> from lp.bugs.interfaces.bug import IBug
742-
743- >>> bug_upstream_firefox_crashes = bugtaskset.get(15)
744-
745- >>> ubuntu_team = personset.getByEmail('support@ubuntu.com')
746- >>> subscription = bug_upstream_firefox_crashes.bug.subscribe(
747- ... ubuntu_team, ubuntu_team)
748-
749- >>> old_state = Snapshot(
750- ... bug_upstream_firefox_crashes.bug, providing=IBug)
751- >>> bug_upstream_firefox_crashes.bug.setPrivate(
752- ... True, getUtility(ILaunchBag).user)
753- True
754- >>> bug_set_private = ObjectModifiedEvent(
755- ... bug_upstream_firefox_crashes.bug, old_state,
756- ... ["id", "title", "private"])
757- >>> notify(bug_set_private)
758-
759- >>> from lp.services.database.sqlbase import flush_database_updates
760- >>> flush_database_updates()
761-
762-If we now login as someone who was neither implicitly nor explicitly
763-subscribed to this bug, e.g. No Privileges Person, they will not be
764-able to access or set properties of the bugtask.
765-
766- >>> login("no-priv@canonical.com")
767- >>> mr_no_privs = launchbag.user
768-
769- >>> bug_upstream_firefox_crashes.status
770- Traceback (most recent call last):
771- ...
772- Unauthorized: (..., 'status', 'launchpad.View')
773-
774- >>> bug_upstream_firefox_crashes.transitionToStatus(
775- ... BugTaskStatus.FIXCOMMITTED, getUtility(ILaunchBag).user)
776- Traceback (most recent call last):
777- ...
778- Unauthorized: (..., 'transitionToStatus', 'launchpad.Edit')
779-
780-The private bugs will be invisible to No Privileges Person in the search
781-results:
782-
783- >>> from lp.services.searchbuilder import any
784- >>> from lp.bugs.interfaces.bugtask import BugTaskSearchParams
785- >>> params = BugTaskSearchParams(
786- ... status=any(STATUS_NEW, STATUS_CONFIRMED),
787- ... orderby="id", user=mr_no_privs)
788- >>> upstream_mozilla = productset.getByName('firefox')
789- >>> bugtasks = upstream_mozilla.searchTasks(params)
790- >>> print bugtasks.count()
791- 3
792- >>> bug_ids = [bt.bug.id for bt in bugtasks]
793- >>> print sorted(bug_ids)
794- [1, 4, 5]
795-
796-We can create an access policy grant on the pillar to which the bug is
797-targeted and No Privileges Person will have access to the private bug.
798-
799- >>> from lp.registry.enums import InformationType
800- >>> from lp.registry.interfaces.accesspolicy import (
801- ... IAccessPolicyGrantSource,
802- ... IAccessPolicySource,
803- ... )
804- >>> aps = getUtility(IAccessPolicySource)
805- >>> [policy] = aps.find(
806- ... [(upstream_mozilla, InformationType.USERDATA)])
807- >>> apgs = getUtility(IAccessPolicyGrantSource)
808- >>> grant = apgs.grant([(policy, mr_no_privs, ubuntu_team)])
809- >>> bugtasks = upstream_mozilla.searchTasks(params)
810- >>> print bugtasks.count()
811- 4
812- >>> bug_ids = [bt.bug.id for bt in bugtasks]
813- >>> print sorted(bug_ids)
814- [1, 4, 5, 6]
815- >>> apgs.revoke([(policy, mr_no_privs)])
816-
817-
818-Open bugtask count for a given list of projects
819------------------------------------------------
820-
821-IBugTaskSet.getOpenBugTasksPerProduct() will return a dictionary
822-of product_id:count entries for bugs in an open status that
823-the user given as a parameter is allowed to see. If a product,
824-such as id=3 does not have any open bugs, it will not appear
825-in the result.
826-
827- >>> products = [productset.get(id) for id in (3, 5, 20)]
828- >>> bugtask_counts = bugtaskset.getOpenBugTasksPerProduct(
829- ... sample_person, products)
830- >>> for product_id, count in sorted(bugtask_counts.items()):
831- ... print 'product_id=%d count=%d' % (product_id, count)
832- product_id=5 count=1
833- product_id=20 count=2
834-
835-A Launchpad admin will get a higher count for the product with id=20
836-because he can see the private bug.
837-
838- >>> bugtask_counts = bugtaskset.getOpenBugTasksPerProduct(
839- ... foobar, products)
840- >>> for product_id, count in sorted(bugtask_counts.items()):
841- ... print 'product_id=%d count=%d' % (product_id, count)
842- product_id=5 count=1
843- product_id=20 count=3
844-
845-Someone subscribed to the private bug on the product with id=20
846-will also have it added to the count.
847-
848- >>> karl = personset.getByName('karl')
849- >>> bugtask_counts = bugtaskset.getOpenBugTasksPerProduct(
850- ... karl, products)
851- >>> for product_id, count in sorted(bugtask_counts.items()):
852- ... print 'product_id=%d count=%d' % (product_id, count)
853- product_id=5 count=1
854- product_id=20 count=3
855-
856-
857-Privacy and Priviledged Users
858------------------------------
859-
860-Now, we'll log in as Mark Shuttleworth, who was assigned to this bug
861-when it was marked private:
862-
863- >>> login("mark@example.com")
864-
865-And note that he can access and set the bugtask attributes:
866-
867- >>> bug_upstream_firefox_crashes.status.title
868- 'New'
869-
870- >>> bug_upstream_firefox_crashes.transitionToStatus(
871- ... BugTaskStatus.NEW, getUtility(ILaunchBag).user)
872-
873-
874-Privacy and Team Awareness
875---------------------------
876-
877-No Privileges Person can't see the private bug, because he's not a subscriber:
878-
879- >>> login("no-priv@canonical.com")
880- >>> params = BugTaskSearchParams(
881- ... status=any(STATUS_NEW, STATUS_CONFIRMED), user=no_priv)
882- >>> firefox_bugtasks = firefox.searchTasks(params)
883- >>> [bugtask.bug.id for bugtask in firefox_bugtasks]
884- [1, 4, 5]
885-
886-
887-But if we add No Privileges Person to the Ubuntu Team, and because the
888-Ubuntu Team *is* subscribed to the bug, No Privileges Person will see
889-the private bug.
890-
891- >>> login("mark@example.com")
892- >>> ignored = ubuntu_team.addMember(
893- ... no_priv, reviewer=ubuntu_team.teamowner)
894-
895- >>> login("no-priv@canonical.com")
896- >>> params = BugTaskSearchParams(
897- ... status=any(STATUS_NEW, STATUS_CONFIRMED), user=foobar)
898- >>> firefox_bugtasks = firefox.searchTasks(params)
899- >>> [bugtask.bug.id for bugtask in firefox_bugtasks]
900- [1, 4, 5, 6]
901-
902-
903-Privacy and Launchpad Admins
904-----------------------------
905-
906-Let's log in as Daniel Henrique Debonzi:
907-
908- >>> login("daniel.debonzi@canonical.com")
909- >>> debonzi = launchbag.user
910-
911-The same search as above yields the same result, because Daniel Debonzi is an
912-administrator.
913-
914- >>> firefox = productset.get(4)
915- >>> params = BugTaskSearchParams(status=any(STATUS_NEW,
916- ... STATUS_CONFIRMED),
917- ... user=debonzi)
918- >>> firefox_bugtasks = firefox.searchTasks(params)
919- >>> [bugtask.bug.id for bugtask in firefox_bugtasks]
920- [1, 4, 5, 6]
921-
922-Trying to retrieve the bug directly will work fine:
923-
924- >>> bug_upstream_firefox_crashes = bugtaskset.get(15)
925-
926-As will attribute access:
927-
928- >>> bug_upstream_firefox_crashes.status.title
929- 'New'
930-
931-And attribute setting:
932-
933- >>> bug_upstream_firefox_crashes.transitionToStatus(
934- ... BugTaskStatus.CONFIRMED, getUtility(ILaunchBag).user)
935- >>> bug_upstream_firefox_crashes.transitionToStatus(
936- ... BugTaskStatus.NEW, getUtility(ILaunchBag).user)
937-
938-
939-
940-Sorting Bug Tasks
941------------------
942-
943-Bug tasks need to sort in a very particular order. We want product tasks
944-first, then ubuntu tasks, then other distro-related tasks. In the
945-distro-related tasks we want a distribution-task first, then
946-distroseries-tasks for that same distribution. The distroseries tasks
947-should be sorted by distroseries version.
948-
949-Phew.
950-
951-Let's just make sure that the tasks on bug_one sort correctly.
952-
953- >>> tasks = bug_one.bugtasks
954- >>> for task in tasks:
955- ... print task.bugtargetdisplayname
956- Evolution
957- Mozilla Firefox
958- Mozilla Firefox 1.0
959- mozilla-firefox (Ubuntu)
960- Ubuntu Warty
961- mozilla-firefox (Debian)
962- Tubuntu
963-
964-
965-BugTask Adaptation
966-------------------
967-
968-An IBugTask can be adapted to an IBug.
969-
970- >>> from lp.bugs.interfaces.bug import IBug
971-
972- >>> bugtask_four = bugtaskset.get(4)
973- >>> bug = IBug(bugtask_four)
974- >>> bug.title
975- u'Firefox does not support SVG'
976-
977-
978-The targetnamecache attribute of BugTask
979-----------------------------------------
980-
981-The BugTask table has this targetnamecache attribute which stores a
982-computed value to allow us to sort and search on that value without
983-having to do lots of SQL joins. This cached value gets updated daily
984-by the update-bugtask-targetnamecaches cronscript and whenever the
985-bugtask is changed. Of course, it's also computed and set when a
986-bugtask is created.
987-
988-`BugTask.bugtargetdisplayname` simply returns `targetnamecache`, and
989-the latter is not exposed in `IBugTask`, so the `bugtargetdisplayname`
990-is used here.
991-
992- >>> netapplet = productset.get(11)
993- >>> upstream_task = bugtaskset.createTask(
994- ... bug_one, mark, netapplet,
995- ... status=STATUS_NEW, importance=IMPORTANCE_MEDIUM)
996- >>> upstream_task.bugtargetdisplayname
997- u'NetApplet'
998-
999- >>> thunderbird = productset.get(8)
1000- >>> upstream_task_id = upstream_task.id
1001- >>> upstream_task.transitionToTarget(thunderbird)
1002- >>> upstream_task.bugtargetdisplayname
1003- u'Mozilla Thunderbird'
1004-
1005- >>> thunderbird.name = 'thunderbird-ng'
1006- >>> thunderbird.displayname = 'Mozilla Thunderbird NG'
1007-
1008- # XXX Guilherme Salgado 2005-11-07 bug=3989:
1009- # This flush_database_updates() shouldn't be needed because we
1010- # already have the transaction.commit() here, but without it
1011- # (flush_database_updates), the cronscript won't see the thunderbird name
1012- # change.
1013- >>> flush_database_updates()
1014- >>> transaction.commit()
1015-
1016- >>> import subprocess
1017- >>> process = subprocess.Popen(
1018- ... 'cronscripts/update-bugtask-targetnamecaches.py', shell=True,
1019- ... stdin=subprocess.PIPE, stdout=subprocess.PIPE,
1020- ... stderr=subprocess.PIPE)
1021- >>> (out, err) = process.communicate()
1022-
1023- >>> print err
1024- INFO Creating lockfile:
1025- /var/lock/launchpad-launchpad-targetnamecacheupdater.lock
1026- INFO Updating targetname cache of bugtasks.
1027- INFO Calculating targets.
1028- INFO Will check ... targets.
1029- ...
1030- INFO Updating (u'Mozilla Thunderbird',) to 'Mozilla Thunderbird NG'.
1031- ...
1032- INFO Updated 1 target names.
1033- INFO Finished updating targetname cache of bugtasks.
1034-
1035- >>> process.returncode
1036- 0
1037-
1038- # XXX Guilherme Salgado 2005-11-07:
1039- # If we don't call flush_database_caches() here, we won't see the
1040- # changes made by the cronscript in objects we already have cached.
1041- >>> from lp.services.database.sqlbase import flush_database_caches
1042- >>> flush_database_caches()
1043- >>> transaction.commit()
1044-
1045- >>> bugtaskset.get(upstream_task_id).bugtargetdisplayname
1046- u'Mozilla Thunderbird NG'
1047-
1048-With sourcepackage bugtasks that have accepted nominations to a
1049-series, additional sourcepackage bugtasks are automatically nominated
1050-to the same series. The nominations are implicitly accepted and have
1051-targetnamecache updated.
1052-
1053- >>> new_bug, new_bug_event = bugset.createBugWithoutTarget(
1054- ... CreateBugParams(mark, 'New Bug', comment='New Bug'))
1055-
1056- The first message of a new bug has index 0.
1057- >>> new_bug.bug_messages[0].index
1058- 0
1059-
1060- >>> bugtaskset.createTask(
1061- ... new_bug, mark, ubuntu.getSourcePackage('mozilla-firefox'))
1062- <BugTask ...>
1063-
1064- >>> new_bug.addNomination(mark, ubuntu.currentseries).approve(mark)
1065-
1066-The first task has been created and successfully nominated to Hoary.
1067-
1068- >>> for task in new_bug.bugtasks:
1069- ... print task.bugtargetdisplayname
1070- mozilla-firefox (Ubuntu)
1071- mozilla-firefox (Ubuntu Hoary)
1072-
1073- >>> bugtaskset.createTask(
1074- ... new_bug, mark, ubuntu.getSourcePackage('alsa-utils'))
1075- <BugTask ...>
1076-
1077-The second task has been created and has also been successfully
1078-nominated to Hoary.
1079-
1080- >>> for task in new_bug.bugtasks:
1081- ... print task.bugtargetdisplayname
1082- alsa-utils (Ubuntu)
1083- mozilla-firefox (Ubuntu)
1084- alsa-utils (Ubuntu Hoary)
1085- mozilla-firefox (Ubuntu Hoary)
1086-
1087-The updating of targetnamecaches is usually done by the cronjob, however
1088-it can also be invoked directly.
1089-
1090- >>> thunderbird.name = 'thunderbird'
1091- >>> thunderbird.displayname = 'Mozilla Thunderbird'
1092- >>> transaction.commit()
1093-
1094- >>> upstream_task.bugtargetdisplayname
1095- u'Mozilla Thunderbird NG'
1096-
1097- >>> from lp.bugs.scripts.bugtasktargetnamecaches import (
1098- ... BugTaskTargetNameCacheUpdater)
1099- >>> from lp.services.log.logger import FakeLogger
1100- >>> logger = FakeLogger()
1101- >>> updater = BugTaskTargetNameCacheUpdater(transaction, logger)
1102- >>> updater.run()
1103- INFO Updating targetname cache of bugtasks.
1104- INFO Calculating targets.
1105- ...
1106- INFO Updating (u'Mozilla Thunderbird NG',) to 'Mozilla Thunderbird'.
1107- ...
1108- INFO Updated 1 target names.
1109- INFO Finished updating targetname cache of bugtasks.
1110-
1111- >>> flush_database_caches()
1112- >>> transaction.commit()
1113- >>> upstream_task.bugtargetdisplayname
1114- u'Mozilla Thunderbird'
1115-
1116-
1117-Target Uses Malone
1118-------------------
1119-
1120-Bug tasks have a flag, target_uses_malone, that says whether the bugtask
1121-target uses Malone as its official bugtracker.
1122-
1123- >>> for bugtask in bug_one.bugtasks:
1124- ... print "%-30s %s" % (
1125- ... bugtask.bugtargetdisplayname, bugtask.target_uses_malone)
1126- Evolution True
1127- Mozilla Firefox True
1128- Mozilla Firefox 1.0 True
1129- Mozilla Thunderbird False
1130- mozilla-firefox (Ubuntu) True
1131- Ubuntu Warty True
1132- mozilla-firefox (Debian) False
1133- Tubuntu False
1134-
1135-
1136-BugTask badges
1137---------------
1138-
1139-A bug can have certain properties, which results in a badge being
1140-displayed in bug listings. BugTaskSet has a method,
1141-getBugTaskBadgeProperties(), which calculates these properties for
1142-multiple bug tasks in one go.
1143-
1144- >>> from operator import attrgetter
1145- >>> def print_badge_properties(badge_properties):
1146- ... bugtasks = sorted(badge_properties.keys(), key=attrgetter('id'))
1147- ... for bugtask in bugtasks:
1148- ... print "Properties for bug %s:" % (bugtask.bug.id)
1149- ... for key, value in sorted(badge_properties[bugtask].items()):
1150- ... print " %s: %s" % (key, value)
1151-
1152- >>> bug_two = getUtility(IBugSet).get(2)
1153- >>> bug_three = getUtility(IBugSet).get(3)
1154- >>> some_bugtask = bug_two.bugtasks[0]
1155- >>> another_bugtask = bug_three.bugtasks[0]
1156- >>> badge_properties = getUtility(IBugTaskSet).getBugTaskBadgeProperties(
1157- ... [some_bugtask, another_bugtask])
1158- >>> print_badge_properties(badge_properties)
1159- Properties for bug 2:
1160- has_branch: False
1161- has_patch: False
1162- has_specification: False
1163- Properties for bug 3:
1164- has_branch: False
1165- has_patch: False
1166- has_specification: False
1167-
1168-..., a specification gets linked...
1169-
1170- >>> from lp.blueprints.interfaces.specification import ISpecificationSet
1171- >>> spec = getUtility(ISpecificationSet).all_specifications[0]
1172- >>> spec.linkBug(bug_two)
1173- <SpecificationBug at ...>
1174-
1175-... or a branch gets linked to the bug...
1176-
1177- >>> branch = factory.makeAnyBranch()
1178- >>> bug_three.linkBranch(branch, no_priv)
1179- <BugBranch at ...>
1180-
1181-... the properties for the bugtasks reflect this.
1182-
1183- >>> badge_properties = getUtility(IBugTaskSet).getBugTaskBadgeProperties(
1184- ... [some_bugtask, another_bugtask])
1185- >>> print_badge_properties(badge_properties)
1186- Properties for bug 2:
1187- has_branch: False
1188- has_patch: False
1189- has_specification: True
1190- Properties for bug 3:
1191- has_branch: True
1192- has_patch: False
1193- has_specification: False
1194-
1195-
1196-Bugtask Tags
1197-------------
1198-
1199-List of bugtasks often need to display related tags. Since tags are
1200-related to bugtasks via bugs, BugTaskSet has a method getBugTaskTags
1201-that can calculate the tags in one query.
1202-
1203- >>> some_bugtask.bug.tags = [u'foo', u'bar']
1204- >>> another_bugtask.bug.tags = [u'baz', u'bop']
1205- >>> tags_by_task = getUtility(IBugTaskSet).getBugTaskTags([
1206- ... some_bugtask, another_bugtask])
1207- >>> print tags_by_task
1208- {3: [u'bar', u'foo'], 6: [u'baz', u'bop']}
1209-
1210-
1211-Similar bugs
1212-------------
1213-
1214-It's possible to get a list of bugs similar to the current bug by
1215-accessing the similar_bugs property of its bug tasks.
1216-
1217- >>> new_ff_bug = factory.makeBug(product=firefox, title="Firefox")
1218- >>> ff_bugtask = new_ff_bug.bugtasks[0]
1219-
1220- >>> similar_bugs = ff_bugtask.findSimilarBugs(user=sample_person)
1221- >>> for similar_bug in sorted(similar_bugs, key=attrgetter('id')):
1222- ... print "%s: %s" % (similar_bug.id, similar_bug.title)
1223- 1: Firefox does not support SVG
1224- 5: Firefox install instructions should be complete
1225-
1226-This also works for distributions...
1227-
1228- >>> ubuntu_bugtask = factory.makeBugTask(bug=new_ff_bug, target=ubuntu)
1229- >>> similar_bugs = ubuntu_bugtask.findSimilarBugs(user=sample_person)
1230- >>> for similar_bug in sorted(similar_bugs, key=attrgetter('id')):
1231- ... print "%s: %s" % (similar_bug.id, similar_bug.title)
1232- 1: Firefox does not support SVG
1233-
1234-... and for SourcePackages.
1235-
1236- >>> a_ff_bug = factory.makeBug(product=firefox, title="a Firefox")
1237- >>> firefox_package = ubuntu.getSourcePackage('mozilla-firefox')
1238- >>> firefox_package_bugtask = factory.makeBugTask(
1239- ... bug=a_ff_bug, target=firefox_package)
1240-
1241- >>> similar_bugs = firefox_package_bugtask.findSimilarBugs(
1242- ... user=sample_person)
1243- >>> for similar_bug in sorted(similar_bugs, key=attrgetter('id')):
1244- ... print "%s: %s" % (similar_bug.id, similar_bug.title)
1245- 1: Firefox does not support SVG
1246-
1247-Private bugs won't show up in the list of similar bugs unless the user
1248-is a direct subscriber. We'll demonstrate this by creating a new bug
1249-against Firefox.
1250-
1251- >>> second_ff_bug = factory.makeBug(
1252- ... product=firefox, title="Yet another Firefox bug")
1253- >>> similar_bugs = ff_bugtask.findSimilarBugs(user=no_priv)
1254- >>> for similar_bug in sorted(similar_bugs, key=attrgetter('id')):
1255- ... print "%s: %s" % (similar_bug.id, similar_bug.title)
1256- 1: Firefox does not support SVG
1257- 5: Firefox install instructions should be complete
1258- ...Yet another Firefox bug
1259-
1260-If we mark the new bug as private, it won't appear in the similar bugs
1261-list for no_priv any more, since they're not a direct subscriber.
1262-
1263- >>> second_ff_bug.setPrivate(True, foobar)
1264- True
1265-
1266- >>> similar_bugs = ff_bugtask.findSimilarBugs(user=no_priv)
1267- >>> for similar_bug in sorted(similar_bugs, key=attrgetter('id')):
1268- ... print "%s: %s" % (similar_bug.id, similar_bug.title)
1269- 1: Firefox does not support SVG
1270- 5: Firefox install instructions should be complete
1271- ...: a Firefox
1272
1273=== modified file 'lib/lp/bugs/model/tests/test_bugtask.py'
1274--- lib/lp/bugs/model/tests/test_bugtask.py 2012-05-14 07:30:03 +0000
1275+++ lib/lp/bugs/model/tests/test_bugtask.py 2012-05-16 14:27:19 +0000
1276@@ -3,33 +3,48 @@
1277
1278 __metaclass__ = type
1279
1280-from datetime import timedelta
1281+from collections import namedtuple
1282+from datetime import (
1283+ datetime,
1284+ timedelta,
1285+ )
1286+from operator import attrgetter
1287+import subprocess
1288 import unittest
1289
1290 from lazr.lifecycle.event import ObjectModifiedEvent
1291 from lazr.lifecycle.snapshot import Snapshot
1292 from lazr.restfulclient.errors import Unauthorized
1293+from storm.store import Store
1294 from testtools.matchers import Equals
1295 from testtools.testcase import ExpectedException
1296 import transaction
1297 from zope.component import getUtility
1298 from zope.event import notify
1299 from zope.interface import providedBy
1300+from zope.security.interfaces import Unauthorized as ZopeUnAuthorized
1301 from zope.security.proxy import removeSecurityProxy
1302
1303 from lp.app.enums import ServiceUsage
1304 from lp.app.interfaces.launchpad import ILaunchpadCelebrities
1305-from lp.bugs.interfaces.bug import IBugSet
1306+from lp.blueprints.interfaces.specification import ISpecificationSet
1307+from lp.bugs.interfaces.bug import (
1308+ CreateBugParams,
1309+ IBug,
1310+ IBugSet,
1311+ )
1312 from lp.bugs.interfaces.bugtarget import IBugTarget
1313 from lp.bugs.interfaces.bugtask import (
1314 BugTaskImportance,
1315 BugTaskSearchParams,
1316 BugTaskStatus,
1317+ BugTaskStatusSearch,
1318 CannotDeleteBugtask,
1319 DB_UNRESOLVED_BUGTASK_STATUSES,
1320 IBugTaskSet,
1321 RESOLVED_BUGTASK_STATUSES,
1322 UNRESOLVED_BUGTASK_STATUSES,
1323+ UserCannotEditBugTaskMilestone,
1324 )
1325 from lp.bugs.interfaces.bugwatch import IBugWatchSet
1326 from lp.bugs.model.bug import Bug
1327@@ -46,13 +61,24 @@
1328 _build_tag_search_clause,
1329 get_bug_privacy_filter,
1330 )
1331+from lp.bugs.scripts.bugtasktargetnamecaches import (
1332+ BugTaskTargetNameCacheUpdater)
1333 from lp.bugs.tests.bug import create_old_bug
1334 from lp.hardwaredb.interfaces.hwdb import (
1335 HWBus,
1336 IHWDeviceSet,
1337 )
1338 from lp.registry.enums import InformationType
1339+from lp.registry.interfaces.accesspolicy import (
1340+ IAccessPolicyGrantSource,
1341+ IAccessPolicySource,
1342+ )
1343 from lp.registry.interfaces.distribution import IDistributionSet
1344+from lp.registry.interfaces.distributionsourcepackage import (
1345+ IDistributionSourcePackage
1346+)
1347+
1348+from lp.registry.interfaces.distroseries import IDistroSeriesSet
1349 from lp.registry.interfaces.person import (
1350 IPerson,
1351 IPersonSet,
1352@@ -60,10 +86,14 @@
1353 )
1354 from lp.registry.interfaces.product import IProductSet
1355 from lp.registry.interfaces.projectgroup import IProjectGroupSet
1356+from lp.registry.interfaces.sourcepackage import ISourcePackage
1357+from lp.registry.model.sourcepackage import SourcePackage
1358 from lp.services.database.sqlbase import (
1359 convert_storm_clause_to_string,
1360 flush_database_updates,
1361+ flush_database_caches,
1362 )
1363+from lp.services.log.logger import FakeLogger
1364 from lp.services.searchbuilder import (
1365 all,
1366 any,
1367@@ -102,6 +132,542 @@
1368 from lp.testing.matchers import HasQueryCount
1369
1370
1371+BugData = namedtuple("BugData", ['owner', 'distro', 'distro_release',
1372+'source_package', 'bug', 'generic_task', 'series_task', ])
1373+
1374+ConjoinedData = namedtuple("ConjoinedData", ['alsa_utils', 'generic_task',
1375+ 'devel_focus_task'])
1376+
1377+
1378+class TestBugTaskAdaptation(TestCase):
1379+ """Verify bugtask adaptation."""
1380+
1381+ layer = DatabaseFunctionalLayer
1382+
1383+ def test_bugtask_adaptation(self):
1384+ """An IBugTask can be adapted to an IBug"""
1385+ login('foo.bar@canonical.com')
1386+ bugtask_four = getUtility(IBugTaskSet).get(4)
1387+ bug = IBug(bugtask_four)
1388+ self.assertEqual(bug.title,
1389+ u'Firefox does not support SVG')
1390+
1391+
1392+class TestBugTaskCreation(TestCaseWithFactory):
1393+ """Test BugTaskSet creation methods."""
1394+
1395+ layer = DatabaseFunctionalLayer
1396+
1397+ def test_upstream_task(self):
1398+ """A bug that has to be fixed in an upstream product."""
1399+ bugtaskset = getUtility(IBugTaskSet)
1400+ bug_one = getUtility(IBugSet).get(1)
1401+ mark = getUtility(IPersonSet).getByEmail('mark@example.com')
1402+ evolution = getUtility(IProductSet).get(5)
1403+
1404+ upstream_task = bugtaskset.createTask(
1405+ bug_one, mark, evolution,
1406+ status=BugTaskStatus.NEW,
1407+ importance=BugTaskImportance.MEDIUM)
1408+
1409+ self.assertEqual(upstream_task.product, evolution)
1410+ self.assertEqual(upstream_task.target, evolution)
1411+
1412+ def test_distro_specific_bug(self):
1413+ """A bug that needs to be fixed in a specific distro."""
1414+ bugtaskset = getUtility(IBugTaskSet)
1415+ bug_one = getUtility(IBugSet).get(1)
1416+ mark = getUtility(IPersonSet).getByEmail('mark@example.com')
1417+
1418+ a_distro = self.factory.makeDistribution(name='tubuntu')
1419+ distro_task = bugtaskset.createTask(
1420+ bug_one, mark, a_distro,
1421+ status=BugTaskStatus.NEW,
1422+ importance=BugTaskImportance.MEDIUM)
1423+
1424+ self.assertEqual(distro_task.distribution, a_distro)
1425+ self.assertEqual(distro_task.target, a_distro)
1426+
1427+ def test_distroseries_specific_bug(self):
1428+ """A bug that needs to be fixed in a specific distro series
1429+
1430+ These tasks are used for release management and backporting
1431+ """
1432+ bugtaskset = getUtility(IBugTaskSet)
1433+ bug_one = getUtility(IBugSet).get(1)
1434+ mark = getUtility(IPersonSet).getByEmail('mark@example.com')
1435+ warty = getUtility(IDistroSeriesSet).get(1)
1436+
1437+ distro_series_task = bugtaskset.createTask(
1438+ bug_one, mark, warty,
1439+ status=BugTaskStatus.NEW, importance=BugTaskImportance.MEDIUM)
1440+
1441+ self.assertEqual(distro_series_task.distroseries, warty)
1442+ self.assertEqual(distro_series_task.target, warty)
1443+
1444+ def test_createmany_bugtasks(self):
1445+ """We can create a set of bugtasks around different targets"""
1446+ bugtaskset = getUtility(IBugTaskSet)
1447+ mark = getUtility(IPersonSet).getByEmail('mark@example.com')
1448+ evolution = getUtility(IProductSet).get(5)
1449+ warty = getUtility(IDistroSeriesSet).get(1)
1450+ bug_many = getUtility(IBugSet).get(4)
1451+
1452+ a_distro = self.factory.makeDistribution(name='tubuntu')
1453+ taskset = bugtaskset.createManyTasks(
1454+ bug_many, mark,
1455+ [evolution, a_distro, warty],
1456+ status=BugTaskStatus.FIXRELEASED)
1457+ tasks = [(t.product, t.distribution, t.distroseries) for t in taskset]
1458+ tasks.sort()
1459+
1460+ self.assertEqual(tasks[0][2], warty)
1461+ self.assertEqual(tasks[1][1], a_distro)
1462+ self.assertEqual(tasks[2][0], evolution)
1463+
1464+
1465+class TestBugTaskCreationPackageComponent(TestCaseWithFactory):
1466+ """IBugTask contains a convenience method to look up archive component
1467+
1468+ Obviously, it only applies to tasks that specify package information.
1469+ """
1470+
1471+ layer = DatabaseFunctionalLayer
1472+
1473+ def test_doesnot_apply(self):
1474+ """Tasks without package information should return None"""
1475+ login('foo.bar@canonical.com')
1476+ bug_one = getUtility(IBugSet).get(1)
1477+ bugtaskset = getUtility(IBugTaskSet)
1478+ evolution = getUtility(IProductSet).get(5)
1479+ mark = getUtility(IPersonSet).getByEmail('mark@example.com')
1480+ productset = getUtility(IProductSet)
1481+ warty = getUtility(IDistroSeriesSet).get(1)
1482+
1483+ upstream_task = bugtaskset.createTask(
1484+ bug_one, mark, evolution,
1485+ status=BugTaskStatus.NEW,
1486+ importance=BugTaskImportance.MEDIUM)
1487+ self.assertEqual(upstream_task.getPackageComponent(), None)
1488+
1489+ a_distro = self.factory.makeDistribution(name='tubuntu')
1490+ distro_task = bugtaskset.createTask(
1491+ bug_one, mark, a_distro,
1492+ status=BugTaskStatus.NEW,
1493+ importance=BugTaskImportance.MEDIUM)
1494+ self.assertEqual(distro_task.getPackageComponent(), None)
1495+
1496+ distro_series_task = bugtaskset.createTask(
1497+ bug_one, mark, warty,
1498+ status=BugTaskStatus.NEW, importance=BugTaskImportance.MEDIUM)
1499+ self.assertEqual(distro_series_task.getPackageComponent(), None)
1500+
1501+ firefox = productset['firefox']
1502+ firefox_1_0 = firefox.getSeries("1.0")
1503+ productseries_task = bugtaskset.createTask(bug_one, mark, firefox_1_0)
1504+ self.assertEqual(productseries_task.getPackageComponent(), None)
1505+
1506+ debian_ff_task = bugtaskset.get(4)
1507+ self.assertEqual(debian_ff_task.getPackageComponent(), None)
1508+
1509+ def test_does_apply(self):
1510+ """Tasks with package information return the archive component.
1511+
1512+ And it only applies to tasks whose packages which are published in
1513+ IDistribution.currentseries (for bugtasks on IDistributions) or the
1514+ bugtask's series (for bugtasks on IDistroSeries)
1515+ """
1516+ login('foo.bar@canonical.com')
1517+ bugtaskset = getUtility(IBugTaskSet)
1518+ ubuntu_linux_task = bugtaskset.get(25)
1519+ self.assertTrue(
1520+ IDistributionSourcePackage.providedBy(ubuntu_linux_task.target))
1521+ self.assertEqual(ubuntu_linux_task.getPackageComponent().name, 'main')
1522+
1523+ distro_series_sp_task = bugtaskset.get(16)
1524+ self.assertEqual(distro_series_sp_task.getPackageComponent().name,
1525+ 'main')
1526+
1527+
1528+class TestBugTaskTargets(TestCase):
1529+ """Verify we handle various bugtask targets correctly"""
1530+
1531+ layer = DatabaseFunctionalLayer
1532+
1533+ def test_bugtask_target_productseries(self):
1534+ """The 'target' of a task can be a product series"""
1535+ login('foo.bar@canonical.com')
1536+ bugtaskset = getUtility(IBugTaskSet)
1537+ productset = getUtility(IProductSet)
1538+ bug_one = getUtility(IBugSet).get(1)
1539+ mark = getUtility(IPersonSet).getByEmail('mark@example.com')
1540+
1541+ firefox = productset['firefox']
1542+ firefox_1_0 = firefox.getSeries("1.0")
1543+ productseries_task = bugtaskset.createTask(bug_one, mark, firefox_1_0)
1544+
1545+ self.assertEqual(productseries_task.target, firefox_1_0)
1546+ # getPackageComponent only applies to tasks that specify package info.
1547+ self.assertEqual(productseries_task.getPackageComponent(), None)
1548+
1549+ def test_bugtask_target_distro_sourcepackage(self):
1550+ """The 'target' of a task can be a distro sourcepackage"""
1551+ login('foo.bar@canonical.com')
1552+ bugtaskset = getUtility(IBugTaskSet)
1553+
1554+ debian_ff_task = bugtaskset.get(4)
1555+ self.assertTrue(
1556+ IDistributionSourcePackage.providedBy(debian_ff_task.target))
1557+
1558+ target = debian_ff_task.target
1559+ self.assertEqual(target.distribution.name, u'debian')
1560+ self.assertEqual(target.sourcepackagename.name, u'mozilla-firefox')
1561+
1562+ ubuntu_linux_task = bugtaskset.get(25)
1563+ self.assertTrue(
1564+ IDistributionSourcePackage.providedBy(ubuntu_linux_task.target))
1565+
1566+ target = ubuntu_linux_task.target
1567+ self.assertEqual(target.distribution.name, u'ubuntu')
1568+ self.assertEqual(target.sourcepackagename.name, u'linux-source-2.6.15')
1569+
1570+ def test_bugtask_target_distroseries_sourcepackage(self):
1571+ """The 'target' of a task can be a distroseries sourcepackage"""
1572+ login('foo.bar@canonical.com')
1573+ bugtaskset = getUtility(IBugTaskSet)
1574+ distro_series_sp_task = bugtaskset.get(16)
1575+
1576+ expected_target = SourcePackage(
1577+ distroseries=distro_series_sp_task.distroseries,
1578+ sourcepackagename=distro_series_sp_task.sourcepackagename)
1579+ got_target = distro_series_sp_task.target
1580+ self.assertTrue(
1581+ ISourcePackage.providedBy(distro_series_sp_task.target))
1582+ self.assertEqual(got_target.distroseries,
1583+ expected_target.distroseries)
1584+ self.assertEqual(got_target.sourcepackagename,
1585+ expected_target.sourcepackagename)
1586+
1587+
1588+class TestBugTaskTargetName(TestCase):
1589+ """Verify our targetdisplayname and targetname are correct."""
1590+
1591+ layer = DatabaseFunctionalLayer
1592+
1593+ def test_targetname_distribution(self):
1594+ """The distribution name will be concat'd"""
1595+ login('foo.bar@canonical.com')
1596+ bugtaskset = getUtility(IBugTaskSet)
1597+ bugtask = bugtaskset.get(17)
1598+
1599+ self.assertEqual(bugtask.bugtargetdisplayname,
1600+ u'mozilla-firefox (Ubuntu)')
1601+ self.assertEqual(bugtask.bugtargetname,
1602+ u'mozilla-firefox (Ubuntu)')
1603+
1604+ def test_targetname_series_product(self):
1605+ """The targetname for distro series/product versions will be name of
1606+ source package or binary package. """
1607+ login('foo.bar@canonical.com')
1608+ bugtaskset = getUtility(IBugTaskSet)
1609+ bugtask = bugtaskset.get(2)
1610+
1611+ self.assertEqual(bugtask.bugtargetdisplayname, u'Mozilla Firefox')
1612+ self.assertEqual(bugtask.bugtargetname, u'firefox')
1613+
1614+
1615+class TestEditingBugTask(TestCase):
1616+ """Verify out editing functionality of bugtasks."""
1617+
1618+ layer = DatabaseFunctionalLayer
1619+
1620+ def test_edit_upstream(self):
1621+ """You cannot edit upstream tasks as ANONYMOUS"""
1622+ login('foo.bar@canonical.com')
1623+ bugtaskset = getUtility(IBugTaskSet)
1624+ bug_one = getUtility(IBugSet).get(1)
1625+ mark = getUtility(IPersonSet).getByEmail('mark@example.com')
1626+
1627+ evolution = getUtility(IProductSet).get(5)
1628+ upstream_task = bugtaskset.createTask(
1629+ bug_one, mark, evolution,
1630+ status=BugTaskStatus.NEW,
1631+ importance=BugTaskImportance.MEDIUM)
1632+
1633+ # An anonymous user cannot edit the bugtask.
1634+ login(ANONYMOUS)
1635+ with ExpectedException(ZopeUnAuthorized, ''):
1636+ upstream_task.transitionToStatus(BugTaskStatus.CONFIRMED,
1637+ getUtility(ILaunchBag.user))
1638+
1639+ # A logged in user can edit the upstream bugtask.
1640+ login('jeff.waugh@ubuntulinux.com')
1641+ upstream_task.transitionToStatus(BugTaskStatus.FIXRELEASED,
1642+ getUtility(ILaunchBag).user)
1643+
1644+ def test_edit_distro_bugtasks(self):
1645+ """Any logged-in user can edit tasks filed on distros
1646+
1647+ However not if the bug is not marked private.
1648+ So, as an anonymous user, we cannot edit anything:
1649+ """
1650+ login(ANONYMOUS)
1651+
1652+ bugtaskset = getUtility(IBugTaskSet)
1653+ distro_task = bugtaskset.get(25)
1654+
1655+ # Anonymous cannot change the status.
1656+ with ExpectedException(ZopeUnAuthorized):
1657+ distro_task.transitionToStatus(BugTaskStatus.FIXRELEASED,
1658+ getUtility(ILaunchBag).user)
1659+
1660+ # Anonymous cannot change the assignee.
1661+ sample_person = getUtility(IPersonSet).getByEmail('test@canonical.com')
1662+ with ExpectedException(ZopeUnAuthorized):
1663+ distro_task.transitionToAssignee(sample_person)
1664+
1665+ login('test@canonical.com')
1666+
1667+ distro_task.transitionToStatus(BugTaskStatus.FIXRELEASED,
1668+ getUtility(ILaunchBag).user)
1669+ distro_task.transitionToAssignee(sample_person)
1670+
1671+
1672+class TestBugTaskTags(TestCase):
1673+ """List of bugtasks often need to display related tasks."""
1674+
1675+ layer = DatabaseFunctionalLayer
1676+
1677+ def test_getting_tags_from_bugs(self):
1678+ """Tags are related to bugtasks via bugs.
1679+
1680+ BugTaskSet has a method getBugTaskTags that can calculate the tags in
1681+ one query.
1682+ """
1683+ login('foo.bar@canonical.com')
1684+ bug_two = getUtility(IBugSet).get(2)
1685+ some_bugtask = bug_two.bugtasks[0]
1686+ bug_three = getUtility(IBugSet).get(3)
1687+ another_bugtask = bug_three.bugtasks[0]
1688+
1689+ some_bugtask.bug.tags = [u'foo', u'bar']
1690+ another_bugtask.bug.tags = [u'baz', u'bop']
1691+ tags_by_task = getUtility(IBugTaskSet).getBugTaskTags([
1692+ some_bugtask, another_bugtask])
1693+
1694+ self.assertEqual(
1695+ tags_by_task,
1696+ {3: [u'bar', u'foo'], 6: [u'baz', u'bop']})
1697+
1698+
1699+class TestBugTaskBadges(TestCaseWithFactory):
1700+
1701+ """Verify getBugTaskBadgeProperties"""
1702+
1703+ layer = DatabaseFunctionalLayer
1704+
1705+ def test_butask_badges_populated(self):
1706+ """getBugTaskBadgeProperties(), calcs properties for multiple tasks.
1707+
1708+ A bug can have certain properties, which results in a badge being
1709+ displayed in bug listings.
1710+ """
1711+ login('foo.bar@canonical.com')
1712+
1713+ def get_badge_properties(badge_properties):
1714+ bugtasks = sorted(badge_properties.keys(), key=attrgetter('id'))
1715+ res = []
1716+ for bugtask in bugtasks:
1717+ res.append("Properties for bug %s:" % (bugtask.bug.id))
1718+ for key, value in sorted(badge_properties[bugtask].items()):
1719+ res.append(" %s: %s" % (key, value))
1720+ return res
1721+
1722+ bug_two = getUtility(IBugSet).get(2)
1723+ some_bugtask = bug_two.bugtasks[0]
1724+ bug_three = getUtility(IBugSet).get(3)
1725+ another_bugtask = bug_three.bugtasks[0]
1726+ badge_properties = getUtility(IBugTaskSet).getBugTaskBadgeProperties(
1727+ [some_bugtask, another_bugtask])
1728+
1729+ self.assertEqual(get_badge_properties(badge_properties),
1730+ ['Properties for bug 2:',
1731+ ' has_branch: False',
1732+ ' has_patch: False',
1733+ ' has_specification: False',
1734+ 'Properties for bug 3:',
1735+ ' has_branch: False',
1736+ ' has_patch: False',
1737+ ' has_specification: False'])
1738+
1739+ # a specification gets linked...
1740+ spec = getUtility(ISpecificationSet).all_specifications[0]
1741+ spec.linkBug(bug_two)
1742+
1743+ # or a branch gets linked to the bug...
1744+ no_priv = getUtility(IPersonSet).getByEmail('no-priv@canonical.com')
1745+ branch = self.factory.makeAnyBranch()
1746+ bug_three.linkBranch(branch, no_priv)
1747+
1748+ # the properties for the bugtasks reflect this.
1749+ badge_properties = getUtility(IBugTaskSet).getBugTaskBadgeProperties(
1750+ [some_bugtask, another_bugtask])
1751+ self.assertEqual(get_badge_properties(badge_properties), [
1752+ 'Properties for bug 2:',
1753+ ' has_branch: False',
1754+ ' has_patch: False',
1755+ ' has_specification: True',
1756+ 'Properties for bug 3:',
1757+ ' has_branch: True',
1758+ ' has_patch: False',
1759+ ' has_specification: False',
1760+ ])
1761+
1762+
1763+class TestBugTaskPrivacy(TestCase):
1764+ """Verify that the bug is either private or public.
1765+
1766+ XXX: rharding 2012-05-14 bug=999298: These tests are ported from doctests
1767+ and do too much work. They should be split into simpler and better unit
1768+ tests.
1769+ """
1770+
1771+ layer = DatabaseFunctionalLayer
1772+
1773+ def test_bugtask_privacy(self):
1774+ # Let's log in as the user Foo Bar (to be allowed to edit bugs):
1775+ launchbag = getUtility(ILaunchBag)
1776+ login('foo.bar@canonical.com')
1777+ foobar = launchbag.user
1778+
1779+ # Mark one of the Firefox bugs private. While we do this, we're also
1780+ # going to subscribe the Ubuntu team to the bug report to help
1781+ # demonstrate later on the interaction between privacy and teams (see
1782+ # the section entitled _Privacy and Team Awareness_):
1783+ bug_upstream_firefox_crashes = getUtility(IBugTaskSet).get(15)
1784+
1785+ ubuntu_team = getUtility(IPersonSet).getByEmail('support@ubuntu.com')
1786+ bug_upstream_firefox_crashes.bug.subscribe(ubuntu_team, ubuntu_team)
1787+
1788+ old_state = Snapshot(bug_upstream_firefox_crashes.bug,
1789+ providing=IBug)
1790+ self.assertTrue(bug_upstream_firefox_crashes.bug.setPrivate(True,
1791+ foobar))
1792+
1793+ bug_set_private = ObjectModifiedEvent(bug_upstream_firefox_crashes.bug,
1794+ old_state,
1795+ ["id", "title", "private"])
1796+ notify(bug_set_private)
1797+ flush_database_updates()
1798+
1799+ # If we now login as someone who was neither implicitly nor explicitly
1800+ # subscribed to this bug, e.g. No Privileges Person, they will not be
1801+ # able to access or set properties of the bugtask.
1802+ launchbag = getUtility(ILaunchBag)
1803+ login("no-priv@canonical.com")
1804+ mr_no_privs = launchbag.user
1805+
1806+ with ExpectedException(ZopeUnAuthorized):
1807+ bug_upstream_firefox_crashes.status
1808+
1809+ with ExpectedException(ZopeUnAuthorized):
1810+ bug_upstream_firefox_crashes.transitionToStatus(
1811+ BugTaskStatus.FIXCOMMITTED, getUtility(ILaunchBag).user)
1812+
1813+ # The private bugs will be invisible to No Privileges Person in the
1814+ # search results:
1815+ params = BugTaskSearchParams(
1816+ status=any(BugTaskStatus.NEW, BugTaskStatus.CONFIRMED),
1817+ orderby="id", user=mr_no_privs)
1818+ upstream_mozilla = getUtility(IProductSet).getByName('firefox')
1819+ bugtasks = upstream_mozilla.searchTasks(params)
1820+ self.assertEqual(bugtasks.count(), 3)
1821+
1822+ bug_ids = [bt.bug.id for bt in bugtasks]
1823+ self.assertEqual(sorted(bug_ids), [1, 4, 5])
1824+
1825+ # We can create an access policy grant on the pillar to which the bug
1826+ # is targeted and No Privileges Person will have access to the private
1827+ # bug
1828+ aps = getUtility(IAccessPolicySource)
1829+ [policy] = aps.find([(upstream_mozilla, InformationType.USERDATA)])
1830+ apgs = getUtility(IAccessPolicyGrantSource)
1831+ apgs.grant([(policy, mr_no_privs, ubuntu_team)])
1832+ bugtasks = upstream_mozilla.searchTasks(params)
1833+ self.assertEqual(bugtasks.count(), 4)
1834+
1835+ bug_ids = [bt.bug.id for bt in bugtasks]
1836+ self.assertEqual(sorted(bug_ids), [1, 4, 5, 6])
1837+ apgs.revoke([(policy, mr_no_privs)])
1838+
1839+ # Privacy and Priviledged Users
1840+ # Now, we'll log in as Mark Shuttleworth, who was assigned to this bug
1841+ # when it was marked private:
1842+ login("mark@example.com")
1843+
1844+ # And note that he can access and set the bugtask attributes:
1845+ self.assertEqual(bug_upstream_firefox_crashes.status.title, 'New')
1846+ bug_upstream_firefox_crashes.transitionToStatus(
1847+ BugTaskStatus.NEW, getUtility(ILaunchBag).user)
1848+
1849+ # Privacy and Team Awareness
1850+ # No Privileges Person can't see the private bug, because he's not a
1851+ # subscriber:
1852+ no_priv = getUtility(IPersonSet).getByEmail('no-priv@canonical.com')
1853+ params = BugTaskSearchParams(
1854+ status=any(BugTaskStatus.NEW, BugTaskStatus.CONFIRMED),
1855+ user=no_priv)
1856+
1857+ firefox = getUtility(IProductSet)['firefox']
1858+ firefox_bugtasks = firefox.searchTasks(params)
1859+ self.assertEqual(
1860+ [bugtask.bug.id for bugtask in firefox_bugtasks], [1, 4, 5])
1861+
1862+ # But if we add No Privileges Person to the Ubuntu Team, and because
1863+ # the Ubuntu Team *is* subscribed to the bug, No Privileges Person
1864+ # will see the private bug.
1865+
1866+ login("mark@example.com")
1867+ ubuntu_team.addMember(no_priv, reviewer=ubuntu_team.teamowner)
1868+
1869+ login("no-priv@canonical.com")
1870+ params = BugTaskSearchParams(
1871+ status=any(BugTaskStatus.NEW, BugTaskStatus.CONFIRMED),
1872+ user=foobar)
1873+
1874+ firefox_bugtasks = firefox.searchTasks(params)
1875+ self.assertEqual(
1876+ [bugtask.bug.id for bugtask in firefox_bugtasks], [1, 4, 5, 6])
1877+
1878+ # Privacy and Launchpad Admins
1879+ # ----------------------------
1880+ # Let's log in as Daniel Henrique Debonzi:
1881+ launchbag = getUtility(ILaunchBag)
1882+ login("daniel.debonzi@canonical.com")
1883+ debonzi = launchbag.user
1884+
1885+ # The same search as above yields the same result, because Daniel
1886+ # Debonzi is an administrator.
1887+ firefox = getUtility(IProductSet).get(4)
1888+ params = BugTaskSearchParams(status=any(BugTaskStatus.NEW,
1889+ BugTaskStatus.CONFIRMED),
1890+ user=debonzi)
1891+ firefox_bugtasks = firefox.searchTasks(params)
1892+ self.assertEqual(
1893+ [bugtask.bug.id for bugtask in firefox_bugtasks], [1, 4, 5, 6])
1894+
1895+ # Trying to retrieve the bug directly will work fine:
1896+ bug_upstream_firefox_crashes = getUtility(IBugTaskSet).get(15)
1897+ # As will attribute access:
1898+ self.assertEqual(bug_upstream_firefox_crashes.status.title, 'New')
1899+
1900+ # And attribute setting:
1901+ bug_upstream_firefox_crashes.transitionToStatus(
1902+ BugTaskStatus.CONFIRMED, getUtility(ILaunchBag).user)
1903+ bug_upstream_firefox_crashes.transitionToStatus(
1904+ BugTaskStatus.NEW, getUtility(ILaunchBag).user)
1905+
1906+
1907 class TestBugTaskDelta(TestCaseWithFactory):
1908
1909 layer = DatabaseFunctionalLayer
1910@@ -125,8 +691,7 @@
1911 names = set(
1912 name for interface in providedBy(delta) for name in interface)
1913 for name in names:
1914- self.assertEquals(
1915- getattr(delta, name), expected_delta.get(name))
1916+ self.assertEquals(getattr(delta, name), expected_delta.get(name))
1917
1918 def test_get_bugwatch_delta(self):
1919 # Exercise getDelta() with a change to bugwatch.
1920@@ -152,17 +717,16 @@
1921 new_product = self.factory.makeProduct(owner=user)
1922 bug_task.transitionToTarget(new_product)
1923
1924- self.check_delta(
1925- bug_task_before_modification, bug_task,
1926- target=dict(old=product, new=new_product))
1927+ self.check_delta(bug_task_before_modification, bug_task,
1928+ target=dict(old=product, new=new_product))
1929
1930 def test_get_milestone_delta(self):
1931 # Exercise getDelta() with a change to milestone.
1932 user = self.factory.makePerson()
1933 product = self.factory.makeProduct(owner=user)
1934 bug_task = self.factory.makeBugTask(target=product)
1935- bug_task_before_modification = Snapshot(
1936- bug_task, providing=providedBy(bug_task))
1937+ bug_task_before_modification = Snapshot(bug_task,
1938+ providing=providedBy(bug_task))
1939
1940 milestone = self.factory.makeMilestone(product=product)
1941 bug_task.milestone = milestone
1942@@ -675,6 +1239,91 @@
1943 [bugtask.bug.id for bugtask in bugtasks])
1944
1945
1946+class TestSimilarBugs(TestCaseWithFactory):
1947+ """It's possible to get a list of similar bugs."""
1948+
1949+ layer = DatabaseFunctionalLayer
1950+
1951+ def _setupFirefoxBugTask(self):
1952+ """Helper to init the firefox bugtask bits."""
1953+ login('foo.bar@canonical.com')
1954+ firefox = getUtility(IProductSet).getByName("firefox")
1955+ new_ff_bug = self.factory.makeBug(product=firefox, title="Firefox")
1956+ ff_bugtask = new_ff_bug.bugtasks[0]
1957+ return firefox, new_ff_bug, ff_bugtask
1958+
1959+ def test_access_similar_bugs(self):
1960+ """The similar bugs property returns a list of similar bugs."""
1961+ firefox, new_ff_bug, ff_bugtask = self._setupFirefoxBugTask()
1962+ sample_person = getUtility(IPersonSet).getByEmail('test@canonical.com')
1963+ similar_bugs = ff_bugtask.findSimilarBugs(user=sample_person)
1964+ similar_bugs = sorted(similar_bugs, key=attrgetter('id'))
1965+
1966+ self.assertEqual(similar_bugs[0].id, 1)
1967+ self.assertEqual(similar_bugs[0].title, 'Firefox does not support SVG')
1968+ self.assertEqual(similar_bugs[1].id, 5)
1969+ self.assertEqual(
1970+ similar_bugs[1].title,
1971+ 'Firefox install instructions should be complete')
1972+
1973+ def test_similar_bugs_for_distribution(self):
1974+ """This also works for distributions."""
1975+ firefox, new_ff_bug, ff_bugtask = self._setupFirefoxBugTask()
1976+ sample_person = getUtility(IPersonSet).getByEmail('test@canonical.com')
1977+ ubuntu = getUtility(IDistributionSet).getByName('ubuntu')
1978+ ubuntu_bugtask = self.factory.makeBugTask(bug=new_ff_bug,
1979+ target=ubuntu)
1980+ similar_bugs = ubuntu_bugtask.findSimilarBugs(user=sample_person)
1981+ similar_bugs = sorted(similar_bugs, key=attrgetter('id'))
1982+
1983+ self.assertEqual(similar_bugs[0].id, 1)
1984+ self.assertEqual(similar_bugs[0].title, 'Firefox does not support SVG')
1985+
1986+ def test_with_sourcepackages(self):
1987+ """This also works for SourcePackages."""
1988+ firefox, new_ff_bug, ff_bugtask = self._setupFirefoxBugTask()
1989+ sample_person = getUtility(IPersonSet).getByEmail('test@canonical.com')
1990+ ubuntu = getUtility(IDistributionSet).getByName('ubuntu')
1991+ a_ff_bug = self.factory.makeBug(product=firefox, title="a Firefox")
1992+ firefox_package = ubuntu.getSourcePackage('mozilla-firefox')
1993+ firefox_package_bugtask = self.factory.makeBugTask(
1994+ bug=a_ff_bug, target=firefox_package)
1995+
1996+ similar_bugs = firefox_package_bugtask.findSimilarBugs(
1997+ user=sample_person)
1998+ similar_bugs = sorted(similar_bugs, key=attrgetter('id'))
1999+ self.assertEqual(similar_bugs[0].id, 1)
2000+ self.assertEqual(similar_bugs[0].title,
2001+ 'Firefox does not support SVG')
2002+
2003+ def test_private_bugs_do_not_show(self):
2004+ """Private bugs won't show up in the list of similar bugs.
2005+
2006+ Exception: the user is a direct subscriber. We'll demonstrate this by
2007+ creating a new bug against Firefox.
2008+ """
2009+ firefox, new_ff_bug, ff_bugtask = self._setupFirefoxBugTask()
2010+ second_ff_bug = self.factory.makeBug(
2011+ product=firefox, title="Yet another Firefox bug")
2012+ no_priv = getUtility(IPersonSet).getByEmail('no-priv@canonical.com')
2013+ similar_bugs = ff_bugtask.findSimilarBugs(user=no_priv)
2014+ similar_bugs = sorted(similar_bugs, key=attrgetter('id'))
2015+
2016+ self.assertEqual(len(similar_bugs), 3)
2017+
2018+ # If we mark the new bug as private, it won't appear in the similar
2019+ # bugs list for no_priv any more, since they're not a direct
2020+ # subscriber.
2021+ launchbag = getUtility(ILaunchBag)
2022+ login('foo.bar@canonical.com')
2023+ foobar = launchbag.user
2024+ second_ff_bug.setPrivate(True, foobar)
2025+ similar_bugs = ff_bugtask.findSimilarBugs(user=no_priv)
2026+ similar_bugs = sorted(similar_bugs, key=attrgetter('id'))
2027+
2028+ self.assertEqual(len(similar_bugs), 2)
2029+
2030+
2031 class TestBugTaskPermissionsToSetAssigneeMixin:
2032
2033 layer = DatabaseFunctionalLayer
2034@@ -1116,9 +1765,8 @@
2035 # Mark an upstream task on bug #1 "Fix Released"
2036 bug_one = bugset.get(1)
2037 firefox_upstream = self._getBugTaskByTarget(bug_one, firefox)
2038- self.assertEqual(
2039- ServiceUsage.LAUNCHPAD,
2040- firefox_upstream.product.bug_tracking_usage)
2041+ self.assertEqual(ServiceUsage.LAUNCHPAD,
2042+ firefox_upstream.product.bug_tracking_usage)
2043 self.old_firefox_status = firefox_upstream.status
2044 firefox_upstream.transitionToStatus(
2045 BugTaskStatus.FIXRELEASED, getUtility(ILaunchBag).user)
2046@@ -1128,8 +1776,8 @@
2047 bug_nine = bugset.get(9)
2048 thunderbird_upstream = self._getBugTaskByTarget(bug_nine, thunderbird)
2049 self.old_thunderbird_status = thunderbird_upstream.status
2050- thunderbird_upstream.transitionToStatus(
2051- BugTaskStatus.FIXCOMMITTED, getUtility(ILaunchBag).user)
2052+ thunderbird_upstream.transitionToStatus(BugTaskStatus.FIXCOMMITTED,
2053+ getUtility(ILaunchBag).user)
2054 self.thunderbird_upstream = thunderbird_upstream
2055
2056 # Add a watch to a Debian bug for bug #2, and mark the task Fix
2057@@ -1145,8 +1793,8 @@
2058 # Associate the watch to a Fix Released task.
2059 debian = getUtility(IDistributionSet).getByName("debian")
2060 debian_firefox = debian.getSourcePackage("mozilla-firefox")
2061- bug_two_in_debian_firefox = self._getBugTaskByTarget(
2062- bug_two, debian_firefox)
2063+ bug_two_in_debian_firefox = self._getBugTaskByTarget(bug_two,
2064+ debian_firefox)
2065 bug_two_in_debian_firefox.bugwatch = watch_debbugs_327452
2066 bug_two_in_debian_firefox.transitionToStatus(
2067 BugTaskStatus.FIXRELEASED, getUtility(ILaunchBag).user)
2068@@ -1512,105 +2160,585 @@
2069 self.assertEqual(bugtask, bug.default_bugtask)
2070
2071
2072+class TestStatusCountsForProductSeries(TestCaseWithFactory):
2073+ """Test BugTaskSet.getStatusCountsForProductSeries()."""
2074+
2075+ layer = DatabaseFunctionalLayer
2076+
2077+ def setUp(self):
2078+ super(TestStatusCountsForProductSeries, self).setUp()
2079+ self.bugtask_set = getUtility(IBugTaskSet)
2080+ self.owner = self.factory.makePerson()
2081+ login_person(self.owner)
2082+ self.product = self.factory.makeProduct(owner=self.owner)
2083+ self.series = self.factory.makeProductSeries(product=self.product)
2084+ self.milestone = self.factory.makeMilestone(productseries=self.series)
2085+
2086+ def get_counts(self, user):
2087+ return self.bugtask_set.getStatusCountsForProductSeries(
2088+ user, self.series)
2089+
2090+ def createBugs(self):
2091+ self.factory.makeBug(milestone=self.milestone)
2092+ self.factory.makeBug(
2093+ milestone=self.milestone,
2094+ information_type=InformationType.USERDATA)
2095+ self.factory.makeBug(series=self.series)
2096+ self.factory.makeBug(
2097+ series=self.series, information_type=InformationType.USERDATA)
2098+
2099+ def test_privacy_and_counts_for_unauthenticated_user(self):
2100+ # An unauthenticated user should see bug counts for each status
2101+ # that do not include private bugs.
2102+ self.createBugs()
2103+ self.assertEqual(
2104+ {BugTaskStatus.NEW: 2},
2105+ self.get_counts(None))
2106+
2107+ def test_privacy_and_counts_for_owner(self):
2108+ # The owner should see bug counts for each status that do
2109+ # include all private bugs.
2110+ self.createBugs()
2111+ self.assertEqual(
2112+ {BugTaskStatus.NEW: 4},
2113+ self.get_counts(self.owner))
2114+
2115+ def test_privacy_and_counts_for_other_user(self):
2116+ # A random authenticated user should see bug counts for each
2117+ # status that do include all private bugs, since it is costly to
2118+ # query just the private bugs that the user has access to view,
2119+ # and this query may be run many times on a single page.
2120+ self.createBugs()
2121+ other = self.factory.makePerson()
2122+ self.assertEqual(
2123+ {BugTaskStatus.NEW: 4},
2124+ self.get_counts(other))
2125+
2126+ def test_multiple_statuses(self):
2127+ # Test that separate counts are provided for each status that
2128+ # bugs are found in.
2129+ statuses = [
2130+ BugTaskStatus.INVALID,
2131+ BugTaskStatus.OPINION,
2132+ ]
2133+ for status in statuses:
2134+ self.factory.makeBug(milestone=self.milestone, status=status)
2135+ self.factory.makeBug(series=self.series, status=status)
2136+ for i in range(3):
2137+ self.factory.makeBug(series=self.series)
2138+ expected = {
2139+ BugTaskStatus.INVALID: 2,
2140+ BugTaskStatus.OPINION: 2,
2141+ BugTaskStatus.NEW: 3,
2142+ }
2143+ self.assertEqual(expected, self.get_counts(None))
2144+
2145+ def test_incomplete_status(self):
2146+ # INCOMPLETE is stored as either INCOMPLETE_WITH_RESPONSE or
2147+ # INCOMPLETE_WITHOUT_RESPONSE so the stats do not include a count of
2148+ # INCOMPLETE tasks.
2149+ statuses = [
2150+ BugTaskStatusSearch.INCOMPLETE_WITH_RESPONSE,
2151+ BugTaskStatusSearch.INCOMPLETE_WITHOUT_RESPONSE,
2152+ BugTaskStatus.INCOMPLETE,
2153+ ]
2154+ for status in statuses:
2155+ self.factory.makeBug(series=self.series, status=status)
2156+ flush_database_updates()
2157+ expected = {
2158+ BugTaskStatusSearch.INCOMPLETE_WITH_RESPONSE: 1,
2159+ BugTaskStatusSearch.INCOMPLETE_WITHOUT_RESPONSE: 2,
2160+ }
2161+ self.assertEqual(expected, self.get_counts(None))
2162+
2163+
2164+class TestStatusCountsForProductSeries(TestCaseWithFactory):
2165+ """Test BugTaskSet.getStatusCountsForProductSeries()."""
2166+
2167+ layer = DatabaseFunctionalLayer
2168+
2169+ def setUp(self):
2170+ super(TestStatusCountsForProductSeries, self).setUp()
2171+ self.bugtask_set = getUtility(IBugTaskSet)
2172+ self.owner = self.factory.makePerson()
2173+ login_person(self.owner)
2174+ self.product = self.factory.makeProduct(owner=self.owner)
2175+ self.series = self.factory.makeProductSeries(product=self.product)
2176+ self.milestone = self.factory.makeMilestone(productseries=self.series)
2177+
2178+ def get_counts(self, user):
2179+ return self.bugtask_set.getStatusCountsForProductSeries(
2180+ user, self.series)
2181+
2182+ def createBugs(self):
2183+ self.factory.makeBug(milestone=self.milestone)
2184+ self.factory.makeBug(
2185+ milestone=self.milestone,
2186+ information_type=InformationType.USERDATA)
2187+ self.factory.makeBug(series=self.series)
2188+ self.factory.makeBug(
2189+ series=self.series, information_type=InformationType.USERDATA)
2190+
2191+ def test_privacy_and_counts_for_unauthenticated_user(self):
2192+ # An unauthenticated user should see bug counts for each status
2193+ # that do not include private bugs.
2194+ self.createBugs()
2195+ self.assertEqual(
2196+ {BugTaskStatus.NEW: 2},
2197+ self.get_counts(None))
2198+
2199+ def test_privacy_and_counts_for_owner(self):
2200+ # The owner should see bug counts for each status that do
2201+ # include all private bugs.
2202+ self.createBugs()
2203+ self.assertEqual(
2204+ {BugTaskStatus.NEW: 4},
2205+ self.get_counts(self.owner))
2206+
2207+ def test_privacy_and_counts_for_other_user(self):
2208+ # A random authenticated user should see bug counts for each
2209+ # status that do include all private bugs, since it is costly to
2210+ # query just the private bugs that the user has access to view,
2211+ # and this query may be run many times on a single page.
2212+ self.createBugs()
2213+ other = self.factory.makePerson()
2214+ self.assertEqual(
2215+ {BugTaskStatus.NEW: 4},
2216+ self.get_counts(other))
2217+
2218+ def test_multiple_statuses(self):
2219+ # Test that separate counts are provided for each status that
2220+ # bugs are found in.
2221+ statuses = [
2222+ BugTaskStatus.INVALID,
2223+ BugTaskStatus.OPINION,
2224+ ]
2225+ for status in statuses:
2226+ self.factory.makeBug(milestone=self.milestone, status=status)
2227+ self.factory.makeBug(series=self.series, status=status)
2228+ for i in range(3):
2229+ self.factory.makeBug(series=self.series)
2230+ expected = {
2231+ BugTaskStatus.INVALID: 2,
2232+ BugTaskStatus.OPINION: 2,
2233+ BugTaskStatus.NEW: 3,
2234+ }
2235+ self.assertEqual(expected, self.get_counts(None))
2236+
2237+ def test_incomplete_status(self):
2238+ # INCOMPLETE is stored as either INCOMPLETE_WITH_RESPONSE or
2239+ # INCOMPLETE_WITHOUT_RESPONSE so the stats do not include a count of
2240+ # INCOMPLETE tasks.
2241+ statuses = [
2242+ BugTaskStatusSearch.INCOMPLETE_WITH_RESPONSE,
2243+ BugTaskStatusSearch.INCOMPLETE_WITHOUT_RESPONSE,
2244+ BugTaskStatus.INCOMPLETE,
2245+ ]
2246+ for status in statuses:
2247+ self.factory.makeBug(series=self.series, status=status)
2248+ flush_database_updates()
2249+ expected = {
2250+ BugTaskStatusSearch.INCOMPLETE_WITH_RESPONSE: 1,
2251+ BugTaskStatusSearch.INCOMPLETE_WITHOUT_RESPONSE: 2,
2252+ }
2253+ self.assertEqual(expected, self.get_counts(None))
2254+
2255+
2256+class TestBugTaskMilestones(TestCaseWithFactory):
2257+ """Tests that appropriate milestones are returned for bugtasks."""
2258+
2259+ layer = DatabaseFunctionalLayer
2260+
2261+ def setUp(self):
2262+ super(TestBugTaskMilestones, self).setUp()
2263+ self.product = self.factory.makeProduct()
2264+ self.product_bug = self.factory.makeBug(product=self.product)
2265+ self.product_milestone = self.factory.makeMilestone(
2266+ product=self.product)
2267+ self.distribution = self.factory.makeDistribution()
2268+ self.distribution_bug = self.factory.makeBug(
2269+ distribution=self.distribution)
2270+ self.distribution_milestone = self.factory.makeMilestone(
2271+ distribution=self.distribution)
2272+ self.bugtaskset = getUtility(IBugTaskSet)
2273+
2274+ def test_get_target_milestones_with_one_task(self):
2275+ milestones = list(self.bugtaskset.getBugTaskTargetMilestones(
2276+ [self.product_bug.default_bugtask]))
2277+ self.assertEqual(
2278+ [self.product_milestone],
2279+ milestones)
2280+
2281+ def test_get_target_milestones_multiple_tasks(self):
2282+ tasks = [
2283+ self.product_bug.default_bugtask,
2284+ self.distribution_bug.default_bugtask,
2285+ ]
2286+ milestones = sorted(
2287+ self.bugtaskset.getBugTaskTargetMilestones(tasks))
2288+ self.assertEqual(
2289+ sorted([self.product_milestone, self.distribution_milestone]),
2290+ milestones)
2291+
2292+
2293 class TestConjoinedBugTasks(TestCaseWithFactory):
2294- """Tests for conjoined bug task functionality."""
2295+ """Tests for conjoined bug task functionality.
2296+
2297+ They represent the same piece of work. The same is true for product and
2298+ productseries tasks, when the productseries task is targeted to the
2299+ IProduct.developmentfocus. The following attributes are synced:
2300+
2301+ * status
2302+ * assignee
2303+ * importance
2304+ * milestone
2305+ * sourcepackagename
2306+ * date_confirmed
2307+ * date_inprogress
2308+ * date_assigned
2309+ * date_closed
2310+ * date_left_new
2311+ * date_triaged
2312+ * date_fix_committed
2313+ * date_fix_released
2314+
2315+
2316+ XXX: rharding 2012-05-14 bug=999298: These tests are ported from doctests
2317+ and do too much work. They should be split into simpler and better unit
2318+ tests.
2319+ """
2320
2321 layer = DatabaseFunctionalLayer
2322
2323- def setUp(self):
2324+ def _setupBugData(self):
2325 super(TestConjoinedBugTasks, self).setUp()
2326- self.owner = self.factory.makePerson()
2327- self.distro = self.factory.makeDistribution(
2328- name="eggs", owner=self.owner, bug_supervisor=self.owner)
2329- self.distro_release = self.factory.makeDistroSeries(
2330- distribution=self.distro, registrant=self.owner)
2331- self.source_package = self.factory.makeSourcePackage(
2332- sourcepackagename="spam", distroseries=self.distro_release)
2333- self.bug = self.factory.makeBug(
2334- distribution=self.distro,
2335- sourcepackagename=self.source_package.sourcepackagename,
2336- owner=self.owner)
2337- with person_logged_in(self.owner):
2338- nomination = self.bug.addNomination(
2339- self.owner, self.distro_release)
2340- nomination.approve(self.owner)
2341- self.generic_task, self.series_task = self.bug.bugtasks
2342+ owner = self.factory.makePerson()
2343+ distro = self.factory.makeDistribution(name="eggs", owner=owner,
2344+ bug_supervisor=owner)
2345+ distro_release = self.factory.makeDistroSeries(distribution=distro,
2346+ registrant=owner)
2347+ source_package = self.factory.makeSourcePackage(
2348+ sourcepackagename="spam", distroseries=distro_release)
2349+ bug = self.factory.makeBug(
2350+ distribution=distro,
2351+ sourcepackagename=source_package.sourcepackagename,
2352+ owner=owner)
2353+ with person_logged_in(owner):
2354+ nomination = bug.addNomination(owner, distro_release)
2355+ nomination.approve(owner)
2356+ generic_task, series_task = bug.bugtasks
2357+ return BugData(owner, distro, distro_release, source_package, bug,
2358+ generic_task, series_task)
2359
2360 def test_editing_generic_status_reflects_upon_conjoined_master(self):
2361 # If a change is made to the status of a conjoined slave
2362 # (generic) task, that change is reflected upon the conjoined
2363 # master.
2364- with person_logged_in(self.owner):
2365+ data = self._setupBugData()
2366+ with person_logged_in(data.owner):
2367 # Both the generic task and the series task start off with the
2368 # status of NEW.
2369- self.assertEqual(
2370- BugTaskStatus.NEW, self.generic_task.status)
2371- self.assertEqual(
2372- BugTaskStatus.NEW, self.series_task.status)
2373+ self.assertEqual(BugTaskStatus.NEW,
2374+ data.generic_task.status)
2375+ self.assertEqual(BugTaskStatus.NEW,
2376+ data.series_task.status)
2377 # Transitioning the generic task to CONFIRMED.
2378- self.generic_task.transitionToStatus(
2379- BugTaskStatus.CONFIRMED, self.owner)
2380+ data.generic_task.transitionToStatus(BugTaskStatus.CONFIRMED,
2381+ data.owner)
2382 # Also transitions the series_task.
2383- self.assertEqual(
2384- BugTaskStatus.CONFIRMED, self.series_task.status)
2385+ self.assertEqual(BugTaskStatus.CONFIRMED,
2386+ data.series_task.status)
2387
2388 def test_editing_generic_importance_reflects_upon_conjoined_master(self):
2389 # If a change is made to the importance of a conjoined slave
2390 # (generic) task, that change is reflected upon the conjoined
2391 # master.
2392- with person_logged_in(self.owner):
2393- self.generic_task.transitionToImportance(
2394- BugTaskImportance.HIGH, self.owner)
2395- self.assertEqual(
2396- BugTaskImportance.HIGH, self.series_task.importance)
2397+ data = self._setupBugData()
2398+ with person_logged_in(data.owner):
2399+ data.generic_task.transitionToImportance(BugTaskImportance.HIGH,
2400+ data.owner)
2401+ self.assertEqual(BugTaskImportance.HIGH,
2402+ data.series_task.importance)
2403
2404 def test_editing_generic_assignee_reflects_upon_conjoined_master(self):
2405 # If a change is made to the assignee of a conjoined slave
2406 # (generic) task, that change is reflected upon the conjoined
2407 # master.
2408- with person_logged_in(self.owner):
2409- self.generic_task.transitionToAssignee(self.owner)
2410- self.assertEqual(
2411- self.owner, self.series_task.assignee)
2412+ data = self._setupBugData()
2413+ with person_logged_in(data.owner):
2414+ data.generic_task.transitionToAssignee(data.owner)
2415+ self.assertEqual(data.owner, data.series_task.assignee)
2416
2417 def test_editing_generic_package_reflects_upon_conjoined_master(self):
2418 # If a change is made to the source package of a conjoined slave
2419 # (generic) task, that change is reflected upon the conjoined
2420 # master.
2421+ data = self._setupBugData()
2422 source_package_name = self.factory.makeSourcePackageName("ham")
2423 self.factory.makeSourcePackagePublishingHistory(
2424- distroseries=self.distro.currentseries,
2425+ distroseries=data.distro.currentseries,
2426 sourcepackagename=source_package_name)
2427- with person_logged_in(self.owner):
2428- self.generic_task.transitionToTarget(
2429- self.distro.getSourcePackage(source_package_name))
2430- self.assertEqual(
2431- source_package_name, self.series_task.sourcepackagename)
2432-
2433- def test_creating_conjoined_task_gets_synced_attributes(self):
2434- bug = self.factory.makeBug(
2435- distribution=self.distro,
2436- sourcepackagename=self.source_package.sourcepackagename,
2437- owner=self.owner)
2438- generic_task = bug.bugtasks[0]
2439- bugtaskset = getUtility(IBugTaskSet)
2440- with person_logged_in(self.owner):
2441- generic_task.transitionToStatus(
2442- BugTaskStatus.CONFIRMED, self.owner)
2443- self.assertEqual(
2444- BugTaskStatus.CONFIRMED, generic_task.status)
2445- slave_bugtask = bugtaskset.createTask(
2446- bug, self.owner, generic_task.target.development_version)
2447- self.assertEqual(
2448- BugTaskStatus.CONFIRMED, generic_task.status)
2449- self.assertEqual(
2450- BugTaskStatus.CONFIRMED, slave_bugtask.status)
2451+ with person_logged_in(data.owner):
2452+ data.generic_task.transitionToTarget(
2453+ data.distro.getSourcePackage(source_package_name))
2454+ self.assertEqual(source_package_name,
2455+ data.series_task.sourcepackagename)
2456+
2457+ def test_conjoined_milestone(self):
2458+ """Milestone attribute will sync across conjoined tasks."""
2459+ data = self._setupBugData()
2460+ login('foo.bar@canonical.com')
2461+ launchbag = getUtility(ILaunchBag)
2462+ conjoined = getUtility(IProductSet)['alsa-utils']
2463+ con_generic_task = getUtility(IBugTaskSet).createTask(data.bug,
2464+ launchbag.user,
2465+ conjoined)
2466+ con_devel_task = getUtility(IBugTaskSet).createTask(
2467+ data.bug, launchbag.user, conjoined.getSeries("trunk"))
2468+
2469+ test_milestone = conjoined.development_focus.newMilestone("test")
2470+ noway_milestone = conjoined.development_focus.newMilestone("noway")
2471+
2472+ Store.of(test_milestone).flush()
2473+
2474+ self.assertIsNone(con_generic_task.milestone)
2475+ self.assertIsNone(con_devel_task.milestone)
2476+
2477+ con_devel_task.transitionToMilestone(test_milestone,
2478+ conjoined.owner)
2479+
2480+ self.assertEqual(con_generic_task.milestone.name, 'test')
2481+ self.assertEqual(con_devel_task.milestone.name, 'test')
2482+
2483+ # But a normal unprivileged user can't set the milestone.
2484+ no_priv = getUtility(IPersonSet).getByEmail('no-priv@canonical.com')
2485+ with ExpectedException(UserCannotEditBugTaskMilestone, ''):
2486+ con_devel_task.transitionToMilestone(noway_milestone, no_priv)
2487+ self.assertEqual(con_devel_task.milestone.name, 'test')
2488+
2489+ con_devel_task.transitionToMilestone(test_milestone, conjoined.owner)
2490+
2491+ self.assertEqual(con_generic_task.milestone.name, 'test')
2492+ self.assertEqual(con_devel_task.milestone.name, 'test')
2493+
2494+ def test_non_current_dev_lacks_conjoined(self):
2495+ """Tasks not the current dev focus lacks conjoined masters or slaves.
2496+ """
2497+ # Only owners, experts, or admins can create a series.
2498+ login('foo.bar@canonical.com')
2499+ launchbag = getUtility(ILaunchBag)
2500+ ubuntu = getUtility(IDistributionSet).get(1)
2501+ alsa_utils = getUtility(IProductSet)['alsa-utils']
2502+ ubuntu_netapplet = ubuntu.getSourcePackage("netapplet")
2503+
2504+ params = CreateBugParams(owner=launchbag.user,
2505+ title="a test bug",
2506+ comment="test bug description")
2507+ ubuntu_netapplet_bug = ubuntu_netapplet.createBug(params)
2508+
2509+ alsa_utils_stable = alsa_utils.newSeries(launchbag.user,
2510+ 'stable',
2511+ 'The stable series.')
2512+
2513+ login('test@canonical.com')
2514+ Store.of(alsa_utils_stable).flush()
2515+ self.assertNotEqual(alsa_utils.development_focus, alsa_utils_stable)
2516+
2517+ stable_netapplet_task = getUtility(IBugTaskSet).createTask(
2518+ ubuntu_netapplet_bug, launchbag.user, alsa_utils_stable)
2519+ self.assertIsNone(stable_netapplet_task.conjoined_master)
2520+ self.assertIsNone(stable_netapplet_task.conjoined_slave)
2521+
2522+ warty = ubuntu.getSeries('warty')
2523+ self.assertNotEqual(warty, ubuntu.currentseries)
2524+
2525+ warty_netapplet_task = getUtility(IBugTaskSet).createTask(
2526+ ubuntu_netapplet_bug, launchbag.user,
2527+ warty.getSourcePackage(ubuntu_netapplet.sourcepackagename))
2528+
2529+ self.assertIsNone(warty_netapplet_task.conjoined_master)
2530+ self.assertIsNone(warty_netapplet_task.conjoined_slave)
2531+
2532+ def test_no_conjoined_without_current_series(self):
2533+ """Distributions without current series lack a conjoined master/slave.
2534+ """
2535+ login('foo.bar@canonical.com')
2536+ launchbag = getUtility(ILaunchBag)
2537+ ubuntu = getUtility(IDistributionSet).get(1)
2538+ ubuntu_netapplet = ubuntu.getSourcePackage("netapplet")
2539+ params = CreateBugParams(owner=launchbag.user,
2540+ title="a test bug",
2541+ comment="test bug description")
2542+ ubuntu_netapplet_bug = ubuntu_netapplet.createBug(params)
2543+
2544+ gentoo = getUtility(IDistributionSet).getByName('gentoo')
2545+ self.assertIsNone(gentoo.currentseries)
2546+
2547+ gentoo_netapplet_task = getUtility(IBugTaskSet).createTask(
2548+ ubuntu_netapplet_bug, launchbag.user,
2549+ gentoo.getSourcePackage(ubuntu_netapplet.sourcepackagename))
2550+ self.assertIsNone(gentoo_netapplet_task.conjoined_master)
2551+ self.assertIsNone(gentoo_netapplet_task.conjoined_slave)
2552+
2553+ def test_conjoined_broken_relationship(self):
2554+ """A conjoined relationship can be broken, though.
2555+
2556+ If the development task (i.e the conjoined master) is Won't Fix, it
2557+ means that the bug is deferred to the next series. In this case the
2558+ development task should be Won't Fix, while the generic task keeps the
2559+ value it had before, allowing it to stay open.
2560+ """
2561+ data = self._setupBugData()
2562+ login('foo.bar@canonical.com')
2563+ generic_netapplet_task = data.generic_task
2564+ current_series_netapplet_task = data.series_task
2565+
2566+ # First let's change the status from Fix Released, since it doesn't
2567+ # make sense to reject such a task.
2568+ current_series_netapplet_task.transitionToStatus(
2569+ BugTaskStatus.CONFIRMED, getUtility(ILaunchBag).user)
2570+ self.assertEqual(generic_netapplet_task.status.title,
2571+ 'Confirmed')
2572+ self.assertEqual(current_series_netapplet_task.status.title,
2573+ 'Confirmed')
2574+ self.assertIsNone(generic_netapplet_task.date_closed)
2575+ self.assertIsNone(current_series_netapplet_task.date_closed)
2576+
2577+ # Now, if we set the current series task to Won't Fix, the generic task
2578+ # will still be confirmed.
2579+ netapplet_owner = current_series_netapplet_task.pillar.owner
2580+ current_series_netapplet_task.transitionToStatus(
2581+ BugTaskStatus.WONTFIX, netapplet_owner)
2582+
2583+ self.assertEqual(generic_netapplet_task.status.title,
2584+ 'Confirmed')
2585+ self.assertEqual(current_series_netapplet_task.status.title,
2586+ "Won't Fix")
2587+
2588+ self.assertIsNone(generic_netapplet_task.date_closed)
2589+ self.assertIsNotNone(current_series_netapplet_task.date_closed)
2590+
2591+ # And the bugtasks are no longer conjoined:
2592+ self.assertIsNone(generic_netapplet_task.conjoined_master)
2593+ self.assertIsNone(current_series_netapplet_task.conjoined_slave)
2594+
2595+ # If the current development release is marked as Invalid, then the
2596+ # bug is invalid for all future series too, and so the general bugtask
2597+ # is therefore Invalid also. In other words, conjoined again.
2598+
2599+ current_series_netapplet_task.transitionToStatus(
2600+ BugTaskStatus.NEW, getUtility(ILaunchBag).user)
2601+
2602+ # XXX Gavin Panella 2007-06-06 bug=112746:
2603+ # We must make two transitions.
2604+ current_series_netapplet_task.transitionToStatus(
2605+ BugTaskStatus.INVALID, getUtility(ILaunchBag).user)
2606+
2607+ self.assertEqual(generic_netapplet_task.status.title,
2608+ 'Invalid')
2609+ self.assertEqual(current_series_netapplet_task.status.title,
2610+ 'Invalid')
2611+
2612+ self.assertIsNotNone(generic_netapplet_task.date_closed)
2613+ self.assertIsNotNone(current_series_netapplet_task.date_closed)
2614+
2615+ def test_conjoined_tasks_sync(self):
2616+ """Conjoined properties are sync'd."""
2617+ launchbag = getUtility(ILaunchBag)
2618+ login('foo.bar@canonical.com')
2619+
2620+ sample_person = getUtility(IPersonSet).getByEmail('test@canonical.com')
2621+
2622+ ubuntu = getUtility(IDistributionSet).get(1)
2623+ params = CreateBugParams(owner=launchbag.user,
2624+ title="a test bug",
2625+ comment="test bug description")
2626+ ubuntu_bug = ubuntu.createBug(params)
2627+
2628+ ubuntu_netapplet = ubuntu.getSourcePackage("netapplet")
2629+ ubuntu_netapplet_bug = ubuntu_netapplet.createBug(params)
2630+ generic_netapplet_task = ubuntu_netapplet_bug.bugtasks[0]
2631+
2632+ # First, we'll target the bug for the current Ubuntu series, Hoary.
2633+ # Note that the synced attributes are copied when the series-specific
2634+ # tasks are created. We'll set non-default attribute values for each
2635+ # generic task to demonstrate.
2636+ self.assertEqual('hoary', ubuntu.currentseries.name)
2637+
2638+ # Only owners, experts, or admins can create a milestone.
2639+ ubuntu_edgy_milestone = ubuntu.currentseries.newMilestone("knot1")
2640+
2641+ login('test@canonical.com')
2642+ generic_netapplet_task.transitionToStatus(
2643+ BugTaskStatus.INPROGRESS, getUtility(ILaunchBag).user)
2644+ generic_netapplet_task.transitionToAssignee(sample_person)
2645+ generic_netapplet_task.milestone = ubuntu_edgy_milestone
2646+ generic_netapplet_task.transitionToImportance(
2647+ BugTaskImportance.CRITICAL, ubuntu.owner)
2648+
2649+ getUtility(IBugTaskSet).createTask(ubuntu_bug, launchbag.user,
2650+ ubuntu.currentseries)
2651+ current_series_netapplet_task = getUtility(IBugTaskSet).createTask(
2652+ ubuntu_netapplet_bug, launchbag.user,
2653+ ubuntu_netapplet.development_version)
2654+
2655+ # The attributes were synced with the generic task.
2656+ self.assertEqual('In Progress',
2657+ current_series_netapplet_task.status.title)
2658+ self.assertEqual('Sample Person',
2659+ current_series_netapplet_task.assignee.displayname)
2660+ self.assertEqual('knot1',
2661+ current_series_netapplet_task.milestone.name)
2662+ self.assertEqual('Critical',
2663+ current_series_netapplet_task.importance.title)
2664+
2665+ self.assertEqual(current_series_netapplet_task.date_assigned,
2666+ generic_netapplet_task.date_assigned)
2667+ self.assertEqual(current_series_netapplet_task.date_confirmed,
2668+ generic_netapplet_task.date_confirmed)
2669+ self.assertEqual(current_series_netapplet_task.date_inprogress,
2670+ generic_netapplet_task.date_inprogress)
2671+ self.assertEqual(current_series_netapplet_task.date_closed,
2672+ generic_netapplet_task.date_closed)
2673+
2674+ # We'll also add some product and productseries tasks.
2675+ alsa_utils = getUtility(IProductSet)['alsa-utils']
2676+ self.assertEqual('trunk', alsa_utils.development_focus.name)
2677+
2678+ current_series_netapplet_task.transitionToStatus(
2679+ BugTaskStatus.FIXRELEASED, getUtility(ILaunchBag).user)
2680+
2681+ self.assertIsInstance(generic_netapplet_task.date_left_new, datetime)
2682+ self.assertEqual(generic_netapplet_task.date_left_new,
2683+ current_series_netapplet_task.date_left_new)
2684+
2685+ self.assertIsInstance(generic_netapplet_task.date_triaged, datetime)
2686+ self.assertEqual(generic_netapplet_task.date_triaged,
2687+ current_series_netapplet_task.date_triaged)
2688+
2689+ self.assertIsInstance(generic_netapplet_task.date_fix_committed,
2690+ datetime)
2691+ self.assertEqual(generic_netapplet_task.date_fix_committed,
2692+ current_series_netapplet_task.date_fix_committed)
2693+
2694+ self.assertEqual('Fix Released', generic_netapplet_task.status.title)
2695+ self.assertEqual('Fix Released',
2696+ current_series_netapplet_task.status.title)
2697+
2698+ self.assertIsInstance(generic_netapplet_task.date_closed, datetime)
2699+ self.assertEqual(generic_netapplet_task.date_closed,
2700+ current_series_netapplet_task.date_closed)
2701+ self.assertIsInstance(generic_netapplet_task.date_fix_released,
2702+ datetime)
2703+ self.assertEqual(generic_netapplet_task.date_fix_released,
2704+ current_series_netapplet_task.date_fix_released)
2705
2706
2707 # START TEMPORARY BIT FOR BUGTASK AUTOCONFIRM FEATURE FLAG.
2708 # When feature flag code is removed, delete these tests (up to "# END
2709 # TEMPORARY BIT FOR BUGTASK AUTOCONFIRM FEATURE FLAG.")
2710
2711+
2712 class TestAutoConfirmBugTasksFlagForProduct(TestCaseWithFactory):
2713 """Tests for auto-confirming bug tasks."""
2714 # Tests for _checkAutoconfirmFeatureFlag.
2715@@ -2620,3 +3748,137 @@
2716 def test_sourcepackage(self):
2717 source = self.factory.makeSourcePackage()
2718 self.assert_userHasBugSupervisorPrivilegesContext(source)
2719+
2720+
2721+class TestTargetNameCache(TestCase):
2722+ """BugTask table has a stored computed attribute.
2723+
2724+ This targetnamecache attribute which stores a computed value to allow us
2725+ to sort and search on that value without having to do lots of SQL joins.
2726+ This cached value gets updated daily by the
2727+ update-bugtask-targetnamecaches cronscript and whenever the bugtask is
2728+ changed. Of course, it's also computed and set when a bugtask is
2729+ created.
2730+
2731+ `BugTask.bugtargetdisplayname` simply returns `targetnamecache`, and
2732+ the latter is not exposed in `IBugTask`, so the `bugtargetdisplayname`
2733+ is used here.
2734+
2735+ XXX: rharding 2012-05-14 bug=999298: These tests are ported from doctests
2736+ and do too much work. They should be split into simpler and better unit
2737+ tests.
2738+ """
2739+
2740+ layer = DatabaseFunctionalLayer
2741+
2742+ def test_cron_updating_targetnamecache(self):
2743+ """Verify the initial target name cache."""
2744+ login('foo.bar@canonical.com')
2745+ bug_one = getUtility(IBugSet).get(1)
2746+ mark = getUtility(IPersonSet).getByEmail('mark@example.com')
2747+ netapplet = getUtility(IProductSet).get(11)
2748+
2749+ upstream_task = getUtility(IBugTaskSet).createTask(
2750+ bug_one, mark, netapplet,
2751+ status=BugTaskStatus.NEW, importance=BugTaskImportance.MEDIUM)
2752+ self.assertEqual(upstream_task.bugtargetdisplayname, u'NetApplet')
2753+
2754+ thunderbird = getUtility(IProductSet).get(8)
2755+ upstream_task_id = upstream_task.id
2756+ upstream_task.transitionToTarget(thunderbird)
2757+ self.assertEqual(upstream_task.bugtargetdisplayname,
2758+ u'Mozilla Thunderbird')
2759+
2760+ thunderbird.name = 'thunderbird-ng'
2761+ thunderbird.displayname = 'Mozilla Thunderbird NG'
2762+
2763+ # XXX Guilherme Salgado 2005-11-07 bug=3989:
2764+ # This flush_database_updates() shouldn't be needed because we
2765+ # already have the transaction.commit() here, but without it
2766+ # (flush_database_updates), the cronscript won't see the thunderbird
2767+ # name change.
2768+ flush_database_updates()
2769+ transaction.commit()
2770+
2771+ process = subprocess.Popen(
2772+ 'cronscripts/update-bugtask-targetnamecaches.py', shell=True,
2773+ stdin=subprocess.PIPE, stdout=subprocess.PIPE,
2774+ stderr=subprocess.PIPE)
2775+ (out, err) = process.communicate()
2776+
2777+ self.assertTrue(err.startswith(("INFO Creating lockfile: "
2778+ "/var/lock/launchpad-launchpad-targetnamecacheupdater.lock")))
2779+ self.assertTrue('INFO Updating targetname cache of bugtasks' in err)
2780+ self.assertTrue('INFO Calculating targets.' in err)
2781+ self.assertTrue('INFO Will check ', err)
2782+ self.assertTrue("INFO Updating (u'Mozilla Thunderbird',)" in err)
2783+ self.assertTrue('INFO Updated 1 target names.' in err)
2784+ self.assertTrue('INFO Finished updating targetname cache' in err)
2785+
2786+ self.assertEqual(process.returncode, 0)
2787+
2788+ # XXX Guilherme Salgado 2005-11-07:
2789+ # If we don't call flush_database_caches() here, we won't see the
2790+ # changes made by the cronscript in objects we already have cached.
2791+ flush_database_caches()
2792+ transaction.commit()
2793+
2794+ self.assertEqual(
2795+ getUtility(IBugTaskSet).get(upstream_task_id).bugtargetdisplayname,
2796+ u'Mozilla Thunderbird NG')
2797+
2798+ # With sourcepackage bugtasks that have accepted nominations to a
2799+ # series, additional sourcepackage bugtasks are automatically
2800+ # nominated to the same series. The nominations are implicitly
2801+ # accepted and have targetnamecache updated.
2802+ ubuntu = getUtility(IDistributionSet).get(1)
2803+
2804+ new_bug, new_bug_event = getUtility(IBugSet).createBugWithoutTarget(
2805+ CreateBugParams(mark, 'New Bug', comment='New Bug'))
2806+
2807+ # The first message of a new bug has index 0.
2808+ self.assertEqual(new_bug.bug_messages[0].index, 0)
2809+
2810+ getUtility(IBugTaskSet).createTask(
2811+ new_bug, mark, ubuntu.getSourcePackage('mozilla-firefox'))
2812+
2813+ # The first task has been created and successfully nominated to Hoary.
2814+ new_bug.addNomination(mark, ubuntu.currentseries).approve(mark)
2815+
2816+ task_set = [task.bugtargetdisplayname for task in new_bug.bugtasks]
2817+ self.assertEqual(task_set, [
2818+ 'mozilla-firefox (Ubuntu)',
2819+ 'mozilla-firefox (Ubuntu Hoary)',
2820+ ])
2821+
2822+ getUtility(IBugTaskSet).createTask(
2823+ new_bug, mark, ubuntu.getSourcePackage('alsa-utils'))
2824+
2825+ # The second task has been created and has also been successfully
2826+ # nominated to Hoary.
2827+
2828+ task_set = [task.bugtargetdisplayname for task in new_bug.bugtasks]
2829+ self.assertEqual(task_set, [
2830+ 'alsa-utils (Ubuntu)',
2831+ 'mozilla-firefox (Ubuntu)',
2832+ 'alsa-utils (Ubuntu Hoary)',
2833+ 'mozilla-firefox (Ubuntu Hoary)',
2834+ ])
2835+
2836+ # The updating of targetnamecaches is usually done by the cronjob,
2837+ # however it can also be invoked directly.
2838+ thunderbird.name = 'thunderbird'
2839+ thunderbird.displayname = 'Mozilla Thunderbird'
2840+ transaction.commit()
2841+
2842+ self.assertEqual(upstream_task.bugtargetdisplayname,
2843+ u'Mozilla Thunderbird NG')
2844+
2845+ logger = FakeLogger()
2846+ updater = BugTaskTargetNameCacheUpdater(transaction, logger)
2847+ updater.run()
2848+
2849+ flush_database_caches()
2850+ transaction.commit()
2851+ self.assertEqual(upstream_task.bugtargetdisplayname,
2852+ u'Mozilla Thunderbird')
2853
2854=== modified file 'lib/lp/bugs/tests/test_bugtaskset.py'
2855--- lib/lp/bugs/tests/test_bugtaskset.py 2012-05-02 05:25:11 +0000
2856+++ lib/lp/bugs/tests/test_bugtaskset.py 2012-05-16 14:27:19 +0000
2857@@ -6,145 +6,94 @@
2858 __metaclass__ = type
2859
2860 from zope.component import getUtility
2861-
2862 from lp.bugs.interfaces.bugtask import (
2863- BugTaskStatus,
2864- BugTaskStatusSearch,
2865 IBugTaskSet,
2866 )
2867-from lp.registry.enums import InformationType
2868-from lp.services.database.sqlbase import flush_database_updates
2869+from lp.bugs.interfaces.bug import (
2870+ IBugSet,
2871+ )
2872+from lp.registry.interfaces.person import IPersonSet
2873+from lp.registry.interfaces.product import IProductSet
2874+from lp.services.webapp.interfaces import ILaunchBag
2875 from lp.testing import (
2876- login_person,
2877- TestCaseWithFactory,
2878+ login,
2879+ TestCase,
2880 )
2881 from lp.testing.layers import DatabaseFunctionalLayer
2882
2883
2884-class TestStatusCountsForProductSeries(TestCaseWithFactory):
2885- """Test BugTaskSet.getStatusCountsForProductSeries()."""
2886-
2887- layer = DatabaseFunctionalLayer
2888-
2889- def setUp(self):
2890- super(TestStatusCountsForProductSeries, self).setUp()
2891- self.bugtask_set = getUtility(IBugTaskSet)
2892- self.owner = self.factory.makePerson()
2893- login_person(self.owner)
2894- self.product = self.factory.makeProduct(owner=self.owner)
2895- self.series = self.factory.makeProductSeries(product=self.product)
2896- self.milestone = self.factory.makeMilestone(productseries=self.series)
2897-
2898- def get_counts(self, user):
2899- return self.bugtask_set.getStatusCountsForProductSeries(
2900- user, self.series)
2901-
2902- def createBugs(self):
2903- self.factory.makeBug(milestone=self.milestone)
2904- self.factory.makeBug(
2905- milestone=self.milestone,
2906- information_type=InformationType.USERDATA)
2907- self.factory.makeBug(series=self.series)
2908- self.factory.makeBug(
2909- series=self.series, information_type=InformationType.USERDATA)
2910-
2911- def test_privacy_and_counts_for_unauthenticated_user(self):
2912- # An unauthenticated user should see bug counts for each status
2913- # that do not include private bugs.
2914- self.createBugs()
2915- self.assertEqual(
2916- {BugTaskStatus.NEW: 2},
2917- self.get_counts(None))
2918-
2919- def test_privacy_and_counts_for_owner(self):
2920- # The owner should see bug counts for each status that do
2921- # include all private bugs.
2922- self.createBugs()
2923- self.assertEqual(
2924- {BugTaskStatus.NEW: 4},
2925- self.get_counts(self.owner))
2926-
2927- def test_privacy_and_counts_for_other_user(self):
2928- # A random authenticated user should see bug counts for each
2929- # status that do include all private bugs, since it is costly to
2930- # query just the private bugs that the user has access to view,
2931- # and this query may be run many times on a single page.
2932- self.createBugs()
2933- other = self.factory.makePerson()
2934- self.assertEqual(
2935- {BugTaskStatus.NEW: 4},
2936- self.get_counts(other))
2937-
2938- def test_multiple_statuses(self):
2939- # Test that separate counts are provided for each status that
2940- # bugs are found in.
2941- statuses = [
2942- BugTaskStatus.INVALID,
2943- BugTaskStatus.OPINION,
2944- ]
2945- for status in statuses:
2946- self.factory.makeBug(milestone=self.milestone, status=status)
2947- self.factory.makeBug(series=self.series, status=status)
2948- for i in range(3):
2949- self.factory.makeBug(series=self.series)
2950- expected = {
2951- BugTaskStatus.INVALID: 2,
2952- BugTaskStatus.OPINION: 2,
2953- BugTaskStatus.NEW: 3,
2954- }
2955- self.assertEqual(expected, self.get_counts(None))
2956-
2957- def test_incomplete_status(self):
2958- # INCOMPLETE is stored as either INCOMPLETE_WITH_RESPONSE or
2959- # INCOMPLETE_WITHOUT_RESPONSE so the stats do not include a count of
2960- # INCOMPLETE tasks.
2961- statuses = [
2962- BugTaskStatusSearch.INCOMPLETE_WITH_RESPONSE,
2963- BugTaskStatusSearch.INCOMPLETE_WITHOUT_RESPONSE,
2964- BugTaskStatus.INCOMPLETE,
2965- ]
2966- for status in statuses:
2967- self.factory.makeBug(series=self.series, status=status)
2968- flush_database_updates()
2969- expected = {
2970- BugTaskStatusSearch.INCOMPLETE_WITH_RESPONSE: 1,
2971- BugTaskStatusSearch.INCOMPLETE_WITHOUT_RESPONSE: 2,
2972- }
2973- self.assertEqual(expected, self.get_counts(None))
2974-
2975-
2976-class TestBugTaskMilestones(TestCaseWithFactory):
2977- """Tests that appropriate milestones are returned for bugtasks."""
2978-
2979- layer = DatabaseFunctionalLayer
2980-
2981- def setUp(self):
2982- super(TestBugTaskMilestones, self).setUp()
2983- self.product = self.factory.makeProduct()
2984- self.product_bug = self.factory.makeBug(product=self.product)
2985- self.product_milestone = self.factory.makeMilestone(
2986- product=self.product)
2987- self.distribution = self.factory.makeDistribution()
2988- self.distribution_bug = self.factory.makeBug(
2989- distribution=self.distribution)
2990- self.distribution_milestone = self.factory.makeMilestone(
2991- distribution=self.distribution)
2992- self.bugtaskset = getUtility(IBugTaskSet)
2993-
2994- def test_get_target_milestones_with_one_task(self):
2995- milestones = list(self.bugtaskset.getBugTaskTargetMilestones(
2996- [self.product_bug.default_bugtask]))
2997- self.assertEqual(
2998- [self.product_milestone],
2999- milestones)
3000-
3001- def test_get_target_milestones_multiple_tasks(self):
3002- tasks = [
3003- self.product_bug.default_bugtask,
3004- self.distribution_bug.default_bugtask,
3005- ]
3006- milestones = sorted(
3007- self.bugtaskset.getBugTaskTargetMilestones(tasks))
3008- self.assertEqual(
3009- sorted([self.product_milestone, self.distribution_milestone]),
3010- milestones)
3011+class TestCountsForProducts(TestCase):
3012+ """Test BugTaskSet.getOpenBugTasksPerProduct"""
3013+
3014+ layer = DatabaseFunctionalLayer
3015+
3016+ def test_open_product_counts(self):
3017+ # IBugTaskSet.getOpenBugTasksPerProduct() will return a dictionary
3018+ # of product_id:count entries for bugs in an open status that
3019+ # the user given as a parameter is allowed to see. If a product,
3020+ # such as id=3 does not have any open bugs, it will not appear
3021+ # in the result.
3022+ launchbag = getUtility(ILaunchBag)
3023+ login('foo.bar@canonical.com')
3024+ foobar = launchbag.user
3025+
3026+ productset = getUtility(IProductSet)
3027+ products = [productset.get(id) for id in (3, 5, 20)]
3028+ sample_person = getUtility(IPersonSet).getByEmail('test@canonical.com')
3029+ bugtask_counts = getUtility(IBugTaskSet).getOpenBugTasksPerProduct(
3030+ sample_person, products)
3031+ res = sorted(bugtask_counts.items())
3032+ self.assertEqual(
3033+ 'product_id=%d count=%d' % tuple(res[0]),
3034+ 'product_id=5 count=1')
3035+ self.assertEqual(
3036+ 'product_id=%d count=%d' % tuple(res[1]),
3037+ 'product_id=20 count=2')
3038+
3039+ # A Launchpad admin will get a higher count for the product with id=20
3040+ # because he can see the private bug.
3041+ bugtask_counts = getUtility(IBugTaskSet).getOpenBugTasksPerProduct(
3042+ foobar, products)
3043+ res = sorted(bugtask_counts.items())
3044+ self.assertEqual(
3045+ 'product_id=%d count=%d' % tuple(res[0]),
3046+ 'product_id=5 count=1')
3047+ self.assertEqual(
3048+ 'product_id=%d count=%d' % tuple(res[1]),
3049+ 'product_id=20 count=3')
3050+
3051+ # Someone subscribed to the private bug on the product with id=20
3052+ # will also have it added to the count.
3053+ karl = getUtility(IPersonSet).getByName('karl')
3054+ bugtask_counts = getUtility(IBugTaskSet).getOpenBugTasksPerProduct(
3055+ karl, products)
3056+ res = sorted(bugtask_counts.items())
3057+ self.assertEqual(
3058+ 'product_id=%d count=%d' % tuple(res[0]),
3059+ 'product_id=5 count=1')
3060+ self.assertEqual(
3061+ 'product_id=%d count=%d' % tuple(res[1]),
3062+ 'product_id=20 count=3')
3063+
3064+
3065+class TestSortingBugTasks(TestCase):
3066+ """Bug tasks need to sort in a very particular order."""
3067+
3068+ layer = DatabaseFunctionalLayer
3069+
3070+ def test_sortingorder(self):
3071+ """We want product tasks, then ubuntu, then distro-related.
3072+
3073+ In the distro-related tasks we want a distribution-task first, then
3074+ distroseries-tasks for that same distribution. The distroseries tasks
3075+ should be sorted by distroseries version.
3076+ """
3077+ login('foo.bar@canonical.com')
3078+ bug_one = getUtility(IBugSet).get(1)
3079+ tasks = bug_one.bugtasks
3080+ task_names = [task.bugtargetdisplayname for task in tasks]
3081+ self.assertEqual(task_names, [
3082+ u'Mozilla Firefox',
3083+ 'mozilla-firefox (Ubuntu)',
3084+ 'mozilla-firefox (Debian)',
3085+ ])