launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #01617
[Merge] lp:~adeuring/launchpad/bug-594247-unittests-for-searchtasks into lp:launchpad/devel
Abel Deuring has proposed merging lp:~adeuring/launchpad/bug-594247-unittests-for-searchtasks into lp:launchpad/devel.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
This branch is the start of a sort of "testing madness" for the
methods BugTaskSet.search() and BugTaskSet.bugIds().
The idea is to run tests of both methods for a larger set of
variations of search parameters, where tests for search() are
even run with the parameter _noprejoins set to True and to False.
Reason: When I tried to fix bug 594247 as sketched by stub in
https://bugs.edge.launchpad.net/malone/+bug/594247/comments/5 ,
i.e. by using
LEFT JOIN Product ON BugTask.product = Product.id
JOIN StructuralSubscription ON (longer_expression)
in the FROM clause of the main SELECT query if structural
subscribers are used, it was quite easy to change the query for
one case: calls like BugTaskSet.search(params, _noprejoins=True)
Implementing this properly for _noprejoins=False required a bit more
work, at another location inside BugTaskSet.search(): In this case,
the table Product is LEFT JOINed in order to pre-load Product
instances that are the targets of bugtasks.
So the table Product would be joined twice, if BugTaskSearchParams
specifies a structural subscriber, which is a bit pointless (and it
would require to use an alias for one of the JOINed tables).
This can of course be avoided, but it must be tested: The cases
_noprejoins == (True|False) and (len(args) == 0 | len(args) > 0)
combined with avoiding duplicate JOINs of the same table should
all be tested. Since they affect not only the WHERE expression
of the query, but a mix of tables, prejoins and the WHERE expression,
all combinations should tested. (While working on the branch that
fixes 594247, I could easily break existing tests of searching
for bugtasks with structural subscribers by simply adding/removing
a parameter _noprejoins=True, or by replacing bugtaskset.search(params)
by bugtaskset.searchBugIds())
Finally, it seems that using
SELECT something FROM A (LEFT) JOIN B ON join_expr WHERE condition
can be more efficient than
SELECT something FROM A, B WHERE join_expr AND condition
Doing such changes for other search parameters would also require
compresive tests. So I started a slightly mad unit test:
- define most tests in a base class SearchTestBase
- define bug targets in other base classes like ProductTarget,
ProductSeriesTarget etc. These classes create a few bug tasks
for their respective targets, and the provide a method
getBugTaskSearchParams() which creates a BugTaskSearchParams
object where the "right" bug target is specified.
- Three search varians are defined in other base classes:
- run BugTaskSet.search() with _noprejoins==True
- run BugTaskSet.search() with _noprejoins==False
- run BugTaskSet.searchBugIds()
These base classes are combined using the classobj() factory; the test
classes created this way are sneaked into the module via
module.__dict__[class_name] = test_class
The result is that each test defined in SearchtestBase is run 24
times: For eight bug targets and three search variants...
The test themsleves are not yet very comprehensive, but the diff is
already 400 lines long...
test: ./bin/test -vvt test_bugtask_search
no lint
--
https://code.launchpad.net/~adeuring/launchpad/bug-594247-unittests-for-searchtasks/+merge/38863
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~adeuring/launchpad/bug-594247-unittests-for-searchtasks into lp:launchpad/devel.
=== added file 'lib/lp/bugs/tests/test_bugtask_search.py'
--- lib/lp/bugs/tests/test_bugtask_search.py 1970-01-01 00:00:00 +0000
+++ lib/lp/bugs/tests/test_bugtask_search.py 2010-10-19 17:28:51 +0000
@@ -0,0 +1,394 @@
+# Copyright 2010 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+__metaclass__ = type
+
+from new import classobj
+import sys
+import unittest
+
+from zope.component import getUtility
+
+from canonical.testing.layers import DatabaseFunctionalLayer
+
+from lp.bugs.interfaces.bugtask import (
+ BugTaskImportance,
+ BugTaskSearchParams,
+ BugTaskStatus,
+ IBugTaskSet,
+ )
+from lp.testing import (
+ person_logged_in,
+ TestCaseWithFactory,
+ )
+
+
+class SearchTestBase(object):
+
+ layer = DatabaseFunctionalLayer
+
+ def setUp(self):
+ super(SearchTestBase, self).setUp()
+ self.bugtask_set = getUtility(IBugTaskSet)
+
+ def test_search_all_bugtasks_for_target(self):
+ # BugTaskSet.search() returns all bug tasks for a given bug
+ # target, if only the bug target is passed as a search parameter.
+ params = self.getBugTaskSearchParams(user=None)
+ search_result = self.runSearch(params)
+ expected = self.resultValuesForBugtasks(self.bugtasks)
+ self.assertEqual(expected, search_result)
+
+ def test_private_bug_not_in_search_result_for_anonymous(self):
+ # Private bugs are not included in search results for anonymous users.
+ with person_logged_in(self.owner):
+ self.bugtasks[-1].bug.setPrivate(True, self.owner)
+ params = self.getBugTaskSearchParams(user=None)
+ search_result = self.runSearch(params)
+ expected = self.resultValuesForBugtasks(self.bugtasks)[:-1]
+ self.assertEqual(expected, search_result)
+
+ def test_private_bug_not_in_search_result_for_regular_user(self):
+ # Private bugs are not included in search results for ordinary users.
+ with person_logged_in(self.owner):
+ self.bugtasks[-1].bug.setPrivate(True, self.owner)
+ user = self.factory.makePerson()
+ params = self.getBugTaskSearchParams(user=user)
+ search_result = self.runSearch(params)
+ expected = self.resultValuesForBugtasks(self.bugtasks)[:-1]
+ self.assertEqual(expected, search_result)
+
+ def test_private_bug_in_search_result_for_subscribed_user(self):
+ # Private bugs are included in search results for ordinary users
+ # which are subscribed to the bug.
+ user = self.factory.makePerson()
+ with person_logged_in(self.owner):
+ self.bugtasks[-1].bug.setPrivate(True, self.owner)
+ self.bugtasks[-1].bug.subscribe(user, self.owner)
+ params = self.getBugTaskSearchParams(user=user)
+ search_result = self.runSearch(params)
+ expected = self.resultValuesForBugtasks(self.bugtasks)
+ self.assertEqual(expected, search_result)
+
+
+class BugTargetTestBase:
+ """A base class for the bug target mixin classes."""
+
+ def makeBugTasks(self, bugtarget):
+ self.bugtasks = []
+ with person_logged_in(self.owner):
+ self.bugtasks.append(
+ self.factory.makeBugTask(target=bugtarget))
+ self.bugtasks[-1].importance = BugTaskImportance.HIGH
+ self.bugtasks[-1].transitionToStatus(
+ BugTaskStatus.TRIAGED, self.owner)
+
+ self.bugtasks.append(
+ self.factory.makeBugTask(target=bugtarget))
+ self.bugtasks[-1].importance = BugTaskImportance.LOW
+ self.bugtasks[-1].transitionToStatus(
+ BugTaskStatus.NEW, self.owner)
+
+ self.bugtasks.append(
+ self.factory.makeBugTask(target=bugtarget))
+ self.bugtasks[-1].importance = BugTaskImportance.CRITICAL
+ self.bugtasks[-1].transitionToStatus(
+ BugTaskStatus.FIXCOMMITTED, self.owner)
+
+
+class BugTargetWithBugSuperVisor:
+ """A base class for bug targets which have a bug supervisor."""
+
+ def test_search_by_bug_supervisor(self):
+ # We can search for bugs by bug supervisor.
+ # We have by default no bug supervisor set, so searching for
+ # bugs by supervisor returns no data.
+ supervisor = self.factory.makeTeam(owner=self.owner)
+ params = self.getBugTaskSearchParams(
+ user=None, bug_supervisor=supervisor)
+ search_result = self.runSearch(params)
+ self.assertEqual([], search_result)
+
+ # If we appoint a bug supervisor, searching for bug tasks
+ # by supervisor will return all bugs for our test target.
+ self.setSupervisor(supervisor)
+ search_result = self.runSearch(params)
+ expected = self.resultValuesForBugtasks(self.bugtasks)
+ self.assertEqual(expected, search_result)
+
+ def setSupervisor(self, supervisor):
+ """Set the bug supervisor for the bug task target."""
+ with person_logged_in(self.owner):
+ self.searchtarget.setBugSupervisor(supervisor, self.owner)
+
+
+class ProductTarget(BugTargetTestBase, BugTargetWithBugSuperVisor):
+ """Use a product as the bug target."""
+
+ def setUp(self):
+ super(ProductTarget, self).setUp()
+ self.searchtarget = self.factory.makeProduct()
+ self.owner = self.searchtarget.owner
+ self.makeBugTasks(self.searchtarget)
+
+ def getBugTaskSearchParams(self, *args, **kw):
+ """Return a BugTaskSearchParams object for the given parameters.
+
+ Also, set the bug target.
+ """
+ params = BugTaskSearchParams(*args, **kw)
+ params.setProduct(self.searchtarget)
+ return params
+
+
+class ProductSeriesTarget(BugTargetTestBase):
+ """Use a product series as the bug target."""
+
+ def setUp(self):
+ super(ProductSeriesTarget, self).setUp()
+ self.searchtarget = self.factory.makeProductSeries()
+ self.owner = self.searchtarget.owner
+ self.makeBugTasks(self.searchtarget)
+
+ def getBugTaskSearchParams(self, *args, **kw):
+ """Return a BugTaskSearchParams object for the given parameters.
+
+ Also, set the bug target.
+ """
+ params = BugTaskSearchParams(*args, **kw)
+ params.setProductSeries(self.searchtarget)
+ return params
+
+
+class ProjectGroupTarget(BugTargetTestBase, BugTargetWithBugSuperVisor):
+ """Use a project group as the bug target."""
+
+ def setUp(self):
+ super(ProjectGroupTarget, self).setUp()
+ self.searchtarget = self.factory.makeProject()
+ self.owner = self.searchtarget.owner
+ self.makeBugTasks()
+
+ def getBugTaskSearchParams(self, *args, **kw):
+ """Return a BugTaskSearchParams object for the given parameters.
+
+ Also, set the bug target.
+ """
+ params = BugTaskSearchParams(*args, **kw)
+ params.setProject(self.searchtarget)
+ return params
+
+ def makeBugTasks(self):
+ """Create bug tasks for the search target."""
+ self.bugtasks = []
+ with person_logged_in(self.owner):
+ product = self.factory.makeProduct(owner=self.owner)
+ product.project = self.searchtarget
+ self.bugtasks.append(
+ self.factory.makeBugTask(target=product))
+ self.bugtasks[-1].importance = BugTaskImportance.HIGH
+ self.bugtasks[-1].transitionToStatus(
+ BugTaskStatus.TRIAGED, self.owner)
+
+ product = self.factory.makeProduct(owner=self.owner)
+ product.project = self.searchtarget
+ self.bugtasks[-1].importance = BugTaskImportance.LOW
+ self.bugtasks[-1].transitionToStatus(
+ BugTaskStatus.NEW, self.owner)
+
+ product = self.factory.makeProduct(owner=self.owner)
+ product.project = self.searchtarget
+ self.bugtasks.append(
+ self.factory.makeBugTask(target=product))
+ self.bugtasks[-1].importance = BugTaskImportance.CRITICAL
+ self.bugtasks[-1].transitionToStatus(
+ BugTaskStatus.FIXCOMMITTED, self.owner)
+
+ def setSupervisor(self, supervisor):
+ """Set the bug supervisor for the bug task targets."""
+ with person_logged_in(self.owner):
+ # We must set the bug supervisor for each bug task target
+ for bugtask in self.bugtasks:
+ bugtask.target.setBugSupervisor(supervisor, self.owner)
+
+
+class MilestoneTarget(BugTargetTestBase):
+ """Use a milestone as the bug target."""
+
+ def setUp(self):
+ super(MilestoneTarget, self).setUp()
+ self.product = self.factory.makeProduct()
+ self.searchtarget = self.factory.makeMilestone(product=self.product)
+ self.owner = self.product.owner
+ self.makeBugTasks(self.product)
+
+ def getBugTaskSearchParams(self, *args, **kw):
+ """Return a BugTaskSearchParams object for the given parameters.
+
+ Also, set the bug target.
+ """
+ params = BugTaskSearchParams(milestone=self.searchtarget, *args, **kw)
+ return params
+
+ def makeBugTasks(self, bugtarget):
+ """Create bug tasks for a product and assign them to a milestone."""
+ super(MilestoneTarget, self).makeBugTasks(bugtarget)
+ with person_logged_in(self.owner):
+ for bugtask in self.bugtasks:
+ bugtask.transitionToMilestone(self.searchtarget, self.owner)
+
+
+class DistributionTarget(BugTargetTestBase, BugTargetWithBugSuperVisor):
+ """Use a dirstibution as the bug target."""
+
+ def setUp(self):
+ super(DistributionTarget, self).setUp()
+ self.searchtarget = self.factory.makeDistribution()
+ self.owner = self.searchtarget.owner
+ self.makeBugTasks(self.searchtarget)
+
+ def getBugTaskSearchParams(self, *args, **kw):
+ """Return a BugTaskSearchParams object for the given parameters.
+
+ Also, set the bug target.
+ """
+ params = BugTaskSearchParams(*args, **kw)
+ params.setDistribution(self.searchtarget)
+ return params
+
+
+class DistroseriesTarget(BugTargetTestBase):
+ """Use a distro series as the bug target."""
+
+ def setUp(self):
+ super(DistroseriesTarget, self).setUp()
+ self.searchtarget = self.factory.makeDistroSeries()
+ self.owner = self.searchtarget.owner
+ self.makeBugTasks(self.searchtarget)
+
+ def getBugTaskSearchParams(self, *args, **kw):
+ """Return a BugTaskSearchParams object for the given parameters.
+
+ Also, set the bug target.
+ """
+ params = BugTaskSearchParams(*args, **kw)
+ params.setDistroSeries(self.searchtarget)
+ return params
+
+
+class SourcePackageTarget(BugTargetTestBase):
+ """Use a source package as the bug target."""
+
+ def setUp(self):
+ super(SourcePackageTarget, self).setUp()
+ self.searchtarget = self.factory.makeSourcePackage()
+ self.owner = self.searchtarget.distroseries.owner
+ self.makeBugTasks(self.searchtarget)
+
+ def getBugTaskSearchParams(self, *args, **kw):
+ """Return a BugTaskSearchParams object for the given parameters.
+
+ Also, set the bug target.
+ """
+ params = BugTaskSearchParams(*args, **kw)
+ params.setSourcePackage(self.searchtarget)
+ return params
+
+
+class DistributionSourcePackageTarget(BugTargetTestBase,
+ BugTargetWithBugSuperVisor):
+ """Use a distribution source package as the bug target."""
+
+ def setUp(self):
+ super(DistributionSourcePackageTarget, self).setUp()
+ self.searchtarget = self.factory.makeDistributionSourcePackage()
+ self.owner = self.searchtarget.distribution.owner
+ self.makeBugTasks(self.searchtarget)
+
+ def getBugTaskSearchParams(self, *args, **kw):
+ """Return a BugTaskSearchParams object for the given parameters.
+
+ Also, set the bug target.
+ """
+ params = BugTaskSearchParams(*args, **kw)
+ params.setSourcePackage(self.searchtarget)
+ return params
+
+ def setSupervisor(self, supervisor):
+ """Set the bug supervisor for the bug task target."""
+ with person_logged_in(self.owner):
+ self.searchtarget.distribution.setBugSupervisor(
+ supervisor, self.owner)
+
+
+bug_targets_mixins = (
+ DistributionTarget,
+ DistributionSourcePackageTarget,
+ DistroseriesTarget,
+ MilestoneTarget,
+ ProductSeriesTarget,
+ ProductTarget,
+ ProjectGroupTarget,
+ SourcePackageTarget,
+ )
+
+
+class PreloadBugtaskTargets:
+ """Preload bug targets during a BugTaskSet.search() query."""
+
+ def setUp(self):
+ super(PreloadBugtaskTargets, self).setUp()
+
+ def runSearch(self, params, *args):
+ """Run BugTaskSet.search() and preload bugtask target objects."""
+ return list(self.bugtask_set.search(params, *args, _noprejoins=False))
+
+ def resultValuesForBugtasks(self, expected_bugtasks):
+ return expected_bugtasks
+
+
+class NoPreloadBugtaskTargets:
+ """Do not preload bug targets during a BugTaskSet.search() query."""
+
+ def setUp(self):
+ super(NoPreloadBugtaskTargets, self).setUp()
+
+ def runSearch(self, params, *args):
+ """Run BugTaskSet.search() without preloading bugtask targets."""
+ return list(self.bugtask_set.search(params, *args, _noprejoins=True))
+
+ def resultValuesForBugtasks(self, expected_bugtasks):
+ return expected_bugtasks
+
+
+class QueryBugIDs:
+ """Do not preload bug targets during a BugTaskSet.search() query."""
+
+ def setUp(self):
+ super(QueryBugIDs, self).setUp()
+
+ def runSearch(self, params, *args):
+ """Run BugTaskSet.searchBugIds()."""
+ return list(self.bugtask_set.searchBugIds(params))
+
+ def resultValuesForBugtasks(self, expected_bugtasks):
+ return [bugtask.bug.id for bugtask in expected_bugtasks]
+
+
+def test_suite():
+ module = sys.modules[__name__]
+ for bug_targetsearch_type_class in (
+ PreloadBugtaskTargets, NoPreloadBugtaskTargets, QueryBugIDs):
+ for target_mixin in bug_targets_mixins:
+ class_name = 'Test%s%s' % (
+ bug_targetsearch_type_class.__name__,
+ target_mixin.__name__)
+ test_class = classobj(
+ class_name,
+ (target_mixin, bug_targetsearch_type_class, SearchTestBase,
+ TestCaseWithFactory),
+ {})
+ module.__dict__[class_name] = test_class
+ suite = unittest.TestSuite()
+ suite.addTest(unittest.TestLoader().loadTestsFromName(__name__))
+ return suite