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