diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 213e8c139..ab9ef2ef0 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -57,8 +57,6 @@ jobs: python -Wonce -m unittest discover -s tests -v - name: Run tests for Django example app run: | - python -m pip install "django<=4.1" - python -Wonce runtests.py python -Wonce examples/django_example/manage.py test examples/django_example/ # -------------------------------------------------------------- # TODO: Fix & re-enable later diff --git a/chatterbot/chatterbot.py b/chatterbot/chatterbot.py index 298ab4cf1..010f4f798 100644 --- a/chatterbot/chatterbot.py +++ b/chatterbot/chatterbot.py @@ -77,7 +77,12 @@ def __init__(self, name, stream=False, **kwargs): try: Tagger = kwargs.get('tagger', PosLemmaTagger) - self.tagger = Tagger(language=tagger_language) + # Allow instances to be provided for performance optimization + # (Example: a pre-loaded model in a tagger when unit testing) + if not isinstance(Tagger, type): + self.tagger = Tagger + else: + self.tagger = Tagger(language=tagger_language) except IOError as io_error: # Return a more helpful error message if possible if "Can't find model" in str(io_error): @@ -224,7 +229,6 @@ def get_response(self, statement: Union[Statement, str, dict] = None, **kwargs) # Save the response generated for the input self.learn_response(response, previous_statement=input_statement) - return response def generate_response(self, input_statement, additional_response_selection_parameters=None): @@ -345,8 +349,6 @@ def get_latest_response(self, conversation: str): Returns the latest response in a conversation if it exists. Returns None if a matching conversation cannot be found. """ - from chatterbot.conversation import Statement as StatementObject - conversation_statements = list(self.storage.filter( conversation=conversation, order_by=['id'] diff --git a/chatterbot/comparisons.py b/chatterbot/comparisons.py index 04a74e216..533397b2c 100644 --- a/chatterbot/comparisons.py +++ b/chatterbot/comparisons.py @@ -85,7 +85,10 @@ class SpacySimilarity(Comparator): python -m spacy download en_core_web_sm python -m spacy download de_core_news_sm - Alternatively, the ``spacy`` models can be installed as Python packages. The following lines could be included in a ``requirements.txt`` or ``pyproject.yml`` file if you needed to pin specific versions: + Alternatively, the ``spacy`` models can be installed as Python + packages. The following lines could be included in a + ``requirements.txt`` or ``pyproject.yml`` file if you needed to pin + specific versions: .. code-block:: text diff --git a/chatterbot/logic/best_match.py b/chatterbot/logic/best_match.py index f0852f233..a7ff66e5d 100644 --- a/chatterbot/logic/best_match.py +++ b/chatterbot/logic/best_match.py @@ -78,7 +78,6 @@ def process(self, input_statement: Statement, additional_response_selection_para additional_response_selection_parameters ) - # Get all statements with text similar to the closest match response_list = list(self.chatbot.storage.filter(**response_selection_parameters)) diff --git a/chatterbot/logic/unit_conversion.py b/chatterbot/logic/unit_conversion.py index 98a80262f..c0b3ca721 100644 --- a/chatterbot/logic/unit_conversion.py +++ b/chatterbot/logic/unit_conversion.py @@ -85,7 +85,7 @@ def get_unit(self, unit_variations): for unit in unit_variations: try: return getattr(self.unit_registry, unit) - except Exception: + except AttributeError: continue return None diff --git a/chatterbot/response_selection.py b/chatterbot/response_selection.py index 957cccd5d..58f1ce841 100644 --- a/chatterbot/response_selection.py +++ b/chatterbot/response_selection.py @@ -18,17 +18,28 @@ def get_most_frequent_response(input_statement: Statement, response_list: list[S :return: The response statement with the greatest number of occurrences. """ - matching_response = None - occurrence_count = -1 - logger = logging.getLogger(__name__) logger.info('Selecting response with greatest number of occurrences.') + # Collect all unique text values from response_list + response_texts = set(statement.text for statement in response_list) + + # Fetch all statements matching the input in a single query + # Then count occurrences in memory + all_matching = list(storage.filter(in_response_to=input_statement.text)) + + # Count how many times each response text appears in the database + occurrence_counts = {} + for statement in all_matching: + if statement.text in response_texts: + occurrence_counts[statement.text] = occurrence_counts.get(statement.text, 0) + 1 + + # Find the response with the highest occurrence count + matching_response = None + occurrence_count = -1 + for statement in response_list: - count = len(list(storage.filter( - text=statement.text, - in_response_to=input_statement.text) - )) + count = occurrence_counts.get(statement.text, 0) # Keep the more common statement if count >= occurrence_count: diff --git a/chatterbot/search.py b/chatterbot/search.py index 1bfa27f2c..01daec5a6 100644 --- a/chatterbot/search.py +++ b/chatterbot/search.py @@ -74,6 +74,10 @@ def search(self, input_statement, **additional_parameters): yield statement + if confidence >= 1.0: + self.chatbot.logger.info('Exact match found, stopping search') + break + class TextSearch: """ @@ -149,3 +153,7 @@ def search(self, input_statement, **additional_parameters): )) yield statement + + if confidence >= 1.0: + self.chatbot.logger.info('Exact match found, stopping search') + break diff --git a/chatterbot/storage/django_storage.py b/chatterbot/storage/django_storage.py index 7966efde3..70cd6f07d 100644 --- a/chatterbot/storage/django_storage.py +++ b/chatterbot/storage/django_storage.py @@ -47,7 +47,7 @@ def filter(self, **kwargs): search_in_response_to_contains = kwargs.pop('search_in_response_to_contains', None) # Convert a single sting into a list if only one tag is provided - if type(tags) == str: + if isinstance(tags, str): tags = [tags] if tags: diff --git a/chatterbot/storage/mongodb.py b/chatterbot/storage/mongodb.py index 6b0588616..0c1e60659 100644 --- a/chatterbot/storage/mongodb.py +++ b/chatterbot/storage/mongodb.py @@ -146,15 +146,17 @@ def filter(self, **kwargs): for order in order_by: mongo_ordering.append((order, pymongo.ASCENDING)) - total_statements = self.statements.count_documents(kwargs) - - for start_index in range(0, total_statements, page_size): - if mongo_ordering: - for match in self.statements.find(kwargs).sort(mongo_ordering).skip(start_index).limit(page_size): - yield self.mongo_to_object(match) - else: - for match in self.statements.find(kwargs).skip(start_index).limit(page_size): - yield self.mongo_to_object(match) + # Build the query cursor + if mongo_ordering: + cursor = self.statements.find(kwargs).sort(mongo_ordering) + else: + cursor = self.statements.find(kwargs) + + # Use batch_size for efficient pagination without counting total documents + cursor = cursor.batch_size(page_size) + + for match in cursor: + yield self.mongo_to_object(match) def create(self, **kwargs): """ diff --git a/chatterbot/storage/redis.py b/chatterbot/storage/redis.py index 5c8e2489d..3028cc9ef 100644 --- a/chatterbot/storage/redis.py +++ b/chatterbot/storage/redis.py @@ -16,6 +16,7 @@ REDIS_TRANSLATION_TABLE = str.maketrans(REDIS_ESCAPE_CHARACTERS) + def _escape_redis_special_characters(text): """ Escape special characters in a string that are used in redis queries. @@ -369,7 +370,7 @@ def get_random(self): if documents: return self.model_to_object(documents[0]) - + raise self.EmptyDatabaseException() def drop(self): diff --git a/chatterbot/storage/sql_storage.py b/chatterbot/storage/sql_storage.py index 5d8948c08..ff05e7434 100644 --- a/chatterbot/storage/sql_storage.py +++ b/chatterbot/storage/sql_storage.py @@ -142,7 +142,7 @@ def filter(self, **kwargs): search_in_response_to_contains = kwargs.pop('search_in_response_to_contains', None) # Convert a single sting into a list if only one tag is provided - if type(tags) == str: + if isinstance(tags, str): tags = [tags] if len(kwargs) == 0: @@ -240,15 +240,18 @@ def create( ) tags = frozenset(tags) if tags else frozenset() - for tag_name in frozenset(tags): - # TODO: Query existing tags in bulk - tag = session.query(Tag).filter_by(name=tag_name).first() - if not tag: - # Create the tag - tag = Tag(name=tag_name) + # Batch query tags + if tags: + existing_tags = session.query(Tag).filter(Tag.name.in_(tags)).all() + existing_tag_dict = {tag.name: tag for tag in existing_tags} - statement.tags.append(tag) + for tag_name in tags: + tag = existing_tag_dict.get(tag_name) + if not tag: + # Create the tag if it doesn't exist + tag = Tag(name=tag_name) + statement.tags.append(tag) session.add(statement) diff --git a/chatterbot/trainers.py b/chatterbot/trainers.py index 722f64df0..76a600592 100644 --- a/chatterbot/trainers.py +++ b/chatterbot/trainers.py @@ -364,6 +364,7 @@ def train(self, data_path: str, limit=None): ) ) + class CsvFileTrainer(GenericFileTrainer): """ .. note:: @@ -550,7 +551,7 @@ def safe_extract(tar, path='.', members=None, *, numeric_owner=False): self.chatbot.logger.info('File extracted to {}'.format(self.data_path)) return True - + def _get_file_list(self, data_path: str, limit: Union[int, None]): """ Get a list of files to read from the data set. diff --git a/chatterbot/vectorstores.py b/chatterbot/vectorstores.py index cf0358aae..91eb3cff2 100644 --- a/chatterbot/vectorstores.py +++ b/chatterbot/vectorstores.py @@ -3,13 +3,12 @@ """ from __future__ import annotations -from typing import Any, List, Sequence +from typing import List from langchain_core.documents import Document from redisvl.redis.utils import convert_bytes from redisvl.query import FilterQuery -from langchain_core.documents import Document from langchain_redis.vectorstores import RedisVectorStore as LangChainRedisVectorStore diff --git a/docs/testing.rst b/docs/testing.rst index df94ba7ed..603b6b733 100644 --- a/docs/testing.rst +++ b/docs/testing.rst @@ -15,6 +15,8 @@ You can run ChatterBot's main test suite using Python's built-in test runner. Fo python -m unittest discover -s tests -v +This command will run all tests including Django integration tests (if Django is installed). + *Note* that the ``unittest`` command also allows you to specify individual test cases to run. For example, the following command will run all tests in the test-module `tests/logic/` @@ -34,22 +36,21 @@ Tests can also be run in "fail fast" mode, in which case they will run until the python -m unittest discover -f tests -For more information on ``unittest`` functionality, see the `unittest documentation`_. - Django integration tests ------------------------ -Tests for Django integration have been included in the `tests_django` directory and -can be run with: +Django integration tests are included in ``tests/django_integration/`` and will automatically run +when you execute the main test suite (if Django is installed). If Django is not available, +these tests will be gracefully skipped. -.. sourcecode:: sh +To run only Django integration tests: - python runtests.py +.. sourcecode:: sh -Django example app tests ------------------------- + python -m unittest discover -s tests/django_integration/ -v -Tests for the example Django app can be run with the following command from within the `examples/django_example` directory. +The Django example app tests can be run separately with the following command from within +the `examples/django_example` directory: .. sourcecode:: sh diff --git a/pyproject.toml b/pyproject.toml index 5e1280256..a46fbd191 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -60,7 +60,7 @@ classifiers = [ "Programming Language :: Python :: 3 :: Only", ] dependencies = [ - "mathparse>=0.1,<0.2", + "mathparse>=0.2,<0.3", "python-dateutil>=2.9,<2.10", "sqlalchemy>=2.0,<2.1", "spacy>=3.8,<3.9", @@ -74,6 +74,7 @@ test = [ "sphinx>=5.3,<8.2", "sphinx-sitemap>=2.6.0", "huggingface_hub", + "django<=4.1,<6.0" ] dev = [ "pint>=0.8.1", diff --git a/runtests.py b/runtests.py deleted file mode 100644 index e68150282..000000000 --- a/runtests.py +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/env python - -""" -This is the test runner for the ChatterBot's Django tests. -""" - -import os -import sys - -import django -from django.conf import settings -from django.test.utils import get_runner - -if __name__ == '__main__': - os.environ['DJANGO_SETTINGS_MODULE'] = 'tests_django.test_settings' - django.setup() - TestRunner = get_runner(settings) - test_runner = TestRunner( - verbosity=2 - ) - failures = test_runner.run_tests(['tests_django']) - sys.exit(bool(failures)) diff --git a/tests/base_case.py b/tests/base_case.py index 05a750269..2e44b7b9b 100644 --- a/tests/base_case.py +++ b/tests/base_case.py @@ -4,9 +4,25 @@ class ChatBotTestCase(TestCase): + """ + Base test case class that provides common test utilities. + """ + + # Share a single tagger instance across all tests in a test class to avoid + # repeatedly loading the spaCy model (saves 1-3 seconds per test) + _shared_tagger = None + + @classmethod + def setUpClass(cls): + super().setUpClass() + if cls._shared_tagger is None: + from chatterbot.tagging import PosLemmaTagger + cls._shared_tagger = PosLemmaTagger() def setUp(self): - self.chatbot = ChatBot('Test Bot', **self.get_kwargs()) + kwargs = self.get_kwargs() + kwargs['tagger'] = self._shared_tagger + self.chatbot = ChatBot('Test Bot', **kwargs) def _add_search_text(self, **kwargs): """ @@ -106,6 +122,9 @@ def setUpClass(cls): except ServerSelectionTimeoutError: raise SkipTest('Unable to connect to Mongo DB.') + # Initialize the shared tagger + super().setUpClass() + def get_kwargs(self): kwargs = super().get_kwargs() kwargs['database_uri'] = 'mongodb://localhost:27017/chatterbot_test_database' diff --git a/tests/django_integration/__init__.py b/tests/django_integration/__init__.py new file mode 100644 index 000000000..ca675e47f --- /dev/null +++ b/tests/django_integration/__init__.py @@ -0,0 +1,53 @@ +""" +Django integration tests for ChatterBot. + +This package contains tests that require Django to be installed. +If Django is not available, these tests will be gracefully skipped. +""" + +import os +import sys + +# Check if Django is available +try: + import django + from django.core.management import call_command + DJANGO_AVAILABLE = True + + # Configure Django settings immediately upon import + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'tests.django_integration.test_settings') + + try: + django.setup() + + # Run migrations to create database tables + call_command('migrate', '--run-syncdb', verbosity=0, interactive=False) + + except Exception as e: + print(f"Warning: Django setup failed: {e}", file=sys.stderr) + DJANGO_AVAILABLE = False + +except ImportError: + DJANGO_AVAILABLE = False + + +def load_tests(loader, tests, pattern): + """ + Custom test loader that configures Django before running tests. + + This function is called automatically by unittest's discovery mechanism. + If Django is not installed, it returns an empty test suite. + """ + if not DJANGO_AVAILABLE: + # Return empty test suite if Django is not available + import unittest + return unittest.TestSuite() + + # Load all tests from this package + package_tests = loader.discover( + start_dir=os.path.dirname(__file__), + pattern=pattern or 'test*.py', + top_level_dir=os.path.dirname(os.path.dirname(__file__)) + ) + + return package_tests diff --git a/tests_django/base_case.py b/tests/django_integration/base_case.py similarity index 96% rename from tests_django/base_case.py rename to tests/django_integration/base_case.py index 8e8e080fb..2b118acb2 100644 --- a/tests_django/base_case.py +++ b/tests/django_integration/base_case.py @@ -1,6 +1,6 @@ from chatterbot import ChatBot from django.test import TransactionTestCase -from tests_django import test_settings +from tests.django_integration import test_settings class ChatterBotTestCase(TransactionTestCase): diff --git a/tests_django/test_chatbot.py b/tests/django_integration/test_chatbot.py similarity index 99% rename from tests_django/test_chatbot.py rename to tests/django_integration/test_chatbot.py index ddaf3481d..c34d99b9a 100644 --- a/tests_django/test_chatbot.py +++ b/tests/django_integration/test_chatbot.py @@ -1,4 +1,4 @@ -from tests_django.base_case import ChatterBotTestCase +from tests.django_integration.base_case import ChatterBotTestCase from chatterbot.conversation import Statement diff --git a/tests_django/test_chatterbot_corpus_training.py b/tests/django_integration/test_chatterbot_corpus_training.py similarity index 96% rename from tests_django/test_chatterbot_corpus_training.py rename to tests/django_integration/test_chatterbot_corpus_training.py index 185191b6a..df0e99dc1 100644 --- a/tests_django/test_chatterbot_corpus_training.py +++ b/tests/django_integration/test_chatterbot_corpus_training.py @@ -1,4 +1,4 @@ -from tests_django.base_case import ChatterBotTestCase +from tests.django_integration.base_case import ChatterBotTestCase from chatterbot.trainers import ChatterBotCorpusTrainer diff --git a/tests_django/test_chatterbot_settings.py b/tests/django_integration/test_chatterbot_settings.py similarity index 100% rename from tests_django/test_chatterbot_settings.py rename to tests/django_integration/test_chatterbot_settings.py diff --git a/tests_django/test_django_adapter.py b/tests/django_integration/test_django_adapter.py similarity index 100% rename from tests_django/test_django_adapter.py rename to tests/django_integration/test_django_adapter.py diff --git a/tests_django/test_logic_adapter_integration.py b/tests/django_integration/test_logic_adapter_integration.py similarity index 95% rename from tests_django/test_logic_adapter_integration.py rename to tests/django_integration/test_logic_adapter_integration.py index 9791c6b9a..f2b044faa 100644 --- a/tests_django/test_logic_adapter_integration.py +++ b/tests/django_integration/test_logic_adapter_integration.py @@ -1,4 +1,4 @@ -from tests_django.base_case import ChatterBotTestCase +from tests.django_integration.base_case import ChatterBotTestCase from chatterbot.conversation import Statement diff --git a/tests_django/test_settings.py b/tests/django_integration/test_settings.py similarity index 98% rename from tests_django/test_settings.py rename to tests/django_integration/test_settings.py index ed84020c4..db6c1e927 100644 --- a/tests_django/test_settings.py +++ b/tests/django_integration/test_settings.py @@ -15,7 +15,7 @@ 'django.contrib.contenttypes', 'django.contrib.sessions', 'chatterbot.ext.django_chatterbot', - 'tests_django', + 'tests.django_integration', ] CHATTERBOT = { diff --git a/tests_django/test_statement_integration.py b/tests/django_integration/test_statement_integration.py similarity index 100% rename from tests_django/test_statement_integration.py rename to tests/django_integration/test_statement_integration.py diff --git a/tests/logic/test_unit_conversion.py b/tests/logic/test_unit_conversion.py index c5a0c0f97..d4a214ef8 100644 --- a/tests/logic/test_unit_conversion.py +++ b/tests/logic/test_unit_conversion.py @@ -34,7 +34,7 @@ def test_inches_to_kilometers(self): expected_value = 78740.2 response_statement = self.adapter.process(statement) self.assertIsNotNone(response_statement) - self.assertLessEqual(abs(response_statement.confidence - 1.0), 1e-10) + self.assertEqual(response_statement.confidence, 1) self.assertLessEqual(abs(float(response_statement.text) - expected_value), 0.1) def test_inches_to_kilometers_variation_1(self): @@ -43,7 +43,7 @@ def test_inches_to_kilometers_variation_1(self): expected_value = 78740.2 response_statement = self.adapter.process(statement) self.assertIsNotNone(response_statement) - self.assertLessEqual(abs(response_statement.confidence - 1.0), 1e-10) + self.assertEqual(response_statement.confidence, 1) self.assertLessEqual(abs(float(response_statement.text) - expected_value), 0.1) def test_inches_to_kilometers_variation_2(self): @@ -52,7 +52,7 @@ def test_inches_to_kilometers_variation_2(self): expected_value = 78740.2 response_statement = self.adapter.process(statement) self.assertIsNotNone(response_statement) - self.assertLessEqual(abs(response_statement.confidence - 1.0), 1e-10) + self.assertEqual(response_statement.confidence, 1) self.assertLessEqual(abs(float(response_statement.text) - expected_value), 0.1) def test_inches_to_kilometers_variation_3(self): @@ -61,7 +61,7 @@ def test_inches_to_kilometers_variation_3(self): expected_value = 78740.2 response_statement = self.adapter.process(statement) self.assertIsNotNone(response_statement) - self.assertLessEqual(abs(response_statement.confidence - 1.0), 1e-10) + self.assertEqual(response_statement.confidence, 1) self.assertLessEqual(abs(float(response_statement.text) - expected_value), 0.1) def test_meter_to_kilometer(self): @@ -70,7 +70,7 @@ def test_meter_to_kilometer(self): expected_value = 1000 response_statement = self.adapter.process(statement) self.assertIsNotNone(response_statement) - self.assertLessEqual(abs(response_statement.confidence - 1.0), 0.1) + self.assertEqual(response_statement.confidence, 1) self.assertLessEqual(abs(float(response_statement.text) - expected_value), 0.1) def test_meter_to_kilometer_variation(self): @@ -79,7 +79,7 @@ def test_meter_to_kilometer_variation(self): expected_value = 1000 response_statement = self.adapter.process(statement) self.assertIsNotNone(response_statement) - self.assertLessEqual(abs(response_statement.confidence - 1.0), 0.1) + self.assertEqual(response_statement.confidence, 1) self.assertLessEqual(abs(float(response_statement.text) - expected_value), 0.1) def test_temperature_celsius_to_fahrenheit(self): @@ -88,7 +88,7 @@ def test_temperature_celsius_to_fahrenheit(self): expected_value = 32 response_statement = self.adapter.process(statement) self.assertIsNotNone(response_statement) - self.assertLessEqual(abs(response_statement.confidence - 1.0), 0.1) + self.assertEqual(response_statement.confidence, 1) self.assertLessEqual(abs(float(response_statement.text) - expected_value), 0.1) def test_negative_temperature_celsius_to_fahrenheit(self): @@ -97,7 +97,7 @@ def test_negative_temperature_celsius_to_fahrenheit(self): expected_value = 31.64 response_statement = self.adapter.process(statement) self.assertIsNotNone(response_statement) - self.assertLessEqual(abs(response_statement.confidence - 1.0), 0.1) + self.assertEqual(response_statement.confidence, 1) self.assertLessEqual(abs(float(response_statement.text) - expected_value), 0.1) def test_time_two_hours_to_seconds(self): @@ -106,7 +106,7 @@ def test_time_two_hours_to_seconds(self): expected_value = 7200 response_statement = self.adapter.process(statement) self.assertIsNotNone(response_statement) - self.assertLessEqual(abs(response_statement.confidence - 1.0), 0.1) + self.assertEqual(response_statement.confidence, 1) self.assertLessEqual(abs(float(response_statement.text) - expected_value), 0.1) def test_pattern_x_unit_to_y_unit(self): @@ -115,7 +115,7 @@ def test_pattern_x_unit_to_y_unit(self): expected_value = 262.15 response_statement = self.adapter.process(statement) self.assertIsNotNone(response_statement) - self.assertLessEqual(abs(response_statement.confidence - 1.0), 0.1) + self.assertEqual(response_statement.confidence, 1) self.assertLessEqual(abs(float(response_statement.text) - expected_value), 0.1) def test_pattern_x_unit_is_how_many_y_unit(self): @@ -124,5 +124,5 @@ def test_pattern_x_unit_is_how_many_y_unit(self): expected_value = 2000 response_statement = self.adapter.process(statement) self.assertIsNotNone(response_statement) - self.assertLessEqual(abs(response_statement.confidence - 1.0), 0.1) + self.assertEqual(response_statement.confidence, 1) self.assertLessEqual(abs(float(response_statement.text) - expected_value), 0.1) diff --git a/tests/storage/test_mongo_adapter.py b/tests/storage/test_mongo_adapter.py index c700aa8e4..cd5866b7c 100644 --- a/tests/storage/test_mongo_adapter.py +++ b/tests/storage/test_mongo_adapter.py @@ -33,6 +33,14 @@ def setUpClass(cls): except ServerSelectionTimeoutError: pass + @classmethod + def tearDownClass(cls): + """ + Close the MongoDB client connection after all tests have run. + """ + if cls.has_mongo_connection: + cls.adapter.client.close() + def setUp(self): """ Skip these tests if a mongo client is not running. diff --git a/tests/storage/test_redis_adapter.py b/tests/storage/test_redis_adapter.py index fb7f41886..2a91ee043 100644 --- a/tests/storage/test_redis_adapter.py +++ b/tests/storage/test_redis_adapter.py @@ -209,9 +209,11 @@ def test_filter_page_size(self): results_text_list = [statement.text for statement in results] + # Check that page_size limit is respected self.assertEqual(len(results_text_list), 2) - self.assertIn('A', results_text_list) - self.assertIn('B', results_text_list) + # Verify all returned results are from the created set (order may vary) + for text in results_text_list: + self.assertIn(text, ['A', 'B', 'C']) def test_exclude_text(self): self.adapter.create(text='Hello!') @@ -362,8 +364,9 @@ def test_create_many_text(self): results = list(self.adapter.filter()) self.assertEqual(len(results), 2) - self.assertEqual(results[0].text, 'A') - self.assertEqual(results[1].text, 'B') + results_text = [r.text for r in results] + self.assertIn('A', results_text) + self.assertIn('B', results_text) def test_create_many_tags(self): self.adapter.create_many([ @@ -373,10 +376,18 @@ def test_create_many_tags(self): results = list(self.adapter.filter()) self.assertEqual(len(results), 2) - self.assertIn('letter', results[0].get_tags()) - self.assertIn('letter', results[1].get_tags()) - self.assertIn('first', results[0].get_tags()) - self.assertIn('second', results[1].get_tags()) + + # Find which result is which (order may vary) + result_a = next((r for r in results if r.text == 'A'), None) + result_b = next((r for r in results if r.text == 'B'), None) + + self.assertIsNotNone(result_a, "Statement with text 'A' not found") + self.assertIsNotNone(result_b, "Statement with text 'B' not found") + + self.assertIn('letter', result_a.get_tags()) + self.assertIn('first', result_a.get_tags()) + self.assertIn('letter', result_b.get_tags()) + self.assertIn('second', result_b.get_tags()) def test_create_many_duplicate_tags(self): """ diff --git a/tests_django/__init__.py b/tests_django/__init__.py deleted file mode 100644 index e69de29bb..000000000