Commit 0bf2d337 authored by Julien Phalip's avatar Julien Phalip
Browse files

Added the ability to specify multiple ports available for the...

Added the ability to specify multiple ports available for the `LiveServerTestCase` WSGI server. This allows multiple processes to run the tests simultaneously and is particularly useful in a continuous integration context. Many thanks to Aymeric Augustin for the suggestions and feedback.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@17289 bcc190cf-cafb-0310-a4f2-bffc1f526a37
parent a82204fa
Loading
Loading
Loading
Loading
+56 −12
Original line number Diff line number Diff line
@@ -9,6 +9,7 @@ from xml.dom.minidom import parseString, Node
import select
import socket
import threading
import errno

from django.conf import settings
from django.contrib.staticfiles.handlers import StaticFilesHandler
@@ -17,7 +18,8 @@ from django.core.exceptions import ValidationError, ImproperlyConfigured
from django.core.handlers.wsgi import WSGIHandler
from django.core.management import call_command
from django.core.signals import request_started
from django.core.servers.basehttp import (WSGIRequestHandler, WSGIServer)
from django.core.servers.basehttp import (WSGIRequestHandler, WSGIServer,
    WSGIServerException)
from django.core.urlresolvers import clear_url_caches
from django.core.validators import EMPTY_VALUES
from django.db import (transaction, connection, connections, DEFAULT_DB_ALIAS,
@@ -877,9 +879,10 @@ class LiveServerThread(threading.Thread):
    Thread for running a live http server while the tests are running.
    """

    def __init__(self, address, port, connections_override=None):
        self.address = address
        self.port = port
    def __init__(self, host, possible_ports, connections_override=None):
        self.host = host
        self.port = None
        self.possible_ports = possible_ports
        self.is_ready = threading.Event()
        self.error = None
        self.connections_override = connections_override
@@ -899,9 +902,33 @@ class LiveServerThread(threading.Thread):
        try:
            # Create the handler for serving static and media files
            handler = StaticFilesHandler(_MediaFilesHandler(WSGIHandler()))
            # Instantiate and start the WSGI server

            # Go through the list of possible ports, hoping that we can find
            # one that is free to use for the WSGI server.
            for index, port in enumerate(self.possible_ports):
                try:
                    self.httpd = StoppableWSGIServer(
                (self.address, self.port), QuietWSGIRequestHandler)
                        (self.host, port), QuietWSGIRequestHandler)
                except WSGIServerException, e:
                    if sys.version_info < (2, 6):
                        error_code = e.args[0].args[0]
                    else:
                        error_code = e.args[0].errno
                    if (index + 1 < len(self.possible_ports) and
                        error_code == errno.EADDRINUSE):
                        # This port is already in use, so we go on and try with
                        # the next one in the list.
                        continue
                    else:
                        # Either none of the given ports are free or the error
                        # is something else than "Address already in use". So
                        # we let that error bubble up to the main thread.
                        raise
                else:
                    # A free port was found.
                    self.port = port
                    break

            self.httpd.set_app(handler)
            self.is_ready.set()
            self.httpd.serve_forever()
@@ -931,7 +958,8 @@ class LiveServerTestCase(TransactionTestCase):

    @property
    def live_server_url(self):
        return 'http://%s' % self.__test_server_address
        return 'http://%s:%s' % (
            self.server_thread.host, self.server_thread.port)

    @classmethod
    def setUpClass(cls):
@@ -946,15 +974,31 @@ class LiveServerTestCase(TransactionTestCase):
                connections_override[conn.alias] = conn

        # Launch the live server's thread
        cls.__test_server_address = os.environ.get(
        specified_address = os.environ.get(
            'DJANGO_LIVE_TEST_SERVER_ADDRESS', 'localhost:8081')

        # The specified ports may be of the form '8000-8010,8080,9200-9300'
        # i.e. a comma-separated list of ports or ranges of ports, so we break
        # it down into a detailed list of all possible ports.
        possible_ports = []
        try:
            host, port = cls.__test_server_address.split(':')
            host, port_ranges = specified_address.split(':')
            for port_range in port_ranges.split(','):
                # A port range can be of either form: '8000' or '8000-8010'.
                extremes = map(int, port_range.split('-'))
                assert len(extremes) in [1, 2]
                if len(extremes) == 1:
                    # Port range of the form '8000'
                    possible_ports.append(extremes[0])
                else:
                    # Port range of the form '8000-8010'
                    for port in range(extremes[0], extremes[1] + 1):
                        possible_ports.append(port)
        except Exception:
            raise ImproperlyConfigured('Invalid address ("%s") for live '
                'server.' % cls.__test_server_address)
                'server.' % specified_address)
        cls.server_thread = LiveServerThread(
            host, int(port), connections_override)
            host, possible_ports, connections_override)
        cls.server_thread.daemon = True
        cls.server_thread.start()

+26 −3
Original line number Diff line number Diff line
@@ -1772,15 +1772,38 @@ simulate a real user's actions.
By default the live server's address is `'localhost:8081'` and the full URL
can be accessed during the tests with ``self.live_server_url``. If you'd like
to change the default address (in the case, for example, where the 8081 port is
already taken) you may pass a different one to the :djadmin:`test` command via
the :djadminopt:`--liveserver` option, for example:
already taken) then you may pass a different one to the :djadmin:`test` command
via the :djadminopt:`--liveserver` option, for example:

.. code-block:: bash

    ./manage.py test --liveserver=localhost:8082

Another way of changing the default server address is by setting the
`DJANGO_LIVE_TEST_SERVER_ADDRESS` environment variable.
`DJANGO_LIVE_TEST_SERVER_ADDRESS` environment variable somewhere in your
code (for example in a :ref:`custom test runner<topics-testing-test_runner>`
if you're using one):

.. code-block:: python

    import os
    os.environ['DJANGO_LIVE_TEST_SERVER_ADDRESS'] = 'localhost:8082'

In the case where the tests are run by multiple processes in parallel (for
example in the context of several simultaneous `continuous integration`_
builds), the processes will compete for the same address and therefore your
tests might randomly fail with an "Address already in use" error. To avoid this
problem, you can pass a comma-separated list of ports or ranges of ports (at
least as many as the number of potential parallel processes), for example:

.. code-block:: bash

    ./manage.py test --liveserver=localhost:8082,8090-8100,9000-9200,7041

Then, during the execution of the tests, each new live test server will try
every specified port until it finds one that is free and takes it.

.. _continuous integration: http://en.wikipedia.org/wiki/Continuous_integration

To demonstrate how to use ``LiveServerTestCase``, let's write a simple Selenium
test. First of all, you need to install the `selenium package`_ into your
+20 −22
Original line number Diff line number Diff line
@@ -101,10 +101,7 @@ class LiveServerBase(LiveServerTestCase):
        super(LiveServerBase, cls).tearDownClass()

    def urlopen(self, url):
        server_address = os.environ.get(
            'DJANGO_LIVE_TEST_SERVER_ADDRESS', 'localhost:8081')
        base = 'http://%s' % server_address
        return urllib2.urlopen(base + url)
        return urllib2.urlopen(self.live_server_url + url)


class LiveServerAddress(LiveServerBase):
@@ -120,31 +117,23 @@ class LiveServerAddress(LiveServerBase):
        old_address = os.environ.get('DJANGO_LIVE_TEST_SERVER_ADDRESS')

        # Just the host is not accepted
        os.environ['DJANGO_LIVE_TEST_SERVER_ADDRESS'] = 'localhost'
        try:
            super(LiveServerAddress, cls).setUpClass()
            raise Exception("The line above should have raised an exception")
        except ImproperlyConfigured:
            pass
        cls.raises_exception('localhost', ImproperlyConfigured)

        # The host must be valid
        os.environ['DJANGO_LIVE_TEST_SERVER_ADDRESS'] = 'blahblahblah:8081'
        try:
            super(LiveServerAddress, cls).setUpClass()
            raise Exception("The line above should have raised an exception")
        except WSGIServerException:
            pass
        cls.raises_exception('blahblahblah:8081', WSGIServerException)

        # The list of ports must be in a valid format
        cls.raises_exception('localhost:8081,', ImproperlyConfigured)
        cls.raises_exception('localhost:8081,blah', ImproperlyConfigured)
        cls.raises_exception('localhost:8081-', ImproperlyConfigured)
        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
        os.environ['DJANGO_LIVE_TEST_SERVER_ADDRESS'] = 'localhost:8081'
        try:
            super(LiveServerAddress, cls).setUpClass()
            raise Exception("The line above should have raised an exception")
        except ImproperlyConfigured:
            pass
        cls.raises_exception('localhost:8081', ImproperlyConfigured)
        TEST_SETTINGS['STATIC_URL'] = old_STATIC_URL

        # Restore original environment variable
@@ -153,6 +142,15 @@ class LiveServerAddress(LiveServerBase):
        else:
            del os.environ['DJANGO_LIVE_TEST_SERVER_ADDRESS']

    @classmethod
    def raises_exception(cls, address, exception):
        os.environ['DJANGO_LIVE_TEST_SERVER_ADDRESS'] = address
        try:
            super(LiveServerAddress, cls).setUpClass()
            raise Exception("The line above should have raised an exception")
        except exception:
            pass

    def test_test_test(self):
        # Intentionally empty method so that the test is picked up by the
        # test runner and the overriden setUpClass() method is executed.