blob: 1e4f0c98a12bf21cb0309c14d792422d7938a10c [file] [log] [blame]
# Copyright 2016 The Chromium Authors. All rights reserved.
# Use of this source code is governed 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 fulltext_helpers module."""
from __future__ import print_function
from __future__ import division
from __future__ import absolute_import
import unittest
import mox
from google.appengine.api import search
from proto import ast_pb2
from proto import tracker_pb2
from search import query2ast
from services import fulltext_helpers
TEXT_HAS = ast_pb2.QueryOp.TEXT_HAS
NOT_TEXT_HAS = ast_pb2.QueryOp.NOT_TEXT_HAS
GE = ast_pb2.QueryOp.GE
class MockResult(object):
def __init__(self, doc_id):
self.doc_id = doc_id
class MockSearchResponse(object):
"""Mock object that can be iterated over in batches."""
def __init__(self, results, cursor):
"""Constructor.
Args:
results: list of strings for document IDs.
cursor: search.Cursor object, if there are more results to
retrieve in another round-trip. Or, None if there are not.
"""
self.results = [MockResult(r) for r in results]
self.cursor = cursor
def __iter__(self):
"""The response itself is an iterator over the results."""
return self.results.__iter__()
class FulltextHelpersTest(unittest.TestCase):
def setUp(self):
self.mox = mox.Mox()
self.any_field_fd = tracker_pb2.FieldDef(
field_name='any_field', field_type=tracker_pb2.FieldTypes.STR_TYPE)
self.summary_fd = tracker_pb2.FieldDef(
field_name='summary', field_type=tracker_pb2.FieldTypes.STR_TYPE)
self.milestone_fd = tracker_pb2.FieldDef(
field_name='milestone', field_type=tracker_pb2.FieldTypes.STR_TYPE,
field_id=123)
self.fulltext_fields = ['summary']
self.mock_index = self.mox.CreateMockAnything()
self.mox.StubOutWithMock(search, 'Index')
self.query = None
def tearDown(self):
self.mox.UnsetStubs()
self.mox.ResetAll()
def RecordQuery(self, query):
self.query = query
def testBuildFTSQuery_EmptyQueryConjunction(self):
query_ast_conj = ast_pb2.Conjunction()
fulltext_query = fulltext_helpers.BuildFTSQuery(
query_ast_conj, self.fulltext_fields)
self.assertEqual(None, fulltext_query)
def testBuildFTSQuery_NoFullTextConditions(self):
estimated_hours_fd = tracker_pb2.FieldDef(
field_name='estimate', field_type=tracker_pb2.FieldTypes.INT_TYPE,
field_id=124)
query_ast_conj = ast_pb2.Conjunction(conds=[
ast_pb2.MakeCond(TEXT_HAS, [estimated_hours_fd], [], [40])])
fulltext_query = fulltext_helpers.BuildFTSQuery(
query_ast_conj, self.fulltext_fields)
self.assertEqual(None, fulltext_query)
def testBuildFTSQuery_Normal(self):
query_ast_conj = ast_pb2.Conjunction(conds=[
ast_pb2.MakeCond(TEXT_HAS, [self.summary_fd], ['needle'], []),
ast_pb2.MakeCond(TEXT_HAS, [self.milestone_fd], ['Q3', 'Q4'], [])])
fulltext_query = fulltext_helpers.BuildFTSQuery(
query_ast_conj, self.fulltext_fields)
self.assertEqual(
'(summary:"needle") (custom_123:"Q3" OR custom_123:"Q4")',
fulltext_query)
def testBuildFTSQuery_WithQuotes(self):
query_ast_conj = ast_pb2.Conjunction(conds=[
ast_pb2.MakeCond(TEXT_HAS, [self.summary_fd], ['"needle haystack"'],
[])])
fulltext_query = fulltext_helpers.BuildFTSQuery(
query_ast_conj, self.fulltext_fields)
self.assertEqual('(summary:"needle haystack")', fulltext_query)
def testBuildFTSQuery_IngoreColonInText(self):
query_ast_conj = ast_pb2.Conjunction(conds=[
ast_pb2.MakeCond(TEXT_HAS, [self.summary_fd], ['"needle:haystack"'],
[])])
fulltext_query = fulltext_helpers.BuildFTSQuery(
query_ast_conj, self.fulltext_fields)
self.assertEqual('(summary:"needle haystack")', fulltext_query)
def testBuildFTSQuery_InvalidQuery(self):
query_ast_conj = ast_pb2.Conjunction(conds=[
ast_pb2.MakeCond(TEXT_HAS, [self.summary_fd], ['haystack"needle'], []),
ast_pb2.MakeCond(TEXT_HAS, [self.milestone_fd], ['Q3', 'Q4'], [])])
with self.assertRaises(AssertionError):
fulltext_helpers.BuildFTSQuery(
query_ast_conj, self.fulltext_fields)
def testBuildFTSQuery_SpecialPrefixQuery(self):
special_prefix = query2ast.NON_OP_PREFIXES[0]
# Test with summary field.
query_ast_conj = ast_pb2.Conjunction(conds=[
ast_pb2.MakeCond(TEXT_HAS, [self.summary_fd],
['%s//google.com' % special_prefix], []),
ast_pb2.MakeCond(TEXT_HAS, [self.milestone_fd], ['Q3', 'Q4'], [])])
fulltext_query = fulltext_helpers.BuildFTSQuery(
query_ast_conj, self.fulltext_fields)
self.assertEqual(
'(summary:"%s//google.com") (custom_123:"Q3" OR custom_123:"Q4")' % (
special_prefix),
fulltext_query)
# Test with any field.
any_fd = tracker_pb2.FieldDef(
field_name=ast_pb2.ANY_FIELD,
field_type=tracker_pb2.FieldTypes.STR_TYPE)
query_ast_conj = ast_pb2.Conjunction(conds=[
ast_pb2.MakeCond(
TEXT_HAS, [any_fd], ['%s//google.com' % special_prefix], []),
ast_pb2.MakeCond(TEXT_HAS, [self.milestone_fd], ['Q3', 'Q4'], [])])
fulltext_query = fulltext_helpers.BuildFTSQuery(
query_ast_conj, self.fulltext_fields)
self.assertEqual(
'("%s//google.com") (custom_123:"Q3" OR custom_123:"Q4")' % (
special_prefix),
fulltext_query)
def testBuildFTSCondition_IgnoredOperator(self):
query_cond = ast_pb2.MakeCond(
GE, [self.summary_fd], ['needle'], [])
fulltext_query_clause = fulltext_helpers._BuildFTSCondition(
query_cond, self.fulltext_fields)
self.assertEqual('', fulltext_query_clause)
def testBuildFTSCondition_BuiltinField(self):
query_cond = ast_pb2.MakeCond(
TEXT_HAS, [self.summary_fd], ['needle'], [])
fulltext_query_clause = fulltext_helpers._BuildFTSCondition(
query_cond, self.fulltext_fields)
self.assertEqual('(summary:"needle")', fulltext_query_clause)
def testBuildFTSCondition_NonStringField(self):
est_days_fd = tracker_pb2.FieldDef(
field_name='EstDays', field_id=123,
field_type=tracker_pb2.FieldTypes.INT_TYPE)
query_cond = ast_pb2.MakeCond(
TEXT_HAS, [est_days_fd], ['needle'], [])
fulltext_query_clause = fulltext_helpers._BuildFTSCondition(
query_cond, self.fulltext_fields)
# Ignore in FTS, this search condition is done in SQL.
self.assertEqual('', fulltext_query_clause)
def testBuildFTSCondition_Negatation(self):
query_cond = ast_pb2.MakeCond(
NOT_TEXT_HAS, [self.summary_fd], ['needle'], [])
fulltext_query_clause = fulltext_helpers._BuildFTSCondition(
query_cond, self.fulltext_fields)
self.assertEqual('NOT (summary:"needle")', fulltext_query_clause)
def testBuildFTSCondition_QuickOR(self):
query_cond = ast_pb2.MakeCond(
TEXT_HAS, [self.summary_fd], ['needle', 'pin'], [])
fulltext_query_clause = fulltext_helpers._BuildFTSCondition(
query_cond, self.fulltext_fields)
self.assertEqual(
'(summary:"needle" OR summary:"pin")',
fulltext_query_clause)
def testBuildFTSCondition_NegatedQuickOR(self):
query_cond = ast_pb2.MakeCond(
NOT_TEXT_HAS, [self.summary_fd], ['needle', 'pin'], [])
fulltext_query_clause = fulltext_helpers._BuildFTSCondition(
query_cond, self.fulltext_fields)
self.assertEqual(
'NOT (summary:"needle" OR summary:"pin")',
fulltext_query_clause)
def testBuildFTSCondition_AnyField(self):
query_cond = ast_pb2.MakeCond(
TEXT_HAS, [self.any_field_fd], ['needle'], [])
fulltext_query_clause = fulltext_helpers._BuildFTSCondition(
query_cond, self.fulltext_fields)
self.assertEqual('("needle")', fulltext_query_clause)
def testBuildFTSCondition_NegatedAnyField(self):
query_cond = ast_pb2.MakeCond(
NOT_TEXT_HAS, [self.any_field_fd], ['needle'], [])
fulltext_query_clause = fulltext_helpers._BuildFTSCondition(
query_cond, self.fulltext_fields)
self.assertEqual('NOT ("needle")', fulltext_query_clause)
def testBuildFTSCondition_CrossProjectWithMultipleFieldDescriptors(self):
other_milestone_fd = tracker_pb2.FieldDef(
field_name='milestone', field_type=tracker_pb2.FieldTypes.STR_TYPE,
field_id=456)
query_cond = ast_pb2.MakeCond(
TEXT_HAS, [self.milestone_fd, other_milestone_fd], ['needle'], [])
fulltext_query_clause = fulltext_helpers._BuildFTSCondition(
query_cond, self.fulltext_fields)
self.assertEqual(
'(custom_123:"needle" OR custom_456:"needle")', fulltext_query_clause)
def SetUpComprehensiveSearch(self):
search.Index(name='search index name').AndReturn(
self.mock_index)
self.mock_index.search(mox.IgnoreArg()).WithSideEffects(
self.RecordQuery).AndReturn(
MockSearchResponse(['123', '234'], search.Cursor()))
self.mock_index.search(mox.IgnoreArg()).WithSideEffects(
self.RecordQuery).AndReturn(MockSearchResponse(['345'], None))
def testComprehensiveSearch(self):
self.SetUpComprehensiveSearch()
self.mox.ReplayAll()
project_ids = fulltext_helpers.ComprehensiveSearch(
'browser', 'search index name')
self.mox.VerifyAll()
self.assertItemsEqual([123, 234, 345], project_ids)