summaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
authorAndrew Godwin <andrew@aeracode.org>2019-04-12 06:15:18 -0700
committerMariusz Felisiak <felisiak.mariusz@gmail.com>2019-06-20 12:29:43 +0200
commita415ce70bef6d91036b00dd2c8544aed7aeeaaed (patch)
tree3583cef22e9b56d2ed52456ab586d9c47620bc51 /tests
parentcce47ff65a4dd3786c049ec14ee889e128ca7de9 (diff)
Fixed #30451 -- Added ASGI handler and coroutine-safety.
This adds an ASGI handler, asgi.py file for the default project layout, a few async utilities and adds async-safety to many parts of Django.
Diffstat (limited to 'tests')
-rw-r--r--tests/asgi/__init__.py0
-rw-r--r--tests/asgi/tests.py84
-rw-r--r--tests/asgi/urls.py15
-rw-r--r--tests/async/__init__.py0
-rw-r--r--tests/async/models.py5
-rw-r--r--tests/async/tests.py36
-rw-r--r--tests/i18n/tests.py9
-rw-r--r--tests/template_tests/syntax_tests/i18n/test_blocktrans.py5
-rw-r--r--tests/template_tests/syntax_tests/i18n/test_trans.py4
9 files changed, 149 insertions, 9 deletions
diff --git a/tests/asgi/__init__.py b/tests/asgi/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/tests/asgi/__init__.py
diff --git a/tests/asgi/tests.py b/tests/asgi/tests.py
new file mode 100644
index 0000000000..243e77defb
--- /dev/null
+++ b/tests/asgi/tests.py
@@ -0,0 +1,84 @@
+import sys
+
+from asgiref.sync import async_to_sync
+from asgiref.testing import ApplicationCommunicator
+
+from django.core.asgi import get_asgi_application
+from django.core.signals import request_started
+from django.db import close_old_connections
+from django.test import SimpleTestCase, override_settings
+
+from .urls import test_filename
+
+
+@override_settings(ROOT_URLCONF='asgi.urls')
+class ASGITest(SimpleTestCase):
+
+ def setUp(self):
+ request_started.disconnect(close_old_connections)
+
+ def _get_scope(self, **kwargs):
+ return {
+ 'type': 'http',
+ 'asgi': {'version': '3.0', 'spec_version': '2.1'},
+ 'http_version': '1.1',
+ 'method': 'GET',
+ 'query_string': b'',
+ 'server': ('testserver', 80),
+ **kwargs,
+ }
+
+ def tearDown(self):
+ request_started.connect(close_old_connections)
+
+ @async_to_sync
+ async def test_get_asgi_application(self):
+ """
+ get_asgi_application() returns a functioning ASGI callable.
+ """
+ application = get_asgi_application()
+ # Construct HTTP request.
+ communicator = ApplicationCommunicator(application, self._get_scope(path='/'))
+ await communicator.send_input({'type': 'http.request'})
+ # Read the response.
+ response_start = await communicator.receive_output()
+ self.assertEqual(response_start['type'], 'http.response.start')
+ self.assertEqual(response_start['status'], 200)
+ self.assertEqual(
+ set(response_start['headers']),
+ {
+ (b'Content-Length', b'12'),
+ (b'Content-Type', b'text/html; charset=utf-8'),
+ },
+ )
+ response_body = await communicator.receive_output()
+ self.assertEqual(response_body['type'], 'http.response.body')
+ self.assertEqual(response_body['body'], b'Hello World!')
+
+ @async_to_sync
+ async def test_file_response(self):
+ """
+ Makes sure that FileResponse works over ASGI.
+ """
+ application = get_asgi_application()
+ # Construct HTTP request.
+ communicator = ApplicationCommunicator(application, self._get_scope(path='/file/'))
+ await communicator.send_input({'type': 'http.request'})
+ # Get the file content.
+ with open(test_filename, 'rb') as test_file:
+ test_file_contents = test_file.read()
+ # Read the response.
+ response_start = await communicator.receive_output()
+ self.assertEqual(response_start['type'], 'http.response.start')
+ self.assertEqual(response_start['status'], 200)
+ self.assertEqual(
+ set(response_start['headers']),
+ {
+ (b'Content-Length', str(len(test_file_contents)).encode('ascii')),
+ (b'Content-Type', b'text/plain' if sys.platform.startswith('win') else b'text/x-python'),
+ (b'Content-Disposition', b'inline; filename="urls.py"'),
+ },
+ )
+ response_body = await communicator.receive_output()
+ self.assertEqual(response_body['type'], 'http.response.body')
+ self.assertEqual(response_body['body'], test_file_contents)
diff --git a/tests/asgi/urls.py b/tests/asgi/urls.py
new file mode 100644
index 0000000000..4177ec8c9a
--- /dev/null
+++ b/tests/asgi/urls.py
@@ -0,0 +1,15 @@
+from django.http import FileResponse, HttpResponse
+from django.urls import path
+
+
+def helloworld(request):
+ return HttpResponse('Hello World!')
+
+
+test_filename = __file__
+
+
+urlpatterns = [
+ path('', helloworld),
+ path('file/', lambda x: FileResponse(open(test_filename, 'rb'))),
+]
diff --git a/tests/async/__init__.py b/tests/async/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/tests/async/__init__.py
diff --git a/tests/async/models.py b/tests/async/models.py
new file mode 100644
index 0000000000..0fd606b07e
--- /dev/null
+++ b/tests/async/models.py
@@ -0,0 +1,5 @@
+from django.db import models
+
+
+class SimpleModel(models.Model):
+ field = models.IntegerField()
diff --git a/tests/async/tests.py b/tests/async/tests.py
new file mode 100644
index 0000000000..1e1cabc1c6
--- /dev/null
+++ b/tests/async/tests.py
@@ -0,0 +1,36 @@
+from asgiref.sync import async_to_sync
+
+from django.core.exceptions import SynchronousOnlyOperation
+from django.test import SimpleTestCase
+from django.utils.asyncio import async_unsafe
+
+from .models import SimpleModel
+
+
+class DatabaseConnectionTest(SimpleTestCase):
+ """A database connection cannot be used in an async context."""
+ @async_to_sync
+ async def test_get_async_connection(self):
+ with self.assertRaises(SynchronousOnlyOperation):
+ list(SimpleModel.objects.all())
+
+
+class AsyncUnsafeTest(SimpleTestCase):
+ """
+ async_unsafe decorator should work correctly and returns the correct
+ message.
+ """
+ @async_unsafe
+ def dangerous_method(self):
+ return True
+
+ @async_to_sync
+ async def test_async_unsafe(self):
+ # async_unsafe decorator catches bad access and returns the right
+ # message.
+ msg = (
+ 'You cannot call this from an async context - use a thread or '
+ 'sync_to_async.'
+ )
+ with self.assertRaisesMessage(SynchronousOnlyOperation, msg):
+ self.dangerous_method()
diff --git a/tests/i18n/tests.py b/tests/i18n/tests.py
index 8bb284e0c3..300af388e6 100644
--- a/tests/i18n/tests.py
+++ b/tests/i18n/tests.py
@@ -8,10 +8,9 @@ import tempfile
from contextlib import contextmanager
from importlib import import_module
from pathlib import Path
-from threading import local
from unittest import mock
-import _thread
+from asgiref.local import Local
from django import forms
from django.apps import AppConfig
@@ -289,7 +288,7 @@ class TranslationTests(SimpleTestCase):
@override_settings(LOCALE_PATHS=extended_locale_paths)
def test_pgettext(self):
- trans_real._active = local()
+ trans_real._active = Local()
trans_real._translations = {}
with translation.override('de'):
self.assertEqual(pgettext("unexisting", "May"), "May")
@@ -310,7 +309,7 @@ class TranslationTests(SimpleTestCase):
Translating a string requiring no auto-escaping with gettext or pgettext
shouldn't change the "safe" status.
"""
- trans_real._active = local()
+ trans_real._active = Local()
trans_real._translations = {}
s1 = mark_safe('Password')
s2 = mark_safe('May')
@@ -1882,7 +1881,7 @@ class TranslationFileChangedTests(SimpleTestCase):
self.assertEqual(gettext_module._translations, {})
self.assertEqual(trans_real._translations, {})
self.assertIsNone(trans_real._default)
- self.assertIsInstance(trans_real._active, _thread._local)
+ self.assertIsInstance(trans_real._active, Local)
class UtilsTests(SimpleTestCase):
diff --git a/tests/template_tests/syntax_tests/i18n/test_blocktrans.py b/tests/template_tests/syntax_tests/i18n/test_blocktrans.py
index ac8fc16da8..744b410ea6 100644
--- a/tests/template_tests/syntax_tests/i18n/test_blocktrans.py
+++ b/tests/template_tests/syntax_tests/i18n/test_blocktrans.py
@@ -1,5 +1,6 @@
import os
-from threading import local
+
+from asgiref.local import Local
from django.template import Context, Template, TemplateSyntaxError
from django.test import SimpleTestCase, override_settings
@@ -278,7 +279,7 @@ class TranslationBlockTransTagTests(SimpleTestCase):
@override_settings(LOCALE_PATHS=extended_locale_paths)
def test_template_tags_pgettext(self):
"""{% blocktrans %} takes message contexts into account (#14806)."""
- trans_real._active = local()
+ trans_real._active = Local()
trans_real._translations = {}
with translation.override('de'):
# Nonexistent context
diff --git a/tests/template_tests/syntax_tests/i18n/test_trans.py b/tests/template_tests/syntax_tests/i18n/test_trans.py
index ba5021a5d5..47a79ff74d 100644
--- a/tests/template_tests/syntax_tests/i18n/test_trans.py
+++ b/tests/template_tests/syntax_tests/i18n/test_trans.py
@@ -1,4 +1,4 @@
-from threading import local
+from asgiref.local import Local
from django.template import Context, Template, TemplateSyntaxError
from django.templatetags.l10n import LocalizeNode
@@ -136,7 +136,7 @@ class TranslationTransTagTests(SimpleTestCase):
@override_settings(LOCALE_PATHS=extended_locale_paths)
def test_template_tags_pgettext(self):
"""{% trans %} takes message contexts into account (#14806)."""
- trans_real._active = local()
+ trans_real._active = Local()
trans_real._translations = {}
with translation.override('de'):
# Nonexistent context...