← Back to team overview

launchpad-reviewers team mailing list archive

[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