Made django.test.testcases not depend on staticfiles contrib app.
Do this by introducing a django.contrib.staticfiles.testing.StaticLiveServerCase unittest TestCase subclass. Fixes #20739.
This commit is contained in:
parent
e0643cb676
commit
e909ceae9b
|
@ -0,0 +1,14 @@
|
|||
from django.test import LiveServerTestCase
|
||||
|
||||
from django.contrib.staticfiles.handlers import StaticFilesHandler
|
||||
|
||||
|
||||
class StaticLiveServerCase(LiveServerTestCase):
|
||||
"""
|
||||
Extends django.test.LiveServerTestCase to transparently overlay at test
|
||||
execution-time the assets provided by the staticfiles app finders. This
|
||||
means you don't need to run collectstatic before or as a part of your tests
|
||||
setup.
|
||||
"""
|
||||
|
||||
static_handler = StaticFilesHandler
|
|
@ -27,7 +27,7 @@ def serve(request, path, insecure=False, **kwargs):
|
|||
|
||||
in your URLconf.
|
||||
|
||||
It uses the django.views.static view to serve the found files.
|
||||
It uses the django.views.static.serve() view to serve the found files.
|
||||
"""
|
||||
if not settings.DEBUG and not insecure:
|
||||
raise Http404
|
||||
|
|
|
@ -6,23 +6,25 @@ import errno
|
|||
from functools import wraps
|
||||
import json
|
||||
import os
|
||||
import posixpath
|
||||
import re
|
||||
import sys
|
||||
import socket
|
||||
import threading
|
||||
import unittest
|
||||
from unittest import skipIf # Imported here for backward compatibility
|
||||
from unittest.util import safe_repr
|
||||
try:
|
||||
from urllib.parse import urlsplit, urlunsplit
|
||||
from urllib.parse import urlsplit, urlunsplit, urlparse, unquote
|
||||
from urllib.request import url2pathname
|
||||
except ImportError: # Python 2
|
||||
from urlparse import urlsplit, urlunsplit
|
||||
from urlparse import urlsplit, urlunsplit, urlparse
|
||||
from urllib import url2pathname, unquote
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.staticfiles.handlers import StaticFilesHandler
|
||||
from django.core import mail
|
||||
from django.core.exceptions import ValidationError, ImproperlyConfigured
|
||||
from django.core.handlers.wsgi import WSGIHandler
|
||||
from django.core.handlers.base import get_path_info
|
||||
from django.core.management import call_command
|
||||
from django.core.management.color import no_style
|
||||
from django.core.management.commands import flush
|
||||
|
@ -933,10 +935,70 @@ class QuietWSGIRequestHandler(WSGIRequestHandler):
|
|||
pass
|
||||
|
||||
|
||||
class _MediaFilesHandler(StaticFilesHandler):
|
||||
class FSFilesHandler(WSGIHandler):
|
||||
"""
|
||||
Handler for serving the media files. This is a private class that is
|
||||
meant to be used solely as a convenience by LiveServerThread.
|
||||
WSGI middleware that intercepts calls to a directory, as defined by one of
|
||||
the *_ROOT settings, and serves those files, publishing them under *_URL.
|
||||
"""
|
||||
def __init__(self, application):
|
||||
self.application = application
|
||||
self.base_url = urlparse(self.get_base_url())
|
||||
super(FSFilesHandler, self).__init__()
|
||||
|
||||
def _should_handle(self, path):
|
||||
"""
|
||||
Checks if the path should be handled. Ignores the path if:
|
||||
|
||||
* the host is provided as part of the base_url
|
||||
* the request's path isn't under the media path (or equal)
|
||||
"""
|
||||
return path.startswith(self.base_url[2]) and not self.base_url[1]
|
||||
|
||||
def file_path(self, url):
|
||||
"""
|
||||
Returns the relative path to the file on disk for the given URL.
|
||||
"""
|
||||
relative_url = url[len(self.base_url[2]):]
|
||||
return url2pathname(relative_url)
|
||||
|
||||
def get_response(self, request):
|
||||
from django.http import Http404
|
||||
|
||||
if self._should_handle(request.path):
|
||||
try:
|
||||
return self.serve(request)
|
||||
except Http404:
|
||||
pass
|
||||
return super(FSFilesHandler, self).get_response(request)
|
||||
|
||||
def serve(self, request):
|
||||
os_rel_path = self.file_path(request.path)
|
||||
final_rel_path = posixpath.normpath(unquote(os_rel_path)).lstrip('/')
|
||||
return serve(request, final_rel_path, document_root=self.get_base_dir())
|
||||
|
||||
def __call__(self, environ, start_response):
|
||||
if not self._should_handle(get_path_info(environ)):
|
||||
return self.application(environ, start_response)
|
||||
return super(FSFilesHandler, self).__call__(environ, start_response)
|
||||
|
||||
|
||||
class _StaticFilesHandler(FSFilesHandler):
|
||||
"""
|
||||
Handler for serving static files. A private class that is meant to be used
|
||||
solely as a convenience by LiveServerThread.
|
||||
"""
|
||||
|
||||
def get_base_dir(self):
|
||||
return settings.STATIC_ROOT
|
||||
|
||||
def get_base_url(self):
|
||||
return settings.STATIC_URL
|
||||
|
||||
|
||||
class _MediaFilesHandler(FSFilesHandler):
|
||||
"""
|
||||
Handler for serving the media files. A private class that is meant to be
|
||||
used solely as a convenience by LiveServerThread.
|
||||
"""
|
||||
|
||||
def get_base_dir(self):
|
||||
|
@ -945,22 +1007,19 @@ class _MediaFilesHandler(StaticFilesHandler):
|
|||
def get_base_url(self):
|
||||
return settings.MEDIA_URL
|
||||
|
||||
def serve(self, request):
|
||||
relative_url = request.path[len(self.base_url[2]):]
|
||||
return serve(request, relative_url, document_root=self.get_base_dir())
|
||||
|
||||
|
||||
class LiveServerThread(threading.Thread):
|
||||
"""
|
||||
Thread for running a live http server while the tests are running.
|
||||
"""
|
||||
|
||||
def __init__(self, host, possible_ports, connections_override=None):
|
||||
def __init__(self, host, possible_ports, static_handler, connections_override=None):
|
||||
self.host = host
|
||||
self.port = None
|
||||
self.possible_ports = possible_ports
|
||||
self.is_ready = threading.Event()
|
||||
self.error = None
|
||||
self.static_handler = static_handler
|
||||
self.connections_override = connections_override
|
||||
super(LiveServerThread, self).__init__()
|
||||
|
||||
|
@ -976,7 +1035,7 @@ class LiveServerThread(threading.Thread):
|
|||
connections[alias] = conn
|
||||
try:
|
||||
# Create the handler for serving static and media files
|
||||
handler = StaticFilesHandler(_MediaFilesHandler(WSGIHandler()))
|
||||
handler = self.static_handler(_MediaFilesHandler(WSGIHandler()))
|
||||
|
||||
# Go through the list of possible ports, hoping that we can find
|
||||
# one that is free to use for the WSGI server.
|
||||
|
@ -1028,6 +1087,8 @@ class LiveServerTestCase(TransactionTestCase):
|
|||
other thread can see the changes.
|
||||
"""
|
||||
|
||||
static_handler = _StaticFilesHandler
|
||||
|
||||
@property
|
||||
def live_server_url(self):
|
||||
return 'http://%s:%s' % (
|
||||
|
@ -1069,8 +1130,9 @@ class LiveServerTestCase(TransactionTestCase):
|
|||
except Exception:
|
||||
msg = 'Invalid address ("%s") for live server.' % specified_address
|
||||
six.reraise(ImproperlyConfigured, ImproperlyConfigured(msg), sys.exc_info()[2])
|
||||
cls.server_thread = LiveServerThread(
|
||||
host, possible_ports, connections_override)
|
||||
cls.server_thread = LiveServerThread(host, possible_ports,
|
||||
cls.static_handler,
|
||||
connections_override=connections_override)
|
||||
cls.server_thread.daemon = True
|
||||
cls.server_thread.start()
|
||||
|
||||
|
|
|
@ -100,6 +100,33 @@ this by adding the following snippet to your urls.py::
|
|||
the given prefix is local (e.g. ``/static/``) and not a URL (e.g.
|
||||
``http://static.example.com/``).
|
||||
|
||||
.. _staticfiles-testing-support:
|
||||
|
||||
Testing
|
||||
=======
|
||||
|
||||
When running tests that use actual HTTP requests instead of the built-in
|
||||
testing client (i.e. when using the built-in :class:`LiveServerTestCase
|
||||
<django.test.LiveServerTestCase>`) the static assets need to be served along
|
||||
the rest of the content so the test environment reproduces the real one as
|
||||
faithfully as possible, but ``LiveServerTestCase`` has only very basic static
|
||||
file-serving functionality: It doesn't know about the finders feature of the
|
||||
``staticfiles`` application and assumes the static content has already been
|
||||
collected under :setting:`STATIC_ROOT`.
|
||||
|
||||
Because of this, ``staticfiles`` ships its own
|
||||
:class:`django.contrib.staticfiles.testing.StaticLiveServerCase`, a subclass
|
||||
of the built-in one that has the ability to transparently serve all the assets
|
||||
during execution of these tests in a way very similar to what we get at
|
||||
development time with ``DEBUG = True``, i.e. without having to collect them
|
||||
using :djadmin:`collectstatic` first.
|
||||
|
||||
.. versionadded:: 1.7
|
||||
|
||||
:class:`django.contrib.staticfiles.testing.StaticLiveServerCase` is new in
|
||||
Django 1.7. Previously its functionality was provided by
|
||||
:class:`django.test.LiveServerTestCase`.
|
||||
|
||||
Deployment
|
||||
==========
|
||||
|
||||
|
|
|
@ -406,3 +406,26 @@ files in app directories.
|
|||
That's because this view is **grossly inefficient** and probably
|
||||
**insecure**. This is only intended for local development, and should
|
||||
**never be used in production**.
|
||||
|
||||
Specialized test case to support 'live testing'
|
||||
-----------------------------------------------
|
||||
|
||||
.. class:: testing.StaticLiveServerCase
|
||||
|
||||
This unittest TestCase subclass extends :class:`django.test.LiveServerTestCase`.
|
||||
|
||||
Just like its parent, you can use it to write tests that involve running the
|
||||
code under test and consuming it with testing tools through HTTP (e.g. Selenium,
|
||||
PhantomJS, etc.), because of which it's needed that the static assets are also
|
||||
published.
|
||||
|
||||
But given the fact that it makes use of the
|
||||
:func:`django.contrib.staticfiles.views.serve` view described above, it can
|
||||
transparently overlay at test execution-time the assets provided by the
|
||||
``staticfiles`` finders. This means you don't need to run
|
||||
:djadmin:`collectstatic` before or as a part of your tests setup.
|
||||
|
||||
.. versionadded:: 1.7
|
||||
|
||||
``StaticLiveServerCase`` is new in Django 1.7. Previously its functionality
|
||||
was provided by :class:`django.test.LiveServerTestCase`.
|
||||
|
|
|
@ -332,6 +332,20 @@ Miscellaneous
|
|||
Define a ``get_absolute_url()`` method on your own custom user object or use
|
||||
:setting:`ABSOLUTE_URL_OVERRIDES` if you want a URL for your user.
|
||||
|
||||
* The static asset-serving functionality of the
|
||||
:class:`django.test.LiveServerTestCase` class has been simplified: Now it's
|
||||
only able to serve content already present in :setting:`STATIC_ROOT` when
|
||||
tests are run. The ability to transparently serve all the static assets
|
||||
(similarly to what one gets with :setting:`DEBUG = True <DEBUG>` at
|
||||
development-time) has been moved to a new class that lives in the
|
||||
``staticfiles`` application (the one actually in charge of such feature):
|
||||
:class:`django.contrib.staticfiles.testing.StaticLiveServerCase`. In other
|
||||
words, ``LiveServerTestCase`` itself is less powerful but at the same time
|
||||
has less magic.
|
||||
|
||||
Rationale behind this is removal of dependency of non-contrib code on
|
||||
contrib applications.
|
||||
|
||||
Features deprecated in 1.7
|
||||
==========================
|
||||
|
||||
|
|
|
@ -1041,11 +1041,25 @@ out the `full reference`_ for more details.
|
|||
.. _full reference: http://selenium-python.readthedocs.org/en/latest/api.html
|
||||
.. _Firefox: http://www.mozilla.com/firefox/
|
||||
|
||||
.. note::
|
||||
.. versionchanged:: 1.7
|
||||
|
||||
``LiveServerTestCase`` makes use of the :doc:`staticfiles contrib app
|
||||
</howto/static-files/index>` so you'll need to have your project configured
|
||||
accordingly (in particular by setting :setting:`STATIC_URL`).
|
||||
Before Django 1.7 ``LiveServerTestCase`` used to rely on the
|
||||
:doc:`staticfiles contrib app </howto/static-files/index>` to get the
|
||||
static assets of the application(s) under test transparently served at their
|
||||
expected locations during the execution of these tests.
|
||||
|
||||
In Django 1.7 this dependency of core functionality on a ``contrib``
|
||||
appplication has been removed, because of which ``LiveServerTestCase``
|
||||
ability in this respect has been retrofitted to simply publish the contents
|
||||
of the file system under :setting:`STATIC_ROOT` at the :setting:`STATIC_URL`
|
||||
URL.
|
||||
|
||||
If you use the ``staticfiles`` app in your project and need to perform live
|
||||
testing then you might want to consider using the
|
||||
:class:`~django.contrib.staticfiles.testing.StaticLiveServerCase` subclass
|
||||
shipped with it instead because it's the one that implements the original
|
||||
behavior now. See :ref:`the relevant documentation
|
||||
<staticfiles-testing-support>` for more details.
|
||||
|
||||
.. note::
|
||||
|
||||
|
|
|
@ -82,13 +82,6 @@ class LiveServerAddress(LiveServerBase):
|
|||
cls.raises_exception('localhost:8081-blah', ImproperlyConfigured)
|
||||
cls.raises_exception('localhost:8081-8082-8083', ImproperlyConfigured)
|
||||
|
||||
# If contrib.staticfiles isn't configured properly, the exception
|
||||
# should bubble up to the main thread.
|
||||
old_STATIC_URL = TEST_SETTINGS['STATIC_URL']
|
||||
TEST_SETTINGS['STATIC_URL'] = None
|
||||
cls.raises_exception('localhost:8081', ImproperlyConfigured)
|
||||
TEST_SETTINGS['STATIC_URL'] = old_STATIC_URL
|
||||
|
||||
# Restore original environment variable
|
||||
if address_predefined:
|
||||
os.environ['DJANGO_LIVE_TEST_SERVER_ADDRESS'] = old_address
|
||||
|
@ -145,13 +138,18 @@ class LiveServerViews(LiveServerBase):
|
|||
f = self.urlopen('/static/example_static_file.txt')
|
||||
self.assertEqual(f.read().rstrip(b'\r\n'), b'example static file')
|
||||
|
||||
def test_collectstatic_emulation(self):
|
||||
def test_no_collectstatic_emulation(self):
|
||||
"""
|
||||
Test LiveServerTestCase use of staticfiles' serve() allows it to
|
||||
discover app's static assets without having to collectstatic first.
|
||||
Test that LiveServerTestCase reports a 404 status code when HTTP client
|
||||
tries to access a static file that isn't explictly put under
|
||||
STATIC_ROOT.
|
||||
"""
|
||||
f = self.urlopen('/static/another_app/another_app_static_file.txt')
|
||||
self.assertEqual(f.read().rstrip(b'\r\n'), b'static file from another_app')
|
||||
try:
|
||||
self.urlopen('/static/another_app/another_app_static_file.txt')
|
||||
except HTTPError as err:
|
||||
self.assertEqual(err.code, 404, 'Expected 404 response')
|
||||
else:
|
||||
self.fail('Expected 404 response (got %d)' % err.code)
|
||||
|
||||
def test_media_files(self):
|
||||
"""
|
||||
|
|
|
@ -0,0 +1,101 @@
|
|||
"""
|
||||
A subset of the tests in tests/servers/tests exercicing
|
||||
django.contrib.staticfiles.testing.StaticLiveServerCase instead of
|
||||
django.test.LiveServerTestCase.
|
||||
"""
|
||||
|
||||
import os
|
||||
try:
|
||||
from urllib.request import urlopen
|
||||
except ImportError: # Python 2
|
||||
from urllib2 import urlopen
|
||||
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.test.utils import override_settings
|
||||
from django.utils._os import upath
|
||||
|
||||
from django.contrib.staticfiles.testing import StaticLiveServerCase
|
||||
|
||||
|
||||
TEST_ROOT = os.path.dirname(upath(__file__))
|
||||
TEST_SETTINGS = {
|
||||
'MEDIA_URL': '/media/',
|
||||
'STATIC_URL': '/static/',
|
||||
'MEDIA_ROOT': os.path.join(TEST_ROOT, 'project', 'site_media', 'media'),
|
||||
'STATIC_ROOT': os.path.join(TEST_ROOT, 'project', 'site_media', 'static'),
|
||||
}
|
||||
|
||||
|
||||
class LiveServerBase(StaticLiveServerCase):
|
||||
|
||||
available_apps = []
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
# Override settings
|
||||
cls.settings_override = override_settings(**TEST_SETTINGS)
|
||||
cls.settings_override.enable()
|
||||
super(LiveServerBase, cls).setUpClass()
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
# Restore original settings
|
||||
cls.settings_override.disable()
|
||||
super(LiveServerBase, cls).tearDownClass()
|
||||
|
||||
|
||||
class StaticLiveServerChecks(LiveServerBase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
# Backup original environment variable
|
||||
address_predefined = 'DJANGO_LIVE_TEST_SERVER_ADDRESS' in os.environ
|
||||
old_address = os.environ.get('DJANGO_LIVE_TEST_SERVER_ADDRESS')
|
||||
|
||||
# If contrib.staticfiles isn't configured properly, the exception
|
||||
# should bubble up to the main thread.
|
||||
old_STATIC_URL = TEST_SETTINGS['STATIC_URL']
|
||||
TEST_SETTINGS['STATIC_URL'] = None
|
||||
cls.raises_exception('localhost:8081', ImproperlyConfigured)
|
||||
TEST_SETTINGS['STATIC_URL'] = old_STATIC_URL
|
||||
|
||||
# Restore original environment variable
|
||||
if address_predefined:
|
||||
os.environ['DJANGO_LIVE_TEST_SERVER_ADDRESS'] = old_address
|
||||
else:
|
||||
del os.environ['DJANGO_LIVE_TEST_SERVER_ADDRESS']
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
# skip it, as setUpClass doesn't call its parent either
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def raises_exception(cls, address, exception):
|
||||
os.environ['DJANGO_LIVE_TEST_SERVER_ADDRESS'] = address
|
||||
try:
|
||||
super(StaticLiveServerChecks, cls).setUpClass()
|
||||
raise Exception("The line above should have raised an exception")
|
||||
except exception:
|
||||
pass
|
||||
finally:
|
||||
super(StaticLiveServerChecks, cls).tearDownClass()
|
||||
|
||||
def test_test_test(self):
|
||||
# Intentionally empty method so that the test is picked up by the
|
||||
# test runner and the overridden setUpClass() method is executed.
|
||||
pass
|
||||
|
||||
|
||||
class StaticLiveServerView(LiveServerBase):
|
||||
|
||||
def urlopen(self, url):
|
||||
return urlopen(self.live_server_url + url)
|
||||
|
||||
def test_collectstatic_emulation(self):
|
||||
"""
|
||||
Test that StaticLiveServerCase use of staticfiles' serve() allows it to
|
||||
discover app's static assets without having to collectstatic first.
|
||||
"""
|
||||
f = self.urlopen('/static/test/file.txt')
|
||||
self.assertEqual(f.read().rstrip(b'\r\n'), b'In app media directory.')
|
Loading…
Reference in New Issue