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:
parent
6399b2937a
commit
39676e8f56
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
*.pyc
|
@ -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 #
|
||||
############
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
18
django/test/coverage_report/__init__.py
Normal file
18
django/test/coverage_report/__init__.py
Normal 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 *
|
||||
|
57
django/test/coverage_report/data_storage.py
Normal file
57
django/test/coverage_report/data_storage.py
Normal 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)
|
||||
|
91
django/test/coverage_report/html_module_detail.py
Normal file
91
django/test/coverage_report/html_module_detail.py
Normal 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()
|
||||
|
53
django/test/coverage_report/html_module_errors.py
Normal file
53
django/test/coverage_report/html_module_errors.py
Normal 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)
|
32
django/test/coverage_report/html_module_exceptions.py
Normal file
32
django/test/coverage_report/html_module_exceptions.py
Normal 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()
|
||||
|
55
django/test/coverage_report/html_module_excludes.py
Normal file
55
django/test/coverage_report/html_module_excludes.py
Normal 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)
|
||||
|
143
django/test/coverage_report/html_report.py
Normal file
143
django/test/coverage_report/html_report.py
Normal 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()
|
||||
|
17
django/test/coverage_report/templates/__init__.py
Normal file
17
django/test/coverage_report/templates/__init__.py
Normal 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.
|
||||
"""
|
||||
|
||||
|
193
django/test/coverage_report/templates/default_module_detail.py
Normal file
193
django/test/coverage_report/templates/default_module_detail.py
Normal 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> <<
|
||||
<a href="%(up_link)s">%(up_label)s</a>
|
||||
>> <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>
|
||||
>> <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> <<
|
||||
<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>
|
||||
"""
|
@ -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()
|
||||
|
@ -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>
|
||||
"""
|
@ -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()
|
||||
|
195
django/test/coverage_report/templates/default_module_index.py
Normal file
195
django/test/coverage_report/templates/default_module_index.py
Normal 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> </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>
|
||||
"""
|
@ -146,7 +146,19 @@ 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):
|
||||
"""
|
||||
The original test runner. No coverage reporting.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""
|
||||
Placeholder constructor. Want to make it obvious that it can
|
||||
be overridden.
|
||||
"""
|
||||
pass
|
||||
|
||||
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:
|
||||
|
106
django/test/test_coverage.py
Normal file
106
django/test/test_coverage.py
Normal 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
|
@ -80,7 +80,17 @@ def teardown_test_environment():
|
||||
del mail.outbox
|
||||
|
||||
|
||||
def get_runner(settings):
|
||||
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:
|
||||
|
19
django/utils/module_tools/__init__.py
Normal file
19
django/utils/module_tools/__init__.py
Normal 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 *
|
||||
|
42
django/utils/module_tools/data_storage.py
Normal file
42
django/utils/module_tools/data_storage.py
Normal 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 = []
|
||||
|
79
django/utils/module_tools/module_loader.py
Normal file
79
django/utils/module_tools/module_loader.py
Normal 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
|
||||
|
135
django/utils/module_tools/module_walker.py
Normal file
135
django/utils/module_tools/module_walker.py
Normal 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))
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
"""}
|
BIN
tests/regressiontests/model_fields/4x8.png
Normal file
BIN
tests/regressiontests/model_fields/4x8.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 87 B |
BIN
tests/regressiontests/model_fields/8x4.png
Normal file
BIN
tests/regressiontests/model_fields/8x4.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 87 B |
403
tests/regressiontests/model_fields/imagefield.py
Normal file
403
tests/regressiontests/model_fields/imagefield.py
Normal 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)
|
@ -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)
|
||||
@ -37,3 +50,91 @@ class BigD(models.Model):
|
||||
|
||||
class BigS(models.Model):
|
||||
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')
|
||||
|
||||
###############################################################################
|
||||
|
@ -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)
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
import os, sys, traceback
|
||||
import unittest
|
||||
|
||||
import coverage
|
||||
import django.contrib as contrib
|
||||
|
||||
try:
|
||||
@ -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:
|
||||
|
Loading…
x
Reference in New Issue
Block a user