Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Added the ability to specify multiple ports available for the `LiveSe…

…rverTestCase` 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
  • Loading branch information...
commit 0bf2d337701a41d9fc42c6cac608e18f989a9866 1 parent a82204f
Julien Phalip jphalip authored
68 django/test/testcases.py
View
@@ -9,6 +9,7 @@
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.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 @@ def run(self):
try:
# Create the handler for serving static and media files
handler = StaticFilesHandler(_MediaFilesHandler(WSGIHandler()))
- # Instantiate and start the WSGI server
- self.httpd = StoppableWSGIServer(
- (self.address, self.port), QuietWSGIRequestHandler)
+
+ # 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.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 @@ def setUpClass(cls):
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()
29 docs/topics/testing.txt
View
@@ -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
42 tests/regressiontests/servers/tests.py
View
@@ -101,10 +101,7 @@ def tearDownClass(cls):
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 @@ def setUpClass(cls):
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 @@ def setUpClass(cls):
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.
Please sign in to comment.
Something went wrong with that request. Please try again.