| Index: appengine/monorail/search/test/frontendsearchpipeline_test.py
|
| diff --git a/appengine/monorail/search/test/frontendsearchpipeline_test.py b/appengine/monorail/search/test/frontendsearchpipeline_test.py
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..7e5bd8bb2d2729eec177b0db94a70d03a50a29b0
|
| --- /dev/null
|
| +++ b/appengine/monorail/search/test/frontendsearchpipeline_test.py
|
| @@ -0,0 +1,859 @@
|
| +# Copyright 2016 The Chromium Authors. All rights reserved.
|
| +# Use of this source code is govered by a BSD-style
|
| +# license that can be found in the LICENSE file or at
|
| +# https://developers.google.com/open-source/licenses/bsd
|
| +
|
| +"""Tests for the frontendsearchpipeline module."""
|
| +
|
| +import mox
|
| +import unittest
|
| +
|
| +from google.appengine.api import memcache
|
| +from google.appengine.api import modules
|
| +from google.appengine.ext import testbed
|
| +from google.appengine.api import urlfetch
|
| +
|
| +import settings
|
| +from framework import profiler
|
| +from framework import sorting
|
| +from framework import urls
|
| +from proto import ast_pb2
|
| +from proto import project_pb2
|
| +from proto import tracker_pb2
|
| +from search import frontendsearchpipeline
|
| +from search import searchpipeline
|
| +from services import service_manager
|
| +from testing import fake
|
| +from testing import testing_helpers
|
| +from tracker import tracker_bizobj
|
| +
|
| +
|
| +# Just an example timestamp. The value does not matter.
|
| +NOW = 2444950132
|
| +
|
| +
|
| +class FrontendSearchPipelineTest(unittest.TestCase):
|
| +
|
| + def setUp(self):
|
| + self.cnxn = 'fake cnxn'
|
| + self.config = tracker_bizobj.MakeDefaultProjectIssueConfig(789)
|
| + self.services = service_manager.Services(
|
| + user=fake.UserService(),
|
| + project=fake.ProjectService(),
|
| + issue=fake.IssueService(),
|
| + config=fake.ConfigService(),
|
| + cache_manager=fake.CacheManager())
|
| + self.profiler = profiler.Profiler()
|
| + self.services.user.TestAddUser('a@example.com', 111L)
|
| + self.project = self.services.project.TestAddProject('proj', project_id=789)
|
| + self.mr = testing_helpers.MakeMonorailRequest(
|
| + path='/p/proj/issues/list', project=self.project)
|
| + self.mr.me_user_id = 111L
|
| +
|
| + self.issue_1 = fake.MakeTestIssue(
|
| + 789, 1, 'one', 'New', 111L, labels=['Priority-High'])
|
| + self.services.issue.TestAddIssue(self.issue_1)
|
| + self.issue_2 = fake.MakeTestIssue(
|
| + 789, 2, 'two', 'New', 111L, labels=['Priority-Low'])
|
| + self.services.issue.TestAddIssue(self.issue_2)
|
| + self.issue_3 = fake.MakeTestIssue(
|
| + 789, 3, 'three', 'New', 111L, labels=['Priority-Medium'])
|
| + self.services.issue.TestAddIssue(self.issue_3)
|
| + self.mr.sort_spec = 'Priority'
|
| +
|
| + self.mox = mox.Mox()
|
| + self.testbed = testbed.Testbed()
|
| + self.testbed.activate()
|
| + self.testbed.init_user_stub()
|
| + self.testbed.init_memcache_stub()
|
| + sorting.InitializeArtValues(self.services)
|
| +
|
| + def tearDown(self):
|
| + self.testbed.deactivate()
|
| + self.mox.UnsetStubs()
|
| + self.mox.ResetAll()
|
| +
|
| + def testSearchForIIDs_AllResultsCached_AllAtRiskCached(self):
|
| + unfiltered_iids = {1: [1001, 1011]}
|
| + nonviewable_iids = {1: set()}
|
| + self.mox.StubOutWithMock(frontendsearchpipeline, '_StartBackendSearch')
|
| + frontendsearchpipeline._StartBackendSearch(
|
| + self.mr, set(['proj']), [789], mox.IsA(tracker_pb2.ProjectIssueConfig),
|
| + unfiltered_iids, {}, nonviewable_iids, set(), self.services).AndReturn([])
|
| + self.mox.StubOutWithMock(frontendsearchpipeline, '_FinishBackendSearch')
|
| + frontendsearchpipeline._FinishBackendSearch([])
|
| + self.mox.ReplayAll()
|
| +
|
| + pipeline = frontendsearchpipeline.FrontendSearchPipeline(
|
| + self.mr, self.services, self.profiler, 100)
|
| + pipeline.unfiltered_iids = unfiltered_iids
|
| + pipeline.nonviewable_iids = nonviewable_iids
|
| + pipeline.SearchForIIDs()
|
| + self.mox.VerifyAll()
|
| + self.assertEqual(2, pipeline.total_count)
|
| + self.assertEqual(2, pipeline.counts[1])
|
| + self.assertEqual([1001, 1011], pipeline.filtered_iids[1])
|
| +
|
| + def testMergeAndSortIssues_EmptyResult(self):
|
| + pipeline = frontendsearchpipeline.FrontendSearchPipeline(
|
| + self.mr, self.services, self.profiler, 100)
|
| + pipeline.filtered_iids = {0: [], 1: [], 2: []}
|
| +
|
| + pipeline.MergeAndSortIssues()
|
| + self.assertEqual([], pipeline.allowed_iids)
|
| + self.assertEqual([], pipeline.allowed_results)
|
| + self.assertEqual({}, pipeline.users_by_id)
|
| +
|
| + def testMergeAndSortIssues_Normal(self):
|
| + pipeline = frontendsearchpipeline.FrontendSearchPipeline(
|
| + self.mr, self.services, self.profiler, 100)
|
| + # In this unit test case we are not calling SearchForIIDs(), instead just
|
| + # set pipeline.filtered_iids directly.
|
| + pipeline.filtered_iids = {
|
| + 0: [],
|
| + 1: [self.issue_1.issue_id],
|
| + 2: [self.issue_2.issue_id],
|
| + 3: [self.issue_3.issue_id]
|
| + }
|
| +
|
| + pipeline.MergeAndSortIssues()
|
| + self.assertEqual(
|
| + [self.issue_1.issue_id, self.issue_2.issue_id, self.issue_3.issue_id],
|
| + pipeline.allowed_iids)
|
| + self.assertEqual(
|
| + [self.issue_1, self.issue_3, self.issue_2], # high, medium, low.
|
| + pipeline.allowed_results)
|
| + self.assertEqual([111L], pipeline.users_by_id.keys())
|
| +
|
| + def testDetermineIssuePosition_Normal(self):
|
| + pipeline = frontendsearchpipeline.FrontendSearchPipeline(
|
| + self.mr, self.services, self.profiler, 100)
|
| + # In this unit test case we are not calling SearchForIIDs(), instead just
|
| + # set pipeline.filtered_iids directly.
|
| + pipeline.filtered_iids = {
|
| + 0: [],
|
| + 1: [self.issue_1.issue_id],
|
| + 2: [self.issue_2.issue_id],
|
| + 3: [self.issue_3.issue_id]
|
| + }
|
| +
|
| + prev_iid, index, next_iid = pipeline.DetermineIssuePosition(self.issue_3)
|
| + # The total ordering is issue_1, issue_3, issue_2 for high, med, low.
|
| + self.assertEqual(self.issue_1.issue_id, prev_iid)
|
| + self.assertEqual(1, index)
|
| + self.assertEqual(self.issue_2.issue_id, next_iid)
|
| +
|
| + def testDetermineIssuePosition_NotInResults(self):
|
| + pipeline = frontendsearchpipeline.FrontendSearchPipeline(
|
| + self.mr, self.services, self.profiler, 100)
|
| + # In this unit test case we are not calling SearchForIIDs(), instead just
|
| + # set pipeline.filtered_iids directly.
|
| + pipeline.filtered_iids = {
|
| + 0: [],
|
| + 1: [self.issue_1.issue_id],
|
| + 2: [self.issue_2.issue_id],
|
| + 3: []
|
| + }
|
| +
|
| + prev_iid, index, next_iid = pipeline.DetermineIssuePosition(self.issue_3)
|
| + # The total ordering is issue_1, issue_3, issue_2 for high, med, low.
|
| + self.assertEqual(None, prev_iid)
|
| + self.assertEqual(None, index)
|
| + self.assertEqual(None, next_iid)
|
| +
|
| + def testDetermineIssuePositionInShard_IssueIsInShard(self):
|
| + pipeline = frontendsearchpipeline.FrontendSearchPipeline(
|
| + self.mr, self.services, self.profiler, 100)
|
| + # Let's assume issues 1, 2, and 3 are all in the same shard.
|
| + pipeline.filtered_iids = {
|
| + 0: [self.issue_1.issue_id, self.issue_2.issue_id, self.issue_3.issue_id],
|
| + }
|
| +
|
| + # The total ordering is issue_1, issue_3, issue_2 for high, med, low.
|
| + prev_cand, index, next_cand = pipeline._DetermineIssuePositionInShard(
|
| + 0, self.issue_1, {})
|
| + self.assertEqual(None, prev_cand)
|
| + self.assertEqual(0, index)
|
| + self.assertEqual(self.issue_3, next_cand)
|
| +
|
| + prev_cand, index, next_cand = pipeline._DetermineIssuePositionInShard(
|
| + 0, self.issue_3, {})
|
| + self.assertEqual(self.issue_1, prev_cand)
|
| + self.assertEqual(1, index)
|
| + self.assertEqual(self.issue_2, next_cand)
|
| +
|
| + prev_cand, index, next_cand = pipeline._DetermineIssuePositionInShard(
|
| + 0, self.issue_2, {})
|
| + self.assertEqual(self.issue_3, prev_cand)
|
| + self.assertEqual(2, index)
|
| + self.assertEqual(None, next_cand)
|
| +
|
| + def testDetermineIssuePositionInShard_IssueIsNotInShard(self):
|
| + pipeline = frontendsearchpipeline.FrontendSearchPipeline(
|
| + self.mr, self.services, self.profiler, 100)
|
| +
|
| + # The total ordering is issue_1, issue_3, issue_2 for high, med, low.
|
| + pipeline.filtered_iids = {
|
| + 0: [self.issue_2.issue_id, self.issue_3.issue_id],
|
| + }
|
| + prev_cand, index, next_cand = pipeline._DetermineIssuePositionInShard(
|
| + 0, self.issue_1, {})
|
| + self.assertEqual(None, prev_cand)
|
| + self.assertEqual(0, index)
|
| + self.assertEqual(self.issue_3, next_cand)
|
| +
|
| + pipeline.filtered_iids = {
|
| + 0: [self.issue_1.issue_id, self.issue_2.issue_id],
|
| + }
|
| + prev_cand, index, next_cand = pipeline._DetermineIssuePositionInShard(
|
| + 0, self.issue_3, {})
|
| + self.assertEqual(self.issue_1, prev_cand)
|
| + self.assertEqual(1, index)
|
| + self.assertEqual(self.issue_2, next_cand)
|
| +
|
| + pipeline.filtered_iids = {
|
| + 0: [self.issue_1.issue_id, self.issue_3.issue_id],
|
| + }
|
| + prev_cand, index, next_cand = pipeline._DetermineIssuePositionInShard(
|
| + 0, self.issue_2, {})
|
| + self.assertEqual(self.issue_3, prev_cand)
|
| + self.assertEqual(2, index)
|
| + self.assertEqual(None, next_cand)
|
| +
|
| + def testAccumulateSampleIssues_Empty(self):
|
| + """When the search gave no results, there cannot be any samples."""
|
| + sample_dict = {}
|
| + needed_iids = []
|
| + pipeline = frontendsearchpipeline.FrontendSearchPipeline(
|
| + self.mr, self.services, self.profiler, 100)
|
| + issue_ids = []
|
| + pipeline._AccumulateSampleIssues(issue_ids, sample_dict, needed_iids)
|
| + self.assertEqual({}, sample_dict)
|
| + self.assertEqual([], needed_iids)
|
| +
|
| + def testAccumulateSampleIssues_Small(self):
|
| + """When the search gave few results, don't bother with samples."""
|
| + sample_dict = {}
|
| + needed_iids = []
|
| + pipeline = frontendsearchpipeline.FrontendSearchPipeline(
|
| + self.mr, self.services, self.profiler, 100)
|
| + issue_ids = [78901, 78902]
|
| + pipeline._AccumulateSampleIssues(issue_ids, sample_dict, needed_iids)
|
| + self.assertEqual({}, sample_dict)
|
| + self.assertEqual([], needed_iids)
|
| +
|
| + def testAccumulateSampleIssues_Normal(self):
|
| + """We will choose at least one sample for every 10 results in a shard."""
|
| + sample_dict = {}
|
| + needed_iids = []
|
| + pipeline = frontendsearchpipeline.FrontendSearchPipeline(
|
| + self.mr, self.services, self.profiler, 100)
|
| + issues = []
|
| + for i in range(23):
|
| + issue = fake.MakeTestIssue(789, 100 + i, 'samp test', 'New', 111L)
|
| + issues.append(issue)
|
| + self.services.issue.TestAddIssue(issue)
|
| +
|
| + issue_ids = [issue.issue_id for issue in issues]
|
| + pipeline._AccumulateSampleIssues(issue_ids, sample_dict, needed_iids)
|
| + self.assertEqual(2, len(needed_iids))
|
| + for sample_iid in needed_iids:
|
| + self.assertIn(sample_iid, issue_ids)
|
| +
|
| + def testLookupNeededUsers(self):
|
| + pipeline = frontendsearchpipeline.FrontendSearchPipeline(
|
| + self.mr, self.services, self.profiler, 100)
|
| +
|
| + pipeline._LookupNeededUsers([])
|
| + self.assertEqual([], pipeline.users_by_id.keys())
|
| +
|
| + pipeline._LookupNeededUsers([self.issue_1, self.issue_2, self.issue_3])
|
| + self.assertEqual([111L], pipeline.users_by_id.keys())
|
| +
|
| + def testPaginate_Grid(self):
|
| + self.mr.mode = 'grid'
|
| + pipeline = frontendsearchpipeline.FrontendSearchPipeline(
|
| + self.mr, self.services, self.profiler, 100)
|
| + pipeline.allowed_iids = [
|
| + self.issue_1.issue_id, self.issue_2.issue_id, self.issue_3.issue_id]
|
| + pipeline.allowed_results = [self.issue_1, self.issue_2, self.issue_3]
|
| + pipeline.total_count = len(pipeline.allowed_results)
|
| + pipeline.Paginate()
|
| + self.assertEqual(
|
| + [self.issue_1, self.issue_2, self.issue_3],
|
| + pipeline.visible_results)
|
| +
|
| + def testPaginate_List(self):
|
| + pipeline = frontendsearchpipeline.FrontendSearchPipeline(
|
| + self.mr, self.services, self.profiler, 100)
|
| + pipeline.allowed_iids = [
|
| + self.issue_1.issue_id, self.issue_2.issue_id, self.issue_3.issue_id]
|
| + pipeline.allowed_results = [self.issue_1, self.issue_2, self.issue_3]
|
| + pipeline.total_count = len(pipeline.allowed_results)
|
| + pipeline.Paginate()
|
| + self.assertEqual(
|
| + [self.issue_1, self.issue_2, self.issue_3],
|
| + pipeline.visible_results)
|
| + self.assertFalse(pipeline.pagination.limit_reached)
|
| +
|
| +
|
| +class FrontendSearchPipelineMethodsTest(unittest.TestCase):
|
| +
|
| + def setUp(self):
|
| + self.mox = mox.Mox()
|
| + self.testbed = testbed.Testbed()
|
| + self.testbed.activate()
|
| + self.testbed.init_user_stub()
|
| + self.testbed.init_memcache_stub()
|
| +
|
| + def tearDown(self):
|
| + self.testbed.deactivate()
|
| + self.mox.UnsetStubs()
|
| + self.mox.ResetAll()
|
| +
|
| + def testMakeBackendCallback(self):
|
| + called_with = []
|
| +
|
| + def func(a, b):
|
| + called_with.append((a, b))
|
| +
|
| + callback = frontendsearchpipeline._MakeBackendCallback(func, 10, 20)
|
| + callback()
|
| + self.assertEqual([(10, 20)], called_with)
|
| +
|
| + def testStartBackendSearch(self):
|
| + # TODO(jrobbins): write this test.
|
| + pass
|
| +
|
| + def testFinishBackendSearch(self):
|
| + # TODO(jrobbins): write this test.
|
| + pass
|
| +
|
| + def testGetProjectTimestamps_NoneSet(self):
|
| + project_shard_timestamps = frontendsearchpipeline._GetProjectTimestamps(
|
| + [], [])
|
| + self.assertEqual({}, project_shard_timestamps)
|
| +
|
| + project_shard_timestamps = frontendsearchpipeline._GetProjectTimestamps(
|
| + [], [0, 1, 2, 3, 4])
|
| + self.assertEqual({}, project_shard_timestamps)
|
| +
|
| + project_shard_timestamps = frontendsearchpipeline._GetProjectTimestamps(
|
| + [789], [0, 1, 2, 3, 4])
|
| + self.assertEqual({}, project_shard_timestamps)
|
| +
|
| + def testGetProjectTimestamps_SpecificProjects(self):
|
| + memcache.set('789;0', NOW)
|
| + memcache.set('789;1', NOW - 1000)
|
| + memcache.set('789;2', NOW - 3000)
|
| + project_shard_timestamps = frontendsearchpipeline._GetProjectTimestamps(
|
| + [789], [0, 1, 2])
|
| + self.assertEqual(
|
| + { (789, 0): NOW,
|
| + (789, 1): NOW - 1000,
|
| + (789, 2): NOW - 3000,
|
| + },
|
| + project_shard_timestamps)
|
| +
|
| + memcache.set('790;0', NOW)
|
| + memcache.set('790;1', NOW - 10000)
|
| + memcache.set('790;2', NOW - 30000)
|
| + project_shard_timestamps = frontendsearchpipeline._GetProjectTimestamps(
|
| + [789, 790], [0, 1, 2])
|
| + self.assertEqual(
|
| + { (789, 0): NOW,
|
| + (789, 1): NOW - 1000,
|
| + (789, 2): NOW - 3000,
|
| + (790, 0): NOW,
|
| + (790, 1): NOW - 10000,
|
| + (790, 2): NOW - 30000,
|
| + },
|
| + project_shard_timestamps)
|
| +
|
| + def testGetProjectTimestamps_SiteWide(self):
|
| + memcache.set('all;0', NOW)
|
| + memcache.set('all;1', NOW - 10000)
|
| + memcache.set('all;2', NOW - 30000)
|
| + project_shard_timestamps = frontendsearchpipeline._GetProjectTimestamps(
|
| + [], [0, 1, 2])
|
| + self.assertEqual(
|
| + { ('all', 0): NOW,
|
| + ('all', 1): NOW - 10000,
|
| + ('all', 2): NOW - 30000,
|
| + },
|
| + project_shard_timestamps)
|
| +
|
| + def testGetNonviewableIIDs_SearchMissSoNoOp(self):
|
| + """If search cache missed, don't bother looking up nonviewable IIDs."""
|
| + unfiltered_iids_dict = {} # No cached search results found.
|
| + rpc_tuples = [] # Nothing should accumulate here in this case.
|
| + nonviewable_iids = {} # Nothing should accumulate here in this case.
|
| + processed_invalidations_up_to = 12345
|
| + frontendsearchpipeline._GetNonviewableIIDs(
|
| + [789], 111L, unfiltered_iids_dict.keys(), rpc_tuples, nonviewable_iids,
|
| + {}, processed_invalidations_up_to, True)
|
| + self.assertEqual([], rpc_tuples)
|
| + self.assertEqual({}, nonviewable_iids)
|
| +
|
| + def testGetNonviewableIIDs_SearchHitThenNonviewableHit(self):
|
| + """If search cache hit, get nonviewable info from cache."""
|
| + unfiltered_iids_dict = {
|
| + 1: [10001, 10021],
|
| + 2: ['the search result issue_ids do not matter'],
|
| + }
|
| + rpc_tuples = [] # Nothing should accumulate here in this case.
|
| + nonviewable_iids = {} # Our mock results should end up here.
|
| + processed_invalidations_up_to = 12345
|
| + memcache.set('nonviewable:789;111;1',
|
| + ([10001, 10031], processed_invalidations_up_to - 10))
|
| + memcache.set('nonviewable:789;111;2',
|
| + ([10002, 10042], processed_invalidations_up_to - 30))
|
| +
|
| + project_shard_timestamps = {
|
| + (789, 1): 0, # not stale
|
| + (789, 2): 0, # not stale
|
| + }
|
| + frontendsearchpipeline._GetNonviewableIIDs(
|
| + [789], 111L, unfiltered_iids_dict.keys(), rpc_tuples, nonviewable_iids,
|
| + project_shard_timestamps, processed_invalidations_up_to, True)
|
| + self.assertEqual([], rpc_tuples)
|
| + self.assertEqual({1: {10001, 10031}, 2: {10002, 10042}}, nonviewable_iids)
|
| +
|
| + def testGetNonviewableIIDs_SearchHitNonviewableMissSoStartRPC(self):
|
| + """If search hit and n-v miss, create RPCs to get nonviewable info."""
|
| + self.mox.StubOutWithMock(
|
| + frontendsearchpipeline, '_StartBackendNonviewableCall')
|
| + unfiltered_iids_dict = {
|
| + 2: ['the search result issue_ids do not matter'],
|
| + }
|
| + rpc_tuples = [] # One RPC object should accumulate here.
|
| + nonviewable_iids = {} # This will stay empty until RPCs complete.
|
| + processed_invalidations_up_to = 12345
|
| + # Nothing is set in memcache for this case.
|
| + a_fake_rpc = testing_helpers.Blank(callback=None)
|
| + frontendsearchpipeline._StartBackendNonviewableCall(
|
| + 789, 111L, 2, processed_invalidations_up_to).AndReturn(a_fake_rpc)
|
| + self.mox.ReplayAll()
|
| +
|
| + frontendsearchpipeline._GetNonviewableIIDs(
|
| + [789], 111L, unfiltered_iids_dict.keys(), rpc_tuples, nonviewable_iids,
|
| + {}, processed_invalidations_up_to, True)
|
| + self.mox.VerifyAll()
|
| + _, sid_0, rpc_0 = rpc_tuples[0]
|
| + self.assertEqual(2, sid_0)
|
| + self.assertEqual({}, nonviewable_iids)
|
| + self.assertEqual(a_fake_rpc, rpc_0)
|
| + self.assertIsNotNone(a_fake_rpc.callback)
|
| +
|
| + def testAccumulateNonviewableIIDs_MemcacheHitForProject(self):
|
| + processed_invalidations_up_to = 12345
|
| + cached_dict = {
|
| + '789;111;2': ([10002, 10042], processed_invalidations_up_to - 10),
|
| + '789;111;3': ([10003, 10093], processed_invalidations_up_to - 30),
|
| + }
|
| + rpc_tuples = [] # Nothing should accumulate here.
|
| + nonviewable_iids = {1: {10001}} # This will gain the shard 2 values.
|
| + project_shard_timestamps = {
|
| + (789, 1): 0, # not stale
|
| + (789, 2): 0, # not stale
|
| + }
|
| + frontendsearchpipeline._AccumulateNonviewableIIDs(
|
| + 789, 111L, 2, cached_dict, nonviewable_iids, project_shard_timestamps,
|
| + rpc_tuples, processed_invalidations_up_to)
|
| + self.assertEqual([], rpc_tuples)
|
| + self.assertEqual({1: {10001}, 2: {10002, 10042}}, nonviewable_iids)
|
| +
|
| + def testAccumulateNonviewableIIDs_MemcacheStaleForProject(self):
|
| + self.mox.StubOutWithMock(
|
| + frontendsearchpipeline, '_StartBackendNonviewableCall')
|
| + processed_invalidations_up_to = 12345
|
| + cached_dict = {
|
| + '789;111;2': ([10002, 10042], processed_invalidations_up_to - 10),
|
| + '789;111;3': ([10003, 10093], processed_invalidations_up_to - 30),
|
| + }
|
| + rpc_tuples = [] # Nothing should accumulate here.
|
| + nonviewable_iids = {1: {10001}} # Nothing added here until RPC completes
|
| + project_shard_timestamps = {
|
| + (789, 1): 0, # not stale
|
| + (789, 2): processed_invalidations_up_to, # stale!
|
| + }
|
| + a_fake_rpc = testing_helpers.Blank(callback=None)
|
| + frontendsearchpipeline._StartBackendNonviewableCall(
|
| + 789, 111L, 2, processed_invalidations_up_to).AndReturn(a_fake_rpc)
|
| + self.mox.ReplayAll()
|
| +
|
| + frontendsearchpipeline._AccumulateNonviewableIIDs(
|
| + 789, 111L, 2, cached_dict, nonviewable_iids, project_shard_timestamps,
|
| + rpc_tuples, processed_invalidations_up_to)
|
| + self.mox.VerifyAll()
|
| + _, sid_0, rpc_0 = rpc_tuples[0]
|
| + self.assertEqual(2, sid_0)
|
| + self.assertEqual(a_fake_rpc, rpc_0)
|
| + self.assertIsNotNone(a_fake_rpc.callback)
|
| + self.assertEqual({1: {10001}}, nonviewable_iids)
|
| +
|
| + def testAccumulateNonviewableIIDs_MemcacheHitForWholeSite(self):
|
| + processed_invalidations_up_to = 12345
|
| + cached_dict = {
|
| + 'all;111;2': ([10002, 10042], processed_invalidations_up_to - 10),
|
| + 'all;111;3': ([10003, 10093], processed_invalidations_up_to - 30),
|
| + }
|
| + rpc_tuples = [] # Nothing should accumulate here.
|
| + nonviewable_iids = {1: {10001}} # This will gain the shard 2 values.
|
| + project_shard_timestamps = {
|
| + (None, 1): 0, # not stale
|
| + (None, 2): 0, # not stale
|
| + }
|
| + frontendsearchpipeline._AccumulateNonviewableIIDs(
|
| + None, 111L, 2, cached_dict, nonviewable_iids, project_shard_timestamps,
|
| + rpc_tuples, processed_invalidations_up_to)
|
| + self.assertEqual([], rpc_tuples)
|
| + self.assertEqual({1: {10001}, 2: {10002, 10042}}, nonviewable_iids)
|
| +
|
| + def testAccumulateNonviewableIIDs_MemcacheMissSoStartRPC(self):
|
| + self.mox.StubOutWithMock(
|
| + frontendsearchpipeline, '_StartBackendNonviewableCall')
|
| + cached_dict = {} # Nothing here, so it is an at-risk cache miss.
|
| + rpc_tuples = [] # One RPC should accumulate here.
|
| + nonviewable_iids = {1: {10001}} # Nothing added here until RPC completes.
|
| + processed_invalidations_up_to = 12345
|
| + a_fake_rpc = testing_helpers.Blank(callback=None)
|
| + frontendsearchpipeline._StartBackendNonviewableCall(
|
| + 789, 111L, 2, processed_invalidations_up_to).AndReturn(a_fake_rpc)
|
| + self.mox.ReplayAll()
|
| +
|
| + frontendsearchpipeline._AccumulateNonviewableIIDs(
|
| + 789, 111L, 2, cached_dict, nonviewable_iids, {}, rpc_tuples,
|
| + processed_invalidations_up_to)
|
| + self.mox.VerifyAll()
|
| + _, sid_0, rpc_0 = rpc_tuples[0]
|
| + self.assertEqual(2, sid_0)
|
| + self.assertEqual(a_fake_rpc, rpc_0)
|
| + self.assertIsNotNone(a_fake_rpc.callback)
|
| + self.assertEqual({1: {10001}}, nonviewable_iids)
|
| +
|
| + def testGetCachedSearchResults(self):
|
| + # TODO(jrobbins): Write this test.
|
| + pass
|
| +
|
| + def testMakeBackendRequestHeaders(self):
|
| + headers = frontendsearchpipeline._MakeBackendRequestHeaders(False)
|
| + self.assertNotIn('X-AppEngine-FailFast', headers)
|
| + headers = frontendsearchpipeline._MakeBackendRequestHeaders(True)
|
| + self.assertEqual('Yes', headers['X-AppEngine-FailFast'])
|
| +
|
| + def testStartBackendSearchCall(self):
|
| + self.mox.StubOutWithMock(urlfetch, 'create_rpc')
|
| + self.mox.StubOutWithMock(urlfetch, 'make_fetch_call')
|
| + self.mox.StubOutWithMock(modules, 'get_hostname')
|
| + a_fake_rpc = testing_helpers.Blank(callback=None)
|
| + urlfetch.create_rpc(deadline=settings.backend_deadline).AndReturn(
|
| + a_fake_rpc)
|
| + modules.get_hostname(module='besearch')
|
| + urlfetch.make_fetch_call(
|
| + a_fake_rpc, mox.StrContains(urls.BACKEND_SEARCH), follow_redirects=False,
|
| + headers=mox.IsA(dict))
|
| + self.mox.ReplayAll()
|
| +
|
| + processed_invalidations_up_to = 12345
|
| + mr = testing_helpers.MakeMonorailRequest(path='/p/proj/issues/list?q=foo')
|
| + mr.me_user_id = 111L
|
| + frontendsearchpipeline._StartBackendSearchCall(
|
| + mr, ['proj'], 2, processed_invalidations_up_to)
|
| + self.mox.VerifyAll()
|
| +
|
| + def testStartBackendNonviewableCall(self):
|
| + self.mox.StubOutWithMock(urlfetch, 'create_rpc')
|
| + self.mox.StubOutWithMock(urlfetch, 'make_fetch_call')
|
| + self.mox.StubOutWithMock(modules, 'get_hostname')
|
| + a_fake_rpc = testing_helpers.Blank(callback=None)
|
| + urlfetch.create_rpc(deadline=settings.backend_deadline).AndReturn(
|
| + a_fake_rpc)
|
| + modules.get_hostname(module='besearch')
|
| + urlfetch.make_fetch_call(
|
| + a_fake_rpc, mox.StrContains(urls.BACKEND_NONVIEWABLE),
|
| + follow_redirects=False, headers=mox.IsA(dict))
|
| + self.mox.ReplayAll()
|
| +
|
| + processed_invalidations_up_to = 12345
|
| + frontendsearchpipeline._StartBackendNonviewableCall(
|
| + 789, 111L, 2, processed_invalidations_up_to)
|
| + self.mox.VerifyAll()
|
| +
|
| + def testHandleBackendSearchResponse_Error(self):
|
| + response_str = 'There was a problem processing the query.'
|
| + rpc = testing_helpers.Blank(
|
| + get_result=lambda: testing_helpers.Blank(
|
| + content=response_str, status_code=500))
|
| + rpc_tuple = (NOW, 2, rpc)
|
| + rpc_tuples = [] # Nothing should be added for this case.
|
| + filtered_iids = {} # Search results should accumlate here, per-shard.
|
| + search_limit_reached = {} # Booleans accumulate here, per-shard.
|
| + processed_invalidations_up_to = 12345
|
| +
|
| + mr = testing_helpers.MakeMonorailRequest(path='/p/proj/issues/list?q=foo')
|
| + mr.me_user_id = 111L
|
| + error_responses = set()
|
| +
|
| + self.mox.StubOutWithMock(frontendsearchpipeline, '_StartBackendSearchCall')
|
| + frontendsearchpipeline._HandleBackendSearchResponse(
|
| + mr, ['proj'], rpc_tuple, rpc_tuples, 0, filtered_iids,
|
| + search_limit_reached, processed_invalidations_up_to, error_responses)
|
| + self.assertEqual([], rpc_tuples)
|
| + self.assertIn(2, error_responses)
|
| +
|
| + def testHandleBackendSearchResponse_Normal(self):
|
| + response_str = (
|
| + '})]\'\n'
|
| + '{'
|
| + ' "unfiltered_iids": [10002, 10042],'
|
| + ' "search_limit_reached": false'
|
| + '}'
|
| + )
|
| + rpc = testing_helpers.Blank(
|
| + get_result=lambda: testing_helpers.Blank(
|
| + content=response_str, status_code=200))
|
| + rpc_tuple = (NOW, 2, rpc)
|
| + rpc_tuples = [] # Nothing should be added for this case.
|
| + filtered_iids = {} # Search results should accumlate here, per-shard.
|
| + search_limit_reached = {} # Booleans accumulate here, per-shard.
|
| + processed_invalidations_up_to = 12345
|
| +
|
| + mr = testing_helpers.MakeMonorailRequest(path='/p/proj/issues/list?q=foo')
|
| + mr.me_user_id = 111L
|
| + error_responses = set()
|
| + frontendsearchpipeline._HandleBackendSearchResponse(
|
| + mr, ['proj'], rpc_tuple, rpc_tuples, 2, filtered_iids,
|
| + search_limit_reached, processed_invalidations_up_to, error_responses)
|
| + self.assertEqual([], rpc_tuples)
|
| + self.assertEqual({2: [10002, 10042]}, filtered_iids)
|
| + self.assertEqual({2: False}, search_limit_reached)
|
| +
|
| +
|
| + def testHandleBackendSearchResponse_TriggersRetry(self):
|
| + response_str = None
|
| + rpc = testing_helpers.Blank(
|
| + get_result=lambda: testing_helpers.Blank(content=response_str))
|
| + rpc_tuple = (NOW, 2, rpc)
|
| + rpc_tuples = [] # New RPC should be appended here
|
| + filtered_iids = {} # No change here until retry completes.
|
| + search_limit_reached = {} # No change here until retry completes.
|
| + processed_invalidations_up_to = 12345
|
| + error_responses = set()
|
| +
|
| + mr = testing_helpers.MakeMonorailRequest(path='/p/proj/issues/list?q=foo')
|
| + mr.me_user_id = 111L
|
| +
|
| + self.mox.StubOutWithMock(frontendsearchpipeline, '_StartBackendSearchCall')
|
| + a_fake_rpc = testing_helpers.Blank(callback=None)
|
| + rpc = frontendsearchpipeline._StartBackendSearchCall(
|
| + mr, ['proj'], 2, processed_invalidations_up_to, failfast=False
|
| + ).AndReturn(a_fake_rpc)
|
| + self.mox.ReplayAll()
|
| +
|
| + frontendsearchpipeline._HandleBackendSearchResponse(
|
| + mr, ['proj'], rpc_tuple, rpc_tuples, 2, filtered_iids,
|
| + search_limit_reached, processed_invalidations_up_to, error_responses)
|
| + self.mox.VerifyAll()
|
| + _, retry_shard_id, retry_rpc = rpc_tuples[0]
|
| + self.assertEqual(2, retry_shard_id)
|
| + self.assertEqual(a_fake_rpc, retry_rpc)
|
| + self.assertIsNotNone(retry_rpc.callback)
|
| + self.assertEqual({}, filtered_iids)
|
| + self.assertEqual({}, search_limit_reached)
|
| +
|
| + def testHandleBackendNonviewableResponse_Error(self):
|
| + response_str = 'There was an error.'
|
| + rpc = testing_helpers.Blank(
|
| + get_result=lambda: testing_helpers.Blank(
|
| + content=response_str,
|
| + status_code=500
|
| + ))
|
| + rpc_tuple = (NOW, 2, rpc)
|
| + rpc_tuples = [] # Nothing should be added for this case.
|
| + nonviewable_iids = {} # At-risk issue IDs should accumlate here, per-shard.
|
| + processed_invalidations_up_to = 12345
|
| +
|
| + self.mox.StubOutWithMock(
|
| + frontendsearchpipeline, '_StartBackendNonviewableCall')
|
| + frontendsearchpipeline._HandleBackendNonviewableResponse(
|
| + 789, 111L, 2, rpc_tuple, rpc_tuples, 0, nonviewable_iids,
|
| + processed_invalidations_up_to)
|
| + self.assertEqual([], rpc_tuples)
|
| + self.assertNotEqual({2: {10002, 10042}}, nonviewable_iids)
|
| +
|
| + def testHandleBackendNonviewableResponse_Normal(self):
|
| + response_str = (
|
| + '})]\'\n'
|
| + '{'
|
| + ' "nonviewable": [10002, 10042]'
|
| + '}'
|
| + )
|
| + rpc = testing_helpers.Blank(
|
| + get_result=lambda: testing_helpers.Blank(
|
| + content=response_str,
|
| + status_code=200
|
| + ))
|
| + rpc_tuple = (NOW, 2, rpc)
|
| + rpc_tuples = [] # Nothing should be added for this case.
|
| + nonviewable_iids = {} # At-risk issue IDs should accumlate here, per-shard.
|
| + processed_invalidations_up_to = 12345
|
| +
|
| + frontendsearchpipeline._HandleBackendNonviewableResponse(
|
| + 789, 111L, 2, rpc_tuple, rpc_tuples, 2, nonviewable_iids,
|
| + processed_invalidations_up_to)
|
| + self.assertEqual([], rpc_tuples)
|
| + self.assertEqual({2: {10002, 10042}}, nonviewable_iids)
|
| +
|
| + def testHandleBackendAtRiskResponse_TriggersRetry(self):
|
| + response_str = None
|
| + rpc = testing_helpers.Blank(
|
| + get_result=lambda: testing_helpers.Blank(content=response_str))
|
| + rpc_tuple = (NOW, 2, rpc)
|
| + rpc_tuples = [] # New RPC should be appended here
|
| + nonviewable_iids = {} # No change here until retry completes.
|
| + processed_invalidations_up_to = 12345
|
| +
|
| + self.mox.StubOutWithMock(
|
| + frontendsearchpipeline, '_StartBackendNonviewableCall')
|
| + a_fake_rpc = testing_helpers.Blank(callback=None)
|
| + rpc = frontendsearchpipeline._StartBackendNonviewableCall(
|
| + 789, 111L, 2, processed_invalidations_up_to, failfast=False
|
| + ).AndReturn(a_fake_rpc)
|
| + self.mox.ReplayAll()
|
| +
|
| + frontendsearchpipeline._HandleBackendNonviewableResponse(
|
| + 789, 111L, 2, rpc_tuple, rpc_tuples, 2, nonviewable_iids,
|
| + processed_invalidations_up_to)
|
| + self.mox.VerifyAll()
|
| + _, retry_shard_id, retry_rpc = rpc_tuples[0]
|
| + self.assertEqual(2, retry_shard_id)
|
| + self.assertIsNotNone(retry_rpc.callback)
|
| + self.assertEqual(a_fake_rpc, retry_rpc)
|
| + self.assertEqual({}, nonviewable_iids)
|
| +
|
| + def testSortIssues(self):
|
| + services = service_manager.Services(
|
| + cache_manager=fake.CacheManager())
|
| + sorting.InitializeArtValues(services)
|
| +
|
| + mr = testing_helpers.MakeMonorailRequest(path='/p/proj/issues/list?q=foo')
|
| + mr.sort_spec = 'priority'
|
| + issue_1 = fake.MakeTestIssue(
|
| + 789, 1, 'one', 'New', 111L, labels=['Priority-High'])
|
| + issue_2 = fake.MakeTestIssue(
|
| + 789, 2, 'two', 'New', 111L, labels=['Priority-Low'])
|
| + issue_3 = fake.MakeTestIssue(
|
| + 789, 3, 'three', 'New', 111L, labels=['Priority-Medium'])
|
| + issues = [issue_1, issue_2, issue_3]
|
| + config = tracker_bizobj.MakeDefaultProjectIssueConfig(789)
|
| +
|
| + sorted_issues = frontendsearchpipeline._SortIssues(mr, issues, config, {})
|
| +
|
| + self.assertEqual(
|
| + [issue_1, issue_3, issue_2], # Order is high, medium, low.
|
| + sorted_issues)
|
| +
|
| +
|
| +class FrontendSearchPipelineShardMethodsTest(unittest.TestCase):
|
| +
|
| + def setUp(self):
|
| + self.sharded_iids = {
|
| + 0: [10, 20, 30, 40, 50],
|
| + 1: [21, 41, 61, 81],
|
| + 2: [42, 52, 62, 72, 102],
|
| + 3: [],
|
| + }
|
| +
|
| + def testTotalLength_Empty(self):
|
| + """If there were no results, the length of the sharded list is zero."""
|
| + self.assertEqual(0, frontendsearchpipeline._TotalLength({}))
|
| +
|
| + def testTotalLength_Normal(self):
|
| + """The length of the sharded list is the sum of the shard lengths."""
|
| + self.assertEqual(
|
| + 14, frontendsearchpipeline._TotalLength(self.sharded_iids))
|
| +
|
| + def testReverseShards_Empty(self):
|
| + """Reversing an empty sharded list is still empty."""
|
| + empty_sharded_iids = {}
|
| + frontendsearchpipeline._ReverseShards(empty_sharded_iids)
|
| + self.assertEqual({}, empty_sharded_iids)
|
| +
|
| + def testReverseShards_Normal(self):
|
| + """Reversing a sharded list reverses each shard."""
|
| + frontendsearchpipeline._ReverseShards(self.sharded_iids)
|
| + self.assertEqual(
|
| + {0: [50, 40, 30, 20, 10],
|
| + 1: [81, 61, 41, 21],
|
| + 2: [102, 72, 62, 52, 42],
|
| + 3: [],
|
| + },
|
| + self.sharded_iids)
|
| +
|
| + def testTrimShardedIIDs_Empty(self):
|
| + """If the sharded list is empty, trimming it makes no change."""
|
| + empty_sharded_iids = {}
|
| + frontendsearchpipeline._TrimEndShardedIIDs(empty_sharded_iids, [], 12)
|
| + self.assertEqual({}, empty_sharded_iids)
|
| +
|
| + frontendsearchpipeline._TrimEndShardedIIDs(
|
| + empty_sharded_iids, [100, 88, 99], 12)
|
| + self.assertEqual({}, empty_sharded_iids)
|
| +
|
| + def testTrimShardedIIDs_NoSamples(self):
|
| + """If there are no samples, we don't trim off any IIDs."""
|
| + orig_sharded_iids = {
|
| + shard_id: iids[:] for shard_id, iids in self.sharded_iids.iteritems()}
|
| + num_trimmed = frontendsearchpipeline._TrimEndShardedIIDs(
|
| + self.sharded_iids, [], 12)
|
| + self.assertEqual(0, num_trimmed)
|
| + self.assertEqual(orig_sharded_iids, self.sharded_iids)
|
| +
|
| + num_trimmed = frontendsearchpipeline._TrimEndShardedIIDs(
|
| + self.sharded_iids, [], 1)
|
| + self.assertEqual(0, num_trimmed)
|
| + self.assertEqual(orig_sharded_iids, self.sharded_iids)
|
| +
|
| + def testTrimShardedIIDs_Normal(self):
|
| + """The first 3 samples contribute all needed IIDs, so trim off the rest."""
|
| + samples = [30, 41, 62, 40, 81]
|
| + num_trimmed = frontendsearchpipeline._TrimEndShardedIIDs(
|
| + self.sharded_iids, samples, 5)
|
| + self.assertEqual(2 + 1 + 0 + 0, num_trimmed)
|
| + self.assertEqual(
|
| + { # shard_id: iids before lower-bound + iids before 1st excess sample.
|
| + 0: [10, 20] + [30],
|
| + 1: [21] + [41, 61],
|
| + 2: [42, 52] + [62, 72, 102],
|
| + 3: [] + []},
|
| + self.sharded_iids)
|
| +
|
| + def testCalcSamplePositions_Empty(self):
|
| + sharded_iids = {0: []}
|
| + samples = []
|
| + self.assertEqual(
|
| + [], frontendsearchpipeline._CalcSamplePositions(sharded_iids, samples))
|
| +
|
| + sharded_iids = {0: [10, 20, 30, 40]}
|
| + samples = []
|
| + self.assertEqual(
|
| + [], frontendsearchpipeline._CalcSamplePositions(sharded_iids, samples))
|
| +
|
| + sharded_iids = {0: []}
|
| + # E.g., the IIDs 2 and 4 might have been trimmed out in the forward phase.
|
| + # But we still have them in the list for the backwards phase, and they
|
| + # should just not contribute anything to the result.
|
| + samples = [2, 4]
|
| + self.assertEqual(
|
| + [], frontendsearchpipeline._CalcSamplePositions(sharded_iids, samples))
|
| +
|
| + def testCalcSamplePositions_Normal(self):
|
| + samples = [30, 41, 62, 40, 81]
|
| + self.assertEqual(
|
| + [(30, 2), (41, 1), (62, 2), (40, 3), (81, 3)],
|
| + frontendsearchpipeline._CalcSamplePositions(self.sharded_iids, samples))
|
| +
|
| +
|
| +if __name__ == '__main__':
|
| + unittest.main()
|
|
|