mirror of
https://github.com/django/django.git
synced 2025-10-26 07:06:08 +00:00
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:
@@ -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 @@ from django.test.utils import (get_warnings_state, restore_warnings_state,
|
||||
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 @@ class DocTestRunner(doctest.DocTestRunner):
|
||||
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 @@ class _AssertNumQueriesContext(object):
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class SimpleTestCase(ut2.TestCase):
|
||||
|
||||
def save_warnings_state(self):
|
||||
@@ -335,6 +346,7 @@ class SimpleTestCase(ut2.TestCase):
|
||||
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 @@ class TransactionTestCase(SimpleTestCase):
|
||||
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 @@ class TestCase(TransactionTestCase):
|
||||
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 _deferredSkip(condition, reason):
|
||||
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()
|
||||
|
||||
Reference in New Issue
Block a user