Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Fixed #2879 -- Added support for the integration with Selenium and ot…

…her 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
  • Loading branch information...
commit 2f02a05ffb45be68b4164b4785ff1826833150a3 1 parent 45e3dff
@jphalip jphalip authored
View
52 django/contrib/admin/tests.py
@@ -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))
View
37 django/core/management/commands/test.py
@@ -1,20 +1,32 @@
+import sys
+import os
+from optparse import make_option, OptionParser
+
from django.conf import settings
from django.core.management.base import BaseCommand
-from optparse import make_option, OptionParser
-import sys
from django.test.utils import get_runner
class Command(BaseCommand):
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.'),
- make_option('--failfast', action='store_true', dest='failfast', default=False,
- help='Tells Django to stop running the test suite after first failed test.'),
- 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('--failfast',
+ action='store_true', dest='failfast', default=False,
+ help='Tells Django to stop running the test suite after first '
+ 'failed test.'),
+ 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 ...]'
requires_model_validation = False
@@ -35,7 +47,8 @@ def run_from_argv(self, argv):
def create_parser(self, prog_name, subcommand):
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,
usage=self.usage(subcommand),
version=self.get_version(),
@@ -48,6 +61,10 @@ def handle(self, *test_labels, **options):
TestRunner = get_runner(settings, options.get('testrunner'))
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)
failures = test_runner.run_tests(test_labels)
View
3  django/test/__init__.py
@@ -4,5 +4,6 @@
from django.test.client import Client, RequestFactory
from django.test.testcases import (TestCase, TransactionTestCase,
- SimpleTestCase, skipIfDBFeature, skipUnlessDBFeature)
+ SimpleTestCase, LiveServerTestCase, skipIfDBFeature,
+ skipUnlessDBFeature)
from django.test.utils import Approximate
View
245 django/test/testcases.py
@@ -1,16 +1,23 @@
from __future__ import with_statement
+import os
import re
import sys
from functools import wraps
from urlparse import urlsplit, urlunsplit
from xml.dom.minidom import parseString, Node
+import select
+import socket
+import threading
from django.conf import settings
+from django.contrib.staticfiles.handlers import StaticFilesHandler
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.signals import request_started
+from django.core.servers.basehttp import (WSGIRequestHandler, WSGIServer)
from django.core.urlresolvers import clear_url_caches
from django.core.validators import EMPTY_VALUES
from django.db import (transaction, connection, connections, DEFAULT_DB_ALIAS,
@@ -23,6 +30,7 @@
override_settings)
from django.utils import simplejson, unittest as ut2
from django.utils.encoding import smart_str
+from django.views.static import serve
__all__ = ('DocTestRunner', 'OutputChecker', 'TestCase', 'TransactionTestCase',
'SimpleTestCase', 'skipIfDBFeature', 'skipUnlessDBFeature')
@@ -68,7 +76,8 @@ def restore_transaction_methods():
class OutputChecker(doctest.OutputChecker):
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,
self.check_output_numeric,
@@ -219,6 +228,7 @@ def report_unexpected_exception(self, out, test, example, exc_info):
for conn in connections:
transaction.rollback_unless_managed(using=conn)
+
class _AssertNumQueriesContext(object):
def __init__(self, test_case, num, connection):
self.test_case = test_case
@@ -247,6 +257,7 @@ def __exit__(self, exc_type, exc_value, traceback):
)
)
+
class SimpleTestCase(ut2.TestCase):
def save_warnings_state(self):
@@ -335,6 +346,7 @@ def assertFieldOutput(self, fieldclass, valid, invalid, field_args=None,
self.assertTrue(isinstance(fieldclass(*field_args, **field_kwargs),
fieldclass))
+
class TransactionTestCase(SimpleTestCase):
# The class we'll use for the test client self.client.
# Can be overridden in derived classes.
@@ -643,6 +655,7 @@ def assertNumQueries(self, num, func=None, *args, **kwargs):
with context:
func(*args, **kwargs)
+
def connections_support_transactions():
"""
Returns True if all connections support transactions.
@@ -650,6 +663,7 @@ def connections_support_transactions():
return all(conn.features.supports_transactions
for conn in connections.all())
+
class TestCase(TransactionTestCase):
"""
Does basically the same as TransactionTestCase, but surrounds every test
@@ -703,6 +717,7 @@ def _fixture_teardown(self):
transaction.rollback(using=db)
transaction.leave_transaction_management(using=db)
+
def _deferredSkip(condition, reason):
def decorator(test_func):
if not (isinstance(test_func, type) and
@@ -719,6 +734,7 @@ def skip_wrapper(*args, **kwargs):
return test_item
return decorator
+
def skipIfDBFeature(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),
"Database has feature %s" % feature)
+
def skipUnlessDBFeature(feature):
"""
Skip a test unless a database has the named feature
"""
return _deferredSkip(lambda: not getattr(connection.features, 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()
View
15 docs/internals/contributing/writing-code/unit-tests.txt
@@ -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
+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
~~~~~~~~~~~~~~~~~~~~~
@@ -135,6 +148,7 @@ dependencies:
* setuptools_
* memcached_, plus a :ref:`supported Python binding <memcached>`
* 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
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/
.. _memcached: http://www.danga.com/memcached/
.. _gettext: http://www.gnu.org/software/gettext/manual/gettext.html
+.. _selenium: http://pypi.python.org/pypi/selenium
.. _contrib-apps:
View
17 docs/ref/django-admin.txt
@@ -976,15 +976,22 @@ information.
.. versionadded:: 1.2
.. django-admin-option:: --failfast
-Use the :djadminopt:`--failfast` option to stop running tests and report the failure
-immediately after a test fails.
+The ``--failfast`` option can be used to stop running tests and report the
+failure immediately after a test fails.
.. versionadded:: 1.4
.. django-admin-option:: --testrunner
-The :djadminopt:`--testrunner` option can be used to control the test runner
-class that is used to execute tests. If this value is provided, it overrides
-the value provided by the :setting:`TEST_RUNNER` setting.
+The ``--testrunner`` option can be used to control the test runner class that
+is used to execute tests. If this value is provided, it overrides the value
+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 ...>
--------------------------------
View
13 docs/releases/1.4.txt
@@ -40,6 +40,19 @@ before the release of 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
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
View
104 docs/topics/testing.txt
@@ -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
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
a different focus. In short:
* Use Django's test client to establish that the correct view is being
called and that the view is collecting the correct context data.
-* Use in-browser frameworks such as Twill and Selenium to test *rendered*
- HTML and the *behavior* of Web pages, namely JavaScript functionality.
+* Use in-browser frameworks such as Windmill_ and Selenium_ to test *rendered*
+ 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.
-.. _Twill: http://twill.idyll.org/
-.. _Selenium: http://seleniumhq.org/
-
Overview and a quick example
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -1753,6 +1752,97 @@ under MySQL with MyISAM tables)::
def test_transaction_behavior(self):
# ... 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
==================================
@@ -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
the :djadmin:`test` command can use.
-
Attributes
~~~~~~~~~~
-
.. attribute:: DjangoTestSuiteRunner.option_list
.. versionadded:: 1.4
View
5 tests/regressiontests/admin_inlines/admin.py
@@ -109,6 +109,10 @@ class SottoCapoInline(admin.TabularInline):
model = SottoCapo
+class ProfileInline(admin.TabularInline):
+ model = Profile
+ extra = 1
+
site.register(TitleCollection, inlines=[TitleInline])
# Test bug #12561 and #12778
# only ModelAdmin media
@@ -124,3 +128,4 @@ class SottoCapoInline(admin.TabularInline):
site.register(Holder4, Holder4Admin)
site.register(Author, AuthorAdmin)
site.register(CapoFamiglia, inlines=[ConsigliereInline, SottoCapoInline])
+site.register(ProfileCollection, inlines=[ProfileInline])
View
10 tests/regressiontests/admin_inlines/models.py
@@ -136,3 +136,13 @@ class Consigliere(models.Model):
class SottoCapo(models.Model):
name = models.CharField(max_length=100)
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)
View
106 tests/regressiontests/admin_inlines/tests.py
@@ -1,5 +1,6 @@
from __future__ import absolute_import
+from django.contrib.admin.tests import AdminSeleniumWebDriverTestCase
from django.contrib.admin.helpers import InlineAdminForm
from django.contrib.auth.models import User, Permission
from django.contrib.contenttypes.models import ContentType
@@ -8,7 +9,8 @@
# local test models
from .admin import InnerInline
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):
@@ -380,3 +382,105 @@ def test_inline_change_fk_all_perms(self):
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, '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)
View
45 tests/regressiontests/admin_scripts/tests.py
@@ -13,6 +13,7 @@
from django import conf, bin, get_version
from django.conf import settings
+from django.test.simple import DjangoTestSuiteRunner
from django.utils import unittest
@@ -1058,6 +1059,50 @@ def test_app_with_import(self):
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):
def setUp(self):
from django.core.management.commands.runserver import BaseRunserverCommand
View
50 tests/regressiontests/admin_widgets/tests.py
@@ -7,6 +7,7 @@
from django.conf import settings
from django.contrib import admin
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.uploadedfile import SimpleUploadedFile
from django.db.models import DateField
@@ -407,3 +408,52 @@ def test_no_can_add_related(self):
# Used to fail with a name error.
w = widgets.RelatedFieldWidgetWrapper(w, rel, widget_admin_site)
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')
View
16 tests/regressiontests/servers/fixtures/testdata.json
@@ -0,0 +1,16 @@
+[
+ {
+ "pk": 1,
+ "model": "servers.person",
+ "fields": {
+ "name": "jane"
+ }
+ },
+ {
+ "pk": 2,
+ "model": "servers.person",
+ "fields": {
+ "name": "robert"
+ }
+ }
+]
View
1  tests/regressiontests/servers/media/example_media_file.txt
@@ -0,0 +1 @@
+example media file
View
5 tests/regressiontests/servers/models.py
@@ -0,0 +1,5 @@
+from django.db import models
+
+
+class Person(models.Model):
+ name = models.CharField(max_length=256)
View
1  tests/regressiontests/servers/static/example_static_file.txt
@@ -0,0 +1 @@
+example static file
View
151 tests/regressiontests/servers/tests.py
@@ -3,13 +3,17 @@
"""
import os
from urlparse import urljoin
+import urllib2
import django
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.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):
@@ -68,3 +72,146 @@ def test_media_urls(self):
continue
self.fail('URL: %s should have caused a ValueError exception.'
% 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'])
View
12 tests/regressiontests/servers/urls.py
@@ -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),
+)
View
17 tests/regressiontests/servers/views.py
@@ -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('')
View
62 tests/runtests.py
@@ -49,7 +49,10 @@ def geodjango(settings):
def get_test_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):
if (f.startswith('__init__') or
f.startswith('.') or
@@ -150,7 +153,8 @@ def django_tests(verbosity, interactive, failfast, test_labels):
settings.TEST_RUNNER = 'django.test.simple.DjangoTestSuiteRunner'
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)
teardown(state)
@@ -175,7 +179,8 @@ def bisect_tests(bisection_label, options, test_labels):
except ValueError:
pass
- subprocess_args = [sys.executable, __file__, '--settings=%s' % options.settings]
+ subprocess_args = [
+ sys.executable, __file__, '--settings=%s' % options.settings]
if options.failfast:
subprocess_args.append('--failfast')
if options.verbosity:
@@ -235,7 +240,8 @@ def paired_tests(paired_test, options, test_labels):
except ValueError:
pass
- subprocess_args = [sys.executable, __file__, '--settings=%s' % options.settings]
+ subprocess_args = [
+ sys.executable, __file__, '--settings=%s' % options.settings]
if options.failfast:
subprocess_args.append('--failfast')
if options.verbosity:
@@ -244,7 +250,8 @@ def paired_tests(paired_test, options, test_labels):
subprocess_args.append('--noinput')
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])
if failures:
print '***** Found problem pair with',label
@@ -257,19 +264,36 @@ def paired_tests(paired_test, options, test_labels):
from optparse import OptionParser
usage = "%prog [options] [module module module ...]"
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'],
- help='Verbosity level; 0=minimal output, 1=normal output, 2=all output')
- parser.add_option('--noinput', action='store_false', dest='interactive', default=True,
+ help='Verbosity level; 0=minimal output, 1=normal output, 2=all '
+ '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.')
- parser.add_option('--failfast', action='store_true', dest='failfast', default=False,
- help='Tells Django to stop running the test suite after first failed test.')
- parser.add_option('--settings',
- help='Python path to settings module, e.g. "myproject.settings". If 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(
+ '--failfast', action='store_true', dest='failfast', default=False,
+ help='Tells Django to stop running the test suite after first failed '
+ 'test.')
+ parser.add_option(
+ '--settings',
+ help='Python path to settings module, e.g. "myproject.settings". If '
+ '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()
if options.settings:
os.environ['DJANGO_SETTINGS_MODULE'] = options.settings
@@ -279,11 +303,15 @@ def paired_tests(paired_test, options, test_labels):
else:
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:
bisect_tests(options.bisect, options, args)
elif options.pair:
paired_tests(options.pair, options, args)
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:
sys.exit(bool(failures))
Please sign in to comment.
Something went wrong with that request. Please try again.