Commit e909ceae authored by Ramiro Morales's avatar Ramiro Morales
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.
parent e0643cb6
Loading
Loading
Loading
Loading
+14 −0
Original line number Diff line number Diff line
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
+1 −1
Original line number Diff line number Diff line
@@ -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
+77 −15
Original line number Diff line number Diff line
@@ -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()

+27 −0
Original line number Diff line number Diff line
@@ -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
==========

+23 −0
Original line number Diff line number Diff line
@@ -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`.
Loading