Browse Source

Fixed #26791 -- Replaced LiveServerTestCase port ranges with binding to port 0.

Tim Graham 8 years ago
parent
commit
81cdcb66bc

+ 0 - 11
django/core/management/commands/test.py

@@ -1,4 +1,3 @@
-import os
 import sys
 
 from django.conf import settings
@@ -46,12 +45,6 @@ class Command(BaseCommand):
             help='Tells Django to use specified test runner class instead of '
                  'the one specified by the TEST_RUNNER setting.',
         )
-        parser.add_argument(
-            '--liveserver', action='store', dest='liveserver', default=None,
-            help='Overrides the default address where the live server (used '
-                 'with LiveServerTestCase) is expected to run from. The '
-                 'default value is localhost:8081-8179.',
-        )
 
         test_runner_class = get_runner(settings, self.test_runner)
 
@@ -64,10 +57,6 @@ class Command(BaseCommand):
 
         TestRunner = get_runner(settings, options['testrunner'])
 
-        if options['liveserver'] is not None:
-            os.environ['DJANGO_LIVE_TEST_SERVER_ADDRESS'] = options['liveserver']
-        del options['liveserver']
-
         test_runner = TestRunner(**options)
         failures = test_runner.run_tests(test_labels)
 

+ 10 - 60
django/test/testcases.py

@@ -1,11 +1,8 @@
 from __future__ import unicode_literals
 
 import difflib
-import errno
 import json
-import os
 import posixpath
-import socket
 import sys
 import threading
 import unittest
@@ -19,7 +16,7 @@ from unittest.util import safe_repr
 from django.apps import apps
 from django.conf import settings
 from django.core import mail
-from django.core.exceptions import ImproperlyConfigured, ValidationError
+from django.core.exceptions import ValidationError
 from django.core.files import locks
 from django.core.handlers.wsgi import WSGIHandler, get_path_info
 from django.core.management import call_command
@@ -1235,10 +1232,9 @@ class LiveServerThread(threading.Thread):
     Thread for running a live http server while the tests are running.
     """
 
-    def __init__(self, host, possible_ports, static_handler, connections_override=None):
+    def __init__(self, host, 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
@@ -1258,28 +1254,8 @@ class LiveServerThread(threading.Thread):
         try:
             # Create the handler for serving static and media files
             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.
-            for index, port in enumerate(self.possible_ports):
-                try:
-                    self.httpd = self._create_server(port)
-                except socket.error as e:
-                    if (index + 1 < len(self.possible_ports) and
-                            e.errno == 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 = self._create_server(0)
+            self.port = self.httpd.server_address[1]
             self.httpd.set_app(handler)
             self.is_ready.set()
             self.httpd.serve_forever()
@@ -1308,13 +1284,12 @@ class LiveServerTestCase(TransactionTestCase):
     sqlite) and each thread needs to commit all their transactions so that the
     other thread can see the changes.
     """
-
+    host = 'localhost'
     static_handler = _StaticFilesHandler
 
     @classproperty
     def live_server_url(cls):
-        return 'http://%s:%s' % (
-            cls.server_thread.host, cls.server_thread.port)
+        return 'http://%s:%s' % (cls.host, cls.server_thread.port)
 
     @classmethod
     def setUpClass(cls):
@@ -1328,35 +1303,11 @@ class LiveServerTestCase(TransactionTestCase):
                 conn.allow_thread_sharing = True
                 connections_override[conn.alias] = conn
 
-        specified_address = os.environ.get(
-            'DJANGO_LIVE_TEST_SERVER_ADDRESS', 'localhost:8081-8179')
         cls._live_server_modified_settings = modify_settings(
-            ALLOWED_HOSTS={'append': specified_address.split(':')[0]},
+            ALLOWED_HOSTS={'append': cls.host},
         )
         cls._live_server_modified_settings.enable()
-
-        # 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_ranges = specified_address.split(':')
-            for port_range in port_ranges.split(','):
-                # A port range can be of either form: '8000' or '8000-8010'.
-                extremes = list(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:
-            msg = 'Invalid address ("%s") for live server.' % specified_address
-            six.reraise(ImproperlyConfigured, ImproperlyConfigured(msg), sys.exc_info()[2])
-        # Launch the live server's thread
-        cls.server_thread = cls._create_server_thread(host, possible_ports, connections_override)
+        cls.server_thread = cls._create_server_thread(connections_override)
         cls.server_thread.daemon = True
         cls.server_thread.start()
 
@@ -1369,10 +1320,9 @@ class LiveServerTestCase(TransactionTestCase):
             raise cls.server_thread.error
 
     @classmethod
-    def _create_server_thread(cls, host, possible_ports, connections_override):
+    def _create_server_thread(cls, connections_override):
         return LiveServerThread(
-            host,
-            possible_ports,
+            cls.host,
             cls.static_handler,
             connections_override=connections_override,
         )

+ 0 - 6
docs/ref/django-admin.txt

@@ -1227,12 +1227,6 @@ Stops running tests and reports the failure immediately after a test fails.
 Controls the test runner class that is used to execute tests. This value
 overrides the value provided by the :setting:`TEST_RUNNER` setting.
 
-.. django-admin-option:: --liveserver LIVESERVER
-
-Overrides the default address where the live server (used with
-:class:`~django.test.LiveServerTestCase`) is expected to run from. The default
-value is ``localhost:8081-8179``.
-
 .. django-admin-option:: --noinput, --no-input
 
 Suppresses all user prompts. A typical prompt is a warning about deleting an

+ 9 - 0
docs/releases/1.11.txt

@@ -250,6 +250,15 @@ Django 1.11 sets PostgreSQL 9.3 as the minimum version it officially supports.
 Support for PostGIS 2.0 is also removed as PostgreSQL 9.2 is the last version
 to support it.
 
+``LiveServerTestCase`` binds to port zero
+-----------------------------------------
+
+Rather than taking a port range and iterating to find a free port,
+``LiveServerTestCase`` binds to port zero and relies on the operating system
+to assign a free port. The ``DJANGO_LIVE_TEST_SERVER_ADDRESS`` environment
+variable is no longer used, and as it's also no longer used, the
+``manage.py test --liveserver`` option is removed.
+
 Miscellaneous
 -------------
 

+ 7 - 30
docs/topics/testing/tools.txt

@@ -814,39 +814,16 @@ This allows the use of automated test clients other than the
 client, to execute a series of functional tests inside a browser and simulate a
 real user's actions.
 
-By default the live server listens on ``localhost`` and picks the first
-available port in the ``8081-8179`` range. Its full URL can be accessed with
+The live server listens on ``localhost`` and binds to port 0 which uses a free
+port assigned by the operating system. The server's URL can be accessed with
 ``self.live_server_url`` during the tests.
 
-If you'd like to select another address, you may pass a different one using the
-:option:`test --liveserver` option, for example:
+.. versionchanged:: 1.11
 
-.. code-block:: console
-
-    $ ./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 somewhere in your
-code (for example, in a :ref:`custom test runner<topics-testing-test_runner>`)::
-
-    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:: console
-
-    $ ./manage.py test --liveserver=localhost:8082,8090-8100,9000-9200,7041
-
-Then, during test execution, each new live test server will try every specified
-port until it finds one that is free and takes it.
-
-.. _continuous integration: https://en.wikipedia.org/wiki/Continuous_integration
+    In older versions, Django tried a predefined port range which could be
+    customized in various ways including the ``DJANGO_LIVE_TEST_SERVER_ADDRESS``
+    environment variable. This is removed in favor of the simpler "bind to port
+    0" technique.
 
 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

+ 0 - 41
tests/admin_scripts/tests.py

@@ -28,7 +28,6 @@ from django.db.migrations.recorder import MigrationRecorder
 from django.test import (
     LiveServerTestCase, SimpleTestCase, TestCase, mock, override_settings,
 )
-from django.test.runner import DiscoverRunner
 from django.utils._os import npath, upath
 from django.utils.encoding import force_text
 from django.utils.six import PY2, PY3, StringIO
@@ -1276,46 +1275,6 @@ class ManageCheck(AdminScriptTestCase):
         self.assertNoOutput(out)
 
 
-class CustomTestRunner(DiscoverRunner):
-
-    def __init__(self, *args, **kwargs):
-        assert 'liveserver' not in kwargs
-        super(CustomTestRunner, self).__init__(*args, **kwargs)
-
-    def run_tests(self, test_labels, extra_tests=None, **kwargs):
-        pass
-
-
-class ManageTestCommand(AdminScriptTestCase):
-    def test_liveserver(self):
-        """
-        Ensure that the --liveserver option sets the environment variable
-        correctly.
-        Refs #2879.
-        """
-
-        # Backup original state
-        address_predefined = 'DJANGO_LIVE_TEST_SERVER_ADDRESS' in os.environ
-        old_address = os.environ.get('DJANGO_LIVE_TEST_SERVER_ADDRESS')
-
-        call_command('test', verbosity=0, testrunner='admin_scripts.tests.CustomTestRunner')
-
-        # Original state hasn't changed
-        self.assertEqual('DJANGO_LIVE_TEST_SERVER_ADDRESS' in os.environ, address_predefined)
-        self.assertEqual(os.environ.get('DJANGO_LIVE_TEST_SERVER_ADDRESS'), old_address)
-
-        call_command('test', verbosity=0, testrunner='admin_scripts.tests.CustomTestRunner', liveserver='blah')
-
-        # Variable was correctly set
-        self.assertEqual(os.environ['DJANGO_LIVE_TEST_SERVER_ADDRESS'], 'blah')
-
-        # Restore original state
-        if address_predefined:
-            os.environ['DJANGO_LIVE_TEST_SERVER_ADDRESS'] = old_address
-        else:
-            del os.environ['DJANGO_LIVE_TEST_SERVER_ADDRESS']
-
-
 class ManageRunserver(AdminScriptTestCase):
     def setUp(self):
         from django.core.management.commands.runserver import Command

+ 0 - 9
tests/runtests.py

@@ -417,12 +417,6 @@ if __name__ == "__main__":
         help='Sort test suites and test cases in opposite order to debug '
              'test side effects not apparent with normal execution lineup.',
     )
-    parser.add_argument(
-        '--liveserver',
-        help='Overrides the default address where the live server (used with '
-             'LiveServerTestCase) is expected to run from. The default value '
-             'is localhost:8081-8179.',
-    )
     parser.add_argument(
         '--selenium', dest='selenium', action=ActionSelenium, metavar='BROWSERS',
         help='A comma-separated list of browsers to run the Selenium tests against.',
@@ -467,9 +461,6 @@ if __name__ == "__main__":
             os.environ['DJANGO_SETTINGS_MODULE'] = 'test_sqlite'
         options.settings = os.environ['DJANGO_SETTINGS_MODULE']
 
-    if options.liveserver is not None:
-        os.environ['DJANGO_LIVE_TEST_SERVER_ADDRESS'] = options.liveserver
-
     if options.selenium:
         if not options.tags:
             options.tags = ['selenium']

+ 1 - 44
tests/servers/tests.py

@@ -9,7 +9,6 @@ import errno
 import os
 import socket
 
-from django.core.exceptions import ImproperlyConfigured
 from django.test import LiveServerTestCase, override_settings
 from django.utils._os import upath
 from django.utils.http import urlencode
@@ -44,55 +43,13 @@ class LiveServerBase(LiveServerTestCase):
 
 
 class LiveServerAddress(LiveServerBase):
-    """
-    Ensure that the address set in the environment variable is valid.
-    Refs #2879.
-    """
 
     @classmethod
     def setUpClass(cls):
-        # Backup original environment variable
-        address_predefined = 'DJANGO_LIVE_TEST_SERVER_ADDRESS' in os.environ
-        old_address = os.environ.get('DJANGO_LIVE_TEST_SERVER_ADDRESS')
-
-        # Just the host is not accepted
-        cls.raises_exception('localhost', ImproperlyConfigured)
-
-        # The host must be valid
-        cls.raises_exception('blahblahblah:8081', socket.error)
-
-        # 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)
-
-        # Restore original environment variable
-        if address_predefined:
-            os.environ['DJANGO_LIVE_TEST_SERVER_ADDRESS'] = old_address
-        else:
-            del os.environ['DJANGO_LIVE_TEST_SERVER_ADDRESS']
-
+        super(LiveServerAddress, cls).setUpClass()
         # put it in a list to prevent descriptor lookups in test
         cls.live_server_url_test = [cls.live_server_url]
 
-    @classmethod
-    def tearDownClass(cls):
-        # skip it, as setUpClass doesn't call its parent either
-        pass
-
-    @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
-        finally:
-            super(LiveServerAddress, cls).tearDownClass()
-
     def test_live_server_url_is_class_property(self):
         self.assertIsInstance(self.live_server_url_test[0], text_type)
         self.assertEqual(self.live_server_url_test[0], self.live_server_url)

+ 5 - 14
tests/staticfiles_tests/test_liveserver.py

@@ -44,35 +44,26 @@ class StaticLiveServerChecks(LiveServerBase):
 
     @classmethod
     def setUpClass(cls):
-        # Backup original environment variable
-        address_predefined = 'DJANGO_LIVE_TEST_SERVER_ADDRESS' in os.environ
-        old_address = os.environ.get('DJANGO_LIVE_TEST_SERVER_ADDRESS')
-
         # 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
-        cls.raises_exception('localhost:8081', ImproperlyConfigured)
+        cls.raises_exception()
         TEST_SETTINGS['STATIC_URL'] = old_STATIC_URL
 
-        # Restore original environment variable
-        if address_predefined:
-            os.environ['DJANGO_LIVE_TEST_SERVER_ADDRESS'] = old_address
-        else:
-            del os.environ['DJANGO_LIVE_TEST_SERVER_ADDRESS']
-
     @classmethod
     def tearDownClass(cls):
         # skip it, as setUpClass doesn't call its parent either
         pass
 
     @classmethod
-    def raises_exception(cls, address, exception):
-        os.environ['DJANGO_LIVE_TEST_SERVER_ADDRESS'] = address
+    def raises_exception(cls):
         try:
             super(StaticLiveServerChecks, cls).setUpClass()
             raise Exception("The line above should have raised an exception")
-        except exception:
+        except ImproperlyConfigured:
+            # This raises ImproperlyConfigured("You're using the staticfiles
+            # app without having set the required STATIC_URL setting.")
             pass
         finally:
             super(StaticLiveServerChecks, cls).tearDownClass()