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

This commit is contained in:
Tim Graham 2016-06-23 12:04:05 -04:00 committed by GitHub
parent b5a1c3a6f5
commit 81cdcb66bc
9 changed files with 32 additions and 215 deletions

View File

@ -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)

View File

@ -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,
)

View File

@ -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

View File

@ -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
-------------

View File

@ -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

View File

@ -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

View File

@ -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']

View File

@ -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)

View File

@ -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()