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
This commit is contained in:
parent
a82204fa9a
commit
0bf2d33770
|
@ -9,6 +9,7 @@ from xml.dom.minidom import parseString, Node
|
||||||
import select
|
import select
|
||||||
import socket
|
import socket
|
||||||
import threading
|
import threading
|
||||||
|
import errno
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.staticfiles.handlers import StaticFilesHandler
|
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.handlers.wsgi import WSGIHandler
|
||||||
from django.core.management import call_command
|
from django.core.management import call_command
|
||||||
from django.core.signals import request_started
|
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.urlresolvers import clear_url_caches
|
||||||
from django.core.validators import EMPTY_VALUES
|
from django.core.validators import EMPTY_VALUES
|
||||||
from django.db import (transaction, connection, connections, DEFAULT_DB_ALIAS,
|
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.
|
Thread for running a live http server while the tests are running.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, address, port, connections_override=None):
|
def __init__(self, host, possible_ports, connections_override=None):
|
||||||
self.address = address
|
self.host = host
|
||||||
self.port = port
|
self.port = None
|
||||||
|
self.possible_ports = possible_ports
|
||||||
self.is_ready = threading.Event()
|
self.is_ready = threading.Event()
|
||||||
self.error = None
|
self.error = None
|
||||||
self.connections_override = connections_override
|
self.connections_override = connections_override
|
||||||
|
@ -899,9 +902,33 @@ class LiveServerThread(threading.Thread):
|
||||||
try:
|
try:
|
||||||
# Create the handler for serving static and media files
|
# Create the handler for serving static and media files
|
||||||
handler = StaticFilesHandler(_MediaFilesHandler(WSGIHandler()))
|
handler = StaticFilesHandler(_MediaFilesHandler(WSGIHandler()))
|
||||||
# Instantiate and start the WSGI server
|
|
||||||
self.httpd = StoppableWSGIServer(
|
# Go through the list of possible ports, hoping that we can find
|
||||||
(self.address, self.port), QuietWSGIRequestHandler)
|
# one that is free to use for the WSGI server.
|
||||||
|
for index, port in enumerate(self.possible_ports):
|
||||||
|
try:
|
||||||
|
self.httpd = StoppableWSGIServer(
|
||||||
|
(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.httpd.set_app(handler)
|
||||||
self.is_ready.set()
|
self.is_ready.set()
|
||||||
self.httpd.serve_forever()
|
self.httpd.serve_forever()
|
||||||
|
@ -931,7 +958,8 @@ class LiveServerTestCase(TransactionTestCase):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def live_server_url(self):
|
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
|
@classmethod
|
||||||
def setUpClass(cls):
|
def setUpClass(cls):
|
||||||
|
@ -946,15 +974,31 @@ class LiveServerTestCase(TransactionTestCase):
|
||||||
connections_override[conn.alias] = conn
|
connections_override[conn.alias] = conn
|
||||||
|
|
||||||
# Launch the live server's thread
|
# 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')
|
'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:
|
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:
|
except Exception:
|
||||||
raise ImproperlyConfigured('Invalid address ("%s") for live '
|
raise ImproperlyConfigured('Invalid address ("%s") for live '
|
||||||
'server.' % cls.__test_server_address)
|
'server.' % specified_address)
|
||||||
cls.server_thread = LiveServerThread(
|
cls.server_thread = LiveServerThread(
|
||||||
host, int(port), connections_override)
|
host, possible_ports, connections_override)
|
||||||
cls.server_thread.daemon = True
|
cls.server_thread.daemon = True
|
||||||
cls.server_thread.start()
|
cls.server_thread.start()
|
||||||
|
|
||||||
|
|
|
@ -1772,15 +1772,38 @@ simulate a real user's actions.
|
||||||
By default the live server's address is `'localhost:8081'` and the full URL
|
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
|
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
|
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
|
already taken) then you may pass a different one to the :djadmin:`test` command
|
||||||
the :djadminopt:`--liveserver` option, for example:
|
via the :djadminopt:`--liveserver` option, for example:
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
./manage.py test --liveserver=localhost:8082
|
./manage.py test --liveserver=localhost:8082
|
||||||
|
|
||||||
Another way of changing the default server address is by setting the
|
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
|
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
|
test. First of all, you need to install the `selenium package`_ into your
|
||||||
|
|
|
@ -101,10 +101,7 @@ class LiveServerBase(LiveServerTestCase):
|
||||||
super(LiveServerBase, cls).tearDownClass()
|
super(LiveServerBase, cls).tearDownClass()
|
||||||
|
|
||||||
def urlopen(self, url):
|
def urlopen(self, url):
|
||||||
server_address = os.environ.get(
|
return urllib2.urlopen(self.live_server_url + url)
|
||||||
'DJANGO_LIVE_TEST_SERVER_ADDRESS', 'localhost:8081')
|
|
||||||
base = 'http://%s' % server_address
|
|
||||||
return urllib2.urlopen(base + url)
|
|
||||||
|
|
||||||
|
|
||||||
class LiveServerAddress(LiveServerBase):
|
class LiveServerAddress(LiveServerBase):
|
||||||
|
@ -120,31 +117,23 @@ class LiveServerAddress(LiveServerBase):
|
||||||
old_address = os.environ.get('DJANGO_LIVE_TEST_SERVER_ADDRESS')
|
old_address = os.environ.get('DJANGO_LIVE_TEST_SERVER_ADDRESS')
|
||||||
|
|
||||||
# Just the host is not accepted
|
# Just the host is not accepted
|
||||||
os.environ['DJANGO_LIVE_TEST_SERVER_ADDRESS'] = 'localhost'
|
cls.raises_exception('localhost', ImproperlyConfigured)
|
||||||
try:
|
|
||||||
super(LiveServerAddress, cls).setUpClass()
|
|
||||||
raise Exception("The line above should have raised an exception")
|
|
||||||
except ImproperlyConfigured:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# The host must be valid
|
# The host must be valid
|
||||||
os.environ['DJANGO_LIVE_TEST_SERVER_ADDRESS'] = 'blahblahblah:8081'
|
cls.raises_exception('blahblahblah:8081', WSGIServerException)
|
||||||
try:
|
|
||||||
super(LiveServerAddress, cls).setUpClass()
|
# The list of ports must be in a valid format
|
||||||
raise Exception("The line above should have raised an exception")
|
cls.raises_exception('localhost:8081,', ImproperlyConfigured)
|
||||||
except WSGIServerException:
|
cls.raises_exception('localhost:8081,blah', ImproperlyConfigured)
|
||||||
pass
|
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
|
# If contrib.staticfiles isn't configured properly, the exception
|
||||||
# should bubble up to the main thread.
|
# should bubble up to the main thread.
|
||||||
old_STATIC_URL = TEST_SETTINGS['STATIC_URL']
|
old_STATIC_URL = TEST_SETTINGS['STATIC_URL']
|
||||||
TEST_SETTINGS['STATIC_URL'] = None
|
TEST_SETTINGS['STATIC_URL'] = None
|
||||||
os.environ['DJANGO_LIVE_TEST_SERVER_ADDRESS'] = 'localhost:8081'
|
cls.raises_exception('localhost:8081', ImproperlyConfigured)
|
||||||
try:
|
|
||||||
super(LiveServerAddress, cls).setUpClass()
|
|
||||||
raise Exception("The line above should have raised an exception")
|
|
||||||
except ImproperlyConfigured:
|
|
||||||
pass
|
|
||||||
TEST_SETTINGS['STATIC_URL'] = old_STATIC_URL
|
TEST_SETTINGS['STATIC_URL'] = old_STATIC_URL
|
||||||
|
|
||||||
# Restore original environment variable
|
# Restore original environment variable
|
||||||
|
@ -153,6 +142,15 @@ class LiveServerAddress(LiveServerBase):
|
||||||
else:
|
else:
|
||||||
del os.environ['DJANGO_LIVE_TEST_SERVER_ADDRESS']
|
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):
|
def test_test_test(self):
|
||||||
# Intentionally empty method so that the test is picked up by the
|
# Intentionally empty method so that the test is picked up by the
|
||||||
# test runner and the overriden setUpClass() method is executed.
|
# test runner and the overriden setUpClass() method is executed.
|
||||||
|
|
Loading…
Reference in New Issue