mirror of https://github.com/django/django.git
Fixed #2879 -- Added support for the integration with Selenium and other in-browser testing frameworks. Also added the first Selenium tests for `contrib.admin`. Many thanks to everyone for their contributions and feedback: Mikeal Rogers, Dirk Datzert, mir, Simon G., Almad, Russell Keith-Magee, Denis Golomazov, devin, robertrv, andrewbadr, Idan Gazit, voidspace, Tom Christie, hjwp2, Adam Nelson, Jannis Leidel, Anssi Kääriäinen, Preston Holmes, Bruno Renié and Jacob Kaplan-Moss.
git-svn-id: http://code.djangoproject.com/svn/django/trunk@17241 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
parent
45e3dff5ac
commit
2f02a05ffb
|
@ -0,0 +1,52 @@
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from django.test import LiveServerTestCase
|
||||||
|
from django.utils.importlib import import_module
|
||||||
|
from django.utils.unittest import SkipTest
|
||||||
|
from django.utils.translation import ugettext as _
|
||||||
|
|
||||||
|
class AdminSeleniumWebDriverTestCase(LiveServerTestCase):
|
||||||
|
webdriver_class = 'selenium.webdriver.firefox.webdriver.WebDriver'
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
if sys.version_info < (2, 6):
|
||||||
|
raise SkipTest('Selenium Webdriver does not support Python < 2.6.')
|
||||||
|
try:
|
||||||
|
# Import and start the WebDriver class.
|
||||||
|
module, attr = cls.webdriver_class.rsplit('.', 1)
|
||||||
|
mod = import_module(module)
|
||||||
|
WebDriver = getattr(mod, attr)
|
||||||
|
cls.selenium = WebDriver()
|
||||||
|
except Exception:
|
||||||
|
raise SkipTest('Selenium webdriver "%s" not installed or not '
|
||||||
|
'operational.' % cls.webdriver_class)
|
||||||
|
super(AdminSeleniumWebDriverTestCase, cls).setUpClass()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def tearDownClass(cls):
|
||||||
|
super(AdminSeleniumWebDriverTestCase, cls).tearDownClass()
|
||||||
|
if hasattr(cls, 'selenium'):
|
||||||
|
cls.selenium.quit()
|
||||||
|
|
||||||
|
def admin_login(self, username, password, login_url='/admin/'):
|
||||||
|
"""
|
||||||
|
Helper function to log into the admin.
|
||||||
|
"""
|
||||||
|
self.selenium.get('%s%s' % (self.live_server_url, login_url))
|
||||||
|
username_input = self.selenium.find_element_by_name('username')
|
||||||
|
username_input.send_keys(username)
|
||||||
|
password_input = self.selenium.find_element_by_name('password')
|
||||||
|
password_input.send_keys(password)
|
||||||
|
login_text = _('Log in')
|
||||||
|
self.selenium.find_element_by_xpath(
|
||||||
|
'//input[@value="%s"]' % login_text).click()
|
||||||
|
|
||||||
|
def get_css_value(self, selector, attribute):
|
||||||
|
"""
|
||||||
|
Helper function that returns the value for the CSS attribute of an
|
||||||
|
DOM element specified by the given selector. Uses the jQuery that ships
|
||||||
|
with Django.
|
||||||
|
"""
|
||||||
|
return self.selenium.execute_script(
|
||||||
|
'return django.jQuery("%s").css("%s")' % (selector, attribute))
|
|
@ -1,20 +1,32 @@
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
from optparse import make_option, OptionParser
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.management.base import BaseCommand
|
from django.core.management.base import BaseCommand
|
||||||
from optparse import make_option, OptionParser
|
|
||||||
import sys
|
|
||||||
from django.test.utils import get_runner
|
from django.test.utils import get_runner
|
||||||
|
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
option_list = BaseCommand.option_list + (
|
option_list = BaseCommand.option_list + (
|
||||||
make_option('--noinput', action='store_false', dest='interactive', default=True,
|
make_option('--noinput',
|
||||||
|
action='store_false', dest='interactive', default=True,
|
||||||
help='Tells Django to NOT prompt the user for input of any kind.'),
|
help='Tells Django to NOT prompt the user for input of any kind.'),
|
||||||
make_option('--failfast', action='store_true', dest='failfast', default=False,
|
make_option('--failfast',
|
||||||
help='Tells Django to stop running the test suite after first failed test.'),
|
action='store_true', dest='failfast', default=False,
|
||||||
make_option('--testrunner', action='store', dest='testrunner',
|
help='Tells Django to stop running the test suite after first '
|
||||||
help='Tells Django to use specified test runner class instead of the one '+
|
'failed test.'),
|
||||||
'specified by the TEST_RUNNER setting.')
|
make_option('--testrunner',
|
||||||
|
action='store', dest='testrunner',
|
||||||
|
help='Tells Django to use specified test runner class instead of '
|
||||||
|
'the one specified by the TEST_RUNNER setting.'),
|
||||||
|
make_option('--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.'),
|
||||||
)
|
)
|
||||||
help = 'Runs the test suite for the specified applications, or the entire site if no apps are specified.'
|
help = ('Runs the test suite for the specified applications, or the '
|
||||||
|
'entire site if no apps are specified.')
|
||||||
args = '[appname ...]'
|
args = '[appname ...]'
|
||||||
|
|
||||||
requires_model_validation = False
|
requires_model_validation = False
|
||||||
|
@ -35,7 +47,8 @@ class Command(BaseCommand):
|
||||||
|
|
||||||
def create_parser(self, prog_name, subcommand):
|
def create_parser(self, prog_name, subcommand):
|
||||||
test_runner_class = get_runner(settings, self.test_runner)
|
test_runner_class = get_runner(settings, self.test_runner)
|
||||||
options = self.option_list + getattr(test_runner_class, 'option_list', ())
|
options = self.option_list + getattr(
|
||||||
|
test_runner_class, 'option_list', ())
|
||||||
return OptionParser(prog=prog_name,
|
return OptionParser(prog=prog_name,
|
||||||
usage=self.usage(subcommand),
|
usage=self.usage(subcommand),
|
||||||
version=self.get_version(),
|
version=self.get_version(),
|
||||||
|
@ -48,6 +61,10 @@ class Command(BaseCommand):
|
||||||
TestRunner = get_runner(settings, options.get('testrunner'))
|
TestRunner = get_runner(settings, options.get('testrunner'))
|
||||||
options['verbosity'] = int(options.get('verbosity'))
|
options['verbosity'] = int(options.get('verbosity'))
|
||||||
|
|
||||||
|
if options.get('liveserver') is not None:
|
||||||
|
os.environ['DJANGO_LIVE_TEST_SERVER_ADDRESS'] = options['liveserver']
|
||||||
|
del options['liveserver']
|
||||||
|
|
||||||
test_runner = TestRunner(**options)
|
test_runner = TestRunner(**options)
|
||||||
failures = test_runner.run_tests(test_labels)
|
failures = test_runner.run_tests(test_labels)
|
||||||
|
|
||||||
|
|
|
@ -4,5 +4,6 @@ Django Unit Test and Doctest framework.
|
||||||
|
|
||||||
from django.test.client import Client, RequestFactory
|
from django.test.client import Client, RequestFactory
|
||||||
from django.test.testcases import (TestCase, TransactionTestCase,
|
from django.test.testcases import (TestCase, TransactionTestCase,
|
||||||
SimpleTestCase, skipIfDBFeature, skipUnlessDBFeature)
|
SimpleTestCase, LiveServerTestCase, skipIfDBFeature,
|
||||||
|
skipUnlessDBFeature)
|
||||||
from django.test.utils import Approximate
|
from django.test.utils import Approximate
|
||||||
|
|
|
@ -1,16 +1,23 @@
|
||||||
from __future__ import with_statement
|
from __future__ import with_statement
|
||||||
|
|
||||||
|
import os
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
from urlparse import urlsplit, urlunsplit
|
from urlparse import urlsplit, urlunsplit
|
||||||
from xml.dom.minidom import parseString, Node
|
from xml.dom.minidom import parseString, Node
|
||||||
|
import select
|
||||||
|
import socket
|
||||||
|
import threading
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.contrib.staticfiles.handlers import StaticFilesHandler
|
||||||
from django.core import mail
|
from django.core import mail
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError, ImproperlyConfigured
|
||||||
|
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.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,
|
||||||
|
@ -23,6 +30,7 @@ from django.test.utils import (get_warnings_state, restore_warnings_state,
|
||||||
override_settings)
|
override_settings)
|
||||||
from django.utils import simplejson, unittest as ut2
|
from django.utils import simplejson, unittest as ut2
|
||||||
from django.utils.encoding import smart_str
|
from django.utils.encoding import smart_str
|
||||||
|
from django.views.static import serve
|
||||||
|
|
||||||
__all__ = ('DocTestRunner', 'OutputChecker', 'TestCase', 'TransactionTestCase',
|
__all__ = ('DocTestRunner', 'OutputChecker', 'TestCase', 'TransactionTestCase',
|
||||||
'SimpleTestCase', 'skipIfDBFeature', 'skipUnlessDBFeature')
|
'SimpleTestCase', 'skipIfDBFeature', 'skipUnlessDBFeature')
|
||||||
|
@ -68,7 +76,8 @@ def restore_transaction_methods():
|
||||||
class OutputChecker(doctest.OutputChecker):
|
class OutputChecker(doctest.OutputChecker):
|
||||||
def check_output(self, want, got, optionflags):
|
def check_output(self, want, got, optionflags):
|
||||||
"""
|
"""
|
||||||
The entry method for doctest output checking. Defers to a sequence of child checkers
|
The entry method for doctest output checking. Defers to a sequence of
|
||||||
|
child checkers
|
||||||
"""
|
"""
|
||||||
checks = (self.check_output_default,
|
checks = (self.check_output_default,
|
||||||
self.check_output_numeric,
|
self.check_output_numeric,
|
||||||
|
@ -219,6 +228,7 @@ class DocTestRunner(doctest.DocTestRunner):
|
||||||
for conn in connections:
|
for conn in connections:
|
||||||
transaction.rollback_unless_managed(using=conn)
|
transaction.rollback_unless_managed(using=conn)
|
||||||
|
|
||||||
|
|
||||||
class _AssertNumQueriesContext(object):
|
class _AssertNumQueriesContext(object):
|
||||||
def __init__(self, test_case, num, connection):
|
def __init__(self, test_case, num, connection):
|
||||||
self.test_case = test_case
|
self.test_case = test_case
|
||||||
|
@ -247,6 +257,7 @@ class _AssertNumQueriesContext(object):
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class SimpleTestCase(ut2.TestCase):
|
class SimpleTestCase(ut2.TestCase):
|
||||||
|
|
||||||
def save_warnings_state(self):
|
def save_warnings_state(self):
|
||||||
|
@ -335,6 +346,7 @@ class SimpleTestCase(ut2.TestCase):
|
||||||
self.assertTrue(isinstance(fieldclass(*field_args, **field_kwargs),
|
self.assertTrue(isinstance(fieldclass(*field_args, **field_kwargs),
|
||||||
fieldclass))
|
fieldclass))
|
||||||
|
|
||||||
|
|
||||||
class TransactionTestCase(SimpleTestCase):
|
class TransactionTestCase(SimpleTestCase):
|
||||||
# The class we'll use for the test client self.client.
|
# The class we'll use for the test client self.client.
|
||||||
# Can be overridden in derived classes.
|
# Can be overridden in derived classes.
|
||||||
|
@ -643,6 +655,7 @@ class TransactionTestCase(SimpleTestCase):
|
||||||
with context:
|
with context:
|
||||||
func(*args, **kwargs)
|
func(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
def connections_support_transactions():
|
def connections_support_transactions():
|
||||||
"""
|
"""
|
||||||
Returns True if all connections support transactions.
|
Returns True if all connections support transactions.
|
||||||
|
@ -650,6 +663,7 @@ def connections_support_transactions():
|
||||||
return all(conn.features.supports_transactions
|
return all(conn.features.supports_transactions
|
||||||
for conn in connections.all())
|
for conn in connections.all())
|
||||||
|
|
||||||
|
|
||||||
class TestCase(TransactionTestCase):
|
class TestCase(TransactionTestCase):
|
||||||
"""
|
"""
|
||||||
Does basically the same as TransactionTestCase, but surrounds every test
|
Does basically the same as TransactionTestCase, but surrounds every test
|
||||||
|
@ -703,6 +717,7 @@ class TestCase(TransactionTestCase):
|
||||||
transaction.rollback(using=db)
|
transaction.rollback(using=db)
|
||||||
transaction.leave_transaction_management(using=db)
|
transaction.leave_transaction_management(using=db)
|
||||||
|
|
||||||
|
|
||||||
def _deferredSkip(condition, reason):
|
def _deferredSkip(condition, reason):
|
||||||
def decorator(test_func):
|
def decorator(test_func):
|
||||||
if not (isinstance(test_func, type) and
|
if not (isinstance(test_func, type) and
|
||||||
|
@ -719,6 +734,7 @@ def _deferredSkip(condition, reason):
|
||||||
return test_item
|
return test_item
|
||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
def skipIfDBFeature(feature):
|
def skipIfDBFeature(feature):
|
||||||
"""
|
"""
|
||||||
Skip a test if a database has the named feature
|
Skip a test if a database has the named feature
|
||||||
|
@ -726,9 +742,234 @@ def skipIfDBFeature(feature):
|
||||||
return _deferredSkip(lambda: getattr(connection.features, feature),
|
return _deferredSkip(lambda: getattr(connection.features, feature),
|
||||||
"Database has feature %s" % feature)
|
"Database has feature %s" % feature)
|
||||||
|
|
||||||
|
|
||||||
def skipUnlessDBFeature(feature):
|
def skipUnlessDBFeature(feature):
|
||||||
"""
|
"""
|
||||||
Skip a test unless a database has the named feature
|
Skip a test unless a database has the named feature
|
||||||
"""
|
"""
|
||||||
return _deferredSkip(lambda: not getattr(connection.features, feature),
|
return _deferredSkip(lambda: not getattr(connection.features, feature),
|
||||||
"Database doesn't support feature %s" % feature)
|
"Database doesn't support feature %s" % feature)
|
||||||
|
|
||||||
|
|
||||||
|
class QuietWSGIRequestHandler(WSGIRequestHandler):
|
||||||
|
"""
|
||||||
|
Just a regular WSGIRequestHandler except it doesn't log to the standard
|
||||||
|
output any of the requests received, so as to not clutter the output for
|
||||||
|
the tests' results.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def log_message(*args):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class _ImprovedEvent(threading._Event):
|
||||||
|
"""
|
||||||
|
Does the same as `threading.Event` except it overrides the wait() method
|
||||||
|
with some code borrowed from Python 2.7 to return the set state of the
|
||||||
|
event (see: http://hg.python.org/cpython/rev/b5aa8aa78c0f/). This allows
|
||||||
|
to know whether the wait() method exited normally or because of the
|
||||||
|
timeout. This class can be removed when Django supports only Python >= 2.7.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def wait(self, timeout=None):
|
||||||
|
self._Event__cond.acquire()
|
||||||
|
try:
|
||||||
|
if not self._Event__flag:
|
||||||
|
self._Event__cond.wait(timeout)
|
||||||
|
return self._Event__flag
|
||||||
|
finally:
|
||||||
|
self._Event__cond.release()
|
||||||
|
|
||||||
|
|
||||||
|
class StoppableWSGIServer(WSGIServer):
|
||||||
|
"""
|
||||||
|
The code in this class is borrowed from the `SocketServer.BaseServer` class
|
||||||
|
in Python 2.6. The important functionality here is that the server is non-
|
||||||
|
blocking and that it can be shut down at any moment. This is made possible
|
||||||
|
by the server regularly polling the socket and checking if it has been
|
||||||
|
asked to stop.
|
||||||
|
Note for the future: Once Django stops supporting Python 2.6, this class
|
||||||
|
can be removed as `WSGIServer` will have this ability to shutdown on
|
||||||
|
demand and will not require the use of the _ImprovedEvent class whose code
|
||||||
|
is borrowed from Python 2.7.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(StoppableWSGIServer, self).__init__(*args, **kwargs)
|
||||||
|
self.__is_shut_down = _ImprovedEvent()
|
||||||
|
self.__serving = False
|
||||||
|
|
||||||
|
def serve_forever(self, poll_interval=0.5):
|
||||||
|
"""
|
||||||
|
Handle one request at a time until shutdown.
|
||||||
|
|
||||||
|
Polls for shutdown every poll_interval seconds.
|
||||||
|
"""
|
||||||
|
self.__serving = True
|
||||||
|
self.__is_shut_down.clear()
|
||||||
|
while self.__serving:
|
||||||
|
r, w, e = select.select([self], [], [], poll_interval)
|
||||||
|
if r:
|
||||||
|
self._handle_request_noblock()
|
||||||
|
self.__is_shut_down.set()
|
||||||
|
|
||||||
|
def shutdown(self):
|
||||||
|
"""
|
||||||
|
Stops the serve_forever loop.
|
||||||
|
|
||||||
|
Blocks until the loop has finished. This must be called while
|
||||||
|
serve_forever() is running in another thread, or it will
|
||||||
|
deadlock.
|
||||||
|
"""
|
||||||
|
self.__serving = False
|
||||||
|
if not self.__is_shut_down.wait(2):
|
||||||
|
raise RuntimeError(
|
||||||
|
"Failed to shutdown the live test server in 2 seconds. The "
|
||||||
|
"server might be stuck or generating a slow response.")
|
||||||
|
|
||||||
|
def handle_request(self):
|
||||||
|
"""Handle one request, possibly blocking.
|
||||||
|
"""
|
||||||
|
fd_sets = select.select([self], [], [], None)
|
||||||
|
if not fd_sets[0]:
|
||||||
|
return
|
||||||
|
self._handle_request_noblock()
|
||||||
|
|
||||||
|
def _handle_request_noblock(self):
|
||||||
|
"""
|
||||||
|
Handle one request, without blocking.
|
||||||
|
|
||||||
|
I assume that select.select has returned that the socket is
|
||||||
|
readable before this function was called, so there should be
|
||||||
|
no risk of blocking in get_request().
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
request, client_address = self.get_request()
|
||||||
|
except socket.error:
|
||||||
|
return
|
||||||
|
if self.verify_request(request, client_address):
|
||||||
|
try:
|
||||||
|
self.process_request(request, client_address)
|
||||||
|
except Exception:
|
||||||
|
self.handle_error(request, client_address)
|
||||||
|
self.close_request(request)
|
||||||
|
|
||||||
|
|
||||||
|
class _MediaFilesHandler(StaticFilesHandler):
|
||||||
|
"""
|
||||||
|
Handler for serving the media files. This is a private class that is
|
||||||
|
meant to be used solely as a convenience by LiveServerThread.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get_base_dir(self):
|
||||||
|
return settings.MEDIA_ROOT
|
||||||
|
|
||||||
|
def get_base_url(self):
|
||||||
|
return settings.MEDIA_URL
|
||||||
|
|
||||||
|
def serve(self, request):
|
||||||
|
return serve(request, self.file_path(request.path),
|
||||||
|
document_root=self.get_base_dir())
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
self.is_ready = threading.Event()
|
||||||
|
self.error = None
|
||||||
|
self.connections_override = connections_override
|
||||||
|
super(LiveServerThread, self).__init__()
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
"""
|
||||||
|
Sets up the live server and databases, and then loops over handling
|
||||||
|
http requests.
|
||||||
|
"""
|
||||||
|
if self.connections_override:
|
||||||
|
from django.db import connections
|
||||||
|
# Override this thread's database connections with the ones
|
||||||
|
# provided by the main thread.
|
||||||
|
for alias, conn in self.connections_override.items():
|
||||||
|
connections[alias] = conn
|
||||||
|
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)
|
||||||
|
self.httpd.set_app(handler)
|
||||||
|
self.is_ready.set()
|
||||||
|
self.httpd.serve_forever()
|
||||||
|
except Exception, e:
|
||||||
|
self.error = e
|
||||||
|
self.is_ready.set()
|
||||||
|
|
||||||
|
def join(self, timeout=None):
|
||||||
|
if hasattr(self, 'httpd'):
|
||||||
|
# Stop the WSGI server
|
||||||
|
self.httpd.shutdown()
|
||||||
|
self.httpd.server_close()
|
||||||
|
super(LiveServerThread, self).join(timeout)
|
||||||
|
|
||||||
|
|
||||||
|
class LiveServerTestCase(TransactionTestCase):
|
||||||
|
"""
|
||||||
|
Does basically the same as TransactionTestCase but also launches a live
|
||||||
|
http server in a separate thread so that the tests may use another testing
|
||||||
|
framework, such as Selenium for example, instead of the built-in dummy
|
||||||
|
client.
|
||||||
|
Note that it inherits from TransactionTestCase instead of TestCase because
|
||||||
|
the threads do not share the same transactions (unless if using in-memory
|
||||||
|
sqlite) and each thread needs to commit all their transactions so that the
|
||||||
|
other thread can see the changes.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def live_server_url(self):
|
||||||
|
return 'http://%s' % self.__test_server_address
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
connections_override = {}
|
||||||
|
for conn in connections.all():
|
||||||
|
# If using in-memory sqlite databases, pass the connections to
|
||||||
|
# the server thread.
|
||||||
|
if (conn.settings_dict['ENGINE'] == 'django.db.backends.sqlite3'
|
||||||
|
and conn.settings_dict['NAME'] == ':memory:'):
|
||||||
|
# Explicitly enable thread-shareability for this connection
|
||||||
|
conn.allow_thread_sharing = True
|
||||||
|
connections_override[conn.alias] = conn
|
||||||
|
|
||||||
|
# Launch the live server's thread
|
||||||
|
cls.__test_server_address = os.environ.get(
|
||||||
|
'DJANGO_LIVE_TEST_SERVER_ADDRESS', 'localhost:8081')
|
||||||
|
try:
|
||||||
|
host, port = cls.__test_server_address.split(':')
|
||||||
|
except Exception:
|
||||||
|
raise ImproperlyConfigured('Invalid address ("%s") for live '
|
||||||
|
'server.' % cls.__test_server_address)
|
||||||
|
cls.server_thread = LiveServerThread(
|
||||||
|
host, int(port), connections_override)
|
||||||
|
cls.server_thread.daemon = True
|
||||||
|
cls.server_thread.start()
|
||||||
|
|
||||||
|
# Wait for the live server to be ready
|
||||||
|
cls.server_thread.is_ready.wait()
|
||||||
|
if cls.server_thread.error:
|
||||||
|
raise cls.server_thread.error
|
||||||
|
|
||||||
|
super(LiveServerTestCase, cls).setUpClass()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def tearDownClass(cls):
|
||||||
|
# There may not be a 'server_thread' attribute if setUpClass() for some
|
||||||
|
# reasons has raised an exception.
|
||||||
|
if hasattr(cls, 'server_thread'):
|
||||||
|
# Terminate the live server's thread
|
||||||
|
cls.server_thread.join()
|
||||||
|
super(LiveServerTestCase, cls).tearDownClass()
|
||||||
|
|
|
@ -122,6 +122,19 @@ Going beyond that, you can specify an individual test method like this:
|
||||||
|
|
||||||
./runtests.py --settings=path.to.settings i18n.TranslationTests.test_lazy_objects
|
./runtests.py --settings=path.to.settings i18n.TranslationTests.test_lazy_objects
|
||||||
|
|
||||||
|
Running the Selenium tests
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Some admin tests require Selenium 2, Firefox and Python >= 2.6 to work via a
|
||||||
|
real Web browser. To allow those tests to run and not be skipped, you must
|
||||||
|
install the selenium_ package (version > 2.13) into your Python path.
|
||||||
|
|
||||||
|
Then, run the tests normally, for example:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
./runtests.py --settings=test_sqlite admin_inlines
|
||||||
|
|
||||||
Running all the tests
|
Running all the tests
|
||||||
~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
@ -135,6 +148,7 @@ dependencies:
|
||||||
* setuptools_
|
* setuptools_
|
||||||
* memcached_, plus a :ref:`supported Python binding <memcached>`
|
* memcached_, plus a :ref:`supported Python binding <memcached>`
|
||||||
* gettext_ (:ref:`gettext_on_windows`)
|
* gettext_ (:ref:`gettext_on_windows`)
|
||||||
|
* selenium_ (if also using Python >= 2.6)
|
||||||
|
|
||||||
If you want to test the memcached cache backend, you'll also need to define
|
If you want to test the memcached cache backend, you'll also need to define
|
||||||
a :setting:`CACHES` setting that points at your memcached instance.
|
a :setting:`CACHES` setting that points at your memcached instance.
|
||||||
|
@ -149,6 +163,7 @@ associated tests will be skipped.
|
||||||
.. _setuptools: http://pypi.python.org/pypi/setuptools/
|
.. _setuptools: http://pypi.python.org/pypi/setuptools/
|
||||||
.. _memcached: http://www.danga.com/memcached/
|
.. _memcached: http://www.danga.com/memcached/
|
||||||
.. _gettext: http://www.gnu.org/software/gettext/manual/gettext.html
|
.. _gettext: http://www.gnu.org/software/gettext/manual/gettext.html
|
||||||
|
.. _selenium: http://pypi.python.org/pypi/selenium
|
||||||
|
|
||||||
.. _contrib-apps:
|
.. _contrib-apps:
|
||||||
|
|
||||||
|
|
|
@ -976,15 +976,22 @@ information.
|
||||||
.. versionadded:: 1.2
|
.. versionadded:: 1.2
|
||||||
.. django-admin-option:: --failfast
|
.. django-admin-option:: --failfast
|
||||||
|
|
||||||
Use the :djadminopt:`--failfast` option to stop running tests and report the failure
|
The ``--failfast`` option can be used to stop running tests and report the
|
||||||
immediately after a test fails.
|
failure immediately after a test fails.
|
||||||
|
|
||||||
.. versionadded:: 1.4
|
.. versionadded:: 1.4
|
||||||
.. django-admin-option:: --testrunner
|
.. django-admin-option:: --testrunner
|
||||||
|
|
||||||
The :djadminopt:`--testrunner` option can be used to control the test runner
|
The ``--testrunner`` option can be used to control the test runner class that
|
||||||
class that is used to execute tests. If this value is provided, it overrides
|
is used to execute tests. If this value is provided, it overrides the value
|
||||||
the value provided by the :setting:`TEST_RUNNER` setting.
|
provided by the :setting:`TEST_RUNNER` setting.
|
||||||
|
|
||||||
|
.. versionadded:: 1.4
|
||||||
|
.. django-admin-option:: --liveserver
|
||||||
|
|
||||||
|
The ``--liveserver`` option can be used to override the default address where
|
||||||
|
the live server (used with :class:`~django.test.LiveServerTestCase`) is
|
||||||
|
expected to run from. The default value is ``localhost:8081``.
|
||||||
|
|
||||||
testserver <fixture fixture ...>
|
testserver <fixture fixture ...>
|
||||||
--------------------------------
|
--------------------------------
|
||||||
|
|
|
@ -40,6 +40,19 @@ before the release of Django 1.4.
|
||||||
What's new in Django 1.4
|
What's new in Django 1.4
|
||||||
========================
|
========================
|
||||||
|
|
||||||
|
Support for in-browser testing frameworks
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Django 1.4 now supports the integration with in-browser testing frameworks such
|
||||||
|
as Selenium_ or Windmill_ thanks to the :class:`django.test.LiveServerTestCase`
|
||||||
|
base class, allowing you to test the interactions between your site's front and
|
||||||
|
back ends more comprehensively. See the
|
||||||
|
:class:`documentation<django.test.LiveServerTestCase>` for more details and
|
||||||
|
concrete examples.
|
||||||
|
|
||||||
|
.. _Windmill: http://www.getwindmill.com/
|
||||||
|
.. _Selenium: http://seleniumhq.org/
|
||||||
|
|
||||||
``SELECT FOR UPDATE`` support
|
``SELECT FOR UPDATE`` support
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
|
|
@ -581,21 +581,20 @@ Some of the things you can do with the test client are:
|
||||||
* Test that a given request is rendered by a given Django template, with
|
* Test that a given request is rendered by a given Django template, with
|
||||||
a template context that contains certain values.
|
a template context that contains certain values.
|
||||||
|
|
||||||
Note that the test client is not intended to be a replacement for Twill_,
|
Note that the test client is not intended to be a replacement for Windmill_,
|
||||||
Selenium_, or other "in-browser" frameworks. Django's test client has
|
Selenium_, or other "in-browser" frameworks. Django's test client has
|
||||||
a different focus. In short:
|
a different focus. In short:
|
||||||
|
|
||||||
* Use Django's test client to establish that the correct view is being
|
* Use Django's test client to establish that the correct view is being
|
||||||
called and that the view is collecting the correct context data.
|
called and that the view is collecting the correct context data.
|
||||||
|
|
||||||
* Use in-browser frameworks such as Twill and Selenium to test *rendered*
|
* Use in-browser frameworks such as Windmill_ and Selenium_ to test *rendered*
|
||||||
HTML and the *behavior* of Web pages, namely JavaScript functionality.
|
HTML and the *behavior* of Web pages, namely JavaScript functionality. Django
|
||||||
|
also provides special support for those frameworks; see the section on
|
||||||
|
:class:`~django.test.LiveServerTestCase` for more details.
|
||||||
|
|
||||||
A comprehensive test suite should use a combination of both test types.
|
A comprehensive test suite should use a combination of both test types.
|
||||||
|
|
||||||
.. _Twill: http://twill.idyll.org/
|
|
||||||
.. _Selenium: http://seleniumhq.org/
|
|
||||||
|
|
||||||
Overview and a quick example
|
Overview and a quick example
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
@ -1753,6 +1752,97 @@ under MySQL with MyISAM tables)::
|
||||||
def test_transaction_behavior(self):
|
def test_transaction_behavior(self):
|
||||||
# ... conditional test code
|
# ... conditional test code
|
||||||
|
|
||||||
|
Live test server
|
||||||
|
----------------
|
||||||
|
|
||||||
|
.. versionadded:: 1.4
|
||||||
|
|
||||||
|
.. currentmodule:: django.test
|
||||||
|
|
||||||
|
.. class:: LiveServerTestCase()
|
||||||
|
|
||||||
|
``LiveServerTestCase`` does basically the same as
|
||||||
|
:class:`~django.test.TransactionTestCase` with one extra feature: it launches a
|
||||||
|
live Django server in the background on setup, and shuts it down on teardown.
|
||||||
|
This allows the use of automated test clients other than the
|
||||||
|
:ref:`Django dummy client <test-client>` such as, for example, the Selenium_ or
|
||||||
|
Windmill_ clients, to execute a series of functional tests inside a browser and
|
||||||
|
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:
|
||||||
|
|
||||||
|
.. 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.
|
||||||
|
|
||||||
|
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
|
||||||
|
Python path:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
pip install selenium
|
||||||
|
|
||||||
|
Then, add a ``LiveServerTestCase``-based test to your app's tests module
|
||||||
|
(for example: ``myapp/tests.py``). The code for this test may look as follows:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from django.test import LiveServerTestCase
|
||||||
|
from selenium.webdriver.firefox.webdriver import WebDriver
|
||||||
|
|
||||||
|
class MySeleniumTests(LiveServerTestCase):
|
||||||
|
fixtures = ['user-data.json']
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
cls.selenium = WebDriver()
|
||||||
|
super(MySeleniumTests, cls).setUpClass()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def tearDownClass(cls):
|
||||||
|
super(MySeleniumTests, cls).tearDownClass()
|
||||||
|
cls.selenium.quit()
|
||||||
|
|
||||||
|
def test_login(self):
|
||||||
|
self.selenium.get('%s%s' % (self.live_server_url, '/login/'))
|
||||||
|
username_input = self.selenium.find_element_by_name("username")
|
||||||
|
username_input.send_keys('myuser')
|
||||||
|
password_input = self.selenium.find_element_by_name("password")
|
||||||
|
password_input.send_keys('secret')
|
||||||
|
self.selenium.find_element_by_xpath('//input[@value="Log in"]').click()
|
||||||
|
|
||||||
|
Finally, you may run the test as follows:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
./manage.py test myapp.MySeleniumTests.test_login
|
||||||
|
|
||||||
|
This example will automatically open Firefox then go to the login page, enter
|
||||||
|
the credentials and press the "Log in" button. Selenium offers other drivers in
|
||||||
|
case you do not have Firefox installed or wish to use another browser. The
|
||||||
|
example above is just a tiny fraction of what the Selenium client can do; check
|
||||||
|
out the `full reference`_ for more details.
|
||||||
|
|
||||||
|
.. _Windmill: http://www.getwindmill.com/
|
||||||
|
.. _Selenium: http://seleniumhq.org/
|
||||||
|
.. _selenium package: http://pypi.python.org/pypi/selenium
|
||||||
|
.. _full reference: http://readthedocs.org/docs/selenium-python/en/latest/api.html
|
||||||
|
.. _Firefox: http://www.mozilla.com/firefox/
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
``LiveServerTestCase`` makes use of the :doc:`staticfiles contrib app
|
||||||
|
</howto/static-files>` so you'll need to have your project configured
|
||||||
|
accordingly (in particular by setting :setting:`STATIC_URL`).
|
||||||
|
|
||||||
|
|
||||||
Using different testing frameworks
|
Using different testing frameworks
|
||||||
==================================
|
==================================
|
||||||
|
@ -1833,11 +1923,9 @@ set up, execute and tear down the test suite.
|
||||||
those options will be added to the list of command-line options that
|
those options will be added to the list of command-line options that
|
||||||
the :djadmin:`test` command can use.
|
the :djadmin:`test` command can use.
|
||||||
|
|
||||||
|
|
||||||
Attributes
|
Attributes
|
||||||
~~~~~~~~~~
|
~~~~~~~~~~
|
||||||
|
|
||||||
|
|
||||||
.. attribute:: DjangoTestSuiteRunner.option_list
|
.. attribute:: DjangoTestSuiteRunner.option_list
|
||||||
|
|
||||||
.. versionadded:: 1.4
|
.. versionadded:: 1.4
|
||||||
|
|
|
@ -109,6 +109,10 @@ class SottoCapoInline(admin.TabularInline):
|
||||||
model = SottoCapo
|
model = SottoCapo
|
||||||
|
|
||||||
|
|
||||||
|
class ProfileInline(admin.TabularInline):
|
||||||
|
model = Profile
|
||||||
|
extra = 1
|
||||||
|
|
||||||
site.register(TitleCollection, inlines=[TitleInline])
|
site.register(TitleCollection, inlines=[TitleInline])
|
||||||
# Test bug #12561 and #12778
|
# Test bug #12561 and #12778
|
||||||
# only ModelAdmin media
|
# only ModelAdmin media
|
||||||
|
@ -124,3 +128,4 @@ site.register(Fashionista, inlines=[InlineWeakness])
|
||||||
site.register(Holder4, Holder4Admin)
|
site.register(Holder4, Holder4Admin)
|
||||||
site.register(Author, AuthorAdmin)
|
site.register(Author, AuthorAdmin)
|
||||||
site.register(CapoFamiglia, inlines=[ConsigliereInline, SottoCapoInline])
|
site.register(CapoFamiglia, inlines=[ConsigliereInline, SottoCapoInline])
|
||||||
|
site.register(ProfileCollection, inlines=[ProfileInline])
|
|
@ -136,3 +136,13 @@ class Consigliere(models.Model):
|
||||||
class SottoCapo(models.Model):
|
class SottoCapo(models.Model):
|
||||||
name = models.CharField(max_length=100)
|
name = models.CharField(max_length=100)
|
||||||
capo_famiglia = models.ForeignKey(CapoFamiglia, related_name='+')
|
capo_famiglia = models.ForeignKey(CapoFamiglia, related_name='+')
|
||||||
|
|
||||||
|
# Other models
|
||||||
|
|
||||||
|
class ProfileCollection(models.Model):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class Profile(models.Model):
|
||||||
|
collection = models.ForeignKey(ProfileCollection, blank=True, null=True)
|
||||||
|
first_name = models.CharField(max_length=100)
|
||||||
|
last_name = models.CharField(max_length=100)
|
|
@ -1,5 +1,6 @@
|
||||||
from __future__ import absolute_import
|
from __future__ import absolute_import
|
||||||
|
|
||||||
|
from django.contrib.admin.tests import AdminSeleniumWebDriverTestCase
|
||||||
from django.contrib.admin.helpers import InlineAdminForm
|
from django.contrib.admin.helpers import InlineAdminForm
|
||||||
from django.contrib.auth.models import User, Permission
|
from django.contrib.auth.models import User, Permission
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
@ -8,7 +9,8 @@ from django.test import TestCase
|
||||||
# local test models
|
# local test models
|
||||||
from .admin import InnerInline
|
from .admin import InnerInline
|
||||||
from .models import (Holder, Inner, Holder2, Inner2, Holder3, Inner3, Person,
|
from .models import (Holder, Inner, Holder2, Inner2, Holder3, Inner3, Person,
|
||||||
OutfitItem, Fashionista, Teacher, Parent, Child, Author, Book)
|
OutfitItem, Fashionista, Teacher, Parent, Child, Author, Book, Profile,
|
||||||
|
ProfileCollection)
|
||||||
|
|
||||||
|
|
||||||
class TestInline(TestCase):
|
class TestInline(TestCase):
|
||||||
|
@ -380,3 +382,105 @@ class TestInlinePermissions(TestCase):
|
||||||
self.assertContains(response, 'value="4" id="id_inner2_set-TOTAL_FORMS"')
|
self.assertContains(response, 'value="4" id="id_inner2_set-TOTAL_FORMS"')
|
||||||
self.assertContains(response, '<input type="hidden" name="inner2_set-0-id" value="%i"' % self.inner2_id)
|
self.assertContains(response, '<input type="hidden" name="inner2_set-0-id" value="%i"' % self.inner2_id)
|
||||||
self.assertContains(response, 'id="id_inner2_set-0-DELETE"')
|
self.assertContains(response, 'id="id_inner2_set-0-DELETE"')
|
||||||
|
|
||||||
|
class SeleniumFirefoxTests(AdminSeleniumWebDriverTestCase):
|
||||||
|
webdriver_class = 'selenium.webdriver.firefox.webdriver.WebDriver'
|
||||||
|
fixtures = ['admin-views-users.xml']
|
||||||
|
urls = "regressiontests.admin_inlines.urls"
|
||||||
|
|
||||||
|
def test_add_inlines(self):
|
||||||
|
"""
|
||||||
|
Ensure that the "Add another XXX" link correctly adds items to the
|
||||||
|
inline form.
|
||||||
|
"""
|
||||||
|
self.admin_login(username='super', password='secret')
|
||||||
|
self.selenium.get('%s%s' % (self.live_server_url,
|
||||||
|
'/admin/admin_inlines/profilecollection/add/'))
|
||||||
|
|
||||||
|
# Check that there's only one inline to start with and that it has the
|
||||||
|
# correct ID.
|
||||||
|
self.failUnlessEqual(len(self.selenium.find_elements_by_css_selector(
|
||||||
|
'#profile_set-group table tr.dynamic-profile_set')), 1)
|
||||||
|
self.failUnlessEqual(self.selenium.find_element_by_css_selector(
|
||||||
|
'.dynamic-profile_set:nth-of-type(1)').get_attribute('id'),
|
||||||
|
'profile_set-0')
|
||||||
|
self.failUnlessEqual(len(self.selenium.find_elements_by_css_selector(
|
||||||
|
'form#profilecollection_form tr.dynamic-profile_set#profile_set-0 input[name=profile_set-0-first_name]')), 1)
|
||||||
|
self.failUnlessEqual(len(self.selenium.find_elements_by_css_selector(
|
||||||
|
'form#profilecollection_form tr.dynamic-profile_set#profile_set-0 input[name=profile_set-0-last_name]')), 1)
|
||||||
|
|
||||||
|
# Add an inline
|
||||||
|
self.selenium.find_element_by_link_text('Add another Profile').click()
|
||||||
|
|
||||||
|
# Check that the inline has been added, that it has the right id, and
|
||||||
|
# that it contains the right fields.
|
||||||
|
self.failUnlessEqual(len(self.selenium.find_elements_by_css_selector(
|
||||||
|
'#profile_set-group table tr.dynamic-profile_set')), 2)
|
||||||
|
self.failUnlessEqual(self.selenium.find_element_by_css_selector(
|
||||||
|
'.dynamic-profile_set:nth-of-type(2)').get_attribute('id'), 'profile_set-1')
|
||||||
|
self.failUnlessEqual(len(self.selenium.find_elements_by_css_selector(
|
||||||
|
'form#profilecollection_form tr.dynamic-profile_set#profile_set-1 input[name=profile_set-1-first_name]')), 1)
|
||||||
|
self.failUnlessEqual(len(self.selenium.find_elements_by_css_selector(
|
||||||
|
'form#profilecollection_form tr.dynamic-profile_set#profile_set-1 input[name=profile_set-1-last_name]')), 1)
|
||||||
|
|
||||||
|
# Let's add another one to be sure
|
||||||
|
self.selenium.find_element_by_link_text('Add another Profile').click()
|
||||||
|
self.failUnlessEqual(len(self.selenium.find_elements_by_css_selector(
|
||||||
|
'#profile_set-group table tr.dynamic-profile_set')), 3)
|
||||||
|
self.failUnlessEqual(self.selenium.find_element_by_css_selector(
|
||||||
|
'.dynamic-profile_set:nth-of-type(3)').get_attribute('id'), 'profile_set-2')
|
||||||
|
self.failUnlessEqual(len(self.selenium.find_elements_by_css_selector(
|
||||||
|
'form#profilecollection_form tr.dynamic-profile_set#profile_set-2 input[name=profile_set-2-first_name]')), 1)
|
||||||
|
self.failUnlessEqual(len(self.selenium.find_elements_by_css_selector(
|
||||||
|
'form#profilecollection_form tr.dynamic-profile_set#profile_set-2 input[name=profile_set-2-last_name]')), 1)
|
||||||
|
|
||||||
|
# Enter some data and click 'Save'
|
||||||
|
self.selenium.find_element_by_name('profile_set-0-first_name').send_keys('0 first name 1')
|
||||||
|
self.selenium.find_element_by_name('profile_set-0-last_name').send_keys('0 last name 2')
|
||||||
|
self.selenium.find_element_by_name('profile_set-1-first_name').send_keys('1 first name 1')
|
||||||
|
self.selenium.find_element_by_name('profile_set-1-last_name').send_keys('1 last name 2')
|
||||||
|
self.selenium.find_element_by_name('profile_set-2-first_name').send_keys('2 first name 1')
|
||||||
|
self.selenium.find_element_by_name('profile_set-2-last_name').send_keys('2 last name 2')
|
||||||
|
self.selenium.find_element_by_xpath('//input[@value="Save"]').click()
|
||||||
|
|
||||||
|
# Check that the objects have been created in the database
|
||||||
|
self.assertEqual(ProfileCollection.objects.all().count(), 1)
|
||||||
|
self.assertEqual(Profile.objects.all().count(), 3)
|
||||||
|
|
||||||
|
def test_delete_inlines(self):
|
||||||
|
self.admin_login(username='super', password='secret')
|
||||||
|
self.selenium.get('%s%s' % (self.live_server_url,
|
||||||
|
'/admin/admin_inlines/profilecollection/add/'))
|
||||||
|
|
||||||
|
# Add a few inlines
|
||||||
|
self.selenium.find_element_by_link_text('Add another Profile').click()
|
||||||
|
self.selenium.find_element_by_link_text('Add another Profile').click()
|
||||||
|
self.selenium.find_element_by_link_text('Add another Profile').click()
|
||||||
|
self.selenium.find_element_by_link_text('Add another Profile').click()
|
||||||
|
self.failUnlessEqual(len(self.selenium.find_elements_by_css_selector(
|
||||||
|
'#profile_set-group table tr.dynamic-profile_set')), 5)
|
||||||
|
self.failUnlessEqual(len(self.selenium.find_elements_by_css_selector(
|
||||||
|
'form#profilecollection_form tr.dynamic-profile_set#profile_set-0')), 1)
|
||||||
|
self.failUnlessEqual(len(self.selenium.find_elements_by_css_selector(
|
||||||
|
'form#profilecollection_form tr.dynamic-profile_set#profile_set-1')), 1)
|
||||||
|
self.failUnlessEqual(len(self.selenium.find_elements_by_css_selector(
|
||||||
|
'form#profilecollection_form tr.dynamic-profile_set#profile_set-2')), 1)
|
||||||
|
self.failUnlessEqual(len(self.selenium.find_elements_by_css_selector(
|
||||||
|
'form#profilecollection_form tr.dynamic-profile_set#profile_set-3')), 1)
|
||||||
|
self.failUnlessEqual(len(self.selenium.find_elements_by_css_selector(
|
||||||
|
'form#profilecollection_form tr.dynamic-profile_set#profile_set-4')), 1)
|
||||||
|
|
||||||
|
# Click on a few delete buttons
|
||||||
|
self.selenium.find_element_by_css_selector(
|
||||||
|
'form#profilecollection_form tr.dynamic-profile_set#profile_set-1 td.delete a').click()
|
||||||
|
self.selenium.find_element_by_css_selector(
|
||||||
|
'form#profilecollection_form tr.dynamic-profile_set#profile_set-2 td.delete a').click()
|
||||||
|
# Verify that they're gone and that the IDs have been re-sequenced
|
||||||
|
self.failUnlessEqual(len(self.selenium.find_elements_by_css_selector(
|
||||||
|
'#profile_set-group table tr.dynamic-profile_set')), 3)
|
||||||
|
self.failUnlessEqual(len(self.selenium.find_elements_by_css_selector(
|
||||||
|
'form#profilecollection_form tr.dynamic-profile_set#profile_set-0')), 1)
|
||||||
|
self.failUnlessEqual(len(self.selenium.find_elements_by_css_selector(
|
||||||
|
'form#profilecollection_form tr.dynamic-profile_set#profile_set-1')), 1)
|
||||||
|
self.failUnlessEqual(len(self.selenium.find_elements_by_css_selector(
|
||||||
|
'form#profilecollection_form tr.dynamic-profile_set#profile_set-2')), 1)
|
||||||
|
|
|
@ -13,6 +13,7 @@ import re
|
||||||
|
|
||||||
from django import conf, bin, get_version
|
from django import conf, bin, get_version
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.test.simple import DjangoTestSuiteRunner
|
||||||
from django.utils import unittest
|
from django.utils import unittest
|
||||||
|
|
||||||
|
|
||||||
|
@ -1058,6 +1059,50 @@ class ManageValidate(AdminScriptTestCase):
|
||||||
self.assertOutput(out, '0 errors found')
|
self.assertOutput(out, '0 errors found')
|
||||||
|
|
||||||
|
|
||||||
|
class CustomTestRunner(DjangoTestSuiteRunner):
|
||||||
|
|
||||||
|
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 setUp(self):
|
||||||
|
from django.core.management.commands.test import Command as TestCommand
|
||||||
|
self.cmd = TestCommand()
|
||||||
|
|
||||||
|
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')
|
||||||
|
|
||||||
|
self.cmd.handle(verbosity=0, testrunner='regressiontests.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)
|
||||||
|
|
||||||
|
self.cmd.handle(verbosity=0, testrunner='regressiontests.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):
|
class ManageRunserver(AdminScriptTestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
from django.core.management.commands.runserver import BaseRunserverCommand
|
from django.core.management.commands.runserver import BaseRunserverCommand
|
||||||
|
|
|
@ -7,6 +7,7 @@ from django import forms
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.contrib.admin import widgets
|
from django.contrib.admin import widgets
|
||||||
|
from django.contrib.admin.tests import AdminSeleniumWebDriverTestCase
|
||||||
from django.core.files.storage import default_storage
|
from django.core.files.storage import default_storage
|
||||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||||
from django.db.models import DateField
|
from django.db.models import DateField
|
||||||
|
@ -407,3 +408,52 @@ class RelatedFieldWidgetWrapperTests(DjangoTestCase):
|
||||||
# Used to fail with a name error.
|
# Used to fail with a name error.
|
||||||
w = widgets.RelatedFieldWidgetWrapper(w, rel, widget_admin_site)
|
w = widgets.RelatedFieldWidgetWrapper(w, rel, widget_admin_site)
|
||||||
self.assertFalse(w.can_add_related)
|
self.assertFalse(w.can_add_related)
|
||||||
|
|
||||||
|
|
||||||
|
class SeleniumFirefoxTests(AdminSeleniumWebDriverTestCase):
|
||||||
|
webdriver_class = 'selenium.webdriver.firefox.webdriver.WebDriver'
|
||||||
|
fixtures = ['admin-widgets-users.xml']
|
||||||
|
urls = "regressiontests.admin_widgets.urls"
|
||||||
|
|
||||||
|
def test_show_hide_date_time_picker_widgets(self):
|
||||||
|
"""
|
||||||
|
Ensure that pressing the ESC key closes the date and time picker
|
||||||
|
widgets.
|
||||||
|
Refs #17064.
|
||||||
|
"""
|
||||||
|
from selenium.webdriver.common.keys import Keys
|
||||||
|
|
||||||
|
self.admin_login(username='super', password='secret', login_url='/')
|
||||||
|
# Open a page that has a date and time picker widgets
|
||||||
|
self.selenium.get('%s%s' % (self.live_server_url,
|
||||||
|
'/admin_widgets/member/add/'))
|
||||||
|
|
||||||
|
# First, with the date picker widget ---------------------------------
|
||||||
|
# Check that the date picker is hidden
|
||||||
|
self.assertEqual(
|
||||||
|
self.get_css_value('#calendarbox0', 'display'), 'none')
|
||||||
|
# Click the calendar icon
|
||||||
|
self.selenium.find_element_by_id('calendarlink0').click()
|
||||||
|
# Check that the date picker is visible
|
||||||
|
self.assertEqual(
|
||||||
|
self.get_css_value('#calendarbox0', 'display'), 'block')
|
||||||
|
# Press the ESC key
|
||||||
|
self.selenium.find_element_by_tag_name('html').send_keys([Keys.ESCAPE])
|
||||||
|
# Check that the date picker is hidden again
|
||||||
|
self.assertEqual(
|
||||||
|
self.get_css_value('#calendarbox0', 'display'), 'none')
|
||||||
|
|
||||||
|
# Then, with the time picker widget ----------------------------------
|
||||||
|
# Check that the time picker is hidden
|
||||||
|
self.assertEqual(
|
||||||
|
self.get_css_value('#clockbox0', 'display'), 'none')
|
||||||
|
# Click the time icon
|
||||||
|
self.selenium.find_element_by_id('clocklink0').click()
|
||||||
|
# Check that the time picker is visible
|
||||||
|
self.assertEqual(
|
||||||
|
self.get_css_value('#clockbox0', 'display'), 'block')
|
||||||
|
# Press the ESC key
|
||||||
|
self.selenium.find_element_by_tag_name('html').send_keys([Keys.ESCAPE])
|
||||||
|
# Check that the time picker is hidden again
|
||||||
|
self.assertEqual(
|
||||||
|
self.get_css_value('#clockbox0', 'display'), 'none')
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"pk": 1,
|
||||||
|
"model": "servers.person",
|
||||||
|
"fields": {
|
||||||
|
"name": "jane"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pk": 2,
|
||||||
|
"model": "servers.person",
|
||||||
|
"fields": {
|
||||||
|
"name": "robert"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
|
@ -0,0 +1 @@
|
||||||
|
example media file
|
|
@ -0,0 +1,5 @@
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
|
||||||
|
class Person(models.Model):
|
||||||
|
name = models.CharField(max_length=256)
|
|
@ -0,0 +1 @@
|
||||||
|
example static file
|
|
@ -3,13 +3,17 @@ Tests for django.core.servers.
|
||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
from urlparse import urljoin
|
from urlparse import urljoin
|
||||||
|
import urllib2
|
||||||
|
|
||||||
import django
|
import django
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.test import TestCase
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
|
from django.test import TestCase, LiveServerTestCase
|
||||||
from django.core.handlers.wsgi import WSGIHandler
|
from django.core.handlers.wsgi import WSGIHandler
|
||||||
from django.core.servers.basehttp import AdminMediaHandler
|
from django.core.servers.basehttp import AdminMediaHandler, WSGIServerException
|
||||||
|
from django.test.utils import override_settings
|
||||||
|
|
||||||
|
from .models import Person
|
||||||
|
|
||||||
class AdminMediaHandlerTests(TestCase):
|
class AdminMediaHandlerTests(TestCase):
|
||||||
|
|
||||||
|
@ -68,3 +72,146 @@ class AdminMediaHandlerTests(TestCase):
|
||||||
continue
|
continue
|
||||||
self.fail('URL: %s should have caused a ValueError exception.'
|
self.fail('URL: %s should have caused a ValueError exception.'
|
||||||
% url)
|
% url)
|
||||||
|
|
||||||
|
|
||||||
|
TEST_ROOT = os.path.dirname(__file__)
|
||||||
|
TEST_SETTINGS = {
|
||||||
|
'MEDIA_URL': '/media/',
|
||||||
|
'MEDIA_ROOT': os.path.join(TEST_ROOT, 'media'),
|
||||||
|
'STATIC_URL': '/static/',
|
||||||
|
'STATIC_ROOT': os.path.join(TEST_ROOT, 'static'),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class LiveServerBase(LiveServerTestCase):
|
||||||
|
urls = 'regressiontests.servers.urls'
|
||||||
|
fixtures = ['testdata.json']
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
# Override settings
|
||||||
|
cls.settings_override = override_settings(**TEST_SETTINGS)
|
||||||
|
cls.settings_override.enable()
|
||||||
|
super(LiveServerBase, cls).setUpClass()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def tearDownClass(cls):
|
||||||
|
# Restore original settings
|
||||||
|
cls.settings_override.disable()
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
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
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
# 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
|
||||||
|
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']
|
||||||
|
|
||||||
|
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.
|
||||||
|
pass
|
||||||
|
|
||||||
|
class LiveServerViews(LiveServerBase):
|
||||||
|
def test_404(self):
|
||||||
|
"""
|
||||||
|
Ensure that the LiveServerTestCase serves 404s.
|
||||||
|
Refs #2879.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
self.urlopen('/')
|
||||||
|
except urllib2.HTTPError, err:
|
||||||
|
self.assertEquals(err.code, 404, 'Expected 404 response')
|
||||||
|
else:
|
||||||
|
self.fail('Expected 404 response')
|
||||||
|
|
||||||
|
def test_view(self):
|
||||||
|
"""
|
||||||
|
Ensure that the LiveServerTestCase serves views.
|
||||||
|
Refs #2879.
|
||||||
|
"""
|
||||||
|
f = self.urlopen('/example_view/')
|
||||||
|
self.assertEquals(f.read(), 'example view')
|
||||||
|
|
||||||
|
def test_static_files(self):
|
||||||
|
"""
|
||||||
|
Ensure that the LiveServerTestCase serves static files.
|
||||||
|
Refs #2879.
|
||||||
|
"""
|
||||||
|
f = self.urlopen('/static/example_static_file.txt')
|
||||||
|
self.assertEquals(f.read(), 'example static file\n')
|
||||||
|
|
||||||
|
def test_media_files(self):
|
||||||
|
"""
|
||||||
|
Ensure that the LiveServerTestCase serves media files.
|
||||||
|
Refs #2879.
|
||||||
|
"""
|
||||||
|
f = self.urlopen('/media/example_media_file.txt')
|
||||||
|
self.assertEquals(f.read(), 'example media file\n')
|
||||||
|
|
||||||
|
|
||||||
|
class LiveServerDatabase(LiveServerBase):
|
||||||
|
|
||||||
|
def test_fixtures_loaded(self):
|
||||||
|
"""
|
||||||
|
Ensure that fixtures are properly loaded and visible to the
|
||||||
|
live server thread.
|
||||||
|
Refs #2879.
|
||||||
|
"""
|
||||||
|
f = self.urlopen('/model_view/')
|
||||||
|
self.assertEquals(f.read().splitlines(), ['jane', 'robert'])
|
||||||
|
|
||||||
|
def test_database_writes(self):
|
||||||
|
"""
|
||||||
|
Ensure that data written to the database by a view can be read.
|
||||||
|
Refs #2879.
|
||||||
|
"""
|
||||||
|
self.urlopen('/create_model_instance/')
|
||||||
|
names = [person.name for person in Person.objects.all()]
|
||||||
|
self.assertEquals(names, ['jane', 'robert', 'emily'])
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
from __future__ import absolute_import
|
||||||
|
|
||||||
|
from django.conf.urls import patterns, url
|
||||||
|
|
||||||
|
from . import views
|
||||||
|
|
||||||
|
|
||||||
|
urlpatterns = patterns('',
|
||||||
|
url(r'^example_view/$', views.example_view),
|
||||||
|
url(r'^model_view/$', views.model_view),
|
||||||
|
url(r'^create_model_instance/$', views.create_model_instance),
|
||||||
|
)
|
|
@ -0,0 +1,17 @@
|
||||||
|
from django.http import HttpResponse
|
||||||
|
from .models import Person
|
||||||
|
|
||||||
|
|
||||||
|
def example_view(request):
|
||||||
|
return HttpResponse('example view')
|
||||||
|
|
||||||
|
|
||||||
|
def model_view(request):
|
||||||
|
people = Person.objects.all()
|
||||||
|
return HttpResponse('\n'.join([person.name for person in people]))
|
||||||
|
|
||||||
|
|
||||||
|
def create_model_instance(request):
|
||||||
|
person = Person(name='emily')
|
||||||
|
person.save()
|
||||||
|
return HttpResponse('')
|
|
@ -49,7 +49,10 @@ def geodjango(settings):
|
||||||
|
|
||||||
def get_test_modules():
|
def get_test_modules():
|
||||||
modules = []
|
modules = []
|
||||||
for loc, dirpath in (MODEL_TESTS_DIR_NAME, MODEL_TEST_DIR), (REGRESSION_TESTS_DIR_NAME, REGRESSION_TEST_DIR), (CONTRIB_DIR_NAME, CONTRIB_DIR):
|
for loc, dirpath in (
|
||||||
|
(MODEL_TESTS_DIR_NAME, MODEL_TEST_DIR),
|
||||||
|
(REGRESSION_TESTS_DIR_NAME, REGRESSION_TEST_DIR),
|
||||||
|
(CONTRIB_DIR_NAME, CONTRIB_DIR)):
|
||||||
for f in os.listdir(dirpath):
|
for f in os.listdir(dirpath):
|
||||||
if (f.startswith('__init__') or
|
if (f.startswith('__init__') or
|
||||||
f.startswith('.') or
|
f.startswith('.') or
|
||||||
|
@ -150,7 +153,8 @@ def django_tests(verbosity, interactive, failfast, test_labels):
|
||||||
settings.TEST_RUNNER = 'django.test.simple.DjangoTestSuiteRunner'
|
settings.TEST_RUNNER = 'django.test.simple.DjangoTestSuiteRunner'
|
||||||
TestRunner = get_runner(settings)
|
TestRunner = get_runner(settings)
|
||||||
|
|
||||||
test_runner = TestRunner(verbosity=verbosity, interactive=interactive, failfast=failfast)
|
test_runner = TestRunner(verbosity=verbosity, interactive=interactive,
|
||||||
|
failfast=failfast)
|
||||||
failures = test_runner.run_tests(test_labels, extra_tests=extra_tests)
|
failures = test_runner.run_tests(test_labels, extra_tests=extra_tests)
|
||||||
|
|
||||||
teardown(state)
|
teardown(state)
|
||||||
|
@ -175,7 +179,8 @@ def bisect_tests(bisection_label, options, test_labels):
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
subprocess_args = [sys.executable, __file__, '--settings=%s' % options.settings]
|
subprocess_args = [
|
||||||
|
sys.executable, __file__, '--settings=%s' % options.settings]
|
||||||
if options.failfast:
|
if options.failfast:
|
||||||
subprocess_args.append('--failfast')
|
subprocess_args.append('--failfast')
|
||||||
if options.verbosity:
|
if options.verbosity:
|
||||||
|
@ -235,7 +240,8 @@ def paired_tests(paired_test, options, test_labels):
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
subprocess_args = [sys.executable, __file__, '--settings=%s' % options.settings]
|
subprocess_args = [
|
||||||
|
sys.executable, __file__, '--settings=%s' % options.settings]
|
||||||
if options.failfast:
|
if options.failfast:
|
||||||
subprocess_args.append('--failfast')
|
subprocess_args.append('--failfast')
|
||||||
if options.verbosity:
|
if options.verbosity:
|
||||||
|
@ -244,7 +250,8 @@ def paired_tests(paired_test, options, test_labels):
|
||||||
subprocess_args.append('--noinput')
|
subprocess_args.append('--noinput')
|
||||||
|
|
||||||
for i, label in enumerate(test_labels):
|
for i, label in enumerate(test_labels):
|
||||||
print '***** %d of %d: Check test pairing with %s' % (i+1, len(test_labels), label)
|
print '***** %d of %d: Check test pairing with %s' % (
|
||||||
|
i+1, len(test_labels), label)
|
||||||
failures = subprocess.call(subprocess_args + [label, paired_test])
|
failures = subprocess.call(subprocess_args + [label, paired_test])
|
||||||
if failures:
|
if failures:
|
||||||
print '***** Found problem pair with',label
|
print '***** Found problem pair with',label
|
||||||
|
@ -257,19 +264,36 @@ if __name__ == "__main__":
|
||||||
from optparse import OptionParser
|
from optparse import OptionParser
|
||||||
usage = "%prog [options] [module module module ...]"
|
usage = "%prog [options] [module module module ...]"
|
||||||
parser = OptionParser(usage=usage)
|
parser = OptionParser(usage=usage)
|
||||||
parser.add_option('-v','--verbosity', action='store', dest='verbosity', default='1',
|
parser.add_option(
|
||||||
|
'-v','--verbosity', action='store', dest='verbosity', default='1',
|
||||||
type='choice', choices=['0', '1', '2', '3'],
|
type='choice', choices=['0', '1', '2', '3'],
|
||||||
help='Verbosity level; 0=minimal output, 1=normal output, 2=all output')
|
help='Verbosity level; 0=minimal output, 1=normal output, 2=all '
|
||||||
parser.add_option('--noinput', action='store_false', dest='interactive', default=True,
|
'output')
|
||||||
|
parser.add_option(
|
||||||
|
'--noinput', action='store_false', dest='interactive', default=True,
|
||||||
help='Tells Django to NOT prompt the user for input of any kind.')
|
help='Tells Django to NOT prompt the user for input of any kind.')
|
||||||
parser.add_option('--failfast', action='store_true', dest='failfast', default=False,
|
parser.add_option(
|
||||||
help='Tells Django to stop running the test suite after first failed test.')
|
'--failfast', action='store_true', dest='failfast', default=False,
|
||||||
parser.add_option('--settings',
|
help='Tells Django to stop running the test suite after first failed '
|
||||||
help='Python path to settings module, e.g. "myproject.settings". If this isn\'t provided, the DJANGO_SETTINGS_MODULE environment variable will be used.')
|
'test.')
|
||||||
parser.add_option('--bisect', action='store', dest='bisect', default=None,
|
parser.add_option(
|
||||||
help="Bisect the test suite to discover a test that causes a test failure when combined with the named test.")
|
'--settings',
|
||||||
parser.add_option('--pair', action='store', dest='pair', default=None,
|
help='Python path to settings module, e.g. "myproject.settings". If '
|
||||||
help="Run the test suite in pairs with the named test to find problem pairs.")
|
'this isn\'t provided, the DJANGO_SETTINGS_MODULE environment '
|
||||||
|
'variable will be used.')
|
||||||
|
parser.add_option(
|
||||||
|
'--bisect', action='store', dest='bisect', default=None,
|
||||||
|
help='Bisect the test suite to discover a test that causes a test '
|
||||||
|
'failure when combined with the named test.')
|
||||||
|
parser.add_option(
|
||||||
|
'--pair', action='store', dest='pair', default=None,
|
||||||
|
help='Run the test suite in pairs with the named test to find problem '
|
||||||
|
'pairs.')
|
||||||
|
parser.add_option(
|
||||||
|
'--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.'),
|
||||||
options, args = parser.parse_args()
|
options, args = parser.parse_args()
|
||||||
if options.settings:
|
if options.settings:
|
||||||
os.environ['DJANGO_SETTINGS_MODULE'] = options.settings
|
os.environ['DJANGO_SETTINGS_MODULE'] = options.settings
|
||||||
|
@ -279,11 +303,15 @@ if __name__ == "__main__":
|
||||||
else:
|
else:
|
||||||
options.settings = os.environ['DJANGO_SETTINGS_MODULE']
|
options.settings = os.environ['DJANGO_SETTINGS_MODULE']
|
||||||
|
|
||||||
|
if options.liveserver is not None:
|
||||||
|
os.environ['DJANGO_LIVE_TEST_SERVER_ADDRESS'] = options.liveserver
|
||||||
|
|
||||||
if options.bisect:
|
if options.bisect:
|
||||||
bisect_tests(options.bisect, options, args)
|
bisect_tests(options.bisect, options, args)
|
||||||
elif options.pair:
|
elif options.pair:
|
||||||
paired_tests(options.pair, options, args)
|
paired_tests(options.pair, options, args)
|
||||||
else:
|
else:
|
||||||
failures = django_tests(int(options.verbosity), options.interactive, options.failfast, args)
|
failures = django_tests(int(options.verbosity), options.interactive,
|
||||||
|
options.failfast, args)
|
||||||
if failures:
|
if failures:
|
||||||
sys.exit(bool(failures))
|
sys.exit(bool(failures))
|
||||||
|
|
Loading…
Reference in New Issue