From 6a52ae0494563171088961283a6ea5d0fc3bfd2b Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Thu, 19 Oct 2017 11:18:05 +0100 Subject: [PATCH] Rewrite backend tests --- wagtail/tests/search/fixtures/search.json | 2 +- wagtail/wagtailsearch/tests/test_backends.py | 404 +++++++++++++++++- .../wagtailsearch/tests/test_db_backend.py | 47 +- .../tests/test_elasticsearch2_backend.py | 15 + .../tests/test_elasticsearch5_backend.py | 15 + .../tests/test_elasticsearch_backend.py | 15 + 6 files changed, 464 insertions(+), 34 deletions(-) diff --git a/wagtail/tests/search/fixtures/search.json b/wagtail/tests/search/fixtures/search.json index af308ce63c..921aaae249 100644 --- a/wagtail/tests/search/fixtures/search.json +++ b/wagtail/tests/search/fixtures/search.json @@ -215,7 +215,7 @@ "title": "The Rust Programming Language", "authors": [11, 12], "publication_date": "2018-05-22", - "number_of_pages": 488 + "number_of_pages": 440 } }, diff --git a/wagtail/wagtailsearch/tests/test_backends.py b/wagtail/wagtailsearch/tests/test_backends.py index e9c33c8156..fbd1e32183 100644 --- a/wagtail/wagtailsearch/tests/test_backends.py +++ b/wagtail/wagtailsearch/tests/test_backends.py @@ -2,6 +2,7 @@ from __future__ import absolute_import, unicode_literals +from datetime import date import time import unittest @@ -36,30 +37,401 @@ class BackendTests(WagtailTestUtils): # no conf entry found - skip tests for this backend raise unittest.SkipTest("No WAGTAILSEARCH_BACKENDS entry for the backend %s" % self.backend_path) - self.load_test_data() + management.call_command('update_index', backend_name=self.backend_name, interactive=False, stdout=StringIO()) - def reset_index(self): - if self.backend.rebuilder_class: - for index, indexed_models in group_models_by_index(self.backend, [models.Author, models.Book, models.Novel]).items(): - rebuilder = self.backend.rebuilder_class(index) - index = rebuilder.start() - for model in indexed_models: - index.add_model(model) - rebuilder.finish() + # SEARCH TESTS - def refresh_index(self): - index = self.backend.get_index_for_model(models.Author) - if index: - index.refresh() + def test_search_simple(self): + results = self.backend.search("JavaScript", models.Book) + self.assertEqual(set(r.title for r in results), { + "JavaScript: The good parts", + "JavaScript: The Definitive Guide" + }) + def test_search_count(self): + results = self.backend.search("JavaScript", models.Book) + self.assertEqual(results.count(), 2) + + def test_search_blank(self): + # Blank searches should never return anything + results = self.backend.search("", models.Book) + self.assertEqual(set(results), set()) + + def test_search_all(self): + # Searches on None should return everything in the index + # TODO: we have to put [:100] on the end due to issue #3431 + results = self.backend.search(None, models.Book)[:100] + self.assertEqual(set(results), set(models.Book.objects.all())) + + def test_ranking(self): + # Note: also tests the "or" operator + results = list(self.backend.search("JavaScript Definitive", models.Book, operator='or')) + self.assertEqual(set(r.title for r in results), { + "JavaScript: The good parts", + "JavaScript: The Definitive Guide" + }) + + # "JavaScript: The Definitive Guide" should be first + self.assertEqual(results[0].title, "JavaScript: The Definitive Guide") + + def test_search_and_operator(self): + # Should not return "JavaScript: The good parts" as it does not have "Definitive" + results = self.backend.search("JavaScript Definitive", models.Book, operator='and') + self.assertEqual(set(r.title for r in results), { + "JavaScript: The Definitive Guide" + }) + + def test_search_on_child_class(self): + # Searches on a child class should only return results that have the child class as well + # and all results should be instances of the child class + results = self.backend.search(None, models.Novel) + self.assertEqual(set(results), set(models.Novel.objects.all())) + + self.assertIsInstance(results[0], models.Novel) + + def test_search_child_class_field_from_parent(self): + # Searches the Book model for content that exists in the Novel model + # Note: "Westeros" only occurs in the Novel.setting field + # All results should be instances of the parent class + results = self.backend.search("Westeros", models.Book) + + self.assertEqual(set(r.title for r in results), { + "A Game of Thrones", + "A Clash of Kings", + "A Storm of Swords" + }) + + self.assertIsInstance(results[0], models.Book) + + def test_search_on_individual_field(self): + # The following query shouldn't search the Novel.setting field so none + # of the Novels set in "Westeros" should be returned + results = self.backend.search("Westeros Hobbit", models.Book, fields=['title'], operator='or') + + self.assertEqual(set(r.title for r in results), { + "The Hobbit" + }) + + def test_search_on_unknown_field(self): + with self.assertRaises(FieldError): + list(self.backend.search("Westeros Hobbit", models.Book, fields=['unknown'], operator='or')) + + def test_search_on_non_searchable_field(self): + with self.assertRaises(FieldError): + list(self.backend.search("Westeros Hobbit", models.Book, fields=['number_of_pages'], operator='or')) + + def test_search_on_related_fields(self): + results = self.backend.search("Bilbo Baggins", models.Novel) + + self.assertEqual(set(r.title for r in results), { + "The Hobbit", + "The Fellowship of the Ring", + "The Two Towers", + "The Return of the King" + }) + + def test_search_boosting_on_related_fields(self): + # Bilbo Baggins is the protagonist of "The Hobbit" but not any of the "Lord of the Rings" novels. + # As the protagonist has more boost than other characters, "The Hobbit" should always be returned + # first + results = list(self.backend.search("Bilbo Baggins", models.Novel)) + + self.assertEqual(results[0].title, "The Hobbit") + + # The remaining results should be scored equally so their rank is undefined + self.assertEqual(set(r.title for r in results[1:]), { + "The Fellowship of the Ring", + "The Two Towers", + "The Return of the King" + }) + + def test_search_callable_field(self): + # "Django Two scoops" only mentions "Python" in its "get_programming_language_display" + # callable field + results = self.backend.search("Python", models.Book) + + self.assertEqual(set(r.title for r in results), { + "Learning Python", + "Two Scoops of Django 1.11" + }) + + # FILTERING TESTS + + def test_filter_exact_value(self): + results = self.backend.search(None, models.Book.objects.filter(number_of_pages=440)) + + self.assertEqual(set(r.title for r in results), { + "The Return of the King", + "The Rust Programming Language" + }) + + def test_filter_exact_value_on_parent_model_field(self): + results = self.backend.search(None, models.Novel.objects.filter(number_of_pages=440)) + + self.assertEqual(set(r.title for r in results), { + "The Return of the King" + }) + + def test_filter_lt(self): + results = self.backend.search(None, models.Book.objects.filter(number_of_pages__lt=440)) + + self.assertEqual(set(r.title for r in results), { + "The Hobbit", + "JavaScript: The good parts", + "The Fellowship of the Ring", + "Foundation", + "The Two Towers" + }) + + def test_filter_lte(self): + results = self.backend.search(None, models.Book.objects.filter(number_of_pages__lte=440)) + + self.assertEqual(set(r.title for r in results), { + "The Return of the King", + "The Rust Programming Language", + "The Hobbit", + "JavaScript: The good parts", + "The Fellowship of the Ring", + "Foundation", + "The Two Towers" + }) + + def test_filter_gt(self): + results = self.backend.search(None, models.Book.objects.filter(number_of_pages__gt=440)) + + self.assertEqual(set(r.title for r in results), { + "JavaScript: The Definitive Guide", + "Learning Python", + "A Clash of Kings", + "A Game of Thrones", + "Two Scoops of Django 1.11", + "A Storm of Swords" + }) + + def test_filter_gte(self): + results = self.backend.search(None, models.Book.objects.filter(number_of_pages__gte=440)) + + self.assertEqual(set(r.title for r in results), { + "The Return of the King", + "The Rust Programming Language", + "JavaScript: The Definitive Guide", + "Learning Python", + "A Clash of Kings", + "A Game of Thrones", + "Two Scoops of Django 1.11", + "A Storm of Swords" + }) + + def test_filter_in_list(self): + results = self.backend.search(None, models.Book.objects.filter(number_of_pages__in=[440, 1160])) + + self.assertEqual(set(r.title for r in results), { + "The Return of the King", + "The Rust Programming Language", + "Learning Python" + }) + + def test_filter_in_iterable(self): + results = self.backend.search(None, models.Book.objects.filter(number_of_pages__in=iter([440, 1160]))) + + self.assertEqual(set(r.title for r in results), { + "The Return of the King", + "The Rust Programming Language", + "Learning Python" + }) + + def test_filter_in_values_list_subquery(self): + values = models.Book.objects.filter(number_of_pages__lt=440).values_list('number_of_pages', flat=True) + results = self.backend.search(None, models.Book.objects.filter(number_of_pages__in=values)) + + self.assertEqual(set(r.title for r in results), { + "The Hobbit", + "JavaScript: The good parts", + "The Fellowship of the Ring", + "Foundation", + "The Two Towers" + }) + + def test_filter_isnull_true(self): + # Note: We don't know the birth dates of any of the programming guide authors + results = self.backend.search(None, models.Author.objects.filter(date_of_birth__isnull=True)) + + self.assertEqual(set(r.name for r in results), { + "David Ascher", + "Mark Lutz", + "David Flanagan", + "Douglas Crockford", + "Daniel Roy Greenfeld", + "Audrey Roy Greenfeld", + "Carol Nichols", + "Steve Klabnik" + }) + + def test_filter_isnull_false(self): + # Note: We know the birth dates of all of the novel authors + results = self.backend.search(None, models.Author.objects.filter(date_of_birth__isnull=False)) + + self.assertEqual(set(r.name for r in results), { + "Isaac Asimov", + "George R.R. Martin", + "J. R. R. Tolkien" + }) + + def test_filter_prefix(self): + results = self.backend.search(None, models.Book.objects.filter(title__startswith="Th")) + + self.assertEqual(set(r.title for r in results), { + "The Hobbit", + "The Fellowship of the Ring", + "The Two Towers", + "The Return of the King", + "The Rust Programming Language" + }) + + def test_filter_and_operator(self): + results = self.backend.search( + None, models.Book.objects.filter(number_of_pages=440) & models.Book.objects.filter(publication_date=date(1955, 10, 20))) + + self.assertEqual(set(r.title for r in results), { + "The Return of the King" + }) + + def test_filter_or_operator(self): + results = self.backend.search(None, models.Book.objects.filter(number_of_pages=440) | models.Book.objects.filter(number_of_pages=1160)) + + self.assertEqual(set(r.title for r in results), { + "Learning Python", + "The Return of the King", + "The Rust Programming Language" + }) + + def test_filter_on_non_filterable_field(self): + with self.assertRaises(FieldError): + list(self.backend.search(None, models.Author.objects.filter(name__startswith="Issac"))) + + # ORDER BY RELEVANCE + + def test_order_by_relevance(self): + results = self.backend.search(None, models.Novel.objects.order_by('number_of_pages'), order_by_relevance=False) + + # Ordering should be set to "number_of_pages" + self.assertEqual(list(r.title for r in results), [ + "Foundation", + "The Hobbit", + "The Two Towers", + "The Fellowship of the Ring", + "The Return of the King", + "A Game of Thrones", + "A Clash of Kings", + "A Storm of Swords" + ]) + + def test_order_by_non_filterable_field(self): + with self.assertRaises(FieldError): + list(self.backend.search(None, models.Author.objects.order_by('name'), order_by_relevance=False)) + + # SLICING TESTS + + def test_single_result(self): + # Note: different to test_ranking as that casts the results to the list before doing a key lookup + # This test sends the key IDs back into Elasticsearch, performing two separate queries + results = self.backend.search("JavaScript Definitive", models.Book, operator='or') + self.assertEqual(set(r.title for r in results), { + "JavaScript: The good parts", + "JavaScript: The Definitive Guide" + }) + + self.assertEqual(results[0].title, "JavaScript: The Definitive Guide") + self.assertEqual(results[1].title, "JavaScript: The good parts") + + def test_limit(self): + # Note: we need consistant ordering for this test + results = self.backend.search(None, models.Novel.objects.order_by('number_of_pages'), order_by_relevance=False) + + # Limit the results + results = results[:3] + + self.assertEqual(list(r.title for r in results), [ + "Foundation", + "The Hobbit", + "The Two Towers" + ]) + + def test_offset(self): + # Note: we need consistant ordering for this test + results = self.backend.search(None, models.Novel.objects.order_by('number_of_pages'), order_by_relevance=False) + + # Offset the results + results = results[3:] + + self.assertEqual(list(r.title for r in results), [ + "The Fellowship of the Ring", + "The Return of the King", + "A Game of Thrones", + "A Clash of Kings", + "A Storm of Swords" + ]) + + def test_offset_and_limit(self): + # Note: we need consistant ordering for this test + results = self.backend.search(None, models.Novel.objects.order_by('number_of_pages'), order_by_relevance=False) + + # Offset the results + results = results[3:6] + + self.assertEqual(list(r.title for r in results), [ + "The Fellowship of the Ring", + "The Return of the King", + "A Game of Thrones" + ]) + + # MISC TESTS + + def test_same_rank_pages(self): + # Checks that results with a same ranking cannot be found multiple times + # across pages (see issue #3729). + same_rank_objects = set() + + index = self.backend.get_index_for_model(models.Book) + for i in range(10): + obj = models.Book.objects.create(title='Rank %s' % i, publication_date=date(2017, 10, 18), number_of_pages=100) + index.add_item(obj) + same_rank_objects.add(obj) + index.refresh() + + results = self.backend.search('Rank', models.Book) + results_across_pages = set() + for i, obj in enumerate(same_rank_objects): + results_across_pages.add(results[i:i + 1][0]) + self.assertSetEqual(results_across_pages, same_rank_objects) + + def test_delete(self): + # Delete foundation + obj = models.Book.objects.filter(title="Foundation").delete() + + # Refresh the index + # Note: The delete signal handler should've removed the book, but we still need to refresh the index manually index = self.backend.get_index_for_model(models.Book) if index: index.refresh() - def load_test_data(self): - self.reset_index() + # To test that the book was deleted from the index as well, we will perform the slicing check from an earlier + # test where "Foundation" was the first result. We need to test it this way so we can pick up the case where + # the object still exists in the index but not in the database (in that case, just two objects would be returned + # instead of three). + + # Note: we need consistant ordering for this test + results = self.backend.search(None, models.Novel.objects.order_by('number_of_pages'), order_by_relevance=False) + + # Limit the results + results = results[:3] + + self.assertEqual(list(r.title for r in results), [ + "The Hobbit", + "The Two Towers", + "The Fellowship of the Ring" + ]) - self.refresh_index() @override_settings( WAGTAILSEARCH_BACKENDS={ diff --git a/wagtail/wagtailsearch/tests/test_db_backend.py b/wagtail/wagtailsearch/tests/test_db_backend.py index bbe47f60a6..cd4ffa804f 100644 --- a/wagtail/wagtailsearch/tests/test_db_backend.py +++ b/wagtail/wagtailsearch/tests/test_db_backend.py @@ -12,29 +12,42 @@ from .test_backends import BackendTests class TestDBBackend(BackendTests, TestCase): backend_path = 'wagtail.wagtailsearch.backends.db' + # Doesn't support ranking @unittest.expectedFailure - def test_callable_indexed_field(self): - super(TestDBBackend, self).test_callable_indexed_field() + def test_ranking(self): + super(TestDBBackend, self).test_ranking() + # Doesn't support ranking @unittest.expectedFailure - def test_related_objects_search(self): - super(TestDBBackend, self).test_related_objects_search() + def test_search_boosting_on_related_fields(self): + super(TestDBBackend, self).test_search_boosting_on_related_fields() + # Doesn't support searching specific fields @unittest.expectedFailure - def test_update_index_command(self): - super(TestDBBackend, self).test_update_index_command() - - def test_annotate_score(self): - results = self.backend.search("Python", models.Book).annotate_score('_score') - - for result in results: - # DB backend doesn't do scoring, so annotate_score should just add None - self.assertIsNone(result._score) + def test_search_child_class_field_from_parent(self): + super(TestDBBackend, self).test_search_child_class_field_from_parent() + # Doesn't support searching related fields @unittest.expectedFailure - def test_boost(self): - super(TestDBBackend, self).test_boost() + def test_search_on_related_fields(self): + super(TestDBBackend, self).test_search_on_related_fields() + # Doesn't support searching callable fields @unittest.expectedFailure - def test_order_by_relevance(self): - super(TestDBBackend, self).test_order_by_relevance() + def test_search_callable_field(self): + super(TestDBBackend, self).test_search_callable_field() + + # Broken + @unittest.expectedFailure + def test_order_by_non_filterable_field(self): + super(TestDBBackend, self).test_order_by_non_filterable_field() + + # Broken + @unittest.expectedFailure + def test_single_result(self): + super(TestDBBackend, self).test_single_result() + + # Doesn't support the index API used in this test + @unittest.expectedFailure + def test_same_rank_pages(self): + super(TestDBBackend, self).test_same_rank_pages() diff --git a/wagtail/wagtailsearch/tests/test_elasticsearch2_backend.py b/wagtail/wagtailsearch/tests/test_elasticsearch2_backend.py index e19d8b95fe..3757a599b4 100644 --- a/wagtail/wagtailsearch/tests/test_elasticsearch2_backend.py +++ b/wagtail/wagtailsearch/tests/test_elasticsearch2_backend.py @@ -25,6 +25,21 @@ from .test_backends import BackendTests class TestElasticsearch2SearchBackend(BackendTests, TestCase): backend_path = 'wagtail.wagtailsearch.backends.elasticsearch2' + # Broken + @unittest.expectedFailure + def test_filter_in_values_list_subquery(self): + super(TestElasticsearch2SearchBackend, self).test_filter_in_values_list_subquery() + + # Broken + @unittest.expectedFailure + def test_order_by_non_filterable_field(self): + super(TestElasticsearch2SearchBackend, self).test_order_by_non_filterable_field() + + # Broken + @unittest.expectedFailure + def test_delete(self): + super(TestElasticsearch2SearchBackend, self).test_delete() + class TestElasticsearch2SearchQuery(TestCase): def assertDictEqual(self, a, b): diff --git a/wagtail/wagtailsearch/tests/test_elasticsearch5_backend.py b/wagtail/wagtailsearch/tests/test_elasticsearch5_backend.py index dbc4fda311..616d2e5438 100644 --- a/wagtail/wagtailsearch/tests/test_elasticsearch5_backend.py +++ b/wagtail/wagtailsearch/tests/test_elasticsearch5_backend.py @@ -24,6 +24,21 @@ from .test_backends import BackendTests class TestElasticsearch5SearchBackend(BackendTests, TestCase): backend_path = 'wagtail.wagtailsearch.backends.elasticsearch5' + # Broken + @unittest.expectedFailure + def test_filter_in_values_list_subquery(self): + super(TestElasticsearch5SearchBackend, self).test_filter_in_values_list_subquery() + + # Broken + @unittest.expectedFailure + def test_order_by_non_filterable_field(self): + super(TestElasticsearch5SearchBackend, self).test_order_by_non_filterable_field() + + # Broken + @unittest.expectedFailure + def test_delete(self): + super(TestElasticsearch5SearchBackend, self).test_delete() + class TestElasticsearch5SearchQuery(TestCase): def assertDictEqual(self, a, b): diff --git a/wagtail/wagtailsearch/tests/test_elasticsearch_backend.py b/wagtail/wagtailsearch/tests/test_elasticsearch_backend.py index d7e14b3cce..fcf64d975b 100644 --- a/wagtail/wagtailsearch/tests/test_elasticsearch_backend.py +++ b/wagtail/wagtailsearch/tests/test_elasticsearch_backend.py @@ -24,6 +24,21 @@ from .test_backends import BackendTests class TestElasticsearchSearchBackend(BackendTests, TestCase): backend_path = 'wagtail.wagtailsearch.backends.elasticsearch' + # Broken + @unittest.expectedFailure + def test_filter_in_values_list_subquery(self): + super(TestElasticsearchSearchBackend, self).test_filter_in_values_list_subquery() + + # Broken + @unittest.expectedFailure + def test_order_by_non_filterable_field(self): + super(TestElasticsearchSearchBackend, self).test_order_by_non_filterable_field() + + # Broken + @unittest.expectedFailure + def test_delete(self): + super(TestElasticsearchSearchBackend, self).test_delete() + class TestElasticsearchSearchQuery(TestCase): def assertDictEqual(self, a, b):