-
Notifications
You must be signed in to change notification settings - Fork 409
/
Copy pathsearch_queries_test.py
393 lines (337 loc) · 15 KB
/
search_queries_test.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
# Copyright 2020 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License")
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import testing_config # Must be imported before the module under test.
import datetime
from unittest import mock
from internals import core_enums
from internals.core_models import FeatureEntry, MilestoneSet, Stage
from internals.review_models import Gate, Vote
from internals import search
from internals import search_fulltext
from internals import search_queries
class SearchFeaturesTest(testing_config.CustomTestCase):
def setUp(self):
self.feature_1 = FeatureEntry(
name='feature a', summary='sum',
category=1, impl_status_chrome=3)
self.feature_1.owner_emails = ['[email protected]']
self.feature_1.editor_emails = ['[email protected]']
self.feature_1.cc_emails = ['[email protected]']
self.feature_1.put()
self.feature_1_id = self.feature_1.key.integer_id()
search_fulltext.index_feature(self.feature_1)
self.stage_1_ship = Stage(
feature_id=self.feature_1_id,
stage_type=core_enums.STAGE_BLINK_SHIPPING,
milestones=MilestoneSet(desktop_first=99, android_first=100),
)
self.stage_1_ship.put()
self.feature_2 = FeatureEntry(
name='feature b',
summary='summary of editor stuff',
owner_emails=['[email protected]'],
category=1,
impl_status_chrome=3,
accurate_as_of=datetime.datetime(2023, 6, 1),
)
self.feature_2.put()
search_fulltext.index_feature(self.feature_1)
self.feature_2_id = self.feature_2.key.integer_id()
self.stage_2_ot = Stage(
feature_id=self.feature_2_id,
stage_type=core_enums.STAGE_BLINK_ORIGIN_TRIAL,
milestones=MilestoneSet(desktop_first=89, desktop_last=95, webview_first=100),
)
self.stage_2_ot.put()
self.feature_3 = FeatureEntry(
name='feature c',
summary='sum',
owner_emails=['[email protected]'],
category=1,
impl_status_chrome=4,
accurate_as_of=datetime.datetime(2024, 6, 1),
)
self.feature_3.put()
self.feature_3_id = self.feature_3.key.integer_id()
self.gate_1 = Gate(
feature_id=self.feature_1_id, stage_id=1,
gate_type=core_enums.GATE_API_PROTOTYPE,
state=Vote.APPROVED,
requested_on=datetime.datetime(2022, 7, 1))
self.gate_1.put()
self.gate_1_id = self.gate_1.key.integer_id()
self.vote_1_1 = Vote(
feature_id=self.feature_1_id, gate_type=core_enums.GATE_API_PROTOTYPE,
gate_id=self.gate_1_id,
state=Vote.REVIEW_REQUESTED,
set_on=datetime.datetime(2022, 7, 1),
set_by='[email protected]')
self.vote_1_1.put()
self.vote_1_2 = Vote(
feature_id=self.feature_1_id, gate_type=core_enums.GATE_API_PROTOTYPE,
gate_id=self.gate_1_id,
state=Vote.APPROVED,
set_on=datetime.datetime(2022, 7, 2),
set_by='[email protected]')
self.vote_1_2.put()
self.gate_2 = Gate(
feature_id=self.feature_2_id, stage_id=1,
gate_type=core_enums.GATE_API_SHIP,
state=Vote.REVIEW_REQUESTED,
requested_on=datetime.datetime(2022, 8, 1))
self.gate_2.put()
self.gate_2_id = self.gate_2.key.integer_id()
self.vote_2_1 = Vote(
feature_id=self.feature_2_id, gate_type=core_enums.GATE_API_SHIP,
gate_id=self.gate_2_id,
state=Vote.REVIEW_REQUESTED,
set_on=datetime.datetime(2022, 8, 1),
set_by='[email protected]')
self.vote_2_1.put()
self.vote_2_2 = Vote(
feature_id=self.feature_2_id, gate_type=core_enums.GATE_API_SHIP,
gate_id=self.gate_2_id,
state=Vote.APPROVED,
set_on=datetime.datetime(2022, 8, 2),
set_by='[email protected]')
self.vote_2_2.put()
def tearDown(self):
for kind in [
FeatureEntry, search_fulltext.FeatureWords, Stage, Gate, Vote]:
for entry in kind.query():
entry.key.delete()
def test_single_field_query_async__normal(self):
"""We get a promise to run the DB query, which produces results."""
actual_promise = search_queries.single_field_query_async(
'owner', '=', ['[email protected]'])
actual = actual_promise.get_result()
self.assertCountEqual(
[self.feature_1_id, self.feature_2_id],
[key.integer_id() for key in actual])
actual_promise = search_queries.single_field_query_async(
'unlisted', '=', [True])
actual = actual_promise.get_result()
self.assertCountEqual([], actual)
actual_promise = search_queries.single_field_query_async(
'deleted', '=', [True])
actual = actual_promise.get_result()
self.assertCountEqual([], actual)
def test_single_field_query_async__multiple_vals(self):
"""We get a promise to run the DB query with multiple values."""
actual_promise = search_queries.single_field_query_async(
'owner', '=', ['[email protected]', '[email protected]'])
actual = actual_promise.get_result()
self.assertCountEqual(
[self.feature_1_id, self.feature_2_id, self.feature_3_id],
[key.integer_id() for key in actual])
def test_single_field_query_async__inequality_nulls_first(self):
"""accurate_as_of treats None as before any comparison value."""
actual_promise = search_queries.single_field_query_async(
'accurate_as_of', '<', [datetime.datetime(2024, 1, 1)]
)
actual = actual_promise.get_result()
self.assertCountEqual(
[self.feature_1_id, self.feature_2_id], [key.integer_id() for key in actual]
)
actual_promise = search_queries.single_field_query_async(
'accurate_as_of', '>', [datetime.datetime(2024, 1, 1)]
)
actual = actual_promise.get_result()
self.assertCountEqual([self.feature_3_id], [key.integer_id() for key in actual])
def test_single_field_query_async__any_start_milestone(self):
actual = search_queries.single_field_query_async(
'any_start_milestone', '=', [100]
).get_result()
self.assertEqual(
set([self.feature_1_id, self.feature_2_id]),
set(proj.feature_id for proj in actual),
'Finds across multiple milestones.',
)
actual = search_queries.single_field_query_async(
'any_start_milestone', '=', [95]
).get_result()
self.assertEqual(
set(), set(proj.feature_id for proj in actual), 'Does not find "last" milestones.'
)
actual = search_queries.single_field_query_async(
'any_start_milestone', '=', [search_queries.Interval(97, 99)]
).get_result()
self.assertCountEqual(
set([self.feature_1_id]),
set(proj.feature_id for proj in actual),
'Intervals are constrained to a single milestone.',
)
def check_wrong_type(self, field_name, bad_values):
with self.assertRaises(ValueError) as cm:
search_queries.single_field_query_async(
field_name, '=', bad_values)
self.assertEqual(
cm.exception.args[0], 'Query value does not match field type')
def test_single_field_query_async__wrong_types(self):
"""We reject requests with values that parse to the wrong type."""
# Feature entry fields
self.check_wrong_type('owner', [True])
self.check_wrong_type('owner', [123])
self.check_wrong_type('deleted', ['not a boolean'])
self.check_wrong_type('shipping_year', ['not an integer'])
self.check_wrong_type('star_count', ['not an integer'])
self.check_wrong_type('created.when', ['not a date'])
self.check_wrong_type('owner', ['[email protected]', True])
# Stage fields
self.check_wrong_type('browsers.chrome.android', ['not an integer'])
self.check_wrong_type('finch_url', [123])
self.check_wrong_type('finch_url', [True])
def test_single_field_query_async__normal_stage_field(self):
"""We can find a FeatureEntry based on values in an associated Stage."""
actual_promise = search_queries.single_field_query_async(
'browsers.chrome.desktop', '=', [99])
actual = actual_promise.get_result()
self.assertCountEqual(
[self.feature_1_id],
[projection.feature_id for projection in actual])
def test_single_field_query_async__other_stage_field(self):
"""We only consider the appropriate Stage."""
actual_promise = search_queries.single_field_query_async(
'browsers.chrome.ot.desktop.start', '=', [99])
actual = actual_promise.get_result()
self.assertCountEqual([], actual)
def test_single_field_query_async__zero_results(self):
"""When there are no matching results, we get back a promise for []."""
actual_promise = search_queries.single_field_query_async(
'owner', '=', ['nope'])
actual = actual_promise.get_result()
self.assertCountEqual([], actual)
def test_single_field_query_async__fulltext_in_field(self):
"""We can search for words within a field."""
actual = search_queries.single_field_query_async(
'editor', ':', ['editor'])
self.assertCountEqual([self.feature_1_id], actual)
actual = search_queries.single_field_query_async(
'editor', ':', ['wrongword'])
self.assertCountEqual([], actual)
actual = search_queries.single_field_query_async(
'owner', ':', ['editor'])
self.assertCountEqual([], actual)
@mock.patch('logging.warning')
def test_single_field_query_async__bad_field(self, mock_warn):
"""An unknown field imediately gives zero results."""
actual = search_queries.single_field_query_async('zodiac', '=', ['leo'])
self.assertCountEqual([], actual)
def test_handle_me_query_async__owner_anon(self):
"""We can return a list of features owned by the user."""
testing_config.sign_in('[email protected]', 111)
future = search_queries.handle_me_query_async('owner')
actual = search._resolve_promise_to_id_list(future)
self.assertEqual(actual, [])
def test_handle_me_query__owner_some(self):
"""We can return a list of features owned by the user."""
testing_config.sign_in('[email protected]', 111)
future = search_queries.handle_me_query_async('owner')
actual = search._resolve_promise_to_id_list(future)
self.assertEqual(
[self.feature_1_id, self.feature_2_id], actual)
def test_handle_me_query__editor_none(self):
"""We can return a list of features the user can edit."""
testing_config.sign_in('[email protected]', 111)
future = search_queries.handle_me_query_async('editor')
actual = search._resolve_promise_to_id_list(future)
self.assertEqual([], actual)
def test_handle_me_query__editor_some(self):
"""We can return a list of features the user can edit."""
testing_config.sign_in('[email protected]', 111)
future = search_queries.handle_me_query_async('editor')
actual = search._resolve_promise_to_id_list(future)
self.assertEqual([self.feature_1_id], actual)
def test_handle_me_query__cc_none(self):
"""We can return a list of features the user is CC'd on."""
testing_config.sign_in('[email protected]', 111)
future = search_queries.handle_me_query_async('cc')
actual = search._resolve_promise_to_id_list(future)
self.assertEqual(actual, [])
def test_handle_me_query__cc_some(self):
"""We can return a list of features the user is CC'd on."""
testing_config.sign_in('[email protected]', 111)
future = search_queries.handle_me_query_async('cc')
actual = search._resolve_promise_to_id_list(future)
self.assertEqual([self.feature_1_id], actual)
def test_handle_can_edit_me_query_async__anon(self):
"""Anon cannot edit any features."""
testing_config.sign_out()
future = search_queries.handle_can_edit_me_query_async()
actual = search._resolve_promise_to_id_list(future)
self.assertEqual([], actual)
def test_handle_can_edit_me_query_async__visitor(self):
"""Visitor cannot edit any features."""
testing_config.sign_in('[email protected]', 111)
future = search_queries.handle_can_edit_me_query_async()
actual = search._resolve_promise_to_id_list(future)
self.assertEqual([], actual)
def test_handle_can_edit_me_query_async__owner(self):
"""A feature owner can edit those features."""
testing_config.sign_in('[email protected]', 111)
future = search_queries.handle_can_edit_me_query_async()
actual = search._resolve_promise_to_id_list(future)
self.assertEqual(
[self.feature_1_id, self.feature_2_id], actual)
def test_handle_can_edit_me_query_async__editor(self):
"""A feature editor can edit those features."""
testing_config.sign_in('[email protected]', 111)
future = search_queries.handle_can_edit_me_query_async()
actual = search._resolve_promise_to_id_list(future)
self.assertEqual([self.feature_1_id], actual)
def test_total_order_query_async__field_asc(self):
"""We can get keys used to sort features in ascending order."""
future = search_queries.total_order_query_async('name')
actual = search._resolve_promise_to_id_list(future)
self.assertEqual(
[self.feature_1_id, self.feature_2_id, self.feature_3_id], actual)
def test_total_order_query_async__field_desc(self):
"""We can get keys used to sort features in descending order."""
future = search_queries.total_order_query_async('-name')
actual = search._resolve_promise_to_id_list(future)
self.assertEqual(
[self.feature_3_id, self.feature_2_id, self.feature_1_id], actual)
def test_total_order_query_async__requested_on(self):
"""We can get feature IDs sorted by gate review requests."""
future = search_queries.total_order_query_async('gate.requested_on')
actual = search._resolve_promise_to_id_list(future)
self.assertEqual(
[self.feature_2_id],
actual)
def test_total_order_query_async__reviewed_on(self):
"""We can get feature IDs sorted by gate resolution times."""
future = search_queries.total_order_query_async('gate.reviewed_on')
actual = search._resolve_promise_to_id_list(future)
self.assertEqual(
[self.feature_1_id, self.feature_2_id],
actual)
def test_stage_fields_have_join_conditions(self):
"""Every STAGE_QUERIABLE_FIELDS has a STAGE_TYPES_BY_QUERY_FIELD entry."""
self.assertCountEqual(
search_queries.STAGE_QUERIABLE_FIELDS.keys(),
search_queries.STAGE_TYPES_BY_QUERY_FIELD.keys())
def test_negate_operator(self):
"""We can get correct negated operators"""
actual = search_queries.negate_operator('=')
self.assertEqual('!=', actual)
actual = search_queries.negate_operator('!=')
self.assertEqual('=', actual)
actual = search_queries.negate_operator('<')
self.assertEqual('>=', actual)
actual = search_queries.negate_operator('<=')
self.assertEqual('>', actual)
actual = search_queries.negate_operator('>')
self.assertEqual('<=', actual)
actual = search_queries.negate_operator('>=')
self.assertEqual('<', actual)