2005-07-29 23:15:40 +08:00
#!/usr/bin/env python
2006-01-18 01:56:33 +08:00
import os , re , sys , time , traceback
2005-07-29 23:15:40 +08:00
# doctest is included in the same package as this module, because this testing
# framework uses features only available in the Python 2.4 version of doctest,
# and Django aims to work with Python 2.3+.
import doctest
2006-05-02 09:31:56 +08:00
MODEL_TESTS_DIR_NAME = ' modeltests '
2005-08-02 03:09:07 +08:00
OTHER_TESTS_DIR = " othertests "
2006-06-20 13:29:19 +08:00
REGRESSION_TESTS_DIR_NAME = ' regressiontests '
2005-07-29 23:15:40 +08:00
TEST_DATABASE_NAME = ' django_test_db '
error_list = [ ]
def log_error ( model_name , title , description ) :
error_list . append ( {
2005-08-02 03:09:07 +08:00
' title ' : " %r module: %s " % ( model_name , title ) ,
2005-07-29 23:15:40 +08:00
' description ' : description ,
} )
2006-05-02 09:31:56 +08:00
MODEL_TEST_DIR = os . path . join ( os . path . dirname ( __file__ ) , MODEL_TESTS_DIR_NAME )
2006-06-20 13:29:19 +08:00
REGRESSION_TEST_DIR = os . path . join ( os . path . dirname ( __file__ ) , REGRESSION_TESTS_DIR_NAME )
2005-07-30 06:35:54 +08:00
2006-05-27 05:28:12 +08:00
ALWAYS_INSTALLED_APPS = [
' django.contrib.contenttypes ' ,
2006-06-20 22:27:44 +08:00
' django.contrib.auth ' ,
2006-06-17 02:58:45 +08:00
' django.contrib.sites ' ,
2006-05-27 05:28:12 +08:00
' django.contrib.flatpages ' ,
' django.contrib.redirects ' ,
' django.contrib.sessions ' ,
2006-06-17 02:58:45 +08:00
' django.contrib.comments ' ,
' django.contrib.admin ' ,
2006-05-27 05:28:12 +08:00
]
2005-07-30 06:35:54 +08:00
def get_test_models ( ) :
2006-06-20 15:12:45 +08:00
models = [ ]
2006-06-20 22:27:44 +08:00
for loc , dirpath in ( MODEL_TESTS_DIR_NAME , MODEL_TEST_DIR ) , ( REGRESSION_TESTS_DIR_NAME , REGRESSION_TEST_DIR ) :
for f in os . listdir ( dirpath ) :
2006-06-20 15:12:45 +08:00
if f . startswith ( ' __init__ ' ) or f . startswith ( ' . ' ) or f . startswith ( ' sql ' ) :
continue
models . append ( ( loc , f ) )
return models
2005-07-30 06:35:54 +08:00
2005-07-29 23:15:40 +08:00
class DjangoDoctestRunner ( doctest . DocTestRunner ) :
def __init__ ( self , verbosity_level , * args , * * kwargs ) :
self . verbosity_level = verbosity_level
doctest . DocTestRunner . __init__ ( self , * args , * * kwargs )
2005-08-11 02:00:52 +08:00
self . _checker = DjangoDoctestOutputChecker ( )
2005-08-11 02:10:38 +08:00
self . optionflags = doctest . ELLIPSIS
2005-07-29 23:15:40 +08:00
def report_start ( self , out , test , example ) :
if self . verbosity_level > 1 :
out ( " >>> %s \n " % example . source . strip ( ) )
def report_failure ( self , out , test , example , got ) :
log_error ( test . name , " API test failed " ,
" Code: %r \n Line: %s \n Expected: %r \n Got: %r " % ( example . source . strip ( ) , example . lineno , example . want , got ) )
def report_unexpected_exception ( self , out , test , example , exc_info ) :
2006-05-02 09:31:56 +08:00
from django . db import transaction
2005-07-29 23:15:40 +08:00
tb = ' ' . join ( traceback . format_exception ( * exc_info ) [ 1 : ] )
log_error ( test . name , " API test raised an exception " ,
" Code: %r \n Line: %s \n Exception: %s " % ( example . source . strip ( ) , example . lineno , tb ) )
2006-05-02 09:31:56 +08:00
# Rollback, in case of database errors. Otherwise they'd have
# side effects on other tests.
transaction . rollback_unless_managed ( )
2005-09-19 09:18:04 +08:00
2006-01-18 01:56:33 +08:00
normalize_long_ints = lambda s : re . sub ( r ' (?<![ \ w])( \ d+)L(?![ \ w]) ' , ' \\ 1 ' , s )
2005-08-11 02:00:52 +08:00
class DjangoDoctestOutputChecker ( doctest . OutputChecker ) :
def check_output ( self , want , got , optionflags ) :
ok = doctest . OutputChecker . check_output ( self , want , got , optionflags )
2006-01-18 01:56:33 +08:00
# Doctest does an exact string comparison of output, which means long
# integers aren't equal to normal integers ("22L" vs. "22"). The
# following code normalizes long integers so that they equal normal
# integers.
if not ok :
return normalize_long_ints ( want ) == normalize_long_ints ( got )
2005-08-11 02:00:52 +08:00
return ok
2005-07-29 23:15:40 +08:00
class TestRunner :
2005-09-19 09:18:04 +08:00
def __init__ ( self , verbosity_level = 0 , which_tests = None ) :
2005-07-29 23:15:40 +08:00
self . verbosity_level = verbosity_level
2005-09-19 09:18:04 +08:00
self . which_tests = which_tests
2005-07-29 23:15:40 +08:00
def output ( self , required_level , message ) :
if self . verbosity_level > required_level - 1 :
print message
def run_tests ( self ) :
from django . conf import settings
2006-05-19 02:03:27 +08:00
# An empty access of the settings to force the default options to be
# installed prior to assigning to them.
settings . INSTALLED_APPS
2006-05-17 05:28:06 +08:00
2006-05-02 09:31:56 +08:00
# Manually set INSTALLED_APPS to point to the test models.
2006-06-20 13:29:19 +08:00
settings . INSTALLED_APPS = ALWAYS_INSTALLED_APPS + [ ' . ' . join ( a ) for a in get_test_models ( ) ]
2006-05-02 09:31:56 +08:00
2006-07-11 11:09:14 +08:00
# Manually set DEBUG and USE_I18N.
2006-05-02 09:31:56 +08:00
settings . DEBUG = False
2006-07-11 11:09:14 +08:00
settings . USE_I18N = True
2006-05-02 09:31:56 +08:00
from django . db import connection
from django . core import management
import django . db . models
2005-07-29 23:15:40 +08:00
2005-09-19 09:18:04 +08:00
# Determine which models we're going to test.
test_models = get_test_models ( )
2006-05-02 09:31:56 +08:00
if ' othertests ' in self . which_tests :
self . which_tests . remove ( ' othertests ' )
run_othertests = True
if not self . which_tests :
test_models = [ ]
else :
run_othertests = not self . which_tests
2005-09-19 09:18:04 +08:00
if self . which_tests :
# Only run the specified tests.
2006-06-20 13:29:19 +08:00
bad_models = [ m for m in self . which_tests if ( MODEL_TESTS_DIR_NAME , m ) not in test_models and ( REGRESSION_TESTS_DIR_NAME , m ) not in test_models ]
2005-09-19 09:18:04 +08:00
if bad_models :
sys . stderr . write ( " Models not found: %s \n " % bad_models )
sys . exit ( 1 )
else :
2006-06-20 13:29:19 +08:00
all_tests = [ ]
for test in self . which_tests :
for loc in MODEL_TESTS_DIR_NAME , REGRESSION_TESTS_DIR_NAME :
if ( loc , test ) in test_models :
all_tests . append ( ( loc , test ) )
test_models = all_tests
2005-09-19 09:18:04 +08:00
self . output ( 0 , " Running tests with database %r " % settings . DATABASE_ENGINE )
2005-07-30 06:35:54 +08:00
# If we're using SQLite, it's more convenient to test against an
# in-memory database.
2005-07-30 03:22:50 +08:00
if settings . DATABASE_ENGINE == " sqlite3 " :
global TEST_DATABASE_NAME
TEST_DATABASE_NAME = " :memory: "
2005-07-30 06:35:54 +08:00
else :
2006-05-19 02:25:49 +08:00
# Create the test database and connect to it. We need to autocommit
2006-06-20 22:27:44 +08:00
# if the database supports it because PostgreSQL doesn't allow
2006-05-19 02:25:49 +08:00
# CREATE/DROP DATABASE statements within transactions.
2006-05-02 09:31:56 +08:00
cursor = connection . cursor ( )
2006-05-19 02:25:49 +08:00
self . _set_autocommit ( connection )
2005-07-30 03:22:50 +08:00
self . output ( 1 , " Creating test database " )
try :
2005-07-29 23:15:40 +08:00
cursor . execute ( " CREATE DATABASE %s " % TEST_DATABASE_NAME )
2005-12-01 14:39:39 +08:00
except Exception , e :
sys . stderr . write ( " Got an error creating the test database: %s \n " % e )
confirm = raw_input ( " It appears the test database, %s , already exists. Type ' yes ' to delete it, or ' no ' to cancel: " % TEST_DATABASE_NAME )
2005-07-30 03:22:50 +08:00
if confirm == ' yes ' :
cursor . execute ( " DROP DATABASE %s " % TEST_DATABASE_NAME )
cursor . execute ( " CREATE DATABASE %s " % TEST_DATABASE_NAME )
else :
print " Tests cancelled. "
return
2006-05-02 09:31:56 +08:00
connection . close ( )
2005-07-29 23:15:40 +08:00
old_database_name = settings . DATABASE_NAME
settings . DATABASE_NAME = TEST_DATABASE_NAME
# Initialize the test database.
2006-05-02 09:31:56 +08:00
cursor = connection . cursor ( )
2006-06-20 22:27:44 +08:00
2006-06-23 12:37:00 +08:00
from django . db . models . loading import load_app
2006-06-17 02:58:45 +08:00
# Install the core always installed apps
for app in ALWAYS_INSTALLED_APPS :
self . output ( 1 , " Installing contrib app %s " % app )
2006-06-23 12:37:00 +08:00
mod = load_app ( app )
2006-06-17 02:58:45 +08:00
management . install ( mod )
2005-07-29 23:15:40 +08:00
2005-07-30 06:35:54 +08:00
# Run the tests for each test model.
2005-08-02 03:09:07 +08:00
self . output ( 1 , " Running app tests " )
2006-06-20 13:29:19 +08:00
for model_dir , model_name in test_models :
2005-07-29 23:15:40 +08:00
self . output ( 1 , " %s model: Importing " % model_name )
try :
2006-06-23 12:37:00 +08:00
mod = load_app ( model_dir + ' . ' + model_name )
2005-07-29 23:15:40 +08:00
except Exception , e :
log_error ( model_name , " Error while importing " , ' ' . join ( traceback . format_exception ( * sys . exc_info ( ) ) [ 1 : ] ) )
continue
2005-08-10 07:51:55 +08:00
2006-05-02 09:31:56 +08:00
if not getattr ( mod , ' error_log ' , None ) :
# Model is not marked as an invalid model
2006-06-20 13:29:19 +08:00
self . output ( 1 , " %s . %s model: Installing " % ( model_dir , model_name ) )
2006-05-02 09:31:56 +08:00
management . install ( mod )
# Run the API tests.
p = doctest . DocTestParser ( )
test_namespace = dict ( [ ( m . _meta . object_name , m ) \
for m in django . db . models . get_models ( mod ) ] )
dtest = p . get_doctest ( mod . API_TESTS , test_namespace , model_name , None , None )
# Manually set verbose=False, because "-v" command-line parameter
# has side effects on doctest TestRunner class.
runner = DjangoDoctestRunner ( verbosity_level = verbosity_level , verbose = False )
2006-06-20 13:29:19 +08:00
self . output ( 1 , " %s . %s model: Running tests " % ( model_dir , model_name ) )
2006-05-02 09:31:56 +08:00
runner . run ( dtest , clear_globs = True , out = sys . stdout . write )
else :
# Check that model known to be invalid is invalid for the right reasons.
2006-06-20 13:29:19 +08:00
self . output ( 1 , " %s . %s model: Validating " % ( model_dir , model_name ) )
2006-05-02 09:31:56 +08:00
from cStringIO import StringIO
s = StringIO ( )
count = management . get_validation_errors ( s , mod )
s . seek ( 0 )
error_log = s . read ( )
actual = error_log . split ( ' \n ' )
expected = mod . error_log . split ( ' \n ' )
unexpected = [ err for err in actual if err not in expected ]
missing = [ err for err in expected if err not in actual ]
if unexpected or missing :
unexpected_log = ' \n ' . join ( unexpected )
missing_log = ' \n ' . join ( missing )
log_error ( model_name ,
" Validator found %d validation errors, %d expected " % ( count , len ( expected ) - 1 ) ,
" Missing errors: \n %s \n \n Unexpected errors: \n %s " % ( missing_log , unexpected_log ) )
if run_othertests :
2005-09-19 09:18:04 +08:00
# Run the non-model tests in the other tests dir
self . output ( 1 , " Running other tests " )
other_tests_dir = os . path . join ( os . path . dirname ( __file__ ) , OTHER_TESTS_DIR )
test_modules = [ f [ : - 3 ] for f in os . listdir ( other_tests_dir ) if f . endswith ( ' .py ' ) and not f . startswith ( ' __init__ ' ) ]
for module in test_modules :
self . output ( 1 , " %s module: Importing " % module )
2005-08-02 03:09:07 +08:00
try :
2005-09-19 09:18:04 +08:00
mod = __import__ ( " othertests. " + module , ' ' , ' ' , [ ' ' ] )
2005-08-02 03:09:07 +08:00
except Exception , e :
2005-09-19 09:18:04 +08:00
log_error ( module , " Error while importing " , ' ' . join ( traceback . format_exception ( * sys . exc_info ( ) ) [ 1 : ] ) )
2005-08-02 03:09:07 +08:00
continue
2005-09-19 09:18:04 +08:00
if mod . __doc__ :
p = doctest . DocTestParser ( )
dtest = p . get_doctest ( mod . __doc__ , mod . __dict__ , module , None , None )
runner = DjangoDoctestRunner ( verbosity_level = verbosity_level , verbose = False )
self . output ( 1 , " %s module: running tests " % module )
runner . run ( dtest , clear_globs = True , out = sys . stdout . write )
if hasattr ( mod , " run_tests " ) and callable ( mod . run_tests ) :
self . output ( 1 , " %s module: running tests " % module )
try :
mod . run_tests ( verbosity_level )
except Exception , e :
log_error ( module , " Exception running tests " , ' ' . join ( traceback . format_exception ( * sys . exc_info ( ) ) [ 1 : ] ) )
continue
2005-08-10 07:51:55 +08:00
2005-07-30 06:35:54 +08:00
# Unless we're using SQLite, remove the test database to clean up after
# ourselves. Connect to the previous database (not the test database)
# to do so, because it's not allowed to delete a database while being
2005-07-30 03:22:50 +08:00
# connected to it.
if settings . DATABASE_ENGINE != " sqlite3 " :
2006-05-02 09:31:56 +08:00
connection . close ( )
2005-07-30 03:22:50 +08:00
settings . DATABASE_NAME = old_database_name
2006-05-02 09:31:56 +08:00
cursor = connection . cursor ( )
2005-07-30 03:22:50 +08:00
self . output ( 1 , " Deleting test database " )
2006-05-19 02:25:49 +08:00
self . _set_autocommit ( connection )
time . sleep ( 1 ) # To avoid "database is being accessed by other users" errors.
2005-07-30 03:22:50 +08:00
cursor . execute ( " DROP DATABASE %s " % TEST_DATABASE_NAME )
2005-07-29 23:15:40 +08:00
# Display output.
if error_list :
for d in error_list :
print
print d [ ' title ' ]
print " = " * len ( d [ ' title ' ] )
print d [ ' description ' ]
2005-09-19 09:18:04 +08:00
print " %s error %s : " % ( len ( error_list ) , len ( error_list ) != 1 and ' s ' or ' ' )
2005-07-29 23:15:40 +08:00
else :
print " All tests passed. "
2006-06-20 22:27:44 +08:00
2006-05-19 02:25:49 +08:00
def _set_autocommit ( self , connection ) :
"""
Make sure a connection is in autocommit mode .
"""
if hasattr ( connection . connection , " autocommit " ) :
connection . connection . autocommit ( True )
elif hasattr ( connection . connection , " set_isolation_level " ) :
connection . connection . set_isolation_level ( 0 )
2005-07-29 23:15:40 +08:00
if __name__ == " __main__ " :
from optparse import OptionParser
2005-09-19 09:18:04 +08:00
usage = " % prog [options] [model model model ...] "
2006-02-19 04:08:20 +08:00
parser = OptionParser ( usage = usage )
2005-07-29 23:15:40 +08:00
parser . add_option ( ' -v ' , help = ' How verbose should the output be? Choices are 0, 1 and 2, where 2 is most verbose. Default is 0. ' ,
type = ' choice ' , choices = [ ' 0 ' , ' 1 ' , ' 2 ' ] )
2005-08-10 23:36:16 +08:00
parser . add_option ( ' --settings ' ,
2006-01-02 02:37:33 +08:00
help = ' Python path to settings module, e.g. " myproject.settings " . If this isn \' t provided, the DJANGO_SETTINGS_MODULE environment variable will be used. ' )
2005-07-29 23:15:40 +08:00
options , args = parser . parse_args ( )
verbosity_level = 0
if options . v :
verbosity_level = int ( options . v )
2005-08-10 23:36:16 +08:00
if options . settings :
os . environ [ ' DJANGO_SETTINGS_MODULE ' ] = options . settings
2005-09-19 09:18:04 +08:00
t = TestRunner ( verbosity_level , args )
2005-07-29 23:15:40 +08:00
t . run_tests ( )