blob: e25768ee9b0b31ddcf6dfcfab9d2ecc5c44bee8c [file] [log] [blame]
# Copyright 2016 The Chromium Authors. All rights reserved.
# Use of this source code is govered 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 query2ast module."""
import datetime
import time
import unittest
from proto import ast_pb2
from search import query2ast
from services import fulltext_helpers
from tracker import tracker_bizobj
BOOL = query2ast.BOOL
DATE = query2ast.DATE
NUM = query2ast.NUM
TXT = query2ast.TXT
BUILTIN_ISSUE_FIELDS = query2ast.BUILTIN_ISSUE_FIELDS
ANY_FIELD = query2ast.BUILTIN_ISSUE_FIELDS['any_field']
EQ = query2ast.EQ
NE = query2ast.NE
LT = query2ast.LT
GT = query2ast.GT
LE = query2ast.LE
GE = query2ast.GE
TEXT_HAS = query2ast.TEXT_HAS
NOT_TEXT_HAS = query2ast.NOT_TEXT_HAS
TEXT_MATCHES = query2ast.TEXT_MATCHES
NOT_TEXT_MATCHES = query2ast.NOT_TEXT_MATCHES
IS_DEFINED = query2ast.IS_DEFINED
IS_NOT_DEFINED = query2ast.IS_NOT_DEFINED
KEY_HAS = query2ast.KEY_HAS
MakeCond = ast_pb2.MakeCond
class QueryParsingUnitTest(unittest.TestCase):
default_config = tracker_bizobj.MakeDefaultProjectIssueConfig(789)
@unittest.skip('TODO(jrobbins): fully support OR')
def skip_testParseUserQuery_OrClause(self):
# ParseUserQuery extends _ParseORQuery with specialized
# handling of "OR" operators in a user query
# an "OR" query, which should look like two separate simple querys
# joined together by a pipe.
ast = query2ast.ParseUserQuery(
'ham OR fancy', '', BUILTIN_ISSUE_FIELDS, self.default_config)
conj1 = ast.conjunctions[0]
conj2 = ast.conjunctions[1]
self.assertEqual([MakeCond(TEXT_HAS, [ANY_FIELD], ['ham'], [])],
conj1.conds)
self.assertEqual([MakeCond(TEXT_HAS, [ANY_FIELD], ['fancy'], [])],
conj2.conds)
def testParseUserQuery_Words(self):
# an "ORTerm" is actually anything appearing on either side of an
# "OR" operator. So this could be thought of as "simple" query parsing.
# a simple query with no spaces
ast = query2ast.ParseUserQuery(
'hamfancy', '', BUILTIN_ISSUE_FIELDS, self.default_config)
fulltext_cond = ast.conjunctions[0].conds[0]
self.assertEqual(
MakeCond(TEXT_HAS, [ANY_FIELD], ['hamfancy'], []), fulltext_cond)
# negative word
ast = query2ast.ParseUserQuery(
'-hamfancy', '', BUILTIN_ISSUE_FIELDS, self.default_config)
fulltext_cond = ast.conjunctions[0].conds[0]
self.assertEqual(
# note: not NOT_TEXT_HAS.
MakeCond(NOT_TEXT_HAS, [ANY_FIELD], ['hamfancy'], []),
fulltext_cond)
# an explicit "AND" query in the "featured" context
warnings = []
query2ast.ParseUserQuery(
'ham AND fancy', 'label:featured', BUILTIN_ISSUE_FIELDS,
self.default_config, warnings=warnings)
self.assertEqual(
['The only supported boolean operator is OR (all capitals).'],
warnings)
# an implicit "AND" query
ast = query2ast.ParseUserQuery(
'ham fancy', '-label:deprecated', BUILTIN_ISSUE_FIELDS,
self.default_config)
scope_cond1, ft_cond1, ft_cond2 = ast.conjunctions[0].conds
self.assertEqual(
MakeCond(NOT_TEXT_HAS, [BUILTIN_ISSUE_FIELDS['label']],
['deprecated'], []),
scope_cond1)
self.assertEqual(
MakeCond(TEXT_HAS, [ANY_FIELD], ['ham'], []), ft_cond1)
self.assertEqual(
MakeCond(TEXT_HAS, [ANY_FIELD], ['fancy'], []), ft_cond2)
# Use word with special prefix.
word_with_special_prefix = '%stest' % fulltext_helpers.NON_OP_PREFIXES[0]
ast = query2ast.ParseUserQuery(
word_with_special_prefix, '', BUILTIN_ISSUE_FIELDS, self.default_config)
fulltext_cond = ast.conjunctions[0].conds[0]
self.assertEqual(
MakeCond(TEXT_HAS, [ANY_FIELD], [word_with_special_prefix], []),
fulltext_cond)
# mix positive and negative words
ast = query2ast.ParseUserQuery(
'ham -fancy', '-label:deprecated', BUILTIN_ISSUE_FIELDS,
self.default_config)
scope_cond1, ft_cond1, ft_cond2 = ast.conjunctions[0].conds
self.assertEqual(
MakeCond(NOT_TEXT_HAS, [BUILTIN_ISSUE_FIELDS['label']],
['deprecated'], []),
scope_cond1)
self.assertEqual(
MakeCond(TEXT_HAS, [ANY_FIELD], ['ham'], []), ft_cond1)
self.assertEqual(
MakeCond(NOT_TEXT_HAS, [ANY_FIELD], ['fancy'], []), ft_cond2)
# converts terms to lower case
ast = query2ast.ParseUserQuery(
'AmDude', '-label:deprecated', BUILTIN_ISSUE_FIELDS,
self.default_config)
scope_cond1, fulltext_cond = ast.conjunctions[0].conds
self.assertEqual(
MakeCond(NOT_TEXT_HAS, [BUILTIN_ISSUE_FIELDS['label']],
['deprecated'], []),
scope_cond1)
self.assertEqual(
MakeCond(TEXT_HAS, [ANY_FIELD], ['amdude'], []), fulltext_cond)
def testParseUserQuery_Phrases(self):
# positive phrases
ast = query2ast.ParseUserQuery(
'"one two"', '-label:deprecated', BUILTIN_ISSUE_FIELDS,
self.default_config)
scope_cond1, fulltext_cond = ast.conjunctions[0].conds
self.assertEqual(
MakeCond(NOT_TEXT_HAS, [BUILTIN_ISSUE_FIELDS['label']],
['deprecated'], []),
scope_cond1)
self.assertEqual(
MakeCond(TEXT_HAS, [ANY_FIELD], ['"one two"'], []), fulltext_cond)
# negative phrases
ast = query2ast.ParseUserQuery(
'-"one two"', '-label:deprecated', BUILTIN_ISSUE_FIELDS,
self.default_config)
scope_cond1, fulltext_cond = ast.conjunctions[0].conds
self.assertEqual(
MakeCond(NOT_TEXT_HAS, [BUILTIN_ISSUE_FIELDS['label']],
['deprecated'], []),
scope_cond1)
self.assertEqual(
MakeCond(NOT_TEXT_HAS, [ANY_FIELD], ['"one two"'], []), fulltext_cond)
# multiple phrases
ast = query2ast.ParseUserQuery(
'-"a b" "x y"', '-label:deprecated', BUILTIN_ISSUE_FIELDS,
self.default_config)
scope_cond1, ft_cond1, ft_cond2 = ast.conjunctions[0].conds
self.assertEqual(
MakeCond(NOT_TEXT_HAS, [BUILTIN_ISSUE_FIELDS['label']],
['deprecated'], []),
scope_cond1)
self.assertEqual(
MakeCond(NOT_TEXT_HAS, [ANY_FIELD], ['"a b"'], []), ft_cond1)
self.assertEqual(
MakeCond(TEXT_HAS, [ANY_FIELD], ['"x y"'], []), ft_cond2)
def testParseUserQuery_CodeSyntaxThatWeNeedToCopeWith(self):
# positive phrases
ast = query2ast.ParseUserQuery(
'Base::Tuple', '', BUILTIN_ISSUE_FIELDS,
self.default_config)
cond = ast.conjunctions[0].conds[0]
self.assertEqual(
MakeCond(TEXT_HAS, [ANY_FIELD],
['"base::tuple"'], []),
cond)
def testParseUserQuery_HasOperator(self):
# Search for issues with at least one attachment
ast = query2ast.ParseUserQuery(
'has:attachment', '', BUILTIN_ISSUE_FIELDS, self.default_config)
cond1 = ast.conjunctions[0].conds[0]
self.assertEqual(
MakeCond(IS_DEFINED, [BUILTIN_ISSUE_FIELDS['attachment']], [], []),
cond1)
ast = query2ast.ParseUserQuery(
'-has:attachment', '', BUILTIN_ISSUE_FIELDS, self.default_config)
cond1 = ast.conjunctions[0].conds[0]
self.assertEqual(
MakeCond(IS_NOT_DEFINED, [BUILTIN_ISSUE_FIELDS['attachment']], [], []),
cond1)
ast = query2ast.ParseUserQuery(
'has=attachment', '', BUILTIN_ISSUE_FIELDS, self.default_config)
cond1 = ast.conjunctions[0].conds[0]
self.assertEqual(
MakeCond(IS_DEFINED, [BUILTIN_ISSUE_FIELDS['attachment']], [], []),
cond1)
ast = query2ast.ParseUserQuery(
'-has=attachment', '', BUILTIN_ISSUE_FIELDS, self.default_config)
cond1 = ast.conjunctions[0].conds[0]
self.assertEqual(
MakeCond(IS_NOT_DEFINED, [BUILTIN_ISSUE_FIELDS['attachment']], [], []),
cond1)
# Search for numeric fields for searches with 'has' prefix
ast = query2ast.ParseUserQuery(
'has:attachments', '', BUILTIN_ISSUE_FIELDS, self.default_config)
cond1 = ast.conjunctions[0].conds[0]
self.assertEqual(
MakeCond(IS_DEFINED, [BUILTIN_ISSUE_FIELDS['attachments']], [], []),
cond1)
ast = query2ast.ParseUserQuery(
'-has:attachments', '', BUILTIN_ISSUE_FIELDS, self.default_config)
cond1 = ast.conjunctions[0].conds[0]
self.assertEqual(
MakeCond(IS_NOT_DEFINED, [BUILTIN_ISSUE_FIELDS['attachments']],
[], []),
cond1)
def testParseUserQuery_Components(self):
"""Parse user queries for components"""
ast = query2ast.ParseUserQuery(
'component:UI', '', BUILTIN_ISSUE_FIELDS, self.default_config)
cond1 = ast.conjunctions[0].conds[0]
self.assertEqual(
MakeCond(TEXT_HAS, [BUILTIN_ISSUE_FIELDS['component']],
['ui'], []),
cond1)
ast = query2ast.ParseUserQuery(
'Component:UI>AboutBox', '', BUILTIN_ISSUE_FIELDS, self.default_config)
cond1 = ast.conjunctions[0].conds[0]
self.assertEqual(
MakeCond(TEXT_HAS, [BUILTIN_ISSUE_FIELDS['component']],
['ui>aboutbox'], []),
cond1)
def testParseUserQuery_OwnersReportersAndCc(self):
"""Parse user queries for owner:, reporter: and cc:."""
ast = query2ast.ParseUserQuery(
'owner:user', '', BUILTIN_ISSUE_FIELDS, self.default_config)
cond1 = ast.conjunctions[0].conds[0]
self.assertEqual(
MakeCond(TEXT_HAS, [BUILTIN_ISSUE_FIELDS['owner']],
['user'], []),
cond1)
ast = query2ast.ParseUserQuery(
'owner:user@example.com', '', BUILTIN_ISSUE_FIELDS, self.default_config)
cond1 = ast.conjunctions[0].conds[0]
self.assertEqual(
MakeCond(TEXT_HAS, [BUILTIN_ISSUE_FIELDS['owner']],
['user@example.com'], []),
cond1)
ast = query2ast.ParseUserQuery(
'owner=user@example.com', '', BUILTIN_ISSUE_FIELDS, self.default_config)
cond1 = ast.conjunctions[0].conds[0]
self.assertEqual(
MakeCond(EQ, [BUILTIN_ISSUE_FIELDS['owner']],
['user@example.com'], []),
cond1)
ast = query2ast.ParseUserQuery(
'-reporter=user@example.com', '', BUILTIN_ISSUE_FIELDS,
self.default_config)
cond1 = ast.conjunctions[0].conds[0]
self.assertEqual(
MakeCond(NE, [BUILTIN_ISSUE_FIELDS['reporter']],
['user@example.com'], []),
cond1)
ast = query2ast.ParseUserQuery(
'cc=user@example.com,user2@example.com', '', BUILTIN_ISSUE_FIELDS,
self.default_config)
cond1 = ast.conjunctions[0].conds[0]
self.assertEqual(
MakeCond(EQ, [BUILTIN_ISSUE_FIELDS['cc']],
['user@example.com', 'user2@example.com'], []),
cond1)
ast = query2ast.ParseUserQuery(
'cc:user,user2', '', BUILTIN_ISSUE_FIELDS, self.default_config)
cond1 = ast.conjunctions[0].conds[0]
self.assertEqual(
MakeCond(TEXT_HAS, [BUILTIN_ISSUE_FIELDS['cc']],
['user', 'user2'], []),
cond1)
def testParseUserQuery_SearchWithinFields(self):
# Search for issues with certain filenames
ast = query2ast.ParseUserQuery(
'attachment:filename', '', BUILTIN_ISSUE_FIELDS, self.default_config)
cond1 = ast.conjunctions[0].conds[0]
self.assertEqual(
MakeCond(TEXT_HAS, [BUILTIN_ISSUE_FIELDS['attachment']],
['filename'], []),
cond1)
ast = query2ast.ParseUserQuery(
'-attachment:filename', '', BUILTIN_ISSUE_FIELDS,
self.default_config)
cond1 = ast.conjunctions[0].conds[0]
self.assertEqual(
MakeCond(NOT_TEXT_HAS, [BUILTIN_ISSUE_FIELDS['attachment']],
['filename'], []),
cond1)
# Search for issues with a certain number of attachments
ast = query2ast.ParseUserQuery(
'attachments:2', '', BUILTIN_ISSUE_FIELDS, self.default_config)
cond1 = ast.conjunctions[0].conds[0]
self.assertEqual(
MakeCond(TEXT_HAS, [BUILTIN_ISSUE_FIELDS['attachments']],
['2'], [2]),
cond1)
# Searches with '=' syntax
ast = query2ast.ParseUserQuery(
'attachment=filename', '', BUILTIN_ISSUE_FIELDS, self.default_config)
cond1 = ast.conjunctions[0].conds[0]
self.assertEqual(
MakeCond(EQ, [BUILTIN_ISSUE_FIELDS['attachment']],
['filename'], []),
cond1)
ast = query2ast.ParseUserQuery(
'-attachment=filename', '', BUILTIN_ISSUE_FIELDS, self.default_config)
cond1 = ast.conjunctions[0].conds[0]
self.assertEqual(
MakeCond(NE, [BUILTIN_ISSUE_FIELDS['attachment']],
['filename'], []),
cond1)
ast = query2ast.ParseUserQuery(
'milestone=2009', '', BUILTIN_ISSUE_FIELDS, self.default_config)
cond1 = ast.conjunctions[0].conds[0]
self.assertEqual(
MakeCond(EQ, [BUILTIN_ISSUE_FIELDS['label']], ['milestone-2009'], []),
cond1)
ast = query2ast.ParseUserQuery(
'-milestone=2009', '', BUILTIN_ISSUE_FIELDS, self.default_config)
cond1 = ast.conjunctions[0].conds[0]
self.assertEqual(
MakeCond(NE, [BUILTIN_ISSUE_FIELDS['label']], ['milestone-2009'], []),
cond1)
ast = query2ast.ParseUserQuery(
'milestone=2009-Q1', '', BUILTIN_ISSUE_FIELDS, self.default_config)
cond1 = ast.conjunctions[0].conds[0]
self.assertEqual(
MakeCond(EQ, [BUILTIN_ISSUE_FIELDS['label']],
['milestone-2009-q1'], []),
cond1)
ast = query2ast.ParseUserQuery(
'-milestone=2009-Q1', '', BUILTIN_ISSUE_FIELDS,
self.default_config)
cond1 = ast.conjunctions[0].conds[0]
self.assertEqual(
MakeCond(NE, [BUILTIN_ISSUE_FIELDS['label']],
['milestone-2009-q1'], []),
cond1)
# Searches with ':' syntax
ast = query2ast.ParseUserQuery(
'summary:foo', '', BUILTIN_ISSUE_FIELDS, self.default_config)
cond1 = ast.conjunctions[0].conds[0]
self.assertEqual(
MakeCond(TEXT_HAS,
[BUILTIN_ISSUE_FIELDS['summary']], ['foo'], []),
cond1)
ast = query2ast.ParseUserQuery(
'summary:"greetings programs"', '', BUILTIN_ISSUE_FIELDS,
self.default_config)
cond1 = ast.conjunctions[0].conds[0]
self.assertEqual(
MakeCond(TEXT_HAS,
[BUILTIN_ISSUE_FIELDS['summary']], ['greetings programs'], []),
cond1)
ast = query2ast.ParseUserQuery(
'summary:"Ӓ"', '', BUILTIN_ISSUE_FIELDS,
self.default_config)
cond1 = ast.conjunctions[0].conds[0]
self.assertEqual(
MakeCond(TEXT_HAS,
[BUILTIN_ISSUE_FIELDS['summary']], ['Ӓ'], []),
cond1)
ast = query2ast.ParseUserQuery(
'priority:high', '', BUILTIN_ISSUE_FIELDS, self.default_config)
cond1 = ast.conjunctions[0].conds[0]
self.assertEqual(
MakeCond(KEY_HAS,
[BUILTIN_ISSUE_FIELDS['label']], ['priority-high'], []),
cond1)
ast = query2ast.ParseUserQuery(
'type:security', '', BUILTIN_ISSUE_FIELDS, self.default_config)
cond1 = ast.conjunctions[0].conds[0]
self.assertEqual(
MakeCond(KEY_HAS,
[BUILTIN_ISSUE_FIELDS['label']], ['type-security'], []),
cond1)
ast = query2ast.ParseUserQuery(
'label:priority-high', '', BUILTIN_ISSUE_FIELDS, self.default_config)
cond1 = ast.conjunctions[0].conds[0]
self.assertEqual(
MakeCond(TEXT_HAS,
[BUILTIN_ISSUE_FIELDS['label']], ['priority-high'], []),
cond1)
def testParseUserQuery_QuickOr(self):
# quick-or searches
ast = query2ast.ParseUserQuery(
'milestone:2008,2009,2010', '', BUILTIN_ISSUE_FIELDS,
self.default_config)
cond1 = ast.conjunctions[0].conds[0]
self.assertEqual(
MakeCond(KEY_HAS, [BUILTIN_ISSUE_FIELDS['label']],
['milestone-2008', 'milestone-2009', 'milestone-2010'], []),
cond1)
ast = query2ast.ParseUserQuery(
'label:milestone-2008,milestone-2009,milestone-2010', '',
BUILTIN_ISSUE_FIELDS, self.default_config)
cond1 = ast.conjunctions[0].conds[0]
self.assertEqual(
MakeCond(TEXT_HAS, [BUILTIN_ISSUE_FIELDS['label']],
['milestone-2008', 'milestone-2009', 'milestone-2010'], []),
cond1)
ast = query2ast.ParseUserQuery(
'milestone=2008,2009,2010', '', BUILTIN_ISSUE_FIELDS,
self.default_config)
cond1 = ast.conjunctions[0].conds[0]
self.assertEqual(
MakeCond(EQ, [BUILTIN_ISSUE_FIELDS['label']],
['milestone-2008', 'milestone-2009', 'milestone-2010'], []),
cond1)
def testParseUserQuery_Dates(self):
# query with a daterange
ast = query2ast.ParseUserQuery(
'modified>=2009-5-12', '', BUILTIN_ISSUE_FIELDS,
self.default_config)
cond1 = ast.conjunctions[0].conds[0]
ts1 = int(time.mktime(datetime.datetime(2009, 5, 12).timetuple()))
self.assertEqual(
MakeCond(GE, [BUILTIN_ISSUE_FIELDS['modified']], [], [ts1]), cond1)
# query with multiple dateranges
ast = query2ast.ParseUserQuery(
'modified>=2009-5-12 opened<2008/1/1', '',
BUILTIN_ISSUE_FIELDS, self.default_config)
cond1, cond2 = ast.conjunctions[0].conds
ts1 = int(time.mktime(datetime.datetime(2009, 5, 12).timetuple()))
self.assertEqual(
MakeCond(GE, [BUILTIN_ISSUE_FIELDS['modified']], [], [ts1]), cond1)
ts2 = int(time.mktime(datetime.datetime(2008, 1, 1).timetuple()))
self.assertEqual(
MakeCond(LT, [BUILTIN_ISSUE_FIELDS['opened']], [], [ts2]), cond2)
# query with multiple dateranges plus a search term
ast = query2ast.ParseUserQuery(
'one two modified>=2009-5-12 opened<2008/1/1', '',
BUILTIN_ISSUE_FIELDS, self.default_config)
ft_cond1, ft_cond2, cond1, cond2 = ast.conjunctions[0].conds
ts1 = int(time.mktime(datetime.datetime(2009, 5, 12).timetuple()))
self.assertEqual(
MakeCond(TEXT_HAS, [ANY_FIELD], ['one'], []), ft_cond1)
self.assertEqual(
MakeCond(TEXT_HAS, [ANY_FIELD], ['two'], []), ft_cond2)
self.assertEqual(
MakeCond(GE, [BUILTIN_ISSUE_FIELDS['modified']], [], [ts1]), cond1)
ts2 = int(time.mktime(datetime.datetime(2008, 1, 1).timetuple()))
self.assertEqual(
MakeCond(LT, [BUILTIN_ISSUE_FIELDS['opened']], [], [ts2]), cond2)
# query with a date field compared to "today"
ast = query2ast.ParseUserQuery(
'modified<today', '', BUILTIN_ISSUE_FIELDS,
self.default_config)
cond1 = ast.conjunctions[0].conds[0]
ts1 = query2ast._CalculatePastDate(0)
self.assertEqual(MakeCond(LT, [BUILTIN_ISSUE_FIELDS['modified']],
[], [ts1]),
cond1)
# query with a daterange using today-N alias
ast = query2ast.ParseUserQuery(
'modified>=today-13', '', BUILTIN_ISSUE_FIELDS,
self.default_config)
cond1 = ast.conjunctions[0].conds[0]
ts1 = query2ast._CalculatePastDate(13)
self.assertEqual(MakeCond(GE, [BUILTIN_ISSUE_FIELDS['modified']],
[], [ts1]),
cond1)
ast = query2ast.ParseUserQuery(
'modified>today-13', '', BUILTIN_ISSUE_FIELDS, self.default_config)
cond1 = ast.conjunctions[0].conds[0]
ts1 = query2ast._CalculatePastDate(13)
self.assertEqual(MakeCond(GT, [BUILTIN_ISSUE_FIELDS['modified']],
[], [ts1]),
cond1)
# query with multiple old date query terms.
ast = query2ast.ParseUserQuery(
'modified-after:2009-5-12 opened-before:2008/1/1 '
'closed-after:2007-2-1', '',
BUILTIN_ISSUE_FIELDS, self.default_config)
cond1, cond2, cond3 = ast.conjunctions[0].conds
ts1 = int(time.mktime(datetime.datetime(2009, 5, 12).timetuple()))
self.assertEqual(
MakeCond(GT, [BUILTIN_ISSUE_FIELDS['modified']], [], [ts1]), cond1)
ts2 = int(time.mktime(datetime.datetime(2008, 1, 1).timetuple()))
self.assertEqual(
MakeCond(LT, [BUILTIN_ISSUE_FIELDS['opened']], [], [ts2]), cond2)
ts3 = int(time.mktime(datetime.datetime(2007, 2, 1).timetuple()))
self.assertEqual(
MakeCond(GT, [BUILTIN_ISSUE_FIELDS['closed']], [], [ts3]), cond3)
if __name__ == '__main__':
unittest.main()