Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

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 authored December 29, 2011
68  django/test/testcases.py
@@ -9,6 +9,7 @@
9 9
 import select
10 10
 import socket
11 11
 import threading
  12
+import errno
12 13
 
13 14
 from django.conf import settings
14 15
 from django.contrib.staticfiles.handlers import StaticFilesHandler
@@ -17,7 +18,8 @@
17 18
 from django.core.handlers.wsgi import WSGIHandler
18 19
 from django.core.management import call_command
19 20
 from django.core.signals import request_started
20  
-from django.core.servers.basehttp import (WSGIRequestHandler, WSGIServer)
  21
+from django.core.servers.basehttp import (WSGIRequestHandler, WSGIServer,
  22
+    WSGIServerException)
21 23
 from django.core.urlresolvers import clear_url_caches
22 24
 from django.core.validators import EMPTY_VALUES
23 25
 from django.db import (transaction, connection, connections, DEFAULT_DB_ALIAS,
@@ -877,9 +879,10 @@ class LiveServerThread(threading.Thread):
877 879
     Thread for running a live http server while the tests are running.
878 880
     """
879 881
 
880  
-    def __init__(self, address, port, connections_override=None):
881  
-        self.address = address
882  
-        self.port = port
  882
+    def __init__(self, host, possible_ports, connections_override=None):
  883
+        self.host = host
  884
+        self.port = None
  885
+        self.possible_ports = possible_ports
883 886
         self.is_ready = threading.Event()
884 887
         self.error = None
885 888
         self.connections_override = connections_override
@@ -899,9 +902,33 @@ def run(self):
899 902
         try:
900 903
             # Create the handler for serving static and media files
901 904
             handler = StaticFilesHandler(_MediaFilesHandler(WSGIHandler()))
902  
-            # Instantiate and start the WSGI server
903  
-            self.httpd = StoppableWSGIServer(
904  
-                (self.address, self.port), QuietWSGIRequestHandler)
  905
+
  906
+            # Go through the list of possible ports, hoping that we can find
  907
+            # one that is free to use for the WSGI server.
  908
+            for index, port in enumerate(self.possible_ports):
  909
+                try:
  910
+                    self.httpd = StoppableWSGIServer(
  911
+                        (self.host, port), QuietWSGIRequestHandler)
  912
+                except WSGIServerException, e:
  913
+                    if sys.version_info < (2, 6):
  914
+                        error_code = e.args[0].args[0]
  915
+                    else:
  916
+                        error_code = e.args[0].errno
  917
+                    if (index + 1 < len(self.possible_ports) and
  918
+                        error_code == errno.EADDRINUSE):
  919
+                        # This port is already in use, so we go on and try with
  920
+                        # the next one in the list.
  921
+                        continue
  922
+                    else:
  923
+                        # Either none of the given ports are free or the error
  924
+                        # is something else than "Address already in use". So
  925
+                        # we let that error bubble up to the main thread.
  926
+                        raise
  927
+                else:
  928
+                    # A free port was found.
  929
+                    self.port = port
  930
+                    break
  931
+
905 932
             self.httpd.set_app(handler)
906 933
             self.is_ready.set()
907 934
             self.httpd.serve_forever()
@@ -931,7 +958,8 @@ class LiveServerTestCase(TransactionTestCase):
931 958
 
932 959
     @property
933 960
     def live_server_url(self):
934  
-        return 'http://%s' % self.__test_server_address
  961
+        return 'http://%s:%s' % (
  962
+            self.server_thread.host, self.server_thread.port)
935 963
 
936 964
     @classmethod
937 965
     def setUpClass(cls):
@@ -946,15 +974,31 @@ def setUpClass(cls):
946 974
                 connections_override[conn.alias] = conn
947 975
 
948 976
         # Launch the live server's thread
949  
-        cls.__test_server_address = os.environ.get(
  977
+        specified_address = os.environ.get(
950 978
             'DJANGO_LIVE_TEST_SERVER_ADDRESS', 'localhost:8081')
  979
+
  980
+        # The specified ports may be of the form '8000-8010,8080,9200-9300'
  981
+        # i.e. a comma-separated list of ports or ranges of ports, so we break
  982
+        # it down into a detailed list of all possible ports.
  983
+        possible_ports = []
951 984
         try:
952  
-            host, port = cls.__test_server_address.split(':')
  985
+            host, port_ranges = specified_address.split(':')
  986
+            for port_range in port_ranges.split(','):
  987
+                # A port range can be of either form: '8000' or '8000-8010'.
  988
+                extremes = map(int, port_range.split('-'))
  989
+                assert len(extremes) in [1, 2]
  990
+                if len(extremes) == 1:
  991
+                    # Port range of the form '8000'
  992
+                    possible_ports.append(extremes[0])
  993
+                else:
  994
+                    # Port range of the form '8000-8010'
  995
+                    for port in range(extremes[0], extremes[1] + 1):
  996
+                        possible_ports.append(port)
953 997
         except Exception:
954 998
             raise ImproperlyConfigured('Invalid address ("%s") for live '
955  
-                'server.' % cls.__test_server_address)
  999
+                'server.' % specified_address)
956 1000
         cls.server_thread = LiveServerThread(
957  
-            host, int(port), connections_override)
  1001
+            host, possible_ports, connections_override)
958 1002
         cls.server_thread.daemon = True
959 1003
         cls.server_thread.start()
960 1004
 
29  docs/topics/testing.txt
@@ -1772,15 +1772,38 @@ simulate a real user's actions.
1772 1772
 By default the live server's address is `'localhost:8081'` and the full URL
1773 1773
 can be accessed during the tests with ``self.live_server_url``. If you'd like
1774 1774
 to change the default address (in the case, for example, where the 8081 port is
1775  
-already taken) you may pass a different one to the :djadmin:`test` command via
1776  
-the :djadminopt:`--liveserver` option, for example:
  1775
+already taken) then you may pass a different one to the :djadmin:`test` command
  1776
+via the :djadminopt:`--liveserver` option, for example:
1777 1777
 
1778 1778
 .. code-block:: bash
1779 1779
 
1780 1780
     ./manage.py test --liveserver=localhost:8082
1781 1781
 
1782 1782
 Another way of changing the default server address is by setting the
1783  
-`DJANGO_LIVE_TEST_SERVER_ADDRESS` environment variable.
  1783
+`DJANGO_LIVE_TEST_SERVER_ADDRESS` environment variable somewhere in your
  1784
+code (for example in a :ref:`custom test runner<topics-testing-test_runner>`
  1785
+if you're using one):
  1786
+
  1787
+.. code-block:: python
  1788
+
  1789
+    import os
  1790
+    os.environ['DJANGO_LIVE_TEST_SERVER_ADDRESS'] = 'localhost:8082'
  1791
+
  1792
+In the case where the tests are run by multiple processes in parallel (for
  1793
+example in the context of several simultaneous `continuous integration`_
  1794
+builds), the processes will compete for the same address and therefore your
  1795
+tests might randomly fail with an "Address already in use" error. To avoid this
  1796
+problem, you can pass a comma-separated list of ports or ranges of ports (at
  1797
+least as many as the number of potential parallel processes), for example:
  1798
+
  1799
+.. code-block:: bash
  1800
+
  1801
+    ./manage.py test --liveserver=localhost:8082,8090-8100,9000-9200,7041
  1802
+
  1803
+Then, during the execution of the tests, each new live test server will try
  1804
+every specified port until it finds one that is free and takes it.
  1805
+
  1806
+.. _continuous integration: http://en.wikipedia.org/wiki/Continuous_integration
1784 1807
 
1785 1808
 To demonstrate how to use ``LiveServerTestCase``, let's write a simple Selenium
1786 1809
 test. First of all, you need to install the `selenium package`_ into your
42  tests/regressiontests/servers/tests.py
@@ -101,10 +101,7 @@ def tearDownClass(cls):
101 101
         super(LiveServerBase, cls).tearDownClass()
102 102
 
103 103
     def urlopen(self, url):
104  
-        server_address = os.environ.get(
105  
-            'DJANGO_LIVE_TEST_SERVER_ADDRESS', 'localhost:8081')
106  
-        base = 'http://%s' % server_address
107  
-        return urllib2.urlopen(base + url)
  104
+        return urllib2.urlopen(self.live_server_url + url)
108 105
 
109 106
 
110 107
 class LiveServerAddress(LiveServerBase):
@@ -120,31 +117,23 @@ def setUpClass(cls):
120 117
         old_address = os.environ.get('DJANGO_LIVE_TEST_SERVER_ADDRESS')
121 118
 
122 119
         # Just the host is not accepted
123  
-        os.environ['DJANGO_LIVE_TEST_SERVER_ADDRESS'] = 'localhost'
124  
-        try:
125  
-            super(LiveServerAddress, cls).setUpClass()
126  
-            raise Exception("The line above should have raised an exception")
127  
-        except ImproperlyConfigured:
128  
-            pass
  120
+        cls.raises_exception('localhost', ImproperlyConfigured)
129 121
 
130 122
         # The host must be valid
131  
-        os.environ['DJANGO_LIVE_TEST_SERVER_ADDRESS'] = 'blahblahblah:8081'
132  
-        try:
133  
-            super(LiveServerAddress, cls).setUpClass()
134  
-            raise Exception("The line above should have raised an exception")
135  
-        except WSGIServerException:
136  
-            pass
  123
+        cls.raises_exception('blahblahblah:8081', WSGIServerException)
  124
+
  125
+        # The list of ports must be in a valid format
  126
+        cls.raises_exception('localhost:8081,', ImproperlyConfigured)
  127
+        cls.raises_exception('localhost:8081,blah', ImproperlyConfigured)
  128
+        cls.raises_exception('localhost:8081-', ImproperlyConfigured)
  129
+        cls.raises_exception('localhost:8081-blah', ImproperlyConfigured)
  130
+        cls.raises_exception('localhost:8081-8082-8083', ImproperlyConfigured)
137 131
 
138 132
         # If contrib.staticfiles isn't configured properly, the exception
139 133
         # should bubble up to the main thread.
140 134
         old_STATIC_URL = TEST_SETTINGS['STATIC_URL']
141 135
         TEST_SETTINGS['STATIC_URL'] = None
142  
-        os.environ['DJANGO_LIVE_TEST_SERVER_ADDRESS'] = 'localhost:8081'
143  
-        try:
144  
-            super(LiveServerAddress, cls).setUpClass()
145  
-            raise Exception("The line above should have raised an exception")
146  
-        except ImproperlyConfigured:
147  
-            pass
  136
+        cls.raises_exception('localhost:8081', ImproperlyConfigured)
148 137
         TEST_SETTINGS['STATIC_URL'] = old_STATIC_URL
149 138
 
150 139
         # Restore original environment variable
@@ -153,6 +142,15 @@ def setUpClass(cls):
153 142
         else:
154 143
             del os.environ['DJANGO_LIVE_TEST_SERVER_ADDRESS']
155 144
 
  145
+    @classmethod
  146
+    def raises_exception(cls, address, exception):
  147
+        os.environ['DJANGO_LIVE_TEST_SERVER_ADDRESS'] = address
  148
+        try:
  149
+            super(LiveServerAddress, cls).setUpClass()
  150
+            raise Exception("The line above should have raised an exception")
  151
+        except exception:
  152
+            pass
  153
+
156 154
     def test_test_test(self):
157 155
         # Intentionally empty method so that the test is picked up by the
158 156
         # test runner and the overriden setUpClass() method is executed.

0 notes on commit 0bf2d33

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