Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

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.
  • Loading branch information...
commit e909ceae9b3e72b72e0a2baaa92bba9714f18cd2 1 parent e0643cb
@ramiro ramiro authored
View
14 django/contrib/staticfiles/testing.py
@@ -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
View
2  django/contrib/staticfiles/views.py
@@ -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
View
92 django/test/testcases.py
@@ -6,23 +6,25 @@
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 @@ def log_message(*args):
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 @@ def get_base_dir(self):
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 @@ def run(self):
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 @@ def setUpClass(cls):
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()
View
27 docs/howto/static-files/index.txt
@@ -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
==========
View
23 docs/ref/contrib/staticfiles.txt
@@ -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`.
View
14 docs/releases/1.7.txt
@@ -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
==========================
View
22 docs/topics/testing/overview.txt
@@ -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::
View
22 tests/servers/tests.py
@@ -82,13 +82,6 @@ def setUpClass(cls):
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 @@ def test_static_files(self):
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):
"""
View
101 tests/staticfiles_tests/test_liveserver.py
@@ -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.')

0 comments on commit e909cea

Please sign in to comment.
Something went wrong with that request. Please try again.