= Searching BugTasks = BugTasks are usually searched through an IBugTarget's searchTasks() method, but they all delegate the search to IBugTaskSet.search(). That method accepts a single parameter; an BugTaskSearchParams instance. >>> from lp.bugs.interfaces.bugtask import ( ... BugTaskSearchParams, ... IBugTaskSet, ... ) >>> bugtask_set = getUtility(IBugTaskSet) >>> all_public = BugTaskSearchParams(user=None) >>> found_bugtasks = bugtask_set.search(all_public) >>> from lp.bugs.model.bugtask import BugTask >>> all_public_bugtasks = BugTask.select( ... "BugTask.bug = Bug.id AND Bug.private = false", ... clauseTables=['Bug']) >>> found_bugtasks.count() == all_public_bugtasks.count() True == Searching by bug supervisor == The 'bug_supervisor' parameter allows you to search bugtasks that a certain person is responsible for. A person can be a bug supervisor for a product, a distribution, or a distribution source package. No Privileges Person isn't a bug supervisor, so no bugs are found for him: >>> from lp.registry.interfaces.person import IPersonSet >>> no_priv = getUtility(IPersonSet).getByName('no-priv') >>> no_priv_bug_supervisor = BugTaskSearchParams( ... user=None, bug_supervisor=no_priv) >>> found_bugtasks = bugtask_set.search(no_priv_bug_supervisor) >>> found_bugtasks.count() 0 == Product bugs == Firefox has a few bugs: >>> from lp.registry.interfaces.product import IProductSet >>> firefox = getUtility(IProductSet).getByName('firefox') >>> firefox_bugs = firefox.searchTasks(all_public) >>> firefox_public_bugs = firefox_bugs.count() >>> firefox_public_bugs > 0 True == Distribution and package bugs == Ubuntu does too: >>> from lp.registry.interfaces.distribution import IDistributionSet >>> ubuntu = getUtility(IDistributionSet).getByName("ubuntu") >>> all_public = BugTaskSearchParams(user=None) >>> ubuntu_bugs = ubuntu.searchTasks(all_public) >>> ubuntu_bugs.count() > 0 True and in particular, mozilla-firefox in Ubuntu has 'em: >>> ubuntu_firefox = ubuntu.getSourcePackage("mozilla-firefox") >>> ubuntu_firefox_bugs = ubuntu_firefox.searchTasks(all_public) >>> ubuntu_firefox_bugs.count() > 0 True == Person bugs == To get all related tasks to a person call searchTasks() on the person object: >>> from lp.registry.interfaces.person import IPersonSet >>> user = getUtility(IPersonSet).getByName('name16') >>> user_bugs = user.searchTasks(None, user=None) >>> user_bugs.count() > 0 True == Dupes and Conjoined tasks == You can set flags to omit duplicates: >>> no_dupes = BugTaskSearchParams(user=None, omit_dupes=True) >>> firefox = getUtility(IProductSet).getByName('firefox') >>> sans_dupes = firefox.searchTasks(no_dupes) >>> sans_dupes.count() < firefox_public_bugs True and also series-targeted bugs: >>> no_targeted = BugTaskSearchParams(user=None, omit_targeted=True) >>> sans_targeted = ubuntu.searchTasks(no_targeted) >>> sans_targeted.count() < ubuntu_bugs True === Product bug supervisor === If No Privileges is specified as Firefox's bug supervisor, searching for his bugs return all of Firefox's bugs. >>> login('foo.bar@canonical.com') >>> firefox.setBugSupervisor(no_priv, no_priv) >>> found_bugtasks = bugtask_set.search(no_priv_bug_supervisor) >>> found_bugtasks.count() == firefox_bugs.count() True >>> found_targets = set( ... bugtask.target.bugtargetdisplayname for bugtask in found_bugtasks) >>> for target_name in sorted(found_targets): ... print target_name Mozilla Firefox === Distribution bug supervisor === If someone is bug supervisor for Firefox, Firefox in Ubuntu, and Ubuntu, all bugs in Firefox and Ubuntu are returned. Bugs in the Ubuntu Firefox package are included in the Ubuntu bugs, so they won't be returned twice. >>> all_public = BugTaskSearchParams(user=None) >>> ubuntu_bugs = ubuntu.searchTasks(all_public) >>> ubuntu_bugs.count() > 0 True >>> ubuntu.setBugSupervisor(no_priv, no_priv) >>> found_bugtasks = bugtask_set.search(no_priv_bug_supervisor) >>> found_bugtasks.count() == firefox_bugs.count() + ubuntu_bugs.count() True == Searching in comments == Comments can be searched when specifying a search text in the bugtask search. Whether comments are searched is controlled by the search_comments config option. For example, no Firefox bugs are found when searching for 'wordincomment'. >>> comment_search = BugTaskSearchParams( ... user=None, searchtext='wordincomment') >>> found_bugtasks = firefox.searchTasks(comment_search) >>> found_bugtasks.count() 0 If we add a comment containing the search string to bug one, that bug will be returned by the search only if search_comments is True. >>> from lp.bugs.interfaces.bug import IBugSet >>> bug_one = getUtility(IBugSet).get(1) >>> bug_one.newMessage(no_priv, 'No subject', 'some wordincomment') # Add another comment to make sure that it won't cause the same # bugtask to be returned twice. >>> bug_one.newMessage(no_priv, 'No subject', 'another wordincomment') When the search_comments config option is False, the bug is not found. >>> from lp.services.config import config >>> search_comments = """ ... [malone] ... search_comments: %s ... """ >>> config.push('search_comments', search_comments % False) >>> found_bugtasks = firefox.searchTasks(comment_search) >>> [bugtask.bug.id for bugtask in found_bugtasks] [] >>> config_data = config.pop('search_comments') If the search_comments config option is True, the bug is found. >>> config.push('search_comments', search_comments % True) >>> found_bugtasks = firefox.searchTasks(comment_search) >>> for bugtask in found_bugtasks: ... print "#%s" % bugtask.bug.id #1 # Of course, searching in a project not containing bug #1 doesn't # return any matches. >>> evolution = getUtility(IProductSet).getByName('evolution') >>> found_bugtasks = evolution.searchTasks(comment_search) >>> found_bugtasks.count() 0 >>> config_data = config.pop('search_comments') == Searching using bug full-text index == The searchtext parameter does an extensive and expensive search (it looks through the bug's full text index, bug comments, bugtask target name, etc.) For some use cases, it is often easier and cheaper to simply search on the bug's full text index and omit the more expensive search on other related information. For example, there are no bugs with the word 'Fnord' in Firefox. >>> text_search = BugTaskSearchParams(user=None, fast_searchtext='Fnord') >>> found_bugtasks = firefox.searchTasks(text_search) >>> found_bugtasks.count() 0 But if we put that word in the bug #4 description, it will be found. >>> bug_four = getUtility(IBugSet).get(4) >>> bug_four.description += ( ... '\nThat happens pretty often with the Fnord Highlighter ' ... 'extension installed.') >>> found_bugtasks = firefox.searchTasks(text_search) >>> for bugtask in found_bugtasks: ... print "#%s" % bugtask.bug.id #4 == Searching by bug reporter == The 'bug_reporter' parameter allows you to search for bugs reported by a certain person. >>> foo_bar = getUtility(IPersonSet).getByEmail('foo.bar@canonical.com') >>> reported_by_foo_bar = BugTaskSearchParams( ... user=None, bug_reporter=foo_bar) >>> reported_by_foo_bar.setDistribution(ubuntu) >>> found_bugtasks = bugtask_set.search(reported_by_foo_bar) >>> for bugtask in found_bugtasks: ... print "#%s in %s reported by %s" % ( ... bugtask.bug.id, bugtask.bugtargetname, ... bugtask.bug.owner.displayname) #9 in thunderbird (Ubuntu) reported by Foo Bar #10 in linux-source-2.6.15 (Ubuntu) reported by Foo Bar == Searching for nominated bugs == We can search for bugs nominated to a distribution series by using the nominated_for parameter. >>> ubuntu = getUtility(IDistributionSet).getByName('ubuntu') >>> warty = ubuntu.getSeries('warty') >>> from lp.bugs.model.bugnomination import BugNomination >>> print list(BugNomination.selectBy(distroseries=warty)) [] >>> from lp.bugs.interfaces.bug import CreateBugParams >>> nominated_for_warty = BugTaskSearchParams( ... user=None, nominated_for=warty) >>> list(ubuntu.searchTasks(nominated_for_warty)) [] >>> nominated_bug = ubuntu.createBug( ... CreateBugParams(owner=no_priv, title='Test nominated bug', ... comment='Something')) >>> BugNomination( ... owner=no_priv, distroseries=warty, bug=nominated_bug) >>> for bugtask in ubuntu.searchTasks(nominated_for_warty): ... print bugtask.bug.title Test nominated bug The same parameter is used to search for bugs nominated to a product series. >>> firefox = getUtility(IProductSet).getByName('firefox') >>> firefox_trunk = firefox.getSeries('trunk') >>> print list(BugNomination.selectBy(productseries=firefox_trunk)) [] >>> nominated_for_trunk = BugTaskSearchParams( ... user=None, nominated_for=firefox_trunk) >>> list(firefox.searchTasks(nominated_for_trunk)) [] >>> nominated_bug = firefox.createBug( ... CreateBugParams(owner=no_priv, title='Bug to be fixed in trunk', ... comment='Something')) >>> BugNomination( ... owner=no_priv, productseries=firefox_trunk, bug=nominated_bug) >>> for bugtask in firefox.searchTasks(nominated_for_trunk): ... print bugtask.bug.title Bug to be fixed in trunk == Filter by Upstream Status == Add an Ubuntu bugtask for a bug that is confirmed upstream. >>> from lp.bugs.interfaces.bugtask import ( ... BugTaskImportance, ... BugTaskStatus, ... ) >>> from lp.bugs.model.tests.test_bugtask import ( ... BugTaskSearchBugsElsewhereTest) >>> def bugTaskInfo(bugtask): ... return '%i %i %s %s' % ( ... bugtask.id, bugtask.bug.id, bugtask.bugtargetdisplayname, ... bugtask.bug.title) >>> test_helper = BugTaskSearchBugsElsewhereTest(helper_only=True) >>> bug_twelve = getUtility(IBugSet).get(12) >>> task_open_upstream = bugtask_set.createTask( ... bug_twelve, foo_bar, ubuntu, ... status=BugTaskStatus.NEW, importance=BugTaskImportance.MEDIUM) >>> test_helper.assertBugTaskIsOpenUpstream(task_open_upstream) Pass the resolved_upstream flag to include only bugtasks linked to watches that are rejected, fixed committed or fix released, or bugtasks related to upstream bugtasks (i.e. filed on the same bug) that are fix committed or fix released. >>> test_helper.setUpBugsResolvedUpstreamTests() >>> params = BugTaskSearchParams( ... resolved_upstream=True, orderby='id', user=None) >>> closed_elsewhere_tasks = ubuntu.searchTasks(params) >>> for bugtask in closed_elsewhere_tasks: ... test_helper.assertBugTaskIsResolvedUpstream(bugtask) ... print bugTaskInfo(bugtask) 17 1 mozilla-firefox (Ubuntu) Firefox does not support SVG 26 2 Ubuntu Blackhole Trash folder 23 9 thunderbird (Ubuntu) Thunderbird crashes Pass the open_upstream flag to include only bugtasks linked to those watches or those upstream bugtasks that have the status "unconfirmed", "needs info", "confirmed", "in progress" or "unknown". Note that a bug may be associated with three or more bugtasks. If one upstream task has a state associated with "open upstream", and another upstream task has a state associated with "resolved upstream", the bug is included in the results of the "open upstream" filter as well as the "resolved upstream" filter. (In the examples below, the last bugtask is ellipsized because its ID is generated here and therefore sampledata-dependent.) >>> params = BugTaskSearchParams( ... open_upstream=True, orderby='id', user=None) >>> open_elsewhere_tasks = ubuntu.searchTasks(params) >>> for bugtask in open_elsewhere_tasks: ... test_helper.assertBugTaskIsOpenUpstream(bugtask) ... print bugTaskInfo(bugtask) 17 1 mozilla-firefox (Ubuntu) Firefox does not support SVG 26 2 Ubuntu Blackhole Trash folder ... ... Ubuntu Copy, Cut and Delete operations should work on selections We can also filter our search to include only bugs that are not known to affect upstream, i.e., bugs that don't have an IUpstreamBugTask. >>> params = BugTaskSearchParams( ... has_no_upstream_bugtask=True, orderby='id', user=None) >>> tasks_with_no_upstreams = ubuntu.searchTasks(params) >>> for bugtask in tasks_with_no_upstreams: ... test_helper.assertShouldBeShownOnNoUpstreamTaskSearch(bugtask) ... print bugTaskInfo(bugtask) 25 10 linux-source-2.6.15 (Ubuntu) another test bug ... ... Ubuntu Test nominated bug If we combine upstream-related filters, we get the union of the results of the single filters. >>> params = BugTaskSearchParams( ... has_no_upstream_bugtask=True, resolved_upstream=True, ... orderby='id', user=None) >>> tasks_with_no_upstreams = ubuntu.searchTasks(params) >>> for bugtask in tasks_with_no_upstreams: ... print bugTaskInfo(bugtask) 17 1 mozilla-firefox (Ubuntu) Firefox does not support SVG 26 2 Ubuntu Blackhole Trash folder 23 9 thunderbird (Ubuntu) Thunderbird crashes 25 10 linux-source-2.6.15 (Ubuntu) another test bug ... ... Ubuntu Test nominated bug >>> test_helper.tearDownBugsElsewhereTests() The search filter can also return bugs that are related to CVE reports: >>> from lp.bugs.interfaces.cve import ICveSet >>> def getCves(bugtask): ... bugcve = getUtility(ICveSet).getBugCvesForBugTasks([bugtask])[0] ... return bugcve.cve.sequence >>> params = BugTaskSearchParams( ... has_cve=True, orderby='id', user=None) >>> tasks_with_cves = ubuntu.searchTasks(params) >>> for bugtask in tasks_with_cves: ... print bugTaskInfo(bugtask), getCves(bugtask) 17 1 mozilla-firefox (Ubuntu) Firefox does not support SVG 1999-8979 26 2 Ubuntu Blackhole Trash folder 1999-2345 == Searching by bug commenter == The 'bug_commenter' parameter allows you to search bugtasks on which a certain person has commented. No Privileges Person hasn't commented on any bugs, so no bugs are found for him: >>> from lp.registry.interfaces.person import IPersonSet >>> import transaction >>> transaction.abort() >>> no_priv = getUtility(IPersonSet).getByName('no-priv') >>> no_priv_bug_commenter = BugTaskSearchParams( ... user=None, bug_commenter=no_priv) >>> found_bugtasks = bugtask_set.search(no_priv_bug_commenter) >>> found_bugtasks.count() 0 If No Privileges Person comments on some bugs, those bugs can then be found by a bug commenter search. There will be one bug task instance returned for each bug target that the task is registered against, so in the test below three comments will produce eight found bug tasks (three for bug 1, five for bug 2). >>> bug_one = getUtility(IBugSet).get(1) >>> bug_one.newMessage(no_priv, 'No subject', 'some comment') >>> bug_two = getUtility(IBugSet).get(2) >>> bug_two.newMessage(no_priv, 'No subject', 'another comment') >>> bug_two.newMessage(no_priv, 'No subject', 'yet another comment') >>> for (bug_id, target) in sorted((bugtask.bug.id, bugtask.bugtargetname) ... for bugtask in found_bugtasks): ... print bug_id, target 1 firefox 1 mozilla-firefox (Debian) 1 mozilla-firefox (Ubuntu) 2 Ubuntu Hoary 2 mozilla-firefox (Debian Woody) 2 mozilla-firefox (Debian) 2 tomcat 2 ubuntu If No Privileges Person reports a bug and does not comment on it, that bug will not be included in the results returned by the bug commenter search. >>> from lp.bugs.interfaces.bug import CreateBugParams >>> from lp.registry.interfaces.product import IProductSet >>> firefox = getUtility(IProductSet).getByName('firefox') >>> firefox.createBug( ... CreateBugParams(no_priv, "Some bug", "Some comment")) >>> for (bug_id, target) in sorted((bugtask.bug.id, bugtask.bugtargetname) ... for bugtask in found_bugtasks): ... print bug_id, target 1 firefox 1 mozilla-firefox (Debian) 1 mozilla-firefox (Ubuntu) 2 Ubuntu Hoary 2 mozilla-firefox (Debian Woody) 2 mozilla-firefox (Debian) 2 tomcat 2 ubuntu == Search for BugTasks assigned to milestones == BugTaskSet.search() can return bugtasks associated with milestones. No BugTask is associated yet with firefox milestone 1.0. >>> product_milestone = firefox.getMilestone('1.0') >>> params = BugTaskSearchParams(milestone=product_milestone, user=None) >>> milestone_tasks = bugtask_set.search(params) >>> print milestone_tasks.count() 0 Similary, no BugTasks are associated with the project firexfox belongs to. >>> mozilla = firefox.project >>> project_milestone = mozilla.getMilestone('1.0') >>> params = BugTaskSearchParams(milestone=project_milestone, user=None) >>> milestone_tasks = bugtask_set.search(params) >>> print milestone_tasks.count() 0 When a BugTask is associated with a milestone, it is returned in a search for bugs of this milestone. >>> bugtask = firefox.searchTasks(BugTaskSearchParams(user=None))[0] >>> print bugTaskInfo(bugtask) 2 1 Mozilla Firefox Firefox does not support SVG >>> bugtask.milestone = product_milestone >>> params = BugTaskSearchParams(milestone=product_milestone, user=None) >>> milestone_tasks = bugtask_set.search(params) >>> for bugtask in milestone_tasks: ... print bugTaskInfo(bugtask) 2 1 Mozilla Firefox Firefox does not support SVG This BugTask is also a BugTask of the milestone of the mozilla project. >>> params = BugTaskSearchParams(milestone=project_milestone, user=None) >>> milestone_tasks = bugtask_set.search(params) >>> for bugtask in milestone_tasks: ... print bugTaskInfo(bugtask) 2 1 Mozilla Firefox Firefox does not support SVG If a bug has one bugtask associated with a product and another bugtask associated with a product series, and if both tasks are assigned to the same milestone... >>> firefox_1_0 = firefox.getSeries("1.0") >>> productseries_task = bugtask_set.createTask( ... bug_one, no_priv, firefox_1_0) >>> productseries_task.milestone = product_milestone >>> print bugTaskInfo(productseries_task) 40 1 Mozilla Firefox 1.0 Firefox does not support SVG ...both of them are returned, by a search for bugs associated with the product milestone... >>> params = BugTaskSearchParams(milestone=product_milestone, user=None) >>> milestone_tasks = bugtask_set.search(params) >>> for bugtask in milestone_tasks: ... print bugTaskInfo(bugtask) 2 1 Mozilla Firefox Firefox does not support SVG 40 1 Mozilla Firefox 1.0 Firefox does not support SVG ...as well as by a search for bugs associated with the project milestone. >>> params = BugTaskSearchParams(milestone=project_milestone, user=None) >>> milestone_tasks = bugtask_set.search(params) >>> for bugtask in milestone_tasks: ... print bugTaskInfo(bugtask) 2 1 Mozilla Firefox Firefox does not support SVG 40 1 Mozilla Firefox 1.0 Firefox does not support SVG == Bugs with partner packages == Bugs may also be targeted to partner packages. First turn "cdrkit" into a partner package: >>> from zope.security.proxy import removeSecurityProxy >>> from lp.soyuz.interfaces.component import IComponentSet >>> proxied_cdrkit = ubuntu.getSourcePackage("cdrkit") >>> cdrkit = removeSecurityProxy(proxied_cdrkit) >>> cdrkit.component = getUtility(IComponentSet)['partner'] >>> cdrkit.archive = ubuntu.getArchiveByComponent('partner') >>> transaction.commit() It starts off with no bugs: >>> cdrkit_bugs = cdrkit.searchTasks(all_public) >>> cdrkit_bugs.count() 0 We can file a bug against it and see that show up in a search: >>> from lp.bugs.interfaces.bug import CreateBugParams >>> bug = cdrkit.createBug( ... CreateBugParams(owner=no_priv, title='Bug to be fixed in trunk', ... comment='Something')) >>> cdrkit_bugs = cdrkit.searchTasks(all_public) >>> cdrkit_bugs.count() 1 == Searching by tags == It is possible to search for bugs by their tags. Tags in the search parameters can be combined using either ''any'' or ''all''. First, we create some test bugs. >>> firefox = getUtility(IProductSet).get(4) >>> foobar = getUtility(IPersonSet).get(16) The first bug is tagged with both 'test-tag-1' and 'test-tag-2'. >>> params = CreateBugParams( ... title="test bug a", comment="test bug a", owner=foobar) >>> test_bug_a = firefox.createBug(params) >>> test_bug_a.tags = ['test-tag-1', 'test-tag-2'] The second bug is tagged with only 'test-tag-1'. >>> params = CreateBugParams( ... title="test bug b", comment="test bug b", owner=foobar) >>> test_bug_b = firefox.createBug(params) >>> test_bug_b.tags = ['test-tag-1'] Searching for bugs with any of the tags returns both of them. >>> from operator import attrgetter >>> from lp.services.searchbuilder import all, any >>> def search_tasks_and_print_bugs(user=None, **args): ... params = BugTaskSearchParams(user=user, **args) ... tasks = firefox.searchTasks(params) ... bugs = (task.bug for task in tasks) ... bugs = sorted(bugs, key=attrgetter('id')) ... for bug in bugs: ... print "%s [%s]" % (bug.title, ", ".join(bug.tags)) >>> search_tasks_and_print_bugs( ... tag=any('test-tag-1', 'test-tag-2')) test bug a [test-tag-1, test-tag-2] test bug b [test-tag-1] Searching for bugs with all of the tags returns only test bug a. >>> search_tasks_and_print_bugs( ... tag=all('test-tag-1', 'test-tag-2')) test bug a [test-tag-1, test-tag-2] Search for the absence of a tag is possible by prefixing the tag name with a minus. >>> search_tasks_and_print_bugs(tag=any('-test-tag-2')) Firefox does not support SVG [] Reflow problems with complex page layouts [layout-test] Firefox install instructions should be complete [doc] Firefox crashes when Save As dialog for a nonexistent window is closed [] Some bug [] test bug b [test-tag-1] The any() and all() search combinators are taken into consideration when searching for the absence of tags too. The following search says "give me bugs that don't have the test-tag-2 tag set *OR* that don't have the layout-test tag set". Only test-bug-1 is elimininated because it has no tags other than those requested in the search. >>> search_tasks_and_print_bugs( ... tag=any('-test-tag-1', '-test-tag-2')) Firefox does not support SVG [] Reflow problems with complex page layouts [layout-test] Firefox install instructions should be complete [doc] Firefox crashes when Save As dialog for a nonexistent window is closed [] Some bug [] test bug b [test-tag-1] Whereas the following search says "give me bugs that don't have the test-tag-2 tag set *AND* that don't have the layout-test tag set". >>> search_tasks_and_print_bugs( ... tag=all('-test-tag-2', '-layout-test')) Firefox does not support SVG [] Firefox install instructions should be complete [doc] Firefox crashes when Save As dialog for a nonexistent window is closed [] Some bug [] test bug b [test-tag-1] Searching for the presence of any tags at all is also possible using a wildcard. If prefixed with a minus it searches for the absence of tags. >>> search_tasks_and_print_bugs(tag=all('*')) Reflow problems with complex page layouts [layout-test] Firefox install instructions should be complete [doc] test bug a [test-tag-1, test-tag-2] test bug b [test-tag-1] >>> search_tasks_and_print_bugs(tag=all('-*')) Firefox does not support SVG [] Firefox crashes when Save As dialog for a nonexistent window is closed [] Some bug [] Searching for the presence and absence of tags finds no matches. Unsurprisingly. >>> search_tasks_and_print_bugs(tag=all('*', '-*')) Wildcards can be combined with non-wildcard tags. The following finds all bugs with tags, but without test-tag-1: >>> search_tasks_and_print_bugs(tag=all('*', '-test-tag-1')) Reflow problems with complex page layouts [layout-test] Firefox install instructions should be complete [doc] The following is very similar; it finds all bugs with tags, *or* without test-tag-1: >>> search_tasks_and_print_bugs(tag=any('*', '-test-tag-1')) Firefox does not support SVG [] Reflow problems with complex page layouts [layout-test] Firefox install instructions should be complete [doc] Firefox crashes when Save As dialog for a nonexistent window is closed [] Some bug [] test bug a [test-tag-1, test-tag-2] test bug b [test-tag-1] The following finds all untagged bugs and bugs with the doc tag. >>> search_tasks_and_print_bugs(tag=any('-*', 'doc')) Firefox does not support SVG [] Firefox install instructions should be complete [doc] Firefox crashes when Save As dialog for a nonexistent window is closed [] Some bug [] == Searching by date_closed == It's possible to limit the search by date_closed, to get only bugs closed after a certain date. greater_than is used to search for bugs closed after a certain date. >>> import pytz >>> from datetime import datetime, timedelta >>> from lp.services.searchbuilder import greater_than >>> product = factory.makeProduct() >>> utc_now = datetime(2008, 9, 4, 12, 0, 0, tzinfo=pytz.timezone('UTC')) >>> not_closed_bug = factory.makeBug(product=product, title="Not closed") >>> bug_closed_a_day_ago = factory.makeBug( ... product=product, date_closed=utc_now-timedelta(days=1), ... title="Closed a day ago") >>> bug_closed_a_week_ago = factory.makeBug( ... product=product, date_closed=utc_now-timedelta(days=7), ... title="Closed a week ago") >>> search_params = BugTaskSearchParams( ... user=None, orderby="-date_closed", ... date_closed=greater_than(utc_now)) >>> list(product.searchTasks(search_params)) [] >>> search_params.date_closed = greater_than(utc_now - timedelta(days=2)) >>> for bug_task in product.searchTasks(search_params): ... print bug_task.bug.title Closed a day ago == Searching for bug with attachments == It's possible to search for bugs with an attachment of a certain type. >>> from StringIO import StringIO >>> from lp.services.librarian.interfaces import ILibraryFileAliasSet >>> from lp.services.messages.interfaces.message import IMessageSet >>> from lp.bugs.interfaces.bugattachment import ( ... BugAttachmentType, ... IBugAttachmentSet, ... ) >>> product = factory.makeProduct() >>> patch_bug = factory.makeBug( ... product=product) >>> filecontent = 'Some diff data' >>> filealias = getUtility(ILibraryFileAliasSet).create( ... name='patch.diff', size=len(filecontent), ... file=StringIO(filecontent), contentType='text/plain') >>> message = getUtility(IMessageSet).fromText( ... subject="title", content="added a patch.") >>> attachmentset = getUtility(IBugAttachmentSet) >>> attachment = attachmentset.create( ... bug=patch_bug, filealias=filealias, title='Patch', ... message=message, attach_type=BugAttachmentType.PATCH) >>> patch_bug.attachments.count() 1 We've added an attachment to our new bug with an attachment type PATCH. Searching for bugs with that attachment type we get one result. >>> search_params = BugTaskSearchParams( ... user=None, attachmenttype=BugAttachmentType.PATCH) >>> product.searchTasks(search_params).count() 1 >>> filecontent = 'Some more diff data' >>> filealias = getUtility(ILibraryFileAliasSet).create( ... name='patch.diff', size=len(filecontent), ... file=StringIO(filecontent), contentType='text/plain') >>> message = getUtility(IMessageSet).fromText( ... subject="title", content="added another patch.") >>> attachmentset = getUtility(IBugAttachmentSet) >>> attachment = attachmentset.create( ... bug=patch_bug, filealias=filealias, title='Patch 2', ... message=message, attach_type=BugAttachmentType.PATCH) >>> patch_bug.attachments.count() 2 We've added another patch to the bug. Searching for bugs with patch attachments still returns a single result, since even though a new attachment was added, there is still only one bug with attachments. >>> product.searchTasks(search_params).count() 1 == Searching for bugs affecting a user == We can search for bugs which a user marked as affecting them. >>> affecting_bug = factory.makeBug(title='A bug affecting a user') >>> affected_user = factory.makePerson(name='affected-user') >>> affecting_bug.markUserAffected(affected_user) >>> target = affecting_bug.bugtasks[0].target >>> affecting_tasks = target.searchTasks( ... None, user=None, affected_user=affected_user) >>> for task in affecting_tasks: ... print task.bug.title A bug affecting a user == Searching for bugs related to hardware == We can search for bugs which are related to a given hardware device or a given driver. We can search for bugs whose reporters own a given device. A device must be specified by a bus as enumerated by HWBus, a vendor ID and a product ID. If we search for bugs related to the PCI device (0x10de, 0x0455), which appears in a HWDB submission from Sample Person, bugs reported by him will be returned. >>> from lp.hardwaredb.interfaces.hwdb import HWBus >>> search_params = BugTaskSearchParams( ... user=None, hardware_bus=HWBus.PCI, hardware_vendor_id='0x10de', ... hardware_product_id='0x0455', hardware_owner_is_bug_reporter=True) >>> for bugtask in ubuntu.searchTasks(search_params): ... print bugtask.bug.id, bugtask.bug.owner.displayname 1 Sample Person 2 Sample Person If one of the parameters bus, vendor ID or prodct ID is missing, the query is not limited to any devices. In other words, we get the same result as if we would not have specified any hardware related parameters. >>> search_params = BugTaskSearchParams( ... user=None, hardware_bus=HWBus.PCI, hardware_vendor_id='0x10de', ... hardware_owner_is_bug_reporter=True) >>> for bugtask in ubuntu.searchTasks(search_params): ... print bugtask.bug.id, bugtask.bug.owner.displayname 1 Sample Person 9 Foo Bar 10 Foo Bar 2 Sample Person 19 No Privileges Person Similary, we can search for device drivers appearing in HWDB submissions of a bug reporter. >>> search_params = BugTaskSearchParams( ... user=None, hardware_driver_name='ehci_hcd', ... hardware_owner_is_bug_reporter=True) >>> for bugtask in ubuntu.searchTasks(search_params): ... print bugtask.bug.id, bugtask.bug.owner.displayname 1 Sample Person 2 Sample Person We can additionally specify a packge name. >>> search_params = BugTaskSearchParams( ... user=None, hardware_driver_name='ehci_hcd', ... hardware_driver_package_name='linux-image-2.6.24-19-generic', ... hardware_owner_is_bug_reporter=True) >>> for bugtask in ubuntu.searchTasks(search_params): ... print bugtask.bug.id, bugtask.bug.owner.displayname 1 Sample Person 2 Sample Person >>> search_params = BugTaskSearchParams( ... user=None, hardware_driver_name='ehci_hcd', ... hardware_driver_package_name='linux-image', ... hardware_owner_is_bug_reporter=True) >>> ubuntu.searchTasks(search_params).count() 0 If we specify a driver and a device, we'll get those bugs whose owners use the given device together with the given driver. >>> search_params = BugTaskSearchParams( ... user=None, hardware_bus=HWBus.PCI, hardware_vendor_id='0x10de', ... hardware_product_id='0x0455', hardware_driver_name='ehci_hcd', ... hardware_owner_is_bug_reporter=True) >>> for bugtask in ubuntu.searchTasks(search_params): ... print bugtask.bug.id, bugtask.bug.owner.displayname 1 Sample Person 2 Sample Person The PCI device (0x10de, 0x0455) is not controlled in any HWDB submission by the sd driver, so we'll get an empty result set for this query. >>> search_params = BugTaskSearchParams( ... user=None, hardware_bus=HWBus.PCI, hardware_vendor_id='0x10de', ... hardware_product_id='0x0455', hardware_driver_name='sd', ... hardware_owner_is_bug_reporter=True) >>> firefox.searchTasks(search_params).count() 0 We can also search for device owners which are subscribed to a bug. >>> sample_person = getUtility(IPersonSet).getByEmail('test@canonical.com') >>> search_params = BugTaskSearchParams( ... user=None, hardware_bus=HWBus.PCI, hardware_vendor_id='0x10de', ... hardware_product_id='0x0455', ... hardware_owner_is_subscribed_to_bug=True) >>> for bugtask in ubuntu.searchTasks(search_params): ... print bugtask.bug.id, bugtask.bug.isSubscribed(sample_person) 1 True 9 True And we can search for device owners who are affected by a bug. >>> bug_ten = getUtility(IBugSet).get(10) >>> bug_ten.markUserAffected(sample_person, affected=True) >>> search_params = BugTaskSearchParams( ... user=None, hardware_bus=HWBus.PCI, hardware_vendor_id='0x10de', ... hardware_product_id='0x0455', ... hardware_owner_is_affected_by_bug=True) >>> for bugtask in ubuntu.searchTasks(search_params): ... print bugtask.bug.id, bugtask.bug.isUserAffected(sample_person) 10 True Finally, we can search for who bugs which are directly linked to a HWDB submission, where the submission contains the given device or driver. >>> from lp.hardwaredb.interfaces.hwdb import IHWSubmissionSet >>> hw_submission = getUtility(IHWSubmissionSet).getBySubmissionKey( ... 'sample-submission') >>> bug_19 = getUtility(IBugSet).get(19) >>> bug_19.linkHWSubmission(hw_submission) >>> search_params = BugTaskSearchParams( ... user=None, hardware_bus=HWBus.PCI, hardware_vendor_id='0x10de', ... hardware_product_id='0x0455', hardware_is_linked_to_bug=True) >>> for bugtask in ubuntu.searchTasks(search_params): ... print bugtask.bug.id 19 If a device appears in a private submission, related bugs are shown only if the user running the request is the owner of the submission or an admin. >>> naked_hw_submission = removeSecurityProxy(hw_submission) >>> naked_hw_submission.private = True >>> search_params = BugTaskSearchParams( ... user=sample_person, hardware_bus=HWBus.PCI, ... hardware_vendor_id='0x10de', hardware_product_id='0x0455', ... hardware_is_linked_to_bug=True) >>> for bugtask in ubuntu.searchTasks(search_params): ... print bugtask.bug.id 19 >>> search_params = BugTaskSearchParams( ... user=foo_bar, hardware_bus=HWBus.PCI, hardware_vendor_id='0x10de', ... hardware_product_id='0x0455', hardware_is_linked_to_bug=True) >>> for bugtask in ubuntu.searchTasks(search_params): ... print bugtask.bug.id 19 Other users cannot see that a bug is related to a device from a private submission. >>> search_params = BugTaskSearchParams( ... user=no_priv, hardware_bus=HWBus.PCI, hardware_vendor_id='0x10de', ... hardware_product_id='0x0455', hardware_is_linked_to_bug=True) >>> ubuntu.searchTasks(search_params).count() 0 >>> search_params = BugTaskSearchParams( ... user=None, hardware_bus=HWBus.PCI, hardware_vendor_id='0x10de', ... hardware_product_id='0x0455', hardware_is_linked_to_bug=True) >>> ubuntu.searchTasks(search_params).count() 0 == Searching for bugs affecting me == The user searching for bugs can search for bugs affecting him. We search for bugs affecting foo_bar, then check that all the results return True for isUserAffected(foo_bar). >>> search_params = BugTaskSearchParams( ... user=foo_bar, affects_me=True) >>> print reduce( ... lambda x, y: x and y, ... [task.bug.isUserAffected(foo_bar) ... for task in firefox.searchTasks(search_params)]) True == Searching for bugs linked to branches == We can search for bugs having branches linked to them. >>> from lp.bugs.interfaces.bugtask import BugBranchSearch >>> search_params = BugTaskSearchParams( ... user=None, linked_branches=BugBranchSearch.BUGS_WITH_BRANCHES) >>> for task in firefox.searchTasks(search_params): ... print task.bug.id, task.bug.linked_branches.count() 4 2 5 1 Similarly, we can search for bugs that do not have any linked branches. >>> from lp.bugs.interfaces.bugtask import BugBranchSearch >>> search_params = BugTaskSearchParams( ... user=None, linked_branches=BugBranchSearch.BUGS_WITHOUT_BRANCHES) >>> for task in firefox.searchTasks(search_params): ... print task.bug.id, task.bug.linked_branches.count() 1 0 6 0 18 0 20 0 21 0 And we can search for bugs linked to a specific branch. >>> search_params = BugTaskSearchParams( ... user=None, linked_branches=1) >>> for task in firefox.searchTasks(search_params): ... print task.bug.id 4 5 == Ordering search results == The result returned by bugtask searches can come sorted by a specified order === Ordering by number of duplicates === It is possible to sort the results by the number of duplicates each bag has. Here is the list of bugs for Ubuntu. >>> params = BugTaskSearchParams( ... orderby='-number_of_duplicates', user=None) >>> ubuntu_tasks = ubuntu.searchTasks(params) >>> for bugtask in ubuntu_tasks: ... print bugTaskInfo(bugtask) 17 1 mozilla-firefox (Ubuntu) Firefox does not support SVG 23 9 thunderbird (Ubuntu) Thunderbird crashes 25 10 linux-source-2.6.15 (Ubuntu) another test bug 26 2 Ubuntu Blackhole Trash folder 41 19 cdrkit (Ubuntu) Bug to be fixed in trunk None of these bugs have any duplicates. >>> [bugtask.bug.id for bugtask in ubuntu_tasks ... if bugtask.bug.duplicateof is not None] [] >>> from canonical.database.sqlbase import flush_database_updates We mark bug #10 as a duplicate of bug #9. >>> bug_nine = getUtility(IBugSet).get(9) >>> bug_ten = getUtility(IBugSet).get(10) >>> bug_ten.markAsDuplicate(bug_nine) >>> flush_database_updates() Searching again reveals bug #9 at the top of the list, since it now has a duplicate. >>> ubuntu_tasks = ubuntu.searchTasks(params) >>> for bugtask in ubuntu_tasks: ... print bugTaskInfo(bugtask) 23 9 thunderbird (Ubuntu) Thunderbird crashes 17 1 mozilla-firefox (Ubuntu) Firefox does not support SVG 25 10 linux-source-2.6.15 (Ubuntu) another test bug 26 2 Ubuntu Blackhole Trash folder 41 19 cdrkit (Ubuntu) Bug to be fixed in trunk === Ordering by number of comments === It is also possible to sort the results by the number of comments on a bug. Here is the list of bugs for Ubuntu, sorted by their number of comments. >>> params = BugTaskSearchParams( ... orderby='-message_count', user=None) >>> ubuntu_tasks = ubuntu.searchTasks(params) >>> for bugtask in ubuntu_tasks: ... bug = bugtask.bug ... print '%s %s [%s comments]' % ( ... bug.id, bug.title, bug.message_count) 2 Blackhole Trash folder [5 comments] 1 Firefox does not support SVG [3 comments] 10 another test bug [2 comments] 9 Thunderbird crashes [1 comments] 19 Bug to be fixed in trunk [1 comments] === Ordering by bug heat === Another way of sorting searches is by bug heat. >>> params = BugTaskSearchParams( ... orderby='id', user=None) >>> ubuntu_tasks = ubuntu.searchTasks(params) >>> for task in ubuntu_tasks: ... task.bug.setHeat(task.bug.id) >>> transaction.commit() >>> params = BugTaskSearchParams( ... orderby='-heat', user=None) >>> ubuntu_tasks = ubuntu.searchTasks(params) >>> for bugtask in ubuntu_tasks: ... bug = bugtask.bug ... print '%s %s [heat: %s]' % ( ... bug.id, bug.title, bug.heat) 19 Bug to be fixed in trunk [heat: 19] 10 another test bug [heat: 10] 9 Thunderbird crashes [heat: 9] 2 Blackhole Trash folder [heat: 2] 1 Firefox does not support SVG [heat: 1] === Ordering by patch age === We can also sort search results by the creation time of the youngest patch attached to a bug. Since we have at present no bugs with patches, we use effectively the default sort order, by bug task ID (which is implicitly added as a "second level" sort order to ensure reliable sorting). >>> params = BugTaskSearchParams( ... orderby='latest_patch_uploaded', user=None) >>> ubuntu_tasks = ubuntu.searchTasks(params) >>> for bugtask in ubuntu_tasks: ... print bugTaskInfo(bugtask) 17 1 mozilla-firefox (Ubuntu) Firefox does not support SVG 23 9 thunderbird (Ubuntu) Thunderbird crashes 25 10 linux-source-2.6.15 (Ubuntu) another test bug 26 2 Ubuntu Blackhole Trash folder 41 19 cdrkit (Ubuntu) Bug to be fixed in trunk If we add a patch attachment to bug 2 and bug 10, they are listed first. >>> patch_attachment_bug_2 = factory.makeBugAttachment( ... bug=bug_two, is_patch=True) >>> transaction.commit() >>> patch_attachment_bug_10 = factory.makeBugAttachment( ... bug=bug_ten, is_patch=True) >>> params = BugTaskSearchParams( ... orderby='latest_patch_uploaded', user=None) >>> ubuntu_tasks = ubuntu.searchTasks(params) >>> for bugtask in ubuntu_tasks: ... print bugTaskInfo(bugtask) 26 2 Ubuntu Blackhole Trash folder 25 10 linux-source-2.6.15 (Ubuntu) another test bug 17 1 mozilla-firefox (Ubuntu) Firefox does not support SVG 23 9 thunderbird (Ubuntu) Thunderbird crashes 41 19 cdrkit (Ubuntu) Bug to be fixed in trunk == Searching using a flat interface == An alternative, simplified interface for searching bug tasks is available by passing all search parameters as function arguments to searchTasks. This interface corresponds to the search options available from the web search form and the public API. >>> def print_bugtasks(bugtasks): ... for bugtask in bugtasks: ... print '%s %s %s %s %s' % ( ... bugtask.bug.id, bugtask.bugtargetdisplayname, ... bugtask.bug.title, bugtask.status.title.upper(), ... bugtask.importance.title.upper()) When we call the method on Ubuntu, without any parameters, the result is identical to calling searchTasks. >>> print_bugtasks(ubuntu.searchTasks(None, user=None)) 1 mozilla-firefox (Ubuntu) Firefox does not support SVG NEW MEDIUM 9 thunderbird (Ubuntu) Thunderbird crashes CONFIRMED MEDIUM 2 Ubuntu Blackhole Trash folder NEW MEDIUM 19 cdrkit (Ubuntu) Bug to be fixed in trunk NEW UNDECIDED If we want to restrict the search using certain parameters pass them to the function directly. Here we search Ubuntu again, but only bugs for the firefox package. >>> from lp.registry.interfaces.sourcepackagename import ( ... ISourcePackageNameSet) >>> source_package_name_set = getUtility(ISourcePackageNameSet) >>> firefox_source_package = source_package_name_set['mozilla-firefox'] >>> print_bugtasks(ubuntu.searchTasks( ... None, user=None, sourcepackagename=firefox_source_package)) 1 mozilla-firefox (Ubuntu) Firefox does not support SVG NEW MEDIUM Or search for bugs on a distribution source package directly. >>> print_bugtasks(ubuntu_firefox.searchTasks(None, user=None)) 1 mozilla-firefox (Ubuntu) Firefox does not support SVG NEW MEDIUM Or we can search a certain milestone (only getting bugs targeted to it). >>> firefox_milestone_1 = firefox.getMilestone('1.0') >>> print_bugtasks(firefox_milestone_1.searchTasks(None, user=None)) 1 Mozilla Firefox Firefox does not support SVG NEW LOW 1 Mozilla Firefox 1.0 Firefox does not support SVG NEW UNDECIDED We can restrict our search for firefox bugs with a text search. >>> print_bugtasks(firefox.searchTasks( ... None, user=None, search_text='instructions')) 5 Mozilla Firefox Firefox install instructions should be complete NEW CRITICAL Or restrict our search (over the mozilla project this time) to a list of relevant importance values. >>> print_bugtasks(mozilla.searchTasks( ... None, user=None, ... importance=[BugTaskImportance.LOW, BugTaskImportance.MEDIUM])) 4 Mozilla Firefox Reflow problems with complex page layouts NEW MEDIUM 1 Mozilla Firefox Firefox does not support SVG NEW LOW == Searching by structural subscriber == The 'structural_subscriber' search parameter allows one to search all the bug tasks to which a person is structurally subscribed. A person can be a structural subscriber for a product, a product series, a project, a milestone, a distribution, a distribution series and a distribution source package. No Privileges Person isn't a structural subscriber, so no bug tasks are found: >>> from lp.registry.interfaces.person import IPersonSet >>> no_priv = getUtility(IPersonSet).getByName('no-priv') >>> no_priv_struct_sub = BugTaskSearchParams( ... user=None, structural_subscriber=no_priv) >>> found_bugtasks = bugtask_set.search(no_priv_struct_sub) >>> found_bugtasks.count() 0 Create a new person and make them a subscriber to all Firefox (product) bug reports. Subsequently, we confirm that they are subscribed to all of the Firefox bug tasks. >>> product_struct_subber = factory.makePerson( ... name='product-struct-subber') >>> firefox.addBugSubscription(product_struct_subber, ... product_struct_subber) <...StructuralSubscription object at ...> >>> product_struct_sub_search = BugTaskSearchParams( ... user=None, structural_subscriber=product_struct_subber) >>> found_bugtasks = bugtask_set.search(product_struct_sub_search) >>> found_bugtasks.count() 7 Create a new person and subscribe them to all of the bug tasks for a product series. We then test to see that they are subscribed to all of the bug tasks for the product series in which they are interested. >>> product_series = firefox.getSeries('1.0') >>> all_targeted = BugTaskSearchParams(user=None, omit_targeted=False) >>> series_tasks = product_series.searchTasks(all_targeted) >>> series_struct_subber = factory.makePerson( ... name='series-struct-subber') >>> product_series.addBugSubscription(series_struct_subber, ... series_struct_subber) <...StructuralSubscription object at ...> >>> series_struct_sub_search = BugTaskSearchParams( ... user=None, structural_subscriber=series_struct_subber) >>> found_bugtasks = bugtask_set.search(series_struct_sub_search) >>> found_bugtasks.count() 2 Create a new product which will be a part of a project group. A bug is created for the product which should then show up for structural subscribers of the project group. Then a new person is created who is subscribed to all of the bug reports about the project. Search for bug tasks that this new person is subscribed. >>> product = factory.makeProduct() >>> bug = factory.makeBug(product=product) >>> project = factory.makeProject() >>> product.project = project >>> project_struct_subber = factory.makePerson( ... name='project-struct-subber') >>> project.addBugSubscription(project_struct_subber, ... project_struct_subber) <...StructuralSubscription object at ...> >>> project_struct_sub_search = BugTaskSearchParams( ... user=None, structural_subscriber=project_struct_subber) >>> found_bugtasks = bugtask_set.search(project_struct_sub_search) >>> found_bugtasks.count() 1 We will also subscribe this project subscriber to a product that is a part of the project and ensure that duplicate bug tasks do not appear in the search results. >>> product2 = factory.makeProduct() >>> bug = factory.makeBug(product=product2) >>> product2.project = project >>> product2.addBugSubscription(project_struct_subber, ... project_struct_subber) <...StructuralSubscription object at ...> >>> project_struct_sub_search = BugTaskSearchParams( ... user=None, structural_subscriber=project_struct_subber) >>> found_bugtasks = bugtask_set.search(project_struct_sub_search) >>> found_bugtasks.count() 2 Create a new person and subscribe them to all of bug tasks targeted to a milestone. We then test to see that they are subscribed to all of the bug tasks for the milestone in which they are interested. >>> milestone_struct_subber = factory.makePerson( ... name='milestone-struct-subber') >>> product_milestone.addBugSubscription(milestone_struct_subber, ... milestone_struct_subber) <...StructuralSubscription object at ...> >>> milestone_struct_sub_search = BugTaskSearchParams( ... user=None, structural_subscriber=milestone_struct_subber) >>> found_bugtasks = bugtask_set.search(milestone_struct_sub_search) >>> found_bugtasks.count() 2 Create another new person and subscribe them to all the Ubuntu bug reports - crazy I know. Then test to see that this poor person is subscribed to all bugs with an Ubuntu bug task. >>> distro_struct_subber = factory.makePerson( ... name='distro-struct-subber') >>> ubuntu.addBugSubscription(distro_struct_subber, ... distro_struct_subber) <...StructuralSubscription object at ...> >>> distro_struct_sub_search = BugTaskSearchParams( ... user=None, structural_subscriber=distro_struct_subber) >>> found_bugtasks = bugtask_set.search(distro_struct_sub_search) >>> found_bugtasks.count() 5 Create a new person who will only be subscribed to an Ubuntu series, which is something more reasonable than all of Ubuntu. Test to ensure that this person is subscribed to all of the bug tasks about that distro series. >>> ubuntu = getUtility(IDistributionSet).getByName('ubuntu') >>> hoary = ubuntu.getSeries('hoary') >>> distro_series_struct_subber = factory.makePerson( ... name='distro-series-struct-subber') >>> hoary.addBugSubscription(distro_series_struct_subber, ... distro_series_struct_subber) <...StructuralSubscription object at ...> >>> distro_series_struct_sub_search = BugTaskSearchParams( ... user=None, structural_subscriber=distro_series_struct_subber) >>> found_bugtasks = bugtask_set.search(distro_series_struct_sub_search) >>> all_targeted = BugTaskSearchParams(user=None, omit_targeted=False) >>> hoary_bugtasks = hoary.searchTasks(all_targeted) >>> found_bugtasks.count() == hoary_bugtasks.count() True Create a new person and make them a subscriber to all Ubuntu Firefox (a distribution source package) bug reports. Test to see that the new person is subscribed to all of the Ubuntu Firefox bug tasks. >>> package_struct_subber = factory.makePerson( ... name='package-struct-subber') >>> ubuntu_firefox.addBugSubscription(package_struct_subber, ... package_struct_subber) <...StructuralSubscription object at ...> >>> package_struct_sub_search = BugTaskSearchParams( ... user=None, structural_subscriber=package_struct_subber) >>> found_bugtasks = bugtask_set.search(package_struct_sub_search) >>> found_bugtasks.count() 1 We'll also subscribe the person who is currently subscribed to a package's bug reports, package_struct_subber, to the bug reports of a product series to ensure that the structural_subscriber search is returning the set of both bug tasks. >>> product_series.addBugSubscription(package_struct_subber, ... package_struct_subber) <...StructuralSubscription object at ...> >>> package_struct_sub_search = BugTaskSearchParams( ... user=None, structural_subscriber=package_struct_subber) >>> found_bugtasks = bugtask_set.search(package_struct_sub_search) >>> combined_bugtasks_count = (ubuntu_firefox_bugs.count () + ... series_tasks.count()) >>> found_bugtasks.count() == combined_bugtasks_count True