Skip to content

Commit

Permalink
merge from remote cuker branch with lots of new goodies
Browse files Browse the repository at this point in the history
  • Loading branch information
garethr committed Mar 28, 2010
2 parents 274d840 + 7780104 commit 3516643
Show file tree
Hide file tree
Showing 9 changed files with 1,201 additions and 1,040 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
.DS_Store
*~
*.pyc
build/*
dist/*
src/*.egg-info/*
src/*.egg-info/*
6 changes: 4 additions & 2 deletions README.textile
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,10 @@ Sometimes it's nice to have a file reporting the results of a test run. Some app

h3. Code Coverage

If you want to know what code is being run when you run your test suite then codecoverage is for you. These two flags use two different third party libraries to calculate coverage statistics. The first dumps the results to stdout, the second creates a series of files displaying the results.
If you want to know what code is being run when you run your test suite then codecoverage is for you. These two flags use two different third party libraries to calculate coverage statistics. The first dumps the results to stdout, --xmlcoverage creates a cobertura-compatible xml output, and the last one creates a series of files displaying the results.

<pre>python manage.py test --coverage</pre>
<pre>python manage.py test --xmlcoverage</pre>
<pre>python manage.py test --figleaf</pre>

h3. No Database
Expand All @@ -48,6 +49,7 @@ Sometimes your don't want the overhead of setting up a database during testing,

<pre>python manage.py test --nodb</pre>
<pre>python manage.py test --nodb --coverage</pre>
<pre>python manage.py test --nodb --xmlcoverage</pre>

h2. Local Continuous Integration Command

Expand Down Expand Up @@ -85,4 +87,4 @@ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
OTHER DEALINGS IN THE SOFTWARE.
54 changes: 28 additions & 26 deletions src/test_extensions/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@

# needed to login to the admin
from django.contrib.auth.models import User

from django.utils.encoding import smart_str


class Common(TestCase):
"""
This class contains a number of custom assertions which
Expand Down Expand Up @@ -50,30 +50,33 @@ def execute_sql(*sql):
# Custom assertions

def assert_equal(self, *args, **kwargs):
"Assert that two values are equal"
'Assert that two values are equal'

return self.assertEqual(*args, **kwargs)

def assert_not_equal(self, *args, **kwargs):
"Assert that two values are not equal"
return not self.assertNotEqual(*args, **kwargs)

def assert_contains(self, needle, haystack):
"Assert that one value (the hasystack) contains another value (the needle)"
return self.assert_(needle in haystack, "Content should contain `%s' but doesn't:\n%s" % (needle, haystack))
def assert_contains(self, needle, haystack, diagnostic=''):
'Assert that one value (the hasystack) contains another value (the needle)'
diagnostic = diagnostic + "\nContent should contain `%s' but doesn't:\n%s" % (needle, haystack)
diagnostic = diagnostic.strip()
return self.assert_(needle in haystack, diagnostic)

def assert_doesnt_contain(self, needle, haystack): # CONSIDER deprecate me for deny_contains
"Assert that one value (the hasystack) does not contain another value (the needle)"
return self.assert_(needle not in haystack, "Content should not contain `%s' but does:\n%s" % (needle, haystack))

def deny_contain(self, needle, haystack):
def deny_contains(self, needle, haystack):
"Assert that one value (the hasystack) does not contain another value (the needle)"
return self.assert_(needle not in haystack, "Content should not contain `%s' but does:\n%s" % (needle, haystack))

def assert_regex_contains(self, pattern, string, flags=None):
"Assert that the given regular expression matches the string"
'Assert that the given regular expression matches the string'
flags = flags or 0
disposition = re.search(pattern, string, flags)
self.assertTrue(disposition != None, pattern + ' not found in ' + string)
self.assertTrue(disposition != None, repr(smart_str(pattern)) + ' should match ' + repr(smart_str(string)))

def deny_regex_contains(self, pattern, slug):
'Deny that the given regular expression pattern matches a string'
Expand Down Expand Up @@ -131,39 +134,38 @@ def assert_has_attr(self, obj, attr):
except AttributeError:
assert(False)

def _xml_to_tree(self, xml):
def _xml_to_tree(self, xml, forgiving=False):
from lxml import etree
self._xml = xml

try:
if '<html' in xml[:200]:
return etree.HTML(xml)
else:
return etree.XML(xml)
if not isinstance(xml, basestring):
self._xml = str(xml) # TODO tostring
return xml

except ValueError: # TODO don't rely on exceptions for normal control flow
tree = xml
self._xml = str(tree) # CONSIDER does this reconstitute the nested XML ?
return tree
if '<html' in xml[:200]:
parser = etree.HTMLParser(recover=forgiving)
return etree.HTML(str(xml), parser)
else:
parser = etree.XMLParser(recover=forgiving)
return etree.XML(str(xml))

def assert_xml(self, xml, xpath, **kw):
'Check that a given extent of XML or HTML contains a given XPath, and return its first node'

tree = self._xml_to_tree(xml)
tree = self._xml_to_tree(xml, forgiving=kw.get('forgiving', False))
nodes = tree.xpath(xpath)
self.assertTrue(len(nodes) > 0, xpath + ' not found in ' + self._xml)
self.assertTrue(len(nodes) > 0, xpath + ' should match ' + self._xml)
node = nodes[0]
if kw.get('verbose', False): self.reveal_xml(node) # "here have ye been? What have ye seen?"--Morgoth
if kw.get('verbose', False):
self.reveal_xml(node)
return node

def reveal_xml(self, node):
'Spews an XML node as source, for diagnosis'

print etree.tostring(node, pretty_print=True) # CONSIDER does pretty_print work? why not?
from lxml import etree
print etree.tostring(node, pretty_print=True)

def deny_xml(self, xml, xpath):
'Check that a given extent of XML or HTML does not contain a given XPath'

tree = self._xml_to_tree(xml)
nodes = tree.xpath(xpath)
self.assertEqual(0, len(nodes), xpath + ' should not appear in ' + self._xml)
self.assertEqual(0, len(nodes), xpath + ' should not appear in ' + self._xml)
77 changes: 76 additions & 1 deletion src/test_extensions/django_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,20 @@ def assert_render_matches(self, template, match_regexp, vars={}):

def assert_code(self, response, code):
"Assert that a given response returns a given HTTP status code"
self.assertEqual(code, response.status_code, "HTTP Response status code %d expected, but got %d" % (code, response.status_code))
self.assertEqual(code, response.status_code, "HTTP Response status code should be %d, and is %d" % (code, response.status_code))

def assertNotContains(self, response, text, status_code=200): # overrides Django's assertion, because all diagnostics should be stated positively!!!
"""
Asserts that a response indicates that a page was retrieved
successfully, (i.e., the HTTP status code was as expected), and that
``text`` doesn't occurs in the content of the response.
"""
self.assertEqual(response.status_code, status_code,
"Retrieving page: Response code was %d (expected %d)'" %
(response.status_code, status_code))
text = smart_str(text, response._charset)
self.assertEqual(response.content.count(text),
0, "Response should not contain '%s'" % text)

def assert_render(self, expected, template, **kwargs):
"Asserts than a given template and context render a given fragment"
Expand All @@ -100,3 +113,65 @@ def assert_render_contains(self, expected, template, **kwargs):
def assert_render_doesnt_contain(self, expected, template, **kwargs):
"Asserts than a given template and context rendering does not contain a given fragment"
self.assert_doesnt_contain(expected, self.render(template, **kwargs))

def assert_mail(self, funk):
'''
checks that the called block shouts out to the world
returns either a single mail object or a list of more than one
'''

from django.core import mail
previous_mails = len(mail.outbox)
funk()
mails = mail.outbox[ previous_mails : ]
assert [] != mails, 'the called block produced no mails'
if len(mails) == 1: return mails[0]
return mails

def assert_latest(self, query_set, lamb):
pks = list(query_set.values_list('pk', flat=True).order_by('-pk'))
high_water_mark = (pks+[0])[0]
lamb()

# NOTE we ass-ume the database generates primary keys in monotonic order.
# Don't use these techniques in production,
# or in the presence of a pro DBA

nu_records = list(query_set.filter(pk__gt=high_water_mark).order_by('pk'))
if len(nu_records) == 1: return nu_records[0]
if nu_records: return nu_records # treating the returned value as a scalar or list
# implicitly asserts it is a scalar or list
source = open(lamb.func_code.co_filename, 'r').readlines()[lamb.func_code.co_firstlineno - 1]
source = source.replace('lambda:', '').strip()
model_name = str(query_set.model)

self.assertFalse(True, 'The called block, `' + source +
'` should produce new ' + model_name + ' records')

def deny_mail(self, funk):
'''checks that the called block keeps its opinions to itself'''

from django.core import mail
previous_mails = len(mail.outbox)
funk()
mails = mail.outbox[ previous_mails : ]
assert [] == mails, 'the called block should produce no mails'

def assert_model_changes(self, mod, item, frum, too, lamb):
source = open(lamb.func_code.co_filename, 'r').readlines()[lamb.func_code.co_firstlineno - 1]
source = source.replace('lambda:', '').strip()
model = str(mod.__class__).replace("'>", '').split('.')[-1]

should = '%s.%s should equal `%s` before your activation line, `%s`' % \
(model, item, frum, source)

self.assertEqual(frum, mod.__dict__[item], should)
lamb()
mod = mod.__class__.objects.get(pk=mod.pk)

should = '%s.%s should equal `%s` after your activation line, `%s`' % \
(model, item, too, source)

self.assertEqual(too, mod.__dict__[item], should)
return mod
42 changes: 36 additions & 6 deletions src/test_extensions/management/commands/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@

from django.core import management
from django.conf import settings
from django.db.models import get_app, get_apps
from django.core.management.base import BaseCommand

skippers = []

class Command(BaseCommand):
option_list = BaseCommand.option_list

Expand All @@ -22,13 +25,18 @@ class Command(BaseCommand):
make_option('--coverage', action='store_true', dest='coverage',
default=False,
help='Show coverage details'),
make_option('--xmlcoverage', action='store_true', dest='xmlcoverage',
default=False,
help='Show coverage details and write them into a xml file'),
make_option('--figleaf', action='store_true', dest='figleaf',
default=False,
help='Produce figleaf coverage report'),
make_option('--xml', action='store_true', dest='xml', default=False,
help='Produce xml output for cruise control'),
help='Produce JUnit-type xml output'),
make_option('--nodb', action='store_true', dest='nodb', default=False,
help='No database required for these tests'),
#make_option('--skip', action='store', dest='skip', default=False,
# help='Omit these applications from testing'), # TODO require args if used
)
help = """Custom test command which allows for
specifying different test runners."""
Expand All @@ -49,11 +57,15 @@ def handle(self, *test_labels, **options):
management._commands['syncdb'] = 'django.core'

if options.get('nodb'):
if options.get('coverage'):
if options.get('xmlcoverage'):
test_runner_name = 'test_extensions.testrunners.nodatabase.run_tests_with_xmlcoverage'
elif options.get('coverage'):
test_runner_name = 'test_extensions.testrunners.nodatabase.run_tests_with_coverage'
else:
test_runner_name = 'test_extensions.testrunners.nodatabase.run_tests'
elif options.get('coverage'):
elif options.get('xmlcoverage'):
test_runner_name = 'test_extensions.testrunners.codecoverage.run_tests_xml'
elif options.get ('coverage'):
test_runner_name = 'test_extensions.testrunners.codecoverage.run_tests'
elif options.get('figleaf'):
test_runner_name = 'test_extensions.testrunners.figleafcoverage.run_tests'
Expand All @@ -70,8 +82,26 @@ def handle(self, *test_labels, **options):
test_module_name = '.'
test_module = __import__(test_module_name, {}, {}, test_path[-1])
test_runner = getattr(test_module, test_path[-1])
#print test_runner
# print test_runner.__file__

if hasattr(settings, 'SKIP_TESTS'):
if not test_labels:
test_labels = list()
for app in get_apps():
test_labels.append(app.__name__.split('.')[-2])
for app in settings.SKIP_TESTS:
try:
test_labels = list(test_labels)
test_labels.remove(app)
except ValueError:
pass
try:
failures = test_runner(test_labels, verbosity=verbosity,
interactive=interactive) # , skip_apps=skippers)
except TypeError: #Django 1.2
failures = test_runner(verbosity=verbosity, #TODO extend django.test.simple.DjangoTestSuiteRunner
interactive=interactive).run_tests(test_labels)

failures = test_runner(test_labels, verbosity=verbosity,
interactive=interactive)
if failures:
sys.exit(failures)
sys.exit(failures)
27 changes: 21 additions & 6 deletions src/test_extensions/testrunners/codecoverage.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,20 +49,22 @@ def get_all_coverage_modules(app_module):
return mod_list

def run_tests(test_labels, verbosity=1, interactive=True,
extra_tests=[], nodatabase=False):
extra_tests=[], nodatabase=False, xml_out=False):
"""
Test runner which displays a code coverage report at the end of the
run.
"""
coverage.use_cache(0)
coverage.start()
cov = coverage.coverage()
cov.erase()
cov.use_cache(0)
cov.start()
if nodatabase:
results = nodatabase_run_tests(test_labels, verbosity, interactive,
extra_tests)
else:
results = django_test_runner(test_labels, verbosity, interactive,
extra_tests)
coverage.stop()
cov.stop()

coverage_modules = []
if test_labels:
Expand All @@ -77,6 +79,19 @@ def run_tests(test_labels, verbosity=1, interactive=True,
coverage_modules.extend(get_all_coverage_modules(app))

if coverage_modules:
coverage.report(coverage_modules, show_missing=1)
if xml_out:
# using the same output directory as the --xml function uses for testing
if not os.path.isdir(os.path.join("temp", "xml")):
os.makedirs(os.path.join("temp", "xml"))
output_filename = 'temp/xml/coverage_output.xml'
cov.xml_report(morfs=coverage_modules, outfile=output_filename)

cov.report(coverage_modules, show_missing=1)

return results

return results

def run_tests_xml (test_labels, verbosity=1, interactive=True,
extra_tests=[], nodatabase=False):
return run_tests(test_labels, verbosity, interactive,
extra_tests, nodatabase, xml_out=True)
Loading

0 comments on commit 3516643

Please sign in to comment.