Project import generated by Copybara.

GitOrigin-RevId: d9e9e3fb4e31372ec1fb43b178994ca78fa8fe70
diff --git a/services/test/fulltext_helpers_test.py b/services/test/fulltext_helpers_test.py
new file mode 100644
index 0000000..1e4f0c9
--- /dev/null
+++ b/services/test/fulltext_helpers_test.py
@@ -0,0 +1,247 @@
+# 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)