django / channels

Developer-friendly asynchrony for Django
https://channels.readthedocs.io
BSD 3-Clause "New" or "Revised" License
6.02k stars 793 forks source link

Fix Fixture Loading by Configuring Settings of django in child process that gets spawn #2033

Open ashtonrobinson opened 1 year ago

ashtonrobinson commented 1 year ago

This fix refers to here

Currently, when a test class inherits from ChannelsLiveServerTestCase, all database changes that happen will be written to the default main database, not the test database that is created by the parent class--TransactionTestCase. This is the reason that fixtures are not currently referenced

When ChannelsLiveServerTestCase initiates a DaphneProcess, by calling this following line self._server_process = self.ProtocolServerProcess(self.host, get_application) This instantiates a DaphneProcess object which is a multiprocessing.Process object which takes care of creating a new process. This uses the SpawnProcess of multiprocessing class on Windows and macOS. Linux uses the Fork method, but Fork causes problems. Spawn should be used only.

Here is what is said about spawn. The parent process starts a fresh python interpreter process. The child process will only inherit those resources necessary to run the process objects run() method. In particular, unnecessary file descriptors and handles from the parent process will not be inherited. Starting a process using this method is rather slow compared to using fork or forkserver. [Available on Unix and Windows. The default on Windows and macOS.]

This means that django setup() is called and the settings file it reloaded. This means that `setting.DATABASES['default'] in the process that runs the Daphne server will point to the default database. This is incorrect, and will cause a leakage of all changes made by selenium or another testing framework.

Because ChannelsLiveServerTestCase inherits from TransactionTestCase, it it necessary that all functionality is retained. Therefore, we must point the django module at the test database in the the new process that is running Daphne so that we can correctly use the fixtures, and have the test database removed after testing is finished. Also, so that changes do not leak into the default database.

This is why, we reassign the default main database NAME to the NAME of the TEST attribute(see the codes difference). This is fine because the documentation already describes that you need to have this structure of your settings file here

To reproduce the issue, create a test file in some django project that has authentication, a login page, and that uses an asgi and selenium or another web driver framework.

from channels.testing import ChannelsLiveServerTestCase from django.contrib.auth.models import User

class LiveTest(ChannelsLiveServerTestCase): serve_static = True fixtures = [] # any fixtures you want to be loaded

@classmethod def setUpClass(cls) -> None: super().setUpClass() cls.driver = webdriver.Chrome()

@classmethod def tearDownClass(cls) -> None: super().tearDownClass() cls.driver.quit()

def setUp(self): user = User.objects.create(username='temp') user.set_password('temp')

def login(self, username:str, password:str): time.sleep(2) self.driver.get(self.live_server_url + '/') time.sleep(2) self.driver.get(self.live_server_url + '/login/') # print(settings.DATABASES['default']) user_input_field = #find user input field password_input_field = #find password input field user_input_field.send_keys(username) password_input_field.send_keys(password) time.sleep(3) login_button = #find login button login_button.click() time.sleep(3)

def test_click(self): self.login('temp', 'temp')

Even though we have created a temp user in the test, Django will not let us log in. This because the Daphne server is pointed at the wrong database in the child process.

When we examine settings.DATABASES['default'] in the test file, we see that django returns this. {'ENGINE': 'django.db.backends.sqlite3', 'NAME': '/Users/ashtonrobinson/cNode/testdatabase.sqlite3', 'USER': 'user', 'PASSWORD': 'password', 'HOST': 'localhost', 'PORT': '5432', 'TEST': {'NAME': '/Users/ashtonrobinson/cNode/testdatabase.sqlite3', 'CHARSET': None, 'COLLATION': None, 'MIGRATE': True, 'MIRROR': None}, 'ATOMIC_REQUESTS': False, 'AUTOCOMMIT': True, 'CONN_MAX_AGE': 0, 'OPTIONS': {}, 'TIME_ZONE': None}

We see that the NAME field is correctly the test database, but this needs to be set when django is reloaded in the child process. This is what is being done by the updates to the code.