blob: 1e4f0c98a12bf21cb0309c14d792422d7938a10c [file] [log] [blame]
Copybara854996b2021-09-07 19:36:02 +00001# Copyright 2016 The Chromium Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style
3# license that can be found in the LICENSE file or at
4# https://developers.google.com/open-source/licenses/bsd
5
6"""Tests for the fulltext_helpers module."""
7from __future__ import print_function
8from __future__ import division
9from __future__ import absolute_import
10
11import unittest
12
13import mox
14
15from google.appengine.api import search
16
17from proto import ast_pb2
18from proto import tracker_pb2
19from search import query2ast
20from services import fulltext_helpers
21
22
23TEXT_HAS = ast_pb2.QueryOp.TEXT_HAS
24NOT_TEXT_HAS = ast_pb2.QueryOp.NOT_TEXT_HAS
25GE = ast_pb2.QueryOp.GE
26
27
28class MockResult(object):
29
30 def __init__(self, doc_id):
31 self.doc_id = doc_id
32
33
34class MockSearchResponse(object):
35 """Mock object that can be iterated over in batches."""
36
37 def __init__(self, results, cursor):
38 """Constructor.
39
40 Args:
41 results: list of strings for document IDs.
42 cursor: search.Cursor object, if there are more results to
43 retrieve in another round-trip. Or, None if there are not.
44 """
45 self.results = [MockResult(r) for r in results]
46 self.cursor = cursor
47
48 def __iter__(self):
49 """The response itself is an iterator over the results."""
50 return self.results.__iter__()
51
52
53class FulltextHelpersTest(unittest.TestCase):
54
55 def setUp(self):
56 self.mox = mox.Mox()
57 self.any_field_fd = tracker_pb2.FieldDef(
58 field_name='any_field', field_type=tracker_pb2.FieldTypes.STR_TYPE)
59 self.summary_fd = tracker_pb2.FieldDef(
60 field_name='summary', field_type=tracker_pb2.FieldTypes.STR_TYPE)
61 self.milestone_fd = tracker_pb2.FieldDef(
62 field_name='milestone', field_type=tracker_pb2.FieldTypes.STR_TYPE,
63 field_id=123)
64 self.fulltext_fields = ['summary']
65
66 self.mock_index = self.mox.CreateMockAnything()
67 self.mox.StubOutWithMock(search, 'Index')
68 self.query = None
69
70 def tearDown(self):
71 self.mox.UnsetStubs()
72 self.mox.ResetAll()
73
74 def RecordQuery(self, query):
75 self.query = query
76
77 def testBuildFTSQuery_EmptyQueryConjunction(self):
78 query_ast_conj = ast_pb2.Conjunction()
79 fulltext_query = fulltext_helpers.BuildFTSQuery(
80 query_ast_conj, self.fulltext_fields)
81 self.assertEqual(None, fulltext_query)
82
83 def testBuildFTSQuery_NoFullTextConditions(self):
84 estimated_hours_fd = tracker_pb2.FieldDef(
85 field_name='estimate', field_type=tracker_pb2.FieldTypes.INT_TYPE,
86 field_id=124)
87 query_ast_conj = ast_pb2.Conjunction(conds=[
88 ast_pb2.MakeCond(TEXT_HAS, [estimated_hours_fd], [], [40])])
89 fulltext_query = fulltext_helpers.BuildFTSQuery(
90 query_ast_conj, self.fulltext_fields)
91 self.assertEqual(None, fulltext_query)
92
93 def testBuildFTSQuery_Normal(self):
94 query_ast_conj = ast_pb2.Conjunction(conds=[
95 ast_pb2.MakeCond(TEXT_HAS, [self.summary_fd], ['needle'], []),
96 ast_pb2.MakeCond(TEXT_HAS, [self.milestone_fd], ['Q3', 'Q4'], [])])
97 fulltext_query = fulltext_helpers.BuildFTSQuery(
98 query_ast_conj, self.fulltext_fields)
99 self.assertEqual(
100 '(summary:"needle") (custom_123:"Q3" OR custom_123:"Q4")',
101 fulltext_query)
102
103 def testBuildFTSQuery_WithQuotes(self):
104 query_ast_conj = ast_pb2.Conjunction(conds=[
105 ast_pb2.MakeCond(TEXT_HAS, [self.summary_fd], ['"needle haystack"'],
106 [])])
107 fulltext_query = fulltext_helpers.BuildFTSQuery(
108 query_ast_conj, self.fulltext_fields)
109 self.assertEqual('(summary:"needle haystack")', fulltext_query)
110
111 def testBuildFTSQuery_IngoreColonInText(self):
112 query_ast_conj = ast_pb2.Conjunction(conds=[
113 ast_pb2.MakeCond(TEXT_HAS, [self.summary_fd], ['"needle:haystack"'],
114 [])])
115 fulltext_query = fulltext_helpers.BuildFTSQuery(
116 query_ast_conj, self.fulltext_fields)
117 self.assertEqual('(summary:"needle haystack")', fulltext_query)
118
119 def testBuildFTSQuery_InvalidQuery(self):
120 query_ast_conj = ast_pb2.Conjunction(conds=[
121 ast_pb2.MakeCond(TEXT_HAS, [self.summary_fd], ['haystack"needle'], []),
122 ast_pb2.MakeCond(TEXT_HAS, [self.milestone_fd], ['Q3', 'Q4'], [])])
123 with self.assertRaises(AssertionError):
124 fulltext_helpers.BuildFTSQuery(
125 query_ast_conj, self.fulltext_fields)
126
127 def testBuildFTSQuery_SpecialPrefixQuery(self):
128 special_prefix = query2ast.NON_OP_PREFIXES[0]
129
130 # Test with summary field.
131 query_ast_conj = ast_pb2.Conjunction(conds=[
132 ast_pb2.MakeCond(TEXT_HAS, [self.summary_fd],
133 ['%s//google.com' % special_prefix], []),
134 ast_pb2.MakeCond(TEXT_HAS, [self.milestone_fd], ['Q3', 'Q4'], [])])
135 fulltext_query = fulltext_helpers.BuildFTSQuery(
136 query_ast_conj, self.fulltext_fields)
137 self.assertEqual(
138 '(summary:"%s//google.com") (custom_123:"Q3" OR custom_123:"Q4")' % (
139 special_prefix),
140 fulltext_query)
141
142 # Test with any field.
143 any_fd = tracker_pb2.FieldDef(
144 field_name=ast_pb2.ANY_FIELD,
145 field_type=tracker_pb2.FieldTypes.STR_TYPE)
146 query_ast_conj = ast_pb2.Conjunction(conds=[
147 ast_pb2.MakeCond(
148 TEXT_HAS, [any_fd], ['%s//google.com' % special_prefix], []),
149 ast_pb2.MakeCond(TEXT_HAS, [self.milestone_fd], ['Q3', 'Q4'], [])])
150 fulltext_query = fulltext_helpers.BuildFTSQuery(
151 query_ast_conj, self.fulltext_fields)
152 self.assertEqual(
153 '("%s//google.com") (custom_123:"Q3" OR custom_123:"Q4")' % (
154 special_prefix),
155 fulltext_query)
156
157 def testBuildFTSCondition_IgnoredOperator(self):
158 query_cond = ast_pb2.MakeCond(
159 GE, [self.summary_fd], ['needle'], [])
160 fulltext_query_clause = fulltext_helpers._BuildFTSCondition(
161 query_cond, self.fulltext_fields)
162 self.assertEqual('', fulltext_query_clause)
163
164 def testBuildFTSCondition_BuiltinField(self):
165 query_cond = ast_pb2.MakeCond(
166 TEXT_HAS, [self.summary_fd], ['needle'], [])
167 fulltext_query_clause = fulltext_helpers._BuildFTSCondition(
168 query_cond, self.fulltext_fields)
169 self.assertEqual('(summary:"needle")', fulltext_query_clause)
170
171 def testBuildFTSCondition_NonStringField(self):
172 est_days_fd = tracker_pb2.FieldDef(
173 field_name='EstDays', field_id=123,
174 field_type=tracker_pb2.FieldTypes.INT_TYPE)
175 query_cond = ast_pb2.MakeCond(
176 TEXT_HAS, [est_days_fd], ['needle'], [])
177 fulltext_query_clause = fulltext_helpers._BuildFTSCondition(
178 query_cond, self.fulltext_fields)
179 # Ignore in FTS, this search condition is done in SQL.
180 self.assertEqual('', fulltext_query_clause)
181
182 def testBuildFTSCondition_Negatation(self):
183 query_cond = ast_pb2.MakeCond(
184 NOT_TEXT_HAS, [self.summary_fd], ['needle'], [])
185 fulltext_query_clause = fulltext_helpers._BuildFTSCondition(
186 query_cond, self.fulltext_fields)
187 self.assertEqual('NOT (summary:"needle")', fulltext_query_clause)
188
189 def testBuildFTSCondition_QuickOR(self):
190 query_cond = ast_pb2.MakeCond(
191 TEXT_HAS, [self.summary_fd], ['needle', 'pin'], [])
192 fulltext_query_clause = fulltext_helpers._BuildFTSCondition(
193 query_cond, self.fulltext_fields)
194 self.assertEqual(
195 '(summary:"needle" OR summary:"pin")',
196 fulltext_query_clause)
197
198 def testBuildFTSCondition_NegatedQuickOR(self):
199 query_cond = ast_pb2.MakeCond(
200 NOT_TEXT_HAS, [self.summary_fd], ['needle', 'pin'], [])
201 fulltext_query_clause = fulltext_helpers._BuildFTSCondition(
202 query_cond, self.fulltext_fields)
203 self.assertEqual(
204 'NOT (summary:"needle" OR summary:"pin")',
205 fulltext_query_clause)
206
207 def testBuildFTSCondition_AnyField(self):
208 query_cond = ast_pb2.MakeCond(
209 TEXT_HAS, [self.any_field_fd], ['needle'], [])
210 fulltext_query_clause = fulltext_helpers._BuildFTSCondition(
211 query_cond, self.fulltext_fields)
212 self.assertEqual('("needle")', fulltext_query_clause)
213
214 def testBuildFTSCondition_NegatedAnyField(self):
215 query_cond = ast_pb2.MakeCond(
216 NOT_TEXT_HAS, [self.any_field_fd], ['needle'], [])
217 fulltext_query_clause = fulltext_helpers._BuildFTSCondition(
218 query_cond, self.fulltext_fields)
219 self.assertEqual('NOT ("needle")', fulltext_query_clause)
220
221 def testBuildFTSCondition_CrossProjectWithMultipleFieldDescriptors(self):
222 other_milestone_fd = tracker_pb2.FieldDef(
223 field_name='milestone', field_type=tracker_pb2.FieldTypes.STR_TYPE,
224 field_id=456)
225 query_cond = ast_pb2.MakeCond(
226 TEXT_HAS, [self.milestone_fd, other_milestone_fd], ['needle'], [])
227 fulltext_query_clause = fulltext_helpers._BuildFTSCondition(
228 query_cond, self.fulltext_fields)
229 self.assertEqual(
230 '(custom_123:"needle" OR custom_456:"needle")', fulltext_query_clause)
231
232 def SetUpComprehensiveSearch(self):
233 search.Index(name='search index name').AndReturn(
234 self.mock_index)
235 self.mock_index.search(mox.IgnoreArg()).WithSideEffects(
236 self.RecordQuery).AndReturn(
237 MockSearchResponse(['123', '234'], search.Cursor()))
238 self.mock_index.search(mox.IgnoreArg()).WithSideEffects(
239 self.RecordQuery).AndReturn(MockSearchResponse(['345'], None))
240
241 def testComprehensiveSearch(self):
242 self.SetUpComprehensiveSearch()
243 self.mox.ReplayAll()
244 project_ids = fulltext_helpers.ComprehensiveSearch(
245 'browser', 'search index name')
246 self.mox.VerifyAll()
247 self.assertItemsEqual([123, 234, 345], project_ids)