pytest-dev / pytest-asyncio

Asyncio support for pytest
https://pytest-asyncio.readthedocs.io
Apache License 2.0
1.41k stars 145 forks source link

Tests fail or succeed nondeterministically at teardown of Django DB connection #82

Closed cobalamin closed 2 years ago

cobalamin commented 6 years ago

We have a project in which we're trying to test websockets via Django Channels. The tests we wrote also involve using the Django ORM to create some objects in the database. The test thus uses both the asyncio and django_db mark (with the transaction option set to True, since we want each test to run in a clean DB).

@pytest.mark.asyncio
@pytest.mark.django_db(transaction=True)
async def test_fn():
    # ...

When running our test suite (or just this test on its own), the tests sometimes succeed, and sometimes fail with an error message like below, indicating that there's an open connection and the test database cannot be dropped, which makes the entire test run fail. (This failure always occurs some time after the last test in the suite completes, no matter what that specific test is.)

========================================================= 1 passed in 6.09 seconds =========================================================
=========================================================== test session starts ============================================================
platform linux -- Python 3.6.4, pytest-3.4.1, py-1.5.2, pluggy-0.6.0
Django settings: settings.test_settings (from environment variable)
rootdir: /home/chip/dev/_stuff/project, inifile: pytest.ini
plugins: django-3.1.2, asyncio-0.8.0
collected 1 item                                                                                                                           

dashboard/tests.py .E

================================================================== ERRORS ==================================================================
________________________________________ ERROR at teardown of test_tracker_position_update_consumer ________________________________________

self = <django.db.backends.utils.CursorWrapper object at 0x7fe1bb5c09b0>, sql = 'DROP DATABASE "test_project"', params = None
ignored_wrapper_args = (False, {'connection': <django.contrib.gis.db.backends.postgis.base.DatabaseWrapper object at 0x7fe1da7d0978>, 'cursor': <django.db.backends.utils.CursorWrapper object at 0x7fe1bb5c09b0>})

    def _execute(self, sql, params, *ignored_wrapper_args):
        self.db.validate_no_broken_transaction()
        with self.db.wrap_database_errors:
            if params is None:
>               return self.cursor.execute(sql)
E               psycopg2.OperationalError: database "test_project" is being accessed by other users
E               DETAIL:  There is 1 other session using the database.

../../../.virtualenvs/project/lib/python3.6/site-packages/django/db/backends/utils.py:83: OperationalError

The above exception was the direct cause of the following exception:

    def teardown_database():
        with django_db_blocker.unblock():
            teardown_databases(
                db_cfg,
>               verbosity=pytest.config.option.verbose,
            )

../../../.virtualenvs/project/lib/python3.6/site-packages/pytest_django/fixtures.py:104: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
../../../.virtualenvs/project/lib/python3.6/site-packages/django/test/utils.py:299: in teardown_databases
    connection.creation.destroy_test_db(old_name, verbosity, keepdb)
../../../.virtualenvs/project/lib/python3.6/site-packages/django/db/backends/base/creation.py:259: in destroy_test_db
    self._destroy_test_db(test_database_name, verbosity)
../../../.virtualenvs/project/lib/python3.6/site-packages/django/db/backends/base/creation.py:276: in _destroy_test_db
    % self.connection.ops.quote_name(test_database_name))
../../../.virtualenvs/project/lib/python3.6/site-packages/django/db/backends/utils.py:68: in execute
    return self._execute_with_wrappers(sql, params, many=False, executor=self._execute)
../../../.virtualenvs/project/lib/python3.6/site-packages/django/db/backends/utils.py:77: in _execute_with_wrappers
    return executor(sql, params, many, context)
../../../.virtualenvs/project/lib/python3.6/site-packages/django/db/backends/utils.py:85: in _execute
    return self.cursor.execute(sql, params)
../../../.virtualenvs/project/lib/python3.6/site-packages/django/db/utils.py:89: in __exit__
    raise dj_exc_value.with_traceback(traceback) from exc_value
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <django.db.backends.utils.CursorWrapper object at 0x7fe1bb5c09b0>, sql = 'DROP DATABASE "test_project"', params = None
ignored_wrapper_args = (False, {'connection': <django.contrib.gis.db.backends.postgis.base.DatabaseWrapper object at 0x7fe1da7d0978>, 'cursor': <django.db.backends.utils.CursorWrapper object at 0x7fe1bb5c09b0>})

    def _execute(self, sql, params, *ignored_wrapper_args):
        self.db.validate_no_broken_transaction()
        with self.db.wrap_database_errors:
            if params is None:
>               return self.cursor.execute(sql)
E               django.db.utils.OperationalError: database "test_project" is being accessed by other users
E               DETAIL:  There is 1 other session using the database.

../../../.virtualenvs/project/lib/python3.6/site-packages/django/db/backends/utils.py:83: OperationalError
==================================================== 1 passed, 1 error in 11.61 seconds ====================================================

Further info: We're using django.contrib.gis.db.backends.postgis as the DB backend, also for tests.

Palmy523 commented 6 years ago

I'm experiencing the same issue.

I'm using django channels, pytest-django, and pytest-asyncio to run some websocket tests. Since introducing these tests, the teardown fails.

nnseva commented 5 years ago

almost the same issue, happens always, but

the connection is closed diagnostic, please resolve

_________________________________________________________________________________________________________________ test_ping _________________________________________________________________________________________________________________

self = <django.contrib.gis.db.backends.postgis.base.DatabaseWrapper object at 0x7ff84fc25e10>, name = None

    def _cursor(self, name=None):
        self.ensure_connection()
        with self.wrap_database_errors:
>           return self._prepare_cursor(self.create_cursor(name))

../py3.6/lib/python3.6/site-packages/django/db/backends/base/base.py:234: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = <django.contrib.gis.db.backends.postgis.base.DatabaseWrapper object at 0x7ff84fc25e10>, name = None

    def create_cursor(self, name=None):
        if name:
            # In autocommit mode, the cursor will be used outside of a
            # transaction, hence use a holdable cursor.
            cursor = self.connection.cursor(name, scrollable=False, withhold=self.connection.autocommit)
        else:
>           cursor = self.connection.cursor()
E           psycopg2.InterfaceError: connection already closed

../py3.6/lib/python3.6/site-packages/django/db/backends/postgresql/base.py:212: InterfaceError

The above exception was the direct cause of the following exception:

    @pytest.mark.asyncio
    @pytest.mark.django_db
    async def test_ping():
            """Test for proper connection and ping"""
            settings.AVOID_WEBSOCKET_AUTHORIZATION = True
            User.objects.create(username='test', is_superuser=True, is_active=True)
            communicator = WebsocketCommunicator(application, '/simulation_event/')
>           connected, subprotocol = await communicator.connect()

simulation/tests/test_consumers.py:26: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
../py3.6/lib/python3.6/site-packages/channels/testing/websocket.py:36: in connect
    response = await self.receive_output(timeout)
../py3.6/lib/python3.6/site-packages/asgiref/testing.py:80: in receive_output
    self.future.result()
../py3.6/lib/python3.6/site-packages/asgiref/compatibility.py:33: in new_application
    instance = application(scope)
../py3.6/lib/python3.6/site-packages/channels/routing.py:58: in __call__
    return self.application_mapping[scope["type"]](scope)
simulation/consumers.py:49: in __call__
    user = User.objects.filter(is_superuser=True, is_active=True).first()
../py3.6/lib/python3.6/site-packages/django/db/models/query.py:604: in first
    for obj in (self if self.ordered else self.order_by('pk'))[:1]:
../py3.6/lib/python3.6/site-packages/django/db/models/query.py:272: in __iter__
    self._fetch_all()
../py3.6/lib/python3.6/site-packages/django/db/models/query.py:1179: in _fetch_all
    self._result_cache = list(self._iterable_class(self))
../py3.6/lib/python3.6/site-packages/django/db/models/query.py:54: in __iter__
    results = compiler.execute_sql(chunked_fetch=self.chunked_fetch, chunk_size=self.chunk_size)
../py3.6/lib/python3.6/site-packages/django/db/models/sql/compiler.py:1061: in execute_sql
    cursor = self.connection.cursor()
../py3.6/lib/python3.6/site-packages/django/db/backends/base/base.py:255: in cursor
    return self._cursor()
../py3.6/lib/python3.6/site-packages/django/db/backends/base/base.py:234: in _cursor
    return self._prepare_cursor(self.create_cursor(name))
../py3.6/lib/python3.6/site-packages/django/db/utils.py:89: in __exit__
    raise dj_exc_value.with_traceback(traceback) from exc_value
../py3.6/lib/python3.6/site-packages/django/db/backends/base/base.py:234: in _cursor
    return self._prepare_cursor(self.create_cursor(name))
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = <django.contrib.gis.db.backends.postgis.base.DatabaseWrapper object at 0x7ff84fc25e10>, name = None

    def create_cursor(self, name=None):
        if name:
            # In autocommit mode, the cursor will be used outside of a
            # transaction, hence use a holdable cursor.
            cursor = self.connection.cursor(name, scrollable=False, withhold=self.connection.autocommit)
        else:
>           cursor = self.connection.cursor()
E           django.db.utils.InterfaceError: connection already closed

../py3.6/lib/python3.6/site-packages/django/db/backends/postgresql/base.py:212: InterfaceError
Tinche commented 5 years ago

If someone can create a minimal test case for me to easily install and run (maybe in a dummy github project), I can take a look.

nnseva commented 5 years ago

@Tinche sorry, looks like my case is not caused by your package

LincolnPuzey commented 5 years ago

I am using django-channels and have setup a test for my websocket consumer using pytest-asyncio and pytest-django.

The websocket tests rely on some models being present in the database.

I am using channels.db.database_sync_to_async to decorate all code that creates models for the tests, and this seems to work ok. Note I am using postgres .

For example

@database_sync_to_async
def create_model():
    model = MyModel.objects.create(name="test")
    return model.id

@pytest.mark.asyncio
@pytest.mark.django_db
async def test_consumer():
    model_id = await create_model()

    communicator = WebsocketCommunicator(
        url_router,
        f'/model_url/{model_id}/',
    )

    connected, subprotocol = await communicator.connect()
    assert connected
    # Close
    await communicator.disconnect()
seifertm commented 2 years ago

All participants in this issue could either resolve their problem or didn't provide additional information. I don't think there's much we can about this without a reproducible example.

Cross-referencing this issue with #226 which describes problems with database transaction rollback in Django. The discussion there points to issues in pytest-django and async tests in Django.

I'm closing this issue. Feel free to reopen it, if the problem persists.