333 lines
12 KiB
Python
333 lines
12 KiB
Python
from django.test import SimpleTestCase, TestCase
|
|
|
|
from wagtail.search.query import And, Or, Phrase, PlainText
|
|
from wagtail.search.utils import (
|
|
balanced_reduce,
|
|
normalise_query_string,
|
|
parse_query_string,
|
|
separate_filters_from_query,
|
|
)
|
|
|
|
|
|
class TestQueryStringNormalisation(TestCase):
|
|
def test_truncation(self):
|
|
test_querystring = "a" * 1000
|
|
result = normalise_query_string(test_querystring)
|
|
self.assertEqual(len(result), 255)
|
|
|
|
def test_no_truncation(self):
|
|
test_querystring = "a" * 10
|
|
result = normalise_query_string(test_querystring)
|
|
self.assertEqual(len(result), 10)
|
|
|
|
|
|
class TestSeparateFiltersFromQuery(SimpleTestCase):
|
|
def test_only_query(self):
|
|
filters, query = separate_filters_from_query("hello world")
|
|
|
|
self.assertDictEqual(filters.dict(), {})
|
|
self.assertEqual(query, "hello world")
|
|
|
|
def test_filter(self):
|
|
filters, query = separate_filters_from_query("author:foo")
|
|
|
|
self.assertDictEqual(filters.dict(), {"author": "foo"})
|
|
self.assertEqual(query, "")
|
|
|
|
def test_filter_with_quotation_mark(self):
|
|
filters, query = separate_filters_from_query('author:"foo bar"')
|
|
|
|
self.assertDictEqual(filters.dict(), {"author": "foo bar"})
|
|
self.assertEqual(query, "")
|
|
|
|
def test_filter_and_query(self):
|
|
filters, query = separate_filters_from_query("author:foo hello world")
|
|
|
|
self.assertDictEqual(filters.dict(), {"author": "foo"})
|
|
self.assertEqual(query, "hello world")
|
|
|
|
def test_filter_with_quotation_mark_and_query(self):
|
|
filters, query = separate_filters_from_query('author:"foo bar" hello world')
|
|
|
|
self.assertDictEqual(filters.dict(), {"author": "foo bar"})
|
|
self.assertEqual(query, "hello world")
|
|
|
|
def test_filter_with_unclosed_quotation_mark_and_query(self):
|
|
filters, query = separate_filters_from_query('author:"foo bar hello world')
|
|
|
|
self.assertDictEqual(filters.dict(), {})
|
|
self.assertEqual(query, 'author:"foo bar hello world')
|
|
|
|
def test_two_filters_and_query(self):
|
|
filters, query = separate_filters_from_query(
|
|
'author:"foo bar" hello world bar:beer'
|
|
)
|
|
|
|
self.assertDictEqual(filters.dict(), {"author": "foo bar", "bar": "beer"})
|
|
self.assertEqual(query, "hello world")
|
|
|
|
def test_two_filters_with_quotation_marks_and_query(self):
|
|
filters, query = separate_filters_from_query(
|
|
'author:"foo bar" hello world bar:"two beers"'
|
|
)
|
|
|
|
self.assertDictEqual(filters.dict(), {"author": "foo bar", "bar": "two beers"})
|
|
self.assertEqual(query, "hello world")
|
|
|
|
filters, query = separate_filters_from_query(
|
|
"author:'foo bar' hello world bar:'two beers'"
|
|
)
|
|
|
|
self.assertDictEqual(filters.dict(), {"author": "foo bar", "bar": "two beers"})
|
|
self.assertEqual(query, "hello world")
|
|
|
|
def test_return_list_of_multiple_instances_for_same_filter_key(self):
|
|
filters, query = separate_filters_from_query(
|
|
'foo:test1 hello world foo:test2 foo:"test3" foo2:test4'
|
|
)
|
|
|
|
self.assertDictEqual(filters.dict(), {"foo": "test3", "foo2": "test4"})
|
|
self.assertListEqual(filters.getlist("foo"), ["test1", "test2", "test3"])
|
|
self.assertEqual(query, "hello world")
|
|
|
|
|
|
class TestParseQueryString(SimpleTestCase):
|
|
def test_simple_query(self):
|
|
filters, query = parse_query_string("hello world")
|
|
|
|
self.assertDictEqual(filters.dict(), {})
|
|
self.assertEqual(repr(query), repr(PlainText("hello world")))
|
|
|
|
def test_with_phrase(self):
|
|
filters, query = parse_query_string('"hello world"')
|
|
|
|
self.assertDictEqual(filters.dict(), {})
|
|
self.assertEqual(repr(query), repr(Phrase("hello world")))
|
|
|
|
filters, query = parse_query_string("'hello world'")
|
|
|
|
self.assertDictEqual(filters.dict(), {})
|
|
self.assertEqual(repr(query), repr(Phrase("hello world")))
|
|
|
|
def test_with_simple_and_phrase(self):
|
|
filters, query = parse_query_string('this is simple "hello world"')
|
|
|
|
self.assertDictEqual(filters.dict(), {})
|
|
self.assertEqual(
|
|
repr(query), repr(And([PlainText("this is simple"), Phrase("hello world")]))
|
|
)
|
|
|
|
filters, query = parse_query_string("this is simple 'hello world'")
|
|
|
|
self.assertDictEqual(filters.dict(), {})
|
|
self.assertEqual(
|
|
repr(query), repr(And([PlainText("this is simple"), Phrase("hello world")]))
|
|
)
|
|
|
|
def test_operator(self):
|
|
filters, query = parse_query_string(
|
|
'this is simple "hello world"', operator="or"
|
|
)
|
|
|
|
self.assertDictEqual(filters.dict(), {})
|
|
self.assertEqual(
|
|
repr(query),
|
|
repr(
|
|
Or([PlainText("this is simple", operator="or"), Phrase("hello world")])
|
|
),
|
|
)
|
|
|
|
filters, query = parse_query_string(
|
|
"this is simple 'hello world'", operator="or"
|
|
)
|
|
|
|
self.assertDictEqual(filters.dict(), {})
|
|
self.assertEqual(
|
|
repr(query),
|
|
repr(
|
|
Or([PlainText("this is simple", operator="or"), Phrase("hello world")])
|
|
),
|
|
)
|
|
|
|
def test_with_phrase_unclosed(self):
|
|
filters, query = parse_query_string('"hello world')
|
|
|
|
self.assertDictEqual(filters.dict(), {})
|
|
self.assertEqual(repr(query), repr(Phrase("hello world")))
|
|
|
|
filters, query = parse_query_string("'hello world")
|
|
|
|
self.assertDictEqual(filters.dict(), {})
|
|
self.assertEqual(repr(query), repr(Phrase("hello world")))
|
|
|
|
def test_phrase_with_filter(self):
|
|
filters, query = parse_query_string('"hello world" author:"foo bar" bar:beer')
|
|
|
|
self.assertDictEqual(filters.dict(), {"author": "foo bar", "bar": "beer"})
|
|
self.assertEqual(repr(query), repr(Phrase("hello world")))
|
|
|
|
filters, query = parse_query_string("'hello world' author:'foo bar' bar:beer")
|
|
|
|
self.assertDictEqual(filters.dict(), {"author": "foo bar", "bar": "beer"})
|
|
self.assertEqual(repr(query), repr(Phrase("hello world")))
|
|
|
|
def test_long_queries(self):
|
|
filters, query = parse_query_string("0" * 60_000)
|
|
self.assertEqual(filters.dict(), {})
|
|
self.assertEqual(repr(query), repr(PlainText("0" * 60_000)))
|
|
|
|
filters, _ = parse_query_string(f'{"a" * 60_000}:"foo bar"')
|
|
self.assertEqual(filters.dict(), {"a" * 60_000: "foo bar"})
|
|
|
|
def test_long_filter_value(self):
|
|
filters, _ = parse_query_string(f'foo:ba{"r" * 60_000}')
|
|
self.assertEqual(filters.dict(), {"foo": f'ba{"r" * 60_000}'})
|
|
|
|
def test_joined_filters(self):
|
|
filters, query = parse_query_string("foo:bar:baz")
|
|
self.assertEqual(filters.dict(), {"foo": "bar"})
|
|
self.assertEqual(repr(query), repr(PlainText(":baz")))
|
|
|
|
filters, query = parse_query_string("foo:'bar':baz")
|
|
self.assertEqual(filters.dict(), {"foo": "bar"})
|
|
self.assertEqual(repr(query), repr(PlainText(":baz")))
|
|
|
|
filters, query = parse_query_string("foo:'bar:baz'")
|
|
self.assertEqual(filters.dict(), {"foo": "bar:baz"})
|
|
|
|
def test_multiple_phrases(self):
|
|
filters, query = parse_query_string('"hello world" "hi earth"')
|
|
|
|
self.assertEqual(
|
|
repr(query), repr(And([Phrase("hello world"), Phrase("hi earth")]))
|
|
)
|
|
|
|
filters, query = parse_query_string("'hello world' 'hi earth'")
|
|
|
|
self.assertEqual(
|
|
repr(query), repr(And([Phrase("hello world"), Phrase("hi earth")]))
|
|
)
|
|
|
|
def test_mixed_phrases_with_filters(self):
|
|
filters, query = parse_query_string(
|
|
""""lord of the rings" army_1:"elves" army_2:'humans'"""
|
|
)
|
|
|
|
self.assertDictEqual(filters.dict(), {"army_1": "elves", "army_2": "humans"})
|
|
self.assertEqual(
|
|
repr(query),
|
|
repr(Phrase("lord of the rings")),
|
|
)
|
|
|
|
|
|
class TestBalancedReduce(SimpleTestCase):
|
|
# For simple values, this should behave exactly the same as Pythons reduce()
|
|
# So I've copied its tests: https://github.com/python/cpython/blob/21cdb711e3b1975398c54141e519ead02670610e/Lib/test/test_functools.py#L771
|
|
|
|
def test_reduce(self):
|
|
class Squares:
|
|
def __init__(self, max):
|
|
self.max = max
|
|
self.sofar = []
|
|
|
|
def __len__(self):
|
|
return len(self.sofar)
|
|
|
|
def __getitem__(self, i):
|
|
if not 0 <= i < self.max:
|
|
raise IndexError
|
|
n = len(self.sofar)
|
|
while n <= i:
|
|
self.sofar.append(n * n)
|
|
n += 1
|
|
return self.sofar[i]
|
|
|
|
def add(x, y):
|
|
return x + y
|
|
|
|
self.assertEqual(balanced_reduce(add, ["a", "b", "c"], ""), "abc")
|
|
self.assertEqual(
|
|
balanced_reduce(add, [["a", "c"], [], ["d", "w"]], []), ["a", "c", "d", "w"]
|
|
)
|
|
self.assertEqual(balanced_reduce(lambda x, y: x * y, range(2, 8), 1), 5040)
|
|
self.assertEqual(
|
|
balanced_reduce(lambda x, y: x * y, range(2, 21), 1), 2432902008176640000
|
|
)
|
|
self.assertEqual(balanced_reduce(add, Squares(10)), 285)
|
|
self.assertEqual(balanced_reduce(add, Squares(10), 0), 285)
|
|
self.assertEqual(balanced_reduce(add, Squares(0), 0), 0)
|
|
self.assertRaises(TypeError, balanced_reduce)
|
|
self.assertRaises(TypeError, balanced_reduce, 42, 42)
|
|
self.assertRaises(TypeError, balanced_reduce, 42, 42, 42)
|
|
self.assertEqual(
|
|
balanced_reduce(42, "1"), "1"
|
|
) # func is never called with one item
|
|
self.assertEqual(
|
|
balanced_reduce(42, "", "1"), "1"
|
|
) # func is never called with one item
|
|
self.assertRaises(TypeError, balanced_reduce, 42, (42, 42))
|
|
self.assertRaises(
|
|
TypeError, balanced_reduce, add, []
|
|
) # arg 2 must not be empty sequence with no initial value
|
|
self.assertRaises(TypeError, balanced_reduce, add, "")
|
|
self.assertRaises(TypeError, balanced_reduce, add, ())
|
|
self.assertRaises(TypeError, balanced_reduce, add, object())
|
|
|
|
class TestFailingIter:
|
|
def __iter__(self):
|
|
raise RuntimeError
|
|
|
|
self.assertRaises(RuntimeError, balanced_reduce, add, TestFailingIter())
|
|
|
|
self.assertIsNone(balanced_reduce(add, [], None))
|
|
self.assertEqual(balanced_reduce(add, [], 42), 42)
|
|
|
|
class BadSeq:
|
|
def __getitem__(self, index):
|
|
raise ValueError
|
|
|
|
self.assertRaises(ValueError, balanced_reduce, 42, BadSeq())
|
|
|
|
# Test reduce()'s use of iterators.
|
|
def test_iterator_usage(self):
|
|
class SequenceClass:
|
|
def __init__(self, n):
|
|
self.n = n
|
|
|
|
def __getitem__(self, i):
|
|
if 0 <= i < self.n:
|
|
return i
|
|
else:
|
|
raise IndexError
|
|
|
|
from operator import add
|
|
|
|
self.assertEqual(balanced_reduce(add, SequenceClass(5)), 10)
|
|
self.assertEqual(balanced_reduce(add, SequenceClass(5), 42), 52)
|
|
self.assertRaises(TypeError, balanced_reduce, add, SequenceClass(0))
|
|
self.assertEqual(balanced_reduce(add, SequenceClass(0), 42), 42)
|
|
self.assertEqual(balanced_reduce(add, SequenceClass(1)), 0)
|
|
self.assertEqual(balanced_reduce(add, SequenceClass(1), 42), 42)
|
|
|
|
d = {"one": 1, "two": 2, "three": 3}
|
|
self.assertEqual(balanced_reduce(add, d), "".join(d.keys()))
|
|
|
|
# This test is specific to balanced_reduce
|
|
def test_is_balanced(self):
|
|
# Tests that balanced_reduce returns the object as a balanced tree
|
|
class CombinedNode:
|
|
def __init__(self, a, b):
|
|
self.a = a
|
|
self.b = b
|
|
|
|
def __repr__(self):
|
|
return f"({self.a} {self.b})"
|
|
|
|
self.assertEqual(
|
|
repr(
|
|
balanced_reduce(CombinedNode, ["A", "B", "C", "D", "E", "F", "G", "H"])
|
|
),
|
|
"(((A B) (C D)) ((E F) (G H)))",
|
|
# Note: functools.reduce will return '(((((((A B) C) D) E) F) G) H)'
|
|
)
|