Opened 3 years ago

Closed 12 months ago

#33277 closed New feature (fixed)

SimpleTestCase does not block database connections in threads

Reported by: Daniel Hahler Owned by: David Wobrock
Component: Testing framework Version: 3.2
Severity: Normal Keywords:
Cc: David Wobrock Triage Stage: Ready for checkin
Has patch: yes Needs documentation: no
Needs tests: no Patch needs improvement: no
Easy pickings: no UI/UX: no

Description (last modified by Daniel Hahler)

Due to ConnectionHandler's connections being thread-local [1] new connections will be used in new threads, which then do not have been patched for disallowed methods [2].

Given test_simpletestcase.py:

import threading

from django.db import connection
from django.test import SimpleTestCase


class MySimpleTestCase(SimpleTestCase):
    def test_this(self):
        try:
            with connection.cursor() as cursor:
                cursor.execute("SELECT 1")
            raise Exception("should have failed")
        except AssertionError:
            pass

        res = []

        def thread_func():
            res.append(1)
            try:
                with connection.cursor() as cursor:
                    cursor.execute("SELECT 1")
                raise Exception("should have failed")
            except AssertionError:
                pass
            res.append(2)

        t = threading.Thread(target=thread_func)
        t.start()
        t.join()
        assert res == [1, 2], res

./manage.py test test_simpletestcase.py fails like this:

Exception in thread Thread-1:
Traceback (most recent call last):
  File "/usr/lib/python3.9/threading.py", line 973, in _bootstrap_inner
    self.run()
  File "/usr/lib/python3.9/threading.py", line 910, in run
    self._target(*self._args, **self._kwargs)
  File "…/test_simpletestcase.py", line 23, in thread_func
    raise Exception("should have failed")
Exception: should have failed
F
======================================================================
FAIL: test_this (test_simpletestcase.MySimpleTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "…/test_simpletestcase.py", line 31, in test_this
    assert res == [1, 2], res
AssertionError: [1]

----------------------------------------------------------------------
Ran 1 test in 0.006s

FAILED (failures=1)

(Note that there is some handling of connection.settings_dict for workers of the test runner, which is only slightly related: https://github.com/django/django/blob/dfa1145a22042dcf9e504a5a7edd5557e3e0d07c/django/test/runner.py#L327-L335)

A possible solution might be to use the existing connection_created signal to raise an exception when a connections was created (although that would happen only after the fact - a new pre-connect signal could be used/added for this).

Given that the test DB names are not prefixed with test_ with SimpleTestCase you might accidentally change the production DB from within your tests when something like a ThreadPoolExecutor is being used when mixing sync with async etc.

Note: pytest-django monkeypatches django.db.backends.base.base.BaseDatabaseWrapper.ensure_connection to block DB access, which appears to work better in this regard (across threads).

1: https://github.com/django/django/blob/dfa1145a22042dcf9e504a5a7edd5557e3e0d07c/django/utils/connection.py#L41
2: https://github.com/django/django/blob/dfa1145a22042dcf9e504a5a7edd5557e3e0d07c/django/test/testcases.py#L183

Change History (13)

comment:1 by Daniel Hahler, 3 years ago

Description: modified (diff)

comment:2 by Carlton Gibson, 3 years ago

Triage Stage: UnreviewedAccepted
Type: UncategorizedNew feature

Hi Daniel, thanks for this — Nice report. (Was momentarily confused by the footnotes linking to the initial commit, e.g. [1] :)

Yes, I think SimpleTestCase likely should do the right thing here.

comment:3 by Sulabh Katila, 16 months ago

Owner: changed from nobody to Sulabh Katila
Status: newassigned

comment:4 by David Wobrock, 16 months ago

Cc: David Wobrock added

Hey Sulabh,

I see you assigned yourself the ticket, that's great!
I've started a PR some months ago, feel free to check it out if it can give you some inspiration :)

in reply to:  4 comment:5 by Sulabh Katila, 16 months ago

Hello David,

Thank you for getting the ball rolling on this feature! I've looked at the PR, and it's impressive. I'll definitely use it as inspiration to continue the work. If you have any specific insights or tips based on your progress so far, please feel free to share.
I'm looking forward to working on this together!

Replying to David Wobrock:

Hey Sulabh,

I see you assigned yourself the ticket, that's great!
I've started a PR some months ago, feel free to check it out if it can give you some inspiration :)

in reply to:  4 ; comment:6 by Sulabh Katila, 16 months ago

Hi David,

I've been grappling with this issue for a while now, and I haven't made much progress. Given your expertise in this area, I'm reaching out for some much-needed guidance.

To address this, I've been contemplating the idea of implementing a "pre-signal" solution. However, I must admit that I'm currently stuck, and I'm unsure about how to move forward.

I had initially considered intervening at the BaseDataBaseWrapper.connect() level, but I haven't been able to find a clear and effective path for implementation.

Since you have experience in related areas of this issue, I was hoping you could provide some insights. What should I be looking for, or are there alternative approaches that might be more suitable?

Thank you!

Replying to David Wobrock:

Hey Sulabh,

I see you assigned yourself the ticket, that's great!
I've started a PR some months ago, feel free to check it out if it can give you some inspiration :)

in reply to:  6 comment:7 by David Wobrock, 15 months ago

I do not think I have that much experience.

How did you implement the "pre-signal" solution? Do you have an open PR/branch to check out the code :)

And did you have a look on the patching method approach, and play around with it?
I feel like it could be a working path - it mainly requires some test fixing.

Replying to Sulabh Katila:

Hi David,

I've been grappling with this issue for a while now, and I haven't made much progress. Given your expertise in this area, I'm reaching out for some much-needed guidance.

To address this, I've been contemplating the idea of implementing a "pre-signal" solution. However, I must admit that I'm currently stuck, and I'm unsure about how to move forward.

I had initially considered intervening at the BaseDataBaseWrapper.connect() level, but I haven't been able to find a clear and effective path for implementation.

Since you have experience in related areas of this issue, I was hoping you could provide some insights. What should I be looking for, or are there alternative approaches that might be more suitable?

Thank you!

Replying to David Wobrock:

Hey Sulabh,

I see you assigned yourself the ticket, that's great!
I've started a PR some months ago, feel free to check it out if it can give you some inspiration :)

comment:8 by David Wobrock, 15 months ago

Has patch: set
Patch needs improvement: set

Hey Sulabh,

I took a few more minutes to dig into the issue and I think we are close to a fix :)
djangoci.com is down at the time of writing, so I'm missing the complete test suite, but it looks quite promising for now! We just need to fix a few tests.

=> PR

comment:9 by David Wobrock, 12 months ago

Owner: changed from Sulabh Katila to David Wobrock
Patch needs improvement: unset

I picked up the existing patch and opened a new PR.

comment:10 by Mariusz Felisiak, 12 months ago

Patch needs improvement: set

comment:11 by David Wobrock, 12 months ago

Patch needs improvement: unset

comment:12 by Mariusz Felisiak, 12 months ago

Triage Stage: AcceptedReady for checkin

comment:13 by Mariusz Felisiak <felisiak.mariusz@…>, 12 months ago

Resolution: fixed
Status: assignedclosed

In 8fb0be35:

Fixed #33277 -- Disallowed database connections in threads in SimpleTestCase.

Note: See TracTickets for help on using tickets.
Back to Top