1
0
mirror of https://github.com/django/django.git synced 2025-07-05 02:09:13 +00:00

Merge branch 'gsoc' into local/gsoc

git-svn-id: http://code.djangoproject.com/svn/django/branches/soc2009/test-improvements@10888 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
Kevin Kubasik 2009-06-02 14:52:29 +00:00
parent 6399b2937a
commit 39676e8f56
32 changed files with 2123 additions and 202 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
*.pyc

View File

@ -379,7 +379,7 @@ PASSWORD_RESET_TIMEOUT_DAYS = 3
###########
# The name of the method to use to invoke the test suite
TEST_RUNNER = 'django.test.simple.run_tests'
TEST_RUNNER = 'django.test.simple.DefaultTestRunner'
# The name of the database to use for testing purposes.
# If None, a name of 'test_' + DATABASE_NAME will be assumed
@ -392,6 +392,55 @@ TEST_DATABASE_NAME = None
TEST_DATABASE_CHARSET = None
TEST_DATABASE_COLLATION = None
############
# COVERAGE #
############
# Specify the coverage test runner
COVERAGE_TEST_RUNNER = 'django.test.test_coverage.BaseCoverageRunner'
# Specify regular expressions of code blocks the coverage analyzer should
# ignore as statements (e.g. ``raise NotImplemented``).
# These statements are not figured in as part of the coverage statistics.
# This setting is optional.
COVERAGE_CODE_EXCLUDES = [
'def __unicode__\(self\):', 'def get_absolute_url\(self\):',
'from .* import .*', 'import .*',
]
# Specify a list of regular expressions of paths to exclude from
# coverage analysis.
# Note these paths are ignored by the module introspection tool and take
# precedence over any package/module settings such as:
# TODO: THE SETTING FOR MODULES
# Use this to exclude subdirectories like ``r'.svn'``, for example.
# This setting is optional.
COVERAGE_PATH_EXCLUDES = [r'.svn']
# Specify a list of additional module paths to include
# in the coverage analysis. By default, only modules within installed
# apps are reported. If you have utility modules outside of the app
# structure, you can include them here.
# Note this list is *NOT* regular expression, so you have to be explicit,
# such as 'myproject.utils', and not 'utils$'.
# This setting is optional.
COVERAGE_ADDITIONAL_MODULES = []
# Specify a list of regular expressions of module paths to exclude
# from the coverage analysis. Examples are ``'tests$'`` and ``'urls$'``.
# This setting is optional.
COVERAGE_MODULE_EXCLUDES = ['tests$', 'settings$','urls$', 'common.views.test',
'__init__', 'django']
# Specify the directory where you would like the coverage report to create
# the HTML files.
# You'll need to make sure this directory exists and is writable by the
# user account running the test.
# You should probably set this one explicitly in your own settings file.
COVERAGE_REPORT_HTML_OUTPUT_DIR = 'test_html'
############
# FIXTURES #
############

View File

@ -6,6 +6,10 @@ class Command(BaseCommand):
option_list = BaseCommand.option_list + (
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('--coverage', action='store_true', dest='coverage', default=False,
help='Tells Django to run the coverage runner'),
make_option('--reports', action='store_true', dest='reports', default=False,
help='Tells Django to output coverage results as HTML reports'),
)
help = 'Runs the test suite for the specified applications, or the entire site if no apps are specified.'
args = '[appname ...]'
@ -18,8 +22,10 @@ class Command(BaseCommand):
verbosity = int(options.get('verbosity', 1))
interactive = options.get('interactive', True)
test_runner = get_runner(settings)
failures = test_runner(test_labels, verbosity=verbosity, interactive=interactive)
cover = options.get('coverage', False)
report = options.get('reports', False)
test_runner = get_runner(settings, coverage=cover, reports=report)
tr = test_runner()
failures = tr.run_tests(test_labels, verbosity=verbosity, interactive=interactive)
if failures:
sys.exit(failures)

View File

@ -142,13 +142,13 @@ class FileDescriptor(object):
"""
The descriptor for the file attribute on the model instance. Returns a
FieldFile when accessed so you can do stuff like::
>>> instance.file.size
Assigns a file object on assignment so you can do::
>>> instance.file = File(...)
"""
def __init__(self, field):
self.field = field
@ -156,9 +156,9 @@ class FileDescriptor(object):
def __get__(self, instance=None, owner=None):
if instance is None:
raise AttributeError(
"The '%s' attribute can only be accessed from %s instances."
"The '%s' attribute can only be accessed from %s instances."
% (self.field.name, owner.__name__))
# This is slightly complicated, so worth an explanation.
# instance.file`needs to ultimately return some instance of `File`,
# probably a subclass. Additionally, this returned object needs to have
@ -168,8 +168,8 @@ class FileDescriptor(object):
# peek below you can see that we're not. So depending on the current
# value of the field we have to dynamically construct some sort of
# "thing" to return.
# The instance dict contains whatever was originally assigned
# The instance dict contains whatever was originally assigned
# in __set__.
file = instance.__dict__[self.field.name]
@ -186,14 +186,14 @@ class FileDescriptor(object):
# Other types of files may be assigned as well, but they need to have
# the FieldFile interface added to the. Thus, we wrap any other type of
# File inside a FieldFile (well, the field's attr_class, which is
# File inside a FieldFile (well, the field's attr_class, which is
# usually FieldFile).
elif isinstance(file, File) and not isinstance(file, FieldFile):
file_copy = self.field.attr_class(instance, self.field, file.name)
file_copy.file = file
file_copy._committed = False
instance.__dict__[self.field.name] = file_copy
# Finally, because of the (some would say boneheaded) way pickle works,
# the underlying FieldFile might not actually itself have an associated
# file. So we need to reset the details of the FieldFile in those cases.
@ -201,7 +201,7 @@ class FileDescriptor(object):
file.instance = instance
file.field = self.field
file.storage = self.field.storage
# That was fun, wasn't it?
return instance.__dict__[self.field.name]
@ -212,7 +212,7 @@ class FileField(Field):
# The class to wrap instance attributes in. Accessing the file object off
# the instance will always return an instance of attr_class.
attr_class = FieldFile
# The descriptor to use for accessing the attribute off of the class.
descriptor_class = FileDescriptor
@ -300,40 +300,20 @@ class ImageFileDescriptor(FileDescriptor):
assigning the width/height to the width_field/height_field, if appropriate.
"""
def __set__(self, instance, value):
previous_file = instance.__dict__.get(self.field.name)
super(ImageFileDescriptor, self).__set__(instance, value)
# The rest of this method deals with width/height fields, so we can
# bail early if neither is used.
if not self.field.width_field and not self.field.height_field:
return
# We need to call the descriptor's __get__ to coerce this assigned
# value into an instance of the right type (an ImageFieldFile, in this
# case).
value = self.__get__(instance)
if not value:
return
# Get the image dimensions, making sure to leave the file in the same
# state (opened or closed) that we got it in. However, we *don't* rewind
# the file pointer if the file is already open. This is in keeping with
# most Python standard library file operations that leave it up to the
# user code to reset file pointers after operations that move it.
from django.core.files.images import get_image_dimensions
close = value.closed
value.open()
try:
width, height = get_image_dimensions(value)
finally:
if close:
value.close()
# Update the width and height fields
if self.field.width_field:
setattr(value.instance, self.field.width_field, width)
if self.field.height_field:
setattr(value.instance, self.field.height_field, height)
# To prevent recalculating image dimensions when we are instantiating
# an object from the database (bug #11084), only update dimensions if
# the field had a value before this assignment. Since the default
# value for FileField subclasses is an instance of field.attr_class,
# previous_file will only be None when we are called from
# Model.__init__(). The ImageField.update_dimension_fields method
# hooked up to the post_init signal handles the Model.__init__() cases.
# Assignment happening outside of Model.__init__() will trigger the
# update right here.
if previous_file is not None:
self.field.update_dimension_fields(instance, force=True)
class ImageFieldFile(ImageFile, FieldFile):
def delete(self, save=True):
@ -350,6 +330,69 @@ class ImageField(FileField):
self.width_field, self.height_field = width_field, height_field
FileField.__init__(self, verbose_name, name, **kwargs)
def contribute_to_class(self, cls, name):
super(ImageField, self).contribute_to_class(cls, name)
# Attach update_dimension_fields so that dimension fields declared
# after their corresponding image field don't stay cleared by
# Model.__init__, see bug #11196.
signals.post_init.connect(self.update_dimension_fields, sender=cls)
def update_dimension_fields(self, instance, force=False, *args, **kwargs):
"""
Updates field's width and height fields, if defined.
This method is hooked up to model's post_init signal to update
dimensions after instantiating a model instance. However, dimensions
won't be updated if the dimensions fields are already populated. This
avoids unnecessary recalculation when loading an object from the
database.
Dimensions can be forced to update with force=True, which is how
ImageFileDescriptor.__set__ calls this method.
"""
# Nothing to update if the field doesn't have have dimension fields.
has_dimension_fields = self.width_field or self.height_field
if not has_dimension_fields:
return
# getattr will call the ImageFileDescriptor's __get__ method, which
# coerces the assigned value into an instance of self.attr_class
# (ImageFieldFile in this case).
file = getattr(instance, self.attname)
# Nothing to update if we have no file and not being forced to update.
if not file and not force:
return
dimension_fields_filled = not(
(self.width_field and not getattr(instance, self.width_field))
or (self.height_field and not getattr(instance, self.height_field))
)
# When both dimension fields have values, we are most likely loading
# data from the database or updating an image field that already had
# an image stored. In the first case, we don't want to update the
# dimension fields because we are already getting their values from the
# database. In the second case, we do want to update the dimensions
# fields and will skip this return because force will be True since we
# were called from ImageFileDescriptor.__set__.
if dimension_fields_filled and not force:
return
# file should be an instance of ImageFieldFile or should be None.
if file:
width = file.width
height = file.height
else:
# No file, so clear dimensions fields.
width = None
height = None
# Update the width and height fields.
if self.width_field:
setattr(instance, self.width_field, width)
if self.height_field:
setattr(instance, self.height_field, height)
def formfield(self, **kwargs):
defaults = {'form_class': forms.ImageField}
defaults.update(kwargs)

View File

@ -0,0 +1,18 @@
"""
Copyright 2009 55 Minutes (http://www.55minutes.com)
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
from html_report import *

View File

@ -0,0 +1,57 @@
"""
Copyright 2009 55 Minutes (http://www.55minutes.com)
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
import coverage, time
try:
set
except:
from sets import Set as set
class ModuleVars(object):
modules = dict()
def __new__(cls, module_name, module=None):
if cls.modules.get(module_name, None):
return cls.modules.get(module_name)
else:
obj=super(ModuleVars, cls).__new__(cls)
obj._init(module_name, module)
cls.modules[module_name] = obj
return obj
def _init(self, module_name, module):
source_file, stmts, excluded, missed, missed_display = coverage.analysis2(module)
executed = list(set(stmts).difference(missed))
total = list(set(stmts).union(excluded))
total.sort()
title = module.__name__
total_count = len(total)
executed_count = len(executed)
excluded_count = len(excluded)
missed_count = len(missed)
try:
percent_covered = float(len(executed))/len(stmts)*100
except ZeroDivisionError:
percent_covered = 100
test_timestamp = time.strftime('%a %Y-%m-%d %H:%M %Z')
severity = 'normal'
if percent_covered < 75: severity = 'warning'
if percent_covered < 50: severity = 'critical'
for k, v in locals().iteritems():
setattr(self, k, v)

View File

@ -0,0 +1,91 @@
"""
Copyright 2009 55 Minutes (http://www.55minutes.com)
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
import cgi, os
from data_storage import ModuleVars
from templates import default_module_detail as module_detail
def html_module_detail(filename, module_name, nav=None):
"""
Creates a module detail report based on coverage testing at the specified
filename. If ``nav`` is specified, the nav template will be used as well.
It uses `templates.default_module_detail` to create the page. The template
contains the following sections which need to be rendered and assembled into
the final HTML.
TOP: Contains the HTML declaration and head information, as well as the
inline stylesheet. It requires the following variable:
* %(title)s The module name is probably fitting for this.
CONTENT_HEADER: The header portion of the body. Requires the following variable:
* %(title)s
* %(source_file)s File path to the module
* %(total_count)d
* %(executed_count)d
* %(excluded_count)d
* %(ignored_count)d
* %(percent_covered)0.1f
* %(test_timestamp)s
CONTENT_BODY: Annotated module source code listing. Requires the following variable:
* ``%(source_lines)s`` The actual source listing which is generated by
looping through each line and concatenanting together rendered
``SOURCE_LINE`` template (see below).
BOTTOM: Just a closing ``</body></html>``
SOURCE_LINE: Used to assemble the content of ``%(source_lines)s`` for ``CONTENT_BODY``.
Requires the following variables:
* ``%(line_status)s`` (ignored, executed, missed, excluded) used as CSS class
identifier to style the each source line.
* ``%(source_line)s``
"""
if not nav:
nav = {}
m_vars = ModuleVars(module_name)
m_vars.source_lines = source_lines = list()
i = 0
for i, source_line in enumerate(
[cgi.escape(l.rstrip()) for l in file(m_vars.source_file, 'rb').readlines()]):
line_status = 'ignored'
if i+1 in m_vars.executed: line_status = 'executed'
if i+1 in m_vars.excluded: line_status = 'excluded'
if i+1 in m_vars.missed: line_status = 'missed'
source_lines.append(module_detail.SOURCE_LINE %vars())
m_vars.ignored_count = i+1 - m_vars.total_count
m_vars.source_lines = os.linesep.join(source_lines)
if 'prev_link' in nav and 'next_link' in nav:
nav_html = module_detail.NAV %nav
elif 'prev_link' in nav:
nav_html = module_detail.NAV_NO_NEXT %nav
elif 'next_link' in nav:
nav_html = module_detail.NAV_NO_PREV %nav
fo = file(filename, 'wb+')
print >>fo, module_detail.TOP %m_vars.__dict__
if nav:
print >>fo, nav_html
print >>fo, module_detail.CONTENT_HEADER %m_vars.__dict__
print >>fo, module_detail.CONTENT_BODY %m_vars.__dict__
if nav:
print >>fo, nav_html
print >>fo, module_detail.BOTTOM
fo.close()

View File

@ -0,0 +1,53 @@
"""
Copyright 2009 55 Minutes (http://www.55minutes.com)
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
from html_module_exceptions import html_module_exceptions
from templates import default_module_errors as module_errors
def html_module_errors(filename, errors):
"""
Creates an index page of packages and modules which had problems being imported
for coverage analysis at the specified filename.
It uses `templates.default_module_errors` to create the page. The template
contains the following sections which need to be rendered and assembled into
the final HTML.
TOP: Contains the HTML declaration and head information, as well as the
inline stylesheet.
CONTENT_HEADER: The header portion of the body.
CONTENT_BODY: A list of excluded packages and modules. Requires the following
variable:
* ``%(long_desc)s`` A long description of what this page
is about.
* ``%(exception_list)s`` List of package and module names
which is generated by looping through each line and
concatenanting together rendered ``EXCEPTION_LINE``
template (see below).
BOTTOM: Just a closing ``</body></html>``
EXCEPTION_LINE: Used to assemble the content of ``%(exception_list)s`` for ``CONTENT_BODY``.
Requires the following variable:
* ``%(module_name)s``
"""
long_desc = """\
<code>test_coverage.utils.module_tools.find_or_load_module</code> had
problems importing these packages and modules:
"""
html_module_exceptions(filename, errors, module_errors, long_desc)

View File

@ -0,0 +1,32 @@
"""
Copyright 2009 55 Minutes (http://www.55minutes.com)
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
import os
def html_module_exceptions(filename, exceptions, template, long_desc):
exception_list = []
exceptions.sort()
for module_name in exceptions:
exception_list.append(template.EXCEPTION_LINE %vars())
exception_list = os.linesep.join(exception_list)
fo = file(filename, 'wb+')
print >>fo, template.TOP
print >>fo, template.CONTENT_HEADER
print >>fo, template.CONTENT_BODY %vars()
print >>fo, template.BOTTOM
fo.close()

View File

@ -0,0 +1,55 @@
"""
Copyright 2009 55 Minutes (http://www.55minutes.com)
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
from html_module_exceptions import html_module_exceptions
from templates import default_module_excludes as module_excludes
def html_module_excludes(filename, excludes):
"""
Creates an index page of packages and modules which were excluded from the
coverage analysis at the specified filename.
It uses `templates.default_module_excludes` to create the page. The template
contains the following sections which need to be rendered and assembled into
the final HTML.
TOP: Contains the HTML declaration and head information, as well as the
inline stylesheet.
CONTENT_HEADER: The header portion of the body.
CONTENT_BODY: A list of excluded packages and modules. Requires the following
variable:
* ``%(long_desc)s`` A long description of what this page
is about.
* ``%(exception_list)s`` List of package and module names
which is generated by looping through each line and
concatenanting together rendered ``EXCEPTION_LINE``
template (see below).
BOTTOM: Just a closing ``</body></html>``
EXCEPTION_LINE: Used to assemble the content of ``%(exclude_list)s`` for ``CONTENT_BODY``.
Requires the following variable:
* ``%(module_name)s``
"""
long_desc = """\
These packages and modules were excluded from the coverage analysis in
<code>django.conf.settings.COVERAGE_MODULE_EXCLUDES</code> or they do
not contain any executable statements:
"""
html_module_exceptions(filename, excludes, module_excludes, long_desc)

View File

@ -0,0 +1,143 @@
"""
Copyright 2009 55 Minutes (http://www.55minutes.com)
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
import os, time
from urllib import pathname2url as p2url
from data_storage import ModuleVars
from html_module_detail import html_module_detail
from html_module_errors import html_module_errors
from html_module_excludes import html_module_excludes
from templates import default_module_index as module_index
def html_report(outdir, modules, excludes=None, errors=None):
"""
Creates an ``index.html`` in the specified ``outdir``. Also attempts to create
a ``modules`` subdirectory to create module detail html pages, which are named
`module.__name__` + '.html'.
It uses `templates.default_module_index` to create the index page. The template
contains the following sections which need to be rendered and assembled into
`index.html`.
TOP: Contains the HTML declaration and head information, as well as the
inline stylesheet. It doesn't require any variables.
CONTENT_HEADER: The header portion of the body. Requires the following variable:
* ``%(test_timestamp)s``
CONTENT_BODY: A table of contents to the different module detail pages, with some
basic stats. Requires the following variables:
* ``%(module_stats)s`` This is the actual content of the table and is
generated by looping through the modules we want to report on and
concatenanting together rendered ``MODULE_STAT`` template (see below).
* ``%(total_lines)d``
* ``%(total_executed)d``
* ``%(total_excluded)d``
* ``%(overall_covered)0.1f``
EXCEPTIONS_LINK: Link to the excludes and errors index page which shows
packages and modules which were not part of the coverage
analysis. Requires the following variable:
* ``%(exceptions_link)s`` Link to the index page.
* ``%(exceptions_desc)s`` Describe the exception.
ERRORS_LINK: Link to the errors index page which shows packages and modules which
had problems being imported. Requires the following variable:
* ``%(errors_link)s`` Link to the index page.
BOTTOM: Just a closing ``</body></html>``
MODULE_STAT: Used to assemble the content of ``%(module_stats)s`` for ``CONTENT_BODY``.
Requires the following variables:
* ``%(severity)s`` (normal, warning, critical) used as CSS class identifier
to style the coverage percentage.
* ``%(module_link)s``
* ``%(module_name)s``
* ``%(total_count)d``
* ``%(executed_count)d``
* ``%(excluded_count)d``
* ``%(percent_covered)0.1f``
"""
# TODO: More robust directory checking and creation
outdir = os.path.abspath(outdir)
test_timestamp = time.strftime('%a %Y-%m-%d %H:%M %Z')
m_subdirname = 'modules'
m_dir = os.path.join(outdir, m_subdirname)
try:
os.mkdir(m_dir)
except OSError:
pass
total_lines = 0
total_executed = 0
total_excluded = 0
total_stmts = 0
module_stats = list()
m_names = modules.keys()
m_names.sort()
for n in m_names:
m_vars = ModuleVars(n, modules[n])
if not m_vars.total_count:
excludes.append(m_vars.module_name)
del modules[n]
continue
m_vars.module_link = p2url(os.path.join(m_subdirname, m_vars.module_name + '.html'))
module_stats.append(module_index.MODULE_STAT %m_vars.__dict__)
total_lines += m_vars.total_count
total_executed += m_vars.executed_count
total_excluded += m_vars.excluded_count
total_stmts += len(m_vars.stmts)
module_stats = os.linesep.join(module_stats)
overall_covered = float(total_executed)/total_stmts*100
m_names = modules.keys()
m_names.sort()
i = 0
for i, n in enumerate(m_names):
m_vars = ModuleVars(n)
nav = dict(up_link=p2url(os.path.join('..', 'index.html')),
up_label='index')
if i > 0:
m = ModuleVars(m_names[i-1])
nav['prev_link'] = os.path.basename(m.module_link)
nav['prev_label'] = m.module_name
if i+1 < len(modules):
m = ModuleVars(m_names[i+1])
nav['next_link'] = os.path.basename(m.module_link)
nav['next_label'] = m.module_name
html_module_detail(
os.path.join(m_dir, m_vars.module_name + '.html'), n, nav)
fo = file(os.path.join(outdir, 'index.html'), 'wb+')
print >>fo, module_index.TOP
print >>fo, module_index.CONTENT_HEADER %vars()
print >>fo, module_index.CONTENT_BODY %vars()
if excludes:
_file = 'excludes.html'
exceptions_link = _file
exception_desc = "Excluded packages and modules"
print >>fo, module_index.EXCEPTIONS_LINK %vars()
html_module_excludes(os.path.join(outdir, _file), excludes)
if errors:
_file = 'errors.html'
exceptions_link = _file
exception_desc = "Error packages and modules"
print >>fo, module_index.EXCEPTIONS_LINK %vars()
html_module_errors(os.path.join(outdir, _file), errors)
print >>fo, module_index.BOTTOM
fo.close()

View File

@ -0,0 +1,17 @@
"""
Copyright 2009 55 Minutes (http://www.55minutes.com)
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""

View File

@ -0,0 +1,193 @@
"""
Copyright 2009 55 Minutes (http://www.55minutes.com)
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
TOP = """\
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-type" content="text/html;charset=UTF-8" />
<title>Test coverage report: %(title)s</title>
<style type="text/css" media="screen">
a
{
color: #3d707a;
}
a:hover, a:active
{
color: #bf7d18;
}
body
{
font-family: "Lucida Sans Unicode", "Lucida Grande", sans-serif;
font-size: 13px;
}
.nav
{
font-size: 12px;
margin-left: 50px;
}
.ignored
{
color: #707070;
}
.executed
{
color: #3d9900;
}
.missed
{
color: red;
font-weight: bold;
}
.excluded
{
color: #6090f0;
font-weight: lighter;
}
#content-header
{
font-size: 12px;
padding: 18px 0 18px 50px;
}
#content-header h1
{
font-size: 16px;
margin: 10px 0 0 0;
color: #909090;
}
#module-name
{
color: #583707;
}
#content-header p
{
font-size: 13px;
margin: 0;
color: #909090;
}
#content-header .normal
{
color: #609030;
}
#content-header .warning
{
color: #d0a000;
}
#content-header .critical
{
color: red;
}
#source-listing
{
margin-bottom: 24px;
}
#source-listing ol
{
padding: 0 0 0 50px;
width: 90%%;
font-family: monospace;
list-style-position: outside;
}
#source-listing ol li
{
line-height: 18px;
font-size: small;
}
#source-listing ol code
{
padding: 0 .001em 0 0; /* Firefox doesn't render empty li's properly */
font-size: medium;
white-space: pre;
}
</style>
</head>
<body>
"""
NAV = """\
<div class="nav">
<a href="%(prev_link)s">%(prev_label)s</a> &lt;&lt;
<a href="%(up_link)s">%(up_label)s</a>
&gt;&gt; <a href="%(next_link)s">%(next_label)s</a>
</div>
"""
NAV_NO_PREV = """\
<div class="nav">
<a href="%(up_link)s">%(up_label)s</a>
&gt;&gt; <a href="%(next_link)s">%(next_label)s</a>
</div>
"""
NAV_NO_NEXT = """\
<div class="nav">
<a href="%(prev_link)s">%(prev_label)s</a> &lt;&lt;
<a href="%(up_link)s">%(up_label)s</a>
</div>
"""
CONTENT_HEADER = """\
<div id="content-header">
<h1>
<span id="module-name">%(title)s</span>:
%(total_count)d total statements,
<span class="%(severity)s">%(percent_covered)0.1f%% covered</span>
</h1>
<p>Generated: %(test_timestamp)s</p>
<p>Source file: %(source_file)s</p>
<p>
Stats:
<span class="executed">%(executed_count)d executed</span>,
<span class="missed">%(missed_count)d missed</span>,
<span class="excluded">%(excluded_count)d excluded</span>,
<span class="ignored">%(ignored_count)d ignored</span>
</p>
</div>
"""
CONTENT_BODY = """\
<div id="source-listing">
<ol>
%(source_lines)s
</ol>
</div>
"""
SOURCE_LINE = '<li class="%(line_status)s"><code>%(source_line)s</code></li>'
BOTTOM = """\
</body>
</html>
"""

View File

@ -0,0 +1,23 @@
"""
Copyright 2009 55 Minutes (http://www.55minutes.com)
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
from default_module_exceptions import *
title = "error packages and modules"
TOP = TOP %vars()
CONTENT_HEADER = CONTENT_HEADER %vars()

View File

@ -0,0 +1,89 @@
"""
Copyright 2009 55 Minutes (http://www.55minutes.com)
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
import time
test_timestamp = time.strftime('%a %Y-%m-%d %H:%M %Z')
TOP = """\
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-type" content="text/html;charset=UTF-8" />
<title>Test coverage report: %(title)s</title>
<style type="text/css" media="screen">
body
{
font-family: "Lucida Sans Unicode", "Lucida Grande", sans-serif;
font-size: 13px;
}
#content-header
{
margin-left: 50px;
}
#content-header h1
{
font-size: 18px;
margin-bottom: 0;
}
#content-header p
{
font-size: 13px;
margin: 0;
color: #909090;
}
#result-list
{
margin: 0 50px;
}
#result-list ul
{
padding-left: 13px;
list-style-position: inside;
}
</style>
</head>
<body>
"""
CONTENT_HEADER = """\
<div id="content-header">
<h1>Test Coverage Report: %(title)s</h1>"""
CONTENT_HEADER += "<p>Generated: %(test_timestamp)s</p>" %vars()
CONTENT_HEADER += "</div>"
CONTENT_BODY = """\
<div id="result-list">
<p>%(long_desc)s</p>
<ul>
%(exception_list)s
</ul>
Back to <a href="index.html">index</a>.
</div>
"""
EXCEPTION_LINE = "<li>%(module_name)s</li>"
BOTTOM = """\
</body>
</html>
"""

View File

@ -0,0 +1,23 @@
"""
Copyright 2009 55 Minutes (http://www.55minutes.com)
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
from default_module_exceptions import *
title = "excluded packages and modules"
TOP = TOP %vars()
CONTENT_HEADER = CONTENT_HEADER %vars()

View File

@ -0,0 +1,195 @@
"""
Copyright 2009 55 Minutes (http://www.55minutes.com)
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
TOP = """\
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-type" content="text/html;charset=UTF-8" />
<title>Test coverage report</title>
<style type="text/css" media="screen">
a
{
color: #3d707a;
}
a:hover, a:active
{
color: #bf7d18;
}
body
{
font-family: "Lucida Sans Unicode", "Lucida Grande", sans-serif;
font-size: 13px;
}
tr:hover
{
background: #f5f5f5;
}
#content-header
{
margin-left: 50px;
}
#content-header h1
{
font-size: 18px;
margin-bottom: 0;
}
#content-header p
{
font-size: 13px;
margin: 0;
color: #909090;
}
#result-list table
{
font-size: 13px;
background: white;
margin: 15px 50px;
width: 600px;
border-collapse: collapse;
text-align: right;
}
#result-list thead tr.last th,
th.statements
{
border-bottom: 1px solid #6d5e48;
}
th.statements
{
text-align: center;
}
#result-list th
{
padding: 3px 12px;
font-size: 14px;
font-weight: normal;
color: #937F61;
}
#result-list td
{
border-bottom: 1px solid #e0e0e0;
color: #606060;
padding: 6px 12px;
}
#result-list tfoot td
{
color: #937F61;
font-weight: bold;
}
#result-list .normal
{
color: #609030;
}
#result-list .warning
{
color: #d0a000;
}
#result-list .critical
{
color: red;
}
#result-list .module-name
{
text-align: left;
}
.footer-link
{
margin-left: 62px;
}
</style>
</head>
<body>
"""
CONTENT_HEADER = """\
<div id="content-header">
<h1>Test Coverage Report</h1>
<p>Generated: %(test_timestamp)s</p>
</div>
"""
CONTENT_BODY = """\
<div id="result-list">
<table>
<thead>
<tr>
<th>&nbsp;</th>
<th colspan="3" class="statements">Statements</th>
</tr>
<tr class="last">
<th class="module-name">Module</th>
<th>total</th>
<th>executed</th>
<th>excluded</th>
<th>%% covered</th>
</tr>
</thead>
<tfoot>
<tr>
<td class="module-name">Total</td>
<td>%(total_lines)d</td>
<td>%(total_executed)d</td>
<td>%(total_excluded)d</td>
<td>%(overall_covered)0.1f%%</td>
</tr>
</tfoot>
<tbody>
%(module_stats)s
</tbody>
</table>
</div>
"""
MODULE_STAT = """\
<tr>
<td class="module-name"><a href="%(module_link)s">%(module_name)s</a></td>
<td>%(total_count)d</td>
<td>%(executed_count)d</td>
<td>%(excluded_count)d</td>
<td class="%(severity)s">%(percent_covered)0.1f%%</td>
</tr>
"""
EXCEPTIONS_LINK = """\
<div>
<a class="footer-link" href="%(exceptions_link)s">
%(exception_desc)s
</a>
</div>
"""
BOTTOM = """\
</body>
</html>
"""

View File

@ -146,52 +146,64 @@ def reorder_suite(suite, classes):
bins[0].addTests(bins[i+1])
return bins[0]
def run_tests(test_labels, verbosity=1, interactive=True, extra_tests=[]):
class DefaultTestRunner(object):
"""
Run the unit tests for all the test labels in the provided list.
Labels must be of the form:
- app.TestClass.test_method
Run a single specific test method
- app.TestClass
Run all the test methods in a given class
- app
Search for doctests and unittests in the named application.
When looking for tests, the test runner will look in the models and
tests modules for the application.
A list of 'extra' tests may also be provided; these tests
will be added to the test suite.
Returns the number of tests that failed.
The original test runner. No coverage reporting.
"""
setup_test_environment()
settings.DEBUG = False
suite = unittest.TestSuite()
def __init__(self):
"""
Placeholder constructor. Want to make it obvious that it can
be overridden.
"""
pass
if test_labels:
for label in test_labels:
if '.' in label:
suite.addTest(build_test(label))
else:
app = get_app(label)
def run_tests(self, test_labels, verbosity=1, interactive=True, extra_tests=[]):
"""
Run the unit tests for all the test labels in the provided list.
Labels must be of the form:
- app.TestClass.test_method
Run a single specific test method
- app.TestClass
Run all the test methods in a given class
- app
Search for doctests and unittests in the named application.
When looking for tests, the test runner will look in the models and
tests modules for the application.
A list of 'extra' tests may also be provided; these tests
will be added to the test suite.
Returns the number of tests that failed.
"""
setup_test_environment()
settings.DEBUG = False
suite = unittest.TestSuite()
if test_labels:
for label in test_labels:
if '.' in label:
suite.addTest(build_test(label))
else:
app = get_app(label)
suite.addTest(build_suite(app))
else:
for app in get_apps():
suite.addTest(build_suite(app))
else:
for app in get_apps():
suite.addTest(build_suite(app))
for test in extra_tests:
suite.addTest(test)
for test in extra_tests:
suite.addTest(test)
suite = reorder_suite(suite, (TestCase,))
suite = reorder_suite(suite, (TestCase,))
old_name = settings.DATABASE_NAME
from django.db import connection
connection.creation.create_test_db(verbosity, autoclobber=not interactive)
result = unittest.TextTestRunner(verbosity=verbosity).run(suite)
connection.creation.destroy_test_db(old_name, verbosity)
old_name = settings.DATABASE_NAME
from django.db import connection
connection.creation.create_test_db(verbosity, autoclobber=not interactive)
result = unittest.TextTestRunner(verbosity=verbosity).run(suite)
connection.creation.destroy_test_db(old_name, verbosity)
teardown_test_environment()
teardown_test_environment()
return len(result.failures) + len(result.errors)
return len(result.failures) + len(result.errors)

View File

@ -0,0 +1,106 @@
import coverage
import os, sys
from django.conf import settings
from django.db.models import get_app, get_apps
from django.test.simple import DefaultTestRunner as base_run_tests
from django.utils.module_tools import get_all_modules
from django.test.coverage_report import html_report
from django.utils.translation import ugettext as _
def _get_app_package(app_model_module):
"""
Returns the app module name from the app model module.
"""
return '.'.join(app_model_module.__name__.split('.')[:-1])
class BaseCoverageRunner(object):
"""
Placeholder class for coverage runners. Intended to be easily extended.
"""
def __init__(self):
"""Placeholder (since it is overrideable)"""
pass
def run_tests(self, test_labels, verbosity=1, interactive=True,
extra_tests=[]):
"""
Runs the specified tests while generating code coverage statistics. Upon
the tests' completion, the results are printed to stdout.
"""
#Allow an on-disk cache of coverage stats.
#coverage.use_cache(0)
for e in getattr(settings, 'COVERAGE_CODE_EXCLUDES', []):
coverage.exclude(e)
coverage.start()
brt = base_run_tests()
results = brt.run_tests(test_labels, verbosity, interactive, extra_tests)
coverage.stop()
coverage_modules = []
if test_labels:
for label in test_labels:
label = label.split('.')[0]
app = get_app(label)
coverage_modules.append(_get_app_package(app))
else:
for app in get_apps():
coverage_modules.append(_get_app_package(app))
coverage_modules.extend(getattr(settings, 'COVERAGE_ADDITIONAL_MODULES', []))
packages, self.modules, self.excludes, self.errors = get_all_modules(
coverage_modules, getattr(settings, 'COVERAGE_MODULE_EXCLUDES', []),
getattr(settings, 'COVERAGE_PATH_EXCLUDES', []))
coverage.report(self.modules.values(), show_missing=1)
if self.excludes:
print >> sys.stdout
print >> sys.stdout, _("The following packages or modules were excluded:"),
for e in self.excludes:
print >> sys.stdout, e,
print >>sys.stdout
if self.errors:
print >> sys.stdout
print >> sys.stderr, _("There were problems with the following packages or modules:"),
for e in self.errors:
print >> sys.stderr, e,
print >> sys.stdout
return results
class ReportingCoverageRunner(BaseCoverageRunner):
"""Runs coverage.py analysis, as well as generating detailed HTML reports."""
def __init__(self, outdir = None):
"""
Constructor, overrides BaseCoverageRunner. Sets output directory
for reports. Parameter or setting.
"""
if(outdir):
self.outdir = outdir
else:
# Realistically, we aren't going to ship the entire reporting framework..
# but for the time being I have left it in.
self.outdir = getattr(settings, 'COVERAGE_REPORT_HTML_OUTPUT_DIR', 'test_html')
self.outdir = os.path.abspath(self.outdir)
# Create directory
if( not os.path.exists(self.outdir)):
os.mkdir(self.outdir)
def run_tests(self, *args, **kwargs):
"""
Overrides BaseCoverageRunner.run_tests, and adds html report generation
with the results
"""
res = BaseCoverageRunner.run_tests(self, *args, **kwargs)
html_report(self.outdir, self.modules, self.excludes, self.errors)
print >>sys.stdout
print >>sys.stdout, _("HTML reports were output to '%s'") %self.outdir
return res

View File

@ -80,8 +80,18 @@ def teardown_test_environment():
del mail.outbox
def get_runner(settings):
test_path = settings.TEST_RUNNER.split('.')
def get_runner(settings, coverage = False, reports = False):
"""
Based on the settings and parameters, returns the appropriate test
runner class.
"""
if(coverage):
if(reports):
test_path = 'django.test.test_coverage.ReportingCoverageRunner'.split('.')
else:
test_path = settings.COVERAGE_TEST_RUNNER.split('.')
else:
test_path = settings.TEST_RUNNER.split('.')
# Allow for Python 2.5 relative paths
if len(test_path) > 1:
test_module_name = '.'.join(test_path[:-1])

View File

@ -0,0 +1,19 @@
"""
Copyright 2009 55 Minutes (http://www.55minutes.com)
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
from module_loader import *
from module_walker import *

View File

@ -0,0 +1,42 @@
"""
Copyright 2009 55 Minutes (http://www.55minutes.com)
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
__all__ = ('Packages', 'Modules', 'Excluded', 'Errors')
class SingletonType(type):
def __call__(cls, *args, **kwargs):
if getattr(cls, '__instance__', None) is None:
instance = cls.__new__(cls)
instance.__init__(*args, **kwargs)
cls.__instance__ = instance
return cls.__instance__
class Packages(object):
__metaclass__ = SingletonType
packages = {}
class Modules(object):
__metaclass__ = SingletonType
modules = {}
class Excluded(object):
__metaclass__ = SingletonType
excluded = []
class Errors(object):
__metaclass__ = SingletonType
errors = []

View File

@ -0,0 +1,79 @@
"""
Copyright 2009 55 Minutes (http://www.55minutes.com)
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
import imp, sys, types
__all__ = ('find_or_load_module',)
def _brute_force_find_module(module_name, module_path, module_type):
for m in [m for n, m in sys.modules.iteritems() if type(m) == types.ModuleType]:
m_path = []
try:
if module_type in (imp.PY_COMPILED, imp.PY_SOURCE):
m_path = [m.__file__]
elif module_type==imp.PKG_DIRECTORY:
m_path = m.__path__
except AttributeError:
pass
for p in m_path:
if p.startswith(module_path):
return m
return None
def _load_module(module_name, fo, fp, desc):
suffix, mode, mtype = desc
if module_name in sys.modules and \
sys.modules[module_name].__file__.startswith(fp):
module = sys.modules[module_name]
else:
module = _brute_force_find_module(module_name, fp, mtype)
if not module:
try:
module = imp.load_module(module_name, fo, fp, desc)
except:
raise ImportError
return module
def _load_package(pkg_name, fp, desc):
suffix, mode, mtype = desc
if pkg_name in sys.modules:
if fp in sys.modules[pkg_name].__path__:
pkg = sys.modules[pkg_name]
else:
pkg = _brute_force_find_module(pkg_name, fp, mtype)
if not pkg:
pkg = imp.load_module(pkg_name, None, fp, desc)
return pkg
def find_or_load_module(module_name, path=None):
"""
Attempts to lookup ``module_name`` in ``sys.modules``, else uses the
facilities in the ``imp`` module to load the module.
If module_name specified is not of type ``imp.PY_SOURCE`` or
``imp.PKG_DIRECTORY``, raise ``ImportError`` since we don't know
what to do with those.
"""
fo, fp, desc = imp.find_module(module_name.split('.')[-1], path)
suffix, mode, mtype = desc
if mtype in (imp.PY_SOURCE, imp.PY_COMPILED):
module = _load_module(module_name, fo, fp, desc)
elif mtype==imp.PKG_DIRECTORY:
module = _load_package(module_name, fp, desc)
else:
raise ImportError("Don't know how to handle this module type.")
return module

View File

@ -0,0 +1,135 @@
"""
Copyright 2009 55 Minutes (http://www.55minutes.com)
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
import os, re, sys
from glob import glob
from data_storage import *
from module_loader import find_or_load_module
try:
set
except:
from sets import Set as set
__all__ = ('get_all_modules',)
def _build_pkg_path(pkg_name, pkg, path):
for rp in [x for x in pkg.__path__ if path.startswith(x)]:
p = path.replace(rp, '').replace(os.path.sep, '.')
return pkg_name + p
def _build_module_path(pkg_name, pkg, path):
return _build_pkg_path(pkg_name, pkg, os.path.splitext(path)[0])
def _prune_whitelist(whitelist, blacklist):
excluded = Excluded().excluded
for wp in whitelist[:]:
for bp in blacklist:
if re.search(bp, wp):
whitelist.remove(wp)
excluded.append(wp)
break
return whitelist
def _parse_module_list(m_list):
packages = Packages().packages
modules = Modules().modules
excluded = Excluded().excluded
errors = Errors().errors
for m in m_list:
components = m.split('.')
m_name = ''
search_path = []
processed=False
for i, c in enumerate(components):
m_name = '.'.join([x for x in m_name.split('.') if x] + [c])
try:
module = find_or_load_module(m_name, search_path or None)
except ImportError:
processed=True
errors.append(m)
break
try:
search_path.extend(module.__path__)
except AttributeError:
processed = True
if i+1==len(components):
modules[m_name] = module
else:
errors.append(m)
break
if not processed:
packages[m_name] = module
def prune_dirs(root, dirs, exclude_dirs):
_dirs = [os.path.join(root, d) for d in dirs]
for i, p in enumerate(_dirs):
for e in exclude_dirs:
if re.search(e, p):
del dirs[i]
break
def _get_all_packages(pkg_name, pkg, blacklist, exclude_dirs):
packages = Packages().packages
errors = Errors().errors
for path in pkg.__path__:
for root, dirs, files in os.walk(path):
prune_dirs(root, dirs, exclude_dirs or [])
m_name = _build_pkg_path(pkg_name, pkg, root)
try:
if _prune_whitelist([m_name], blacklist):
m = find_or_load_module(m_name, [os.path.split(root)[0]])
packages[m_name] = m
else:
for d in dirs[:]:
dirs.remove(d)
except ImportError:
errors.append(m_name)
for d in dirs[:]:
dirs.remove(d)
def _get_all_modules(pkg_name, pkg, blacklist):
modules = Modules().modules
errors = Errors().errors
for p in pkg.__path__:
for f in glob('%s/*.py' %p):
m_name = _build_module_path(pkg_name, pkg, f)
try:
if _prune_whitelist([m_name], blacklist):
m = find_or_load_module(m_name, [p])
modules[m_name] = m
except ImportError:
errors.append(m_name)
def get_all_modules(whitelist, blacklist=None, exclude_dirs=None):
packages = Packages().packages
modules = Modules().modules
excluded = Excluded().excluded
errors = Errors().errors
whitelist = _prune_whitelist(whitelist, blacklist or [])
_parse_module_list(whitelist)
for pkg_name, pkg in packages.copy().iteritems():
_get_all_packages(pkg_name, pkg, blacklist, exclude_dirs)
for pkg_name, pkg in packages.copy().iteritems():
_get_all_modules(pkg_name, pkg, blacklist)
return packages, modules, list(set(excluded)), list(set(errors))

View File

@ -1175,8 +1175,9 @@ True
>>> instance.height
16
# Delete the current file since this is not done by Django.
>>> instance.image.delete()
# Delete the current file since this is not done by Django, but don't save
# because the dimension fields are not null=True.
>>> instance.image.delete(save=False)
>>> f = ImageFileForm(data={'description': u'An image'}, files={'image': SimpleUploadedFile('test.png', image_data)})
>>> f.is_valid()
@ -1207,9 +1208,9 @@ True
>>> instance.width
16
# Delete the current image since this is not done by Django.
>>> instance.image.delete()
# Delete the current file since this is not done by Django, but don't save
# because the dimension fields are not null=True.
>>> instance.image.delete(save=False)
# Override the file by uploading a new one.
@ -1224,8 +1225,9 @@ True
>>> instance.width
48
# Delete the current file since this is not done by Django.
>>> instance.image.delete()
# Delete the current file since this is not done by Django, but don't save
# because the dimension fields are not null=True.
>>> instance.image.delete(save=False)
>>> instance.delete()
>>> f = ImageFileForm(data={'description': u'Changed it'}, files={'image': SimpleUploadedFile('test2.png', image_data2)})
@ -1239,8 +1241,9 @@ True
>>> instance.width
48
# Delete the current file since this is not done by Django.
>>> instance.image.delete()
# Delete the current file since this is not done by Django, but don't save
# because the dimension fields are not null=True.
>>> instance.image.delete(save=False)
>>> instance.delete()
# Test the non-required ImageField

View File

@ -1,93 +0,0 @@
import os
import tempfile
import shutil
from django.db import models
from django.core.files.storage import FileSystemStorage
from django.core.files.base import ContentFile
# Test for correct behavior of width_field/height_field.
# Of course, we can't run this without PIL.
try:
# Checking for the existence of Image is enough for CPython, but
# for PyPy, you need to check for the underlying modules
from PIL import Image, _imaging
except ImportError:
Image = None
# If we have PIL, do these tests
if Image:
temp_storage_dir = tempfile.mkdtemp()
temp_storage = FileSystemStorage(temp_storage_dir)
class Person(models.Model):
name = models.CharField(max_length=50)
mugshot = models.ImageField(storage=temp_storage, upload_to='tests',
height_field='mug_height',
width_field='mug_width')
mug_height = models.PositiveSmallIntegerField()
mug_width = models.PositiveSmallIntegerField()
__test__ = {'API_TESTS': """
>>> from django.core.files import File
>>> image_data = open(os.path.join(os.path.dirname(__file__), "test.png"), 'rb').read()
>>> p = Person(name="Joe")
>>> p.mugshot.save("mug", ContentFile(image_data))
>>> p.mugshot.width
16
>>> p.mugshot.height
16
>>> p.mug_height
16
>>> p.mug_width
16
# Bug #9786: Ensure '==' and '!=' work correctly.
>>> image_data = open(os.path.join(os.path.dirname(__file__), "test1.png"), 'rb').read()
>>> p1 = Person(name="Bob")
>>> p1.mugshot.save("mug", ContentFile(image_data))
>>> p2 = Person.objects.get(name="Joe")
>>> p.mugshot == p2.mugshot
True
>>> p.mugshot != p2.mugshot
False
>>> p.mugshot != p1.mugshot
True
Bug #9508: Similarly to the previous test, make sure hash() works as expected
(equal items must hash to the same value).
>>> hash(p.mugshot) == hash(p2.mugshot)
True
# Bug #8175: correctly delete files that have been removed off the file system.
>>> import os
>>> p2 = Person(name="Fred")
>>> p2.mugshot.save("shot", ContentFile(image_data))
>>> os.remove(p2.mugshot.path)
>>> p2.delete()
# Bug #8534: FileField.size should not leave the file open.
>>> p3 = Person(name="Joan")
>>> p3.mugshot.save("shot", ContentFile(image_data))
# Get a "clean" model instance
>>> p3 = Person.objects.get(name="Joan")
# It won't have an opened file.
>>> p3.mugshot.closed
True
# After asking for the size, the file should still be closed.
>>> _ = p3.mugshot.size
>>> p3.mugshot.closed
True
# Make sure that wrapping the file in a file still works
>>> p3.mugshot.file.open()
>>> p = Person.objects.create(name="Bob The Builder", mugshot=File(p3.mugshot.file))
>>> p.save()
>>> p3.mugshot.file.close()
# Delete all test files
>>> shutil.rmtree(temp_storage_dir)
"""}

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 B

View File

@ -0,0 +1,403 @@
import os
import shutil
from django.core.files import File
from django.core.files.base import ContentFile
from django.core.files.images import ImageFile
from django.test import TestCase
from models import Image, Person, PersonWithHeight, PersonWithHeightAndWidth, \
PersonDimensionsFirst, PersonTwoImages, TestImageFieldFile
# If PIL available, do these tests.
if Image:
from models import temp_storage_dir
class ImageFieldTestMixin(object):
"""
Mixin class to provide common functionality to ImageField test classes.
"""
# Person model to use for tests.
PersonModel = PersonWithHeightAndWidth
# File class to use for file instances.
File = ImageFile
def setUp(self):
"""
Creates a pristine temp directory (or deletes and recreates if it
already exists) that the model uses as its storage directory.
Sets up two ImageFile instances for use in tests.
"""
if os.path.exists(temp_storage_dir):
shutil.rmtree(temp_storage_dir)
os.mkdir(temp_storage_dir)
file_path1 = os.path.join(os.path.dirname(__file__), "4x8.png")
self.file1 = self.File(open(file_path1, 'rb'))
file_path2 = os.path.join(os.path.dirname(__file__), "8x4.png")
self.file2 = self.File(open(file_path2, 'rb'))
def tearDown(self):
"""
Removes temp directory and all its contents.
"""
shutil.rmtree(temp_storage_dir)
def check_dimensions(self, instance, width, height,
field_name='mugshot'):
"""
Asserts that the given width and height values match both the
field's height and width attributes and the height and width fields
(if defined) the image field is caching to.
Note, this method will check for dimension fields named by adding
"_width" or "_height" to the name of the ImageField. So, the
models used in these tests must have their fields named
accordingly.
By default, we check the field named "mugshot", but this can be
specified by passing the field_name parameter.
"""
field = getattr(instance, field_name)
# Check height/width attributes of field.
if width is None and height is None:
self.assertRaises(ValueError, getattr, field, 'width')
self.assertRaises(ValueError, getattr, field, 'height')
else:
self.assertEqual(field.width, width)
self.assertEqual(field.height, height)
# Check height/width fields of model, if defined.
width_field_name = field_name + '_width'
if hasattr(instance, width_field_name):
self.assertEqual(getattr(instance, width_field_name), width)
height_field_name = field_name + '_height'
if hasattr(instance, height_field_name):
self.assertEqual(getattr(instance, height_field_name), height)
class ImageFieldTests(ImageFieldTestMixin, TestCase):
"""
Tests for ImageField that don't need to be run with each of the
different test model classes.
"""
def test_equal_notequal_hash(self):
"""
Bug #9786: Ensure '==' and '!=' work correctly.
Bug #9508: make sure hash() works as expected (equal items must
hash to the same value).
"""
# Create two Persons with different mugshots.
p1 = self.PersonModel(name="Joe")
p1.mugshot.save("mug", self.file1)
p2 = self.PersonModel(name="Bob")
p2.mugshot.save("mug", self.file2)
self.assertEqual(p1.mugshot == p2.mugshot, False)
self.assertEqual(p1.mugshot != p2.mugshot, True)
# Test again with an instance fetched from the db.
p1_db = self.PersonModel.objects.get(name="Joe")
self.assertEqual(p1_db.mugshot == p2.mugshot, False)
self.assertEqual(p1_db.mugshot != p2.mugshot, True)
# Instance from db should match the local instance.
self.assertEqual(p1_db.mugshot == p1.mugshot, True)
self.assertEqual(hash(p1_db.mugshot), hash(p1.mugshot))
self.assertEqual(p1_db.mugshot != p1.mugshot, False)
def test_instantiate_missing(self):
"""
If the underlying file is unavailable, still create instantiate the
object without error.
"""
p = self.PersonModel(name="Joan")
p.mugshot.save("shot", self.file1)
p = self.PersonModel.objects.get(name="Joan")
path = p.mugshot.path
shutil.move(path, path + '.moved')
p2 = self.PersonModel.objects.get(name="Joan")
def test_delete_when_missing(self):
"""
Bug #8175: correctly delete an object where the file no longer
exists on the file system.
"""
p = self.PersonModel(name="Fred")
p.mugshot.save("shot", self.file1)
os.remove(p.mugshot.path)
p.delete()
def test_size_method(self):
"""
Bug #8534: FileField.size should not leave the file open.
"""
p = self.PersonModel(name="Joan")
p.mugshot.save("shot", self.file1)
# Get a "clean" model instance
p = self.PersonModel.objects.get(name="Joan")
# It won't have an opened file.
self.assertEqual(p.mugshot.closed, True)
# After asking for the size, the file should still be closed.
_ = p.mugshot.size
self.assertEqual(p.mugshot.closed, True)
class ImageFieldTwoDimensionsTests(ImageFieldTestMixin, TestCase):
"""
Tests behavior of an ImageField and its dimensions fields.
"""
def test_constructor(self):
"""
Tests assigning an image field through the model's constructor.
"""
p = self.PersonModel(name='Joe', mugshot=self.file1)
self.check_dimensions(p, 4, 8)
p.save()
self.check_dimensions(p, 4, 8)
def test_image_after_constructor(self):
"""
Tests behavior when image is not passed in constructor.
"""
p = self.PersonModel(name='Joe')
# TestImageField value will default to being an instance of its
# attr_class, a TestImageFieldFile, with name == None, which will
# cause it to evaluate as False.
self.assertEqual(isinstance(p.mugshot, TestImageFieldFile), True)
self.assertEqual(bool(p.mugshot), False)
# Test setting a fresh created model instance.
p = self.PersonModel(name='Joe')
p.mugshot = self.file1
self.check_dimensions(p, 4, 8)
def test_create(self):
"""
Tests assigning an image in Manager.create().
"""
p = self.PersonModel.objects.create(name='Joe', mugshot=self.file1)
self.check_dimensions(p, 4, 8)
def test_default_value(self):
"""
Tests that the default value for an ImageField is an instance of
the field's attr_class (TestImageFieldFile in this case) with no
name (name set to None).
"""
p = self.PersonModel()
self.assertEqual(isinstance(p.mugshot, TestImageFieldFile), True)
self.assertEqual(bool(p.mugshot), False)
def test_assignment_to_None(self):
"""
Tests that assigning ImageField to None clears dimensions.
"""
p = self.PersonModel(name='Joe', mugshot=self.file1)
self.check_dimensions(p, 4, 8)
# If image assigned to None, dimension fields should be cleared.
p.mugshot = None
self.check_dimensions(p, None, None)
p.mugshot = self.file2
self.check_dimensions(p, 8, 4)
def test_field_save_and_delete_methods(self):
"""
Tests assignment using the field's save method and deletion using
the field's delete method.
"""
p = self.PersonModel(name='Joe')
p.mugshot.save("mug", self.file1)
self.check_dimensions(p, 4, 8)
# A new file should update dimensions.
p.mugshot.save("mug", self.file2)
self.check_dimensions(p, 8, 4)
# Field and dimensions should be cleared after a delete.
p.mugshot.delete(save=False)
self.assertEqual(p.mugshot, None)
self.check_dimensions(p, None, None)
def test_dimensions(self):
"""
Checks that dimensions are updated correctly in various situations.
"""
p = self.PersonModel(name='Joe')
# Dimensions should get set if file is saved.
p.mugshot.save("mug", self.file1)
self.check_dimensions(p, 4, 8)
# Test dimensions after fetching from database.
p = self.PersonModel.objects.get(name='Joe')
# Bug 11084: Dimensions should not get recalculated if file is
# coming from the database. We test this by checking if the file
# was opened.
self.assertEqual(p.mugshot.was_opened, False)
self.check_dimensions(p, 4, 8)
# After checking dimensions on the image field, the file will have
# opened.
self.assertEqual(p.mugshot.was_opened, True)
# Dimensions should now be cached, and if we reset was_opened and
# check dimensions again, the file should not have opened.
p.mugshot.was_opened = False
self.check_dimensions(p, 4, 8)
self.assertEqual(p.mugshot.was_opened, False)
# If we assign a new image to the instance, the dimensions should
# update.
p.mugshot = self.file2
self.check_dimensions(p, 8, 4)
# Dimensions were recalculated, and hence file should have opened.
self.assertEqual(p.mugshot.was_opened, True)
class ImageFieldNoDimensionsTests(ImageFieldTwoDimensionsTests):
"""
Tests behavior of an ImageField with no dimension fields.
"""
PersonModel = Person
class ImageFieldOneDimensionTests(ImageFieldTwoDimensionsTests):
"""
Tests behavior of an ImageField with one dimensions field.
"""
PersonModel = PersonWithHeight
class ImageFieldDimensionsFirstTests(ImageFieldTwoDimensionsTests):
"""
Tests behavior of an ImageField where the dimensions fields are
defined before the ImageField.
"""
PersonModel = PersonDimensionsFirst
class ImageFieldUsingFileTests(ImageFieldTwoDimensionsTests):
"""
Tests behavior of an ImageField when assigning it a File instance
rather than an ImageFile instance.
"""
PersonModel = PersonDimensionsFirst
File = File
class TwoImageFieldTests(ImageFieldTestMixin, TestCase):
"""
Tests a model with two ImageFields.
"""
PersonModel = PersonTwoImages
def test_constructor(self):
p = self.PersonModel(mugshot=self.file1, headshot=self.file2)
self.check_dimensions(p, 4, 8, 'mugshot')
self.check_dimensions(p, 8, 4, 'headshot')
p.save()
self.check_dimensions(p, 4, 8, 'mugshot')
self.check_dimensions(p, 8, 4, 'headshot')
def test_create(self):
p = self.PersonModel.objects.create(mugshot=self.file1,
headshot=self.file2)
self.check_dimensions(p, 4, 8)
self.check_dimensions(p, 8, 4, 'headshot')
def test_assignment(self):
p = self.PersonModel()
self.check_dimensions(p, None, None, 'mugshot')
self.check_dimensions(p, None, None, 'headshot')
p.mugshot = self.file1
self.check_dimensions(p, 4, 8, 'mugshot')
self.check_dimensions(p, None, None, 'headshot')
p.headshot = self.file2
self.check_dimensions(p, 4, 8, 'mugshot')
self.check_dimensions(p, 8, 4, 'headshot')
# Clear the ImageFields one at a time.
p.mugshot = None
self.check_dimensions(p, None, None, 'mugshot')
self.check_dimensions(p, 8, 4, 'headshot')
p.headshot = None
self.check_dimensions(p, None, None, 'mugshot')
self.check_dimensions(p, None, None, 'headshot')
def test_field_save_and_delete_methods(self):
p = self.PersonModel(name='Joe')
p.mugshot.save("mug", self.file1)
self.check_dimensions(p, 4, 8, 'mugshot')
self.check_dimensions(p, None, None, 'headshot')
p.headshot.save("head", self.file2)
self.check_dimensions(p, 4, 8, 'mugshot')
self.check_dimensions(p, 8, 4, 'headshot')
# We can use save=True when deleting the image field with null=True
# dimension fields and the other field has an image.
p.headshot.delete(save=True)
self.check_dimensions(p, 4, 8, 'mugshot')
self.check_dimensions(p, None, None, 'headshot')
p.mugshot.delete(save=False)
self.check_dimensions(p, None, None, 'mugshot')
self.check_dimensions(p, None, None, 'headshot')
def test_dimensions(self):
"""
Checks that dimensions are updated correctly in various situations.
"""
p = self.PersonModel(name='Joe')
# Dimensions should get set for the saved file.
p.mugshot.save("mug", self.file1)
p.headshot.save("head", self.file2)
self.check_dimensions(p, 4, 8, 'mugshot')
self.check_dimensions(p, 8, 4, 'headshot')
# Test dimensions after fetching from database.
p = self.PersonModel.objects.get(name='Joe')
# Bug 11084: Dimensions should not get recalculated if file is
# coming from the database. We test this by checking if the file
# was opened.
self.assertEqual(p.mugshot.was_opened, False)
self.assertEqual(p.headshot.was_opened, False)
self.check_dimensions(p, 4, 8,'mugshot')
self.check_dimensions(p, 8, 4, 'headshot')
# After checking dimensions on the image fields, the files will
# have been opened.
self.assertEqual(p.mugshot.was_opened, True)
self.assertEqual(p.headshot.was_opened, True)
# Dimensions should now be cached, and if we reset was_opened and
# check dimensions again, the file should not have opened.
p.mugshot.was_opened = False
p.headshot.was_opened = False
self.check_dimensions(p, 4, 8,'mugshot')
self.check_dimensions(p, 8, 4, 'headshot')
self.assertEqual(p.mugshot.was_opened, False)
self.assertEqual(p.headshot.was_opened, False)
# If we assign a new image to the instance, the dimensions should
# update.
p.mugshot = self.file2
p.headshot = self.file1
self.check_dimensions(p, 8, 4, 'mugshot')
self.check_dimensions(p, 4, 8, 'headshot')
# Dimensions were recalculated, and hence file should have opened.
self.assertEqual(p.mugshot.was_opened, True)
self.assertEqual(p.headshot.was_opened, True)

View File

@ -1,10 +1,23 @@
from django.db import models
import os
import tempfile
try:
import decimal
except ImportError:
from django.utils import _decimal as decimal # Python 2.3 fallback
try:
# Checking for the existence of Image is enough for CPython, but for PyPy,
# you need to check for the underlying modules.
from PIL import Image, _imaging
except ImportError:
Image = None
from django.core.files.storage import FileSystemStorage
from django.db import models
from django.db.models.fields.files import ImageFieldFile, ImageField
class Foo(models.Model):
a = models.CharField(max_length=10)
d = models.DecimalField(max_digits=5, decimal_places=3)
@ -31,9 +44,97 @@ class Whiz(models.Model):
(0,'Other'),
)
c = models.IntegerField(choices=CHOICES, null=True)
class BigD(models.Model):
d = models.DecimalField(max_digits=38, decimal_places=30)
class BigS(models.Model):
s = models.SlugField(max_length=255)
s = models.SlugField(max_length=255)
###############################################################################
# ImageField
# If PIL available, do these tests.
if Image:
class TestImageFieldFile(ImageFieldFile):
"""
Custom Field File class that records whether or not the underlying file
was opened.
"""
def __init__(self, *args, **kwargs):
self.was_opened = False
super(TestImageFieldFile, self).__init__(*args,**kwargs)
def open(self):
self.was_opened = True
super(TestImageFieldFile, self).open()
class TestImageField(ImageField):
attr_class = TestImageFieldFile
# Set up a temp directory for file storage.
temp_storage_dir = tempfile.mkdtemp()
temp_storage = FileSystemStorage(temp_storage_dir)
temp_upload_to_dir = os.path.join(temp_storage.location, 'tests')
class Person(models.Model):
"""
Model that defines an ImageField with no dimension fields.
"""
name = models.CharField(max_length=50)
mugshot = TestImageField(storage=temp_storage, upload_to='tests')
class PersonWithHeight(models.Model):
"""
Model that defines an ImageField with only one dimension field.
"""
name = models.CharField(max_length=50)
mugshot = TestImageField(storage=temp_storage, upload_to='tests',
height_field='mugshot_height')
mugshot_height = models.PositiveSmallIntegerField()
class PersonWithHeightAndWidth(models.Model):
"""
Model that defines height and width fields after the ImageField.
"""
name = models.CharField(max_length=50)
mugshot = TestImageField(storage=temp_storage, upload_to='tests',
height_field='mugshot_height',
width_field='mugshot_width')
mugshot_height = models.PositiveSmallIntegerField()
mugshot_width = models.PositiveSmallIntegerField()
class PersonDimensionsFirst(models.Model):
"""
Model that defines height and width fields before the ImageField.
"""
name = models.CharField(max_length=50)
mugshot_height = models.PositiveSmallIntegerField()
mugshot_width = models.PositiveSmallIntegerField()
mugshot = TestImageField(storage=temp_storage, upload_to='tests',
height_field='mugshot_height',
width_field='mugshot_width')
class PersonTwoImages(models.Model):
"""
Model that:
* Defines two ImageFields
* Defines the height/width fields before the ImageFields
* Has a nullalble ImageField
"""
name = models.CharField(max_length=50)
mugshot_height = models.PositiveSmallIntegerField()
mugshot_width = models.PositiveSmallIntegerField()
mugshot = TestImageField(storage=temp_storage, upload_to='tests',
height_field='mugshot_height',
width_field='mugshot_width')
headshot_height = models.PositiveSmallIntegerField(
blank=True, null=True)
headshot_width = models.PositiveSmallIntegerField(
blank=True, null=True)
headshot = TestImageField(blank=True, null=True,
storage=temp_storage, upload_to='tests',
height_field='headshot_height',
width_field='headshot_width')
###############################################################################

View File

@ -6,13 +6,26 @@ from django import forms
from django.db import models
from django.core.exceptions import ValidationError
from models import Foo, Bar, Whiz, BigD, BigS
from models import Foo, Bar, Whiz, BigD, BigS, Image
try:
from decimal import Decimal
except ImportError:
from django.utils._decimal import Decimal
# If PIL available, do these tests.
if Image:
from imagefield import \
ImageFieldTests, \
ImageFieldTwoDimensionsTests, \
ImageFieldNoDimensionsTests, \
ImageFieldOneDimensionTests, \
ImageFieldDimensionsFirstTests, \
ImageFieldUsingFileTests, \
TwoImageFieldTests
class DecimalFieldTests(django.test.TestCase):
def test_to_python(self):
f = models.DecimalField(max_digits=4, decimal_places=2)
@ -131,4 +144,3 @@ class SlugFieldTests(django.test.TestCase):
bs = BigS.objects.create(s = 'slug'*50)
bs = BigS.objects.get(pk=bs.pk)
self.assertEqual(bs.s, 'slug'*50)

View File

@ -2,7 +2,7 @@
import os, sys, traceback
import unittest
import coverage
import django.contrib as contrib
try:
@ -100,7 +100,7 @@ def django_tests(verbosity, interactive, test_labels):
# Redirect some settings for the duration of these tests.
settings.INSTALLED_APPS = ALWAYS_INSTALLED_APPS
settings.ROOT_URLCONF = 'urls'
settings.TEMPLATE_DIRS = (os.path.join(os.path.dirname(__file__), TEST_TEMPLATE_DIR),)
settings.TEMPLATE_DIRS = (os.path.join(os.path.dirname(__file__), TEST_TEMPLATE_DIR), )
settings.USE_I18N = True
settings.LANGUAGE_CODE = 'en'
settings.LOGIN_URL = '/accounts/login/'
@ -113,7 +113,7 @@ def django_tests(verbosity, interactive, test_labels):
# For testing comment-utils, we require the MANAGERS attribute
# to be set, so that a test email is sent out which we catch
# in our tests.
settings.MANAGERS = ("admin@djangoproject.com",)
settings.MANAGERS = ("admin@djangoproject.com", )
# Load all the ALWAYS_INSTALLED_APPS.
# (This import statement is intentionally delayed until after we
@ -156,7 +156,11 @@ def django_tests(verbosity, interactive, test_labels):
from django.test.utils import get_runner
if not hasattr(settings, 'TEST_RUNNER'):
settings.TEST_RUNNER = 'django.test.simple.run_tests'
test_runner = get_runner(settings)
settings.COVERAGE_MODULE_EXCLUDES = ['modeltests', 'regressiontests', '__init__']
settings.COVERAGE_CODE_EXCLUDES = ['def __unicode__\(self\):', 'def get_absolute_url\(self\):', ]
settings.COVERAGE_ADDITIONAL_MODULES = ['django']
# 'from .* import .*', 'import .*',
test_runner = get_runner(settings, coverage=True)
failures = test_runner(test_labels, verbosity=verbosity, interactive=interactive, extra_tests=extra_tests)
if failures:
@ -175,7 +179,7 @@ if __name__ == "__main__":
from optparse import OptionParser
usage = "%prog [options] [model model model ...]"
parser = OptionParser(usage=usage)
parser.add_option('-v','--verbosity', action='store', dest='verbosity', default='0',
parser.add_option('-v', '--verbosity', action='store', dest='verbosity', default='0',
type='choice', choices=['0', '1', '2'],
help='Verbosity level; 0=minimal output, 1=normal output, 2=all output')
parser.add_option('--noinput', action='store_false', dest='interactive', default=True,