Commits

Poul Sysolyatin  committed 1561610

Added base implementation for generate html reports.
Added usage jinja3 template system.

  • Participants
  • Parent commits 3ebc9fc

Comments (0)

Files changed (12)

File django_coverage/__init__.py

 limitations under the License.
 """
 
-__version__ = '1.2'
+__version__ = '1.2.1'

File django_coverage/coverage_runner.py

 from django_coverage import settings
 from django_coverage.utils.coverage_report import html_report
 from django_coverage.utils.module_tools import get_all_modules
+from django_coverage.html_test_runner import HTMLTestRunner
 
+def coverage_get_runner():
+  test_path = settings.COVERAGE_TEST_RUNNER.split('.')
+  # Allow for Python 2.5 relative paths
+  if len(test_path) > 1:
+    test_module_name = '.'.join(test_path[:-1])
+  else:
+    test_module_name = '.'
+  test_module = __import__(test_module_name, {}, {}, test_path[-1])
+  test_runner = getattr(test_module, test_path[-1])
+  return test_runner
 
 class CoverageRunner(DjangoTestSuiteRunner):
     """
     Test runner which displays a code coverage report at the end of the run.
     """
+    outdir = settings.COVERAGE_REPORT_HTML_OUTPUT_DIR
+    runner = None
 
     def __new__(cls, *args, **kwargs):
         """
         """
         return '.'.join(app_model_module.__name__.split('.')[:-1])
 
+    def run_suite(self, suite, **kwargs):
+        TestRunner = coverage_get_runner()
+        runner = TestRunner( verbosity=self.verbosity, failfast=self.failfast )
+        if isinstance( runner, HTMLTestRunner ):
+            self.runner = runner
+            self.runner.setReportPath( self.outdir )
+        return runner.run( suite )
+
     def run_tests(self, test_labels, extra_tests=None, **kwargs):
+
         coverage.use_cache(settings.COVERAGE_USE_CACHE)
         for e in settings.COVERAGE_CODE_EXCLUDES:
             coverage.exclude(e)
             coverage_modules, settings.COVERAGE_MODULE_EXCLUDES,
             settings.COVERAGE_PATH_EXCLUDES)
 
-        outdir = settings.COVERAGE_REPORT_HTML_OUTPUT_DIR
-        if outdir is None:
+        if self.outdir is None:
             coverage.report(modules.values(), show_missing=1)
             if excludes:
                 message = "The following packages or modules were excluded:"
                     print >>sys.stderr, e,
                 print >>sys.stdout
         else:
-            outdir = os.path.abspath(outdir)
             if settings.COVERAGE_CUSTOM_REPORTS:
-                html_report(outdir, modules, excludes, errors)
+                if not self.runner is None:
+                    self.runner.generateReport( { 'packages': packages
+                                                , 'modules': modules
+                                                , 'excludes': excludes
+                                                , 'errors': errors } )
+                else:
+                    html_report(self.outdir, modules, excludes, errors)
             else:
-                coverage._the_coverage.html_report(modules.values(), outdir)
+                coverage._the_coverage.html_report(modules.values(), self.outdir)
             print >>sys.stdout
-            print >>sys.stdout, "HTML reports were output to '%s'" %outdir
+            print >>sys.stdout, "HTML reports were output to '%s'" %self.outdir
 
         return results

File django_coverage/html_test_runner.py

+"""Running tests"""
+
+import sys
+import time
+import os
+import cgi
+import traceback
+#import unittest
+from jinja2 import Environment, PackageLoader
+
+from django.utils.unittest.runner import TextTestRunner
+from django.utils.unittest.runner import TextTestResult
+from utils.result_processor import ResultProcessor, CoverageProcessor, Summary, TestInfo, TestGroup
+
+from utils.coverage_report.data_storage import ModuleVars
+
+try:
+    from django.utils.unittest.signals import registerResult
+except ImportError:
+    def registerResult(_):
+        pass
+
+__unittest = True
+
+
+class SourceLine():
+  def __init__(self, line, status):
+    self.line = line.decode('utf-8')
+#    print line
+    self.status = status
+
+class ModuleCoverage():
+  def __init__(self, mVars):
+    self.css = mVars.severity
+    self.name = mVars.module_name
+    self.total_lines = mVars.total_count
+    self.executed_lines = mVars.executed_count
+    self.excluded_lines = mVars.excluded_count
+    self.missed_lines = mVars.missed_count
+    self.ignored_lines = 0
+    self.coverage_percent = "%.2f" % mVars.percent_covered
+    self.url = 'modules/%s.html' % self.name
+
+    self.source_lines = list()
+
+    source_lines = list()
+    i = 0
+    for i, source_line in enumerate(
+        [cgi.escape(l.rstrip()) for l in file(mVars.source_file, 'rb').readlines()]):
+        line_status = 'ignored'
+        if i+1 in mVars.executed: line_status = 'executed'
+        if i+1 in mVars.excluded: line_status = 'excluded'
+        if i+1 in mVars.missed: line_status = 'missed'
+        self.source_lines.append( SourceLine( source_line, line_status ) )
+    self.ignored_count = i+1 - mVars.total_count
+
+
+class HTMLTestResult(TextTestResult):
+    all_tests = dict()
+
+    def __init__(self, stream, descriptions, verbusity):
+        TextTestResult.__init__( self, stream, descriptions, verbusity )
+
+    def appendTest(self, test, type_class, data = ''):
+        info = TestInfo( test = test, result_class = type_class, data = data )
+        if not info.group in self.all_tests:
+          self.all_tests[info.group] = TestGroup( name = info.group )
+        self.all_tests[info.group].appendTestInfo( info )
+
+    def addSuccess(self, test):
+        TextTestResult.addSuccess( self, test )
+        self.appendTest( test, 'success' )
+
+    def addError(self, test, err):
+        TextTestResult.addError( self, test, err )
+        self.appendTest( test, 'error', "".join( traceback.format_exception( err[0], err[1], err[2] ) ) )
+
+    def addFailure(self, test, err):
+        TextTestResult.addFailure( self, test, err )
+        self.appendTest( test, 'failure', "".join( traceback.format_exception( err[0], err[1], err[2] ) ) )
+
+    def addSkip(self, test, reason):
+        TextTestResult.addSkip( self, test, reason )
+        self.appendTest( test, 'skip', reason )
+
+    def addExpectedFalure(self, test, err):
+        TextTestResult.addExpectedFailure( self, test, err)
+        self.appendTest( test, 'expectedfailure', "".join( traceback.format_exception( err[0], err[1], err[2] ) ) )
+
+    def addUnexpectedSuccess(self, test):
+        TextTestResult.addUnexpectedSuccess( self, test )
+        self.appendTest( test, 'unexpectedsuccess' )
+
+
+class HTMLTestRunner(TextTestRunner):
+    """A test runner class that displays results in HTML form.
+
+    """
+    def __init__(self, stream=sys.stderr, descriptions=True, verbosity=1,
+                    failfast=False, buffer=False):
+        TextTestRunner.__init__( self, stream, descriptions, verbosity, failfast, buffer, HTMLTestResult )
+        self.reportPath = None
+        self.result = None
+
+    def run(self, test):
+        "Run the given test case or test suite."
+        result = self._makeResult()
+        result.failfast = self.failfast
+        result.buffer = self.buffer
+        registerResult(result)
+
+        startTime = time.time()
+        startTestRun = getattr(result, 'startTestRun', None)
+        if startTestRun is not None:
+            startTestRun()
+        try:
+            test(result)
+        finally:
+            stopTestRun = getattr(result, 'stopTestRun', None)
+            if stopTestRun is not None:
+                stopTestRun()
+            else:
+                result.printErrors()
+        stopTime = time.time()
+        timeTaken = stopTime - startTime
+        if hasattr(result, 'separator2'):
+            self.stream.writeln(result.separator2)
+        run = result.testsRun
+        self.stream.writeln("Ran %d test%s in %.3fs" %
+                            (run, run != 1 and "s" or "", timeTaken))
+        self.stream.writeln()
+        result.timeTaken = "%.3f s" % timeTaken
+        stat = Summary( result )
+        infos = []
+        if not result.wasSuccessful():
+            self.stream.write("FAILED")
+            if stat.fails:
+                infos.append("failures=%d" % stat.fails)
+            if stat.errors:
+              infos.append("errors=%d" % stat.errors)
+        else:
+            self.stream.write("OK")
+        if stat.skips:
+            infos.append("skipped=%d" % stat.skips)
+        if stat.expected_fails:
+            infos.append("expected failures=%d" % stat.expected_fails)
+        if stat.unexpected_successes:
+            infos.append("unexpected successes=%d" % stat.unexpected_successes)
+        if infos:
+            self.stream.writeln(" (%s)" % (", ".join(infos),))
+        else:
+            self.stream.write("\n")
+
+        self.result = result
+        return result
+
+    def setReportPath(self, aPath):
+        self.reportPath = aPath
+
+    def generateReport(self, coverage_results):
+        if self.result is None or self.reportPath is None:
+            return False
+
+        coverage_path = os.path.join( self.reportPath, 'coverage' )
+        try:
+          os.mkdir( coverage_path )
+        except OSError:
+          pass
+        coverage_modules_path = os.path.join( coverage_path, 'modules' )
+        try:
+          os.mkdir( coverage_modules_path )
+        except OSError:
+          pass
+        env = Environment( loader = PackageLoader( 'django_coverage', 'templates' ) )
+        
+        modules_summary = { 'total_lines': 0
+                          , 'executed_lines': 0
+                          , 'excluded_lines': 0 
+                          , 'coverage_percent': 0
+                          , 'css': 'normal'
+                          }
+        total_stmts = 0
+
+        if coverage_results['modules']:
+        
+          modules = list()
+          m_names = coverage_results['modules'].keys()
+          m_names.sort()
+          
+          for cur_name in m_names:
+            mVars = ModuleVars( cur_name, coverage_results['modules'][cur_name] )
+            if not mVars.total_count:
+              coverage_results['excludes'].append( cur_name )
+              continue
+            modules_summary['total_lines'] += mVars.total_count
+            modules_summary['executed_lines'] += mVars.executed_count
+            modules_summary['excluded_lines'] += mVars.excluded_count
+            total_stmts += len(mVars.stmts)
+
+            moduleInfo = ModuleCoverage( mVars )
+            t = env.get_template( 'coverage_module.html' )
+            t.stream( { 'module': moduleInfo, } ).dump( os.path.join( coverage_modules_path, cur_name + '.html' ), encoding = 'utf-8' )
+            
+            modules.append( ModuleCoverage( mVars ) )
+
+          try:
+            modules_summary['coverage_percent'] = float( modules_summary['executed_lines'] ) / total_stmts * 100
+          except ZeroDivisionError:
+            modules_summary['coverage_percent'] = 100
+
+          if modules_summary['coverage_percent'] < 75:
+            modules_summary['css'] = 'warning'
+          if modules_summary['coverage_percent'] < 50:
+            modules_summary['css'] = 'critical'
+            
+          modules_summary['coverage_percent'] = "%.2f" % modules_summary['coverage_percent']
+          
+          t = env.get_template( 'coverage.html' )
+          t.stream( { 'modules': modules
+                    , 'summary': modules_summary } ).dump( os.path.join( coverage_path, 'index.html' ), encoding = 'utf-8' )
+
+        if coverage_results['errors']:
+          t = env.get_template( 'coverage_errors.html' )
+          t.stream( { 'list': coverage_results['errors'] } ).dump( os.path.join( coverage_path, 'errors.html' ), encoding = 'utf-8' )
+
+        if coverage_results['excludes']:
+          t = env.get_template( 'coverage_excludes.html' )
+          t.stream( { 'list': coverage_results['excludes'] } ).dump( os.path.join( coverage_path, 'excludes.html' ), encoding = 'utf-8' )
+
+        t = env.get_template( 'index.html' )
+        t.stream( ResultProcessor( self.result, modules_summary ).context() ).dump( os.path.join( self.reportPath, 'index.html' ), encoding = 'utf-8' )
+
+        return True

File django_coverage/settings.py

 # True => html reports by 55minutes
 # False => html reports by coverage.py
 COVERAGE_CUSTOM_REPORTS = getattr(settings, 'COVERAGE_CUSTOM_REPORTS', True)
+
+# Define a TestRunner for Coverage
+COVERAGE_TEST_RUNNER = getattr(settings, 'COVERAGE_TEST_RUNNER', 'django.utils.unittest.runner.TextTestRunner' )
+
+

File django_coverage/templates/base.html

+<!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>{% block title %}{% endblock %}</title>
+  {% block header %}{% endblock %}
+</head>
+<body>
+{% block content %}{% endblock %}
+<div class="footer"><p>Report created {{ creation_time }} by {{ report_generator }}<p></div>
+</body>
+</html>

File django_coverage/templates/coverage.html

+{% extends "base.html" %}
+{% block title %}Coverage summary{% endblock %}
+{% block content %}
+<div class="heade"><h1>Coverage summary</h1></div>
+<ul class="modules_list">
+  <li class="title">
+    <span class="module-name">Module</span>
+    <span class="total-lines">Total lines</span>
+    <span class="executed-lines">Executed</span>
+    <span class="excluded-lines">Excluded</span>
+    <span class="coverage-percent">% coverage</span>
+  </li>
+{% for m in modules %}
+  <li class="{{ m.css }}">
+    <span class="module-name"><a href="{{ m.url }}">{{ m.name }}</a></span>
+    <span class="total-lines">{{ m.total_lines }}</span>
+    <span class="executed-lines">{{ m.executed_lines }}</span>
+    <span class="excluded-lines">{{ m.excluded_lines }}</span>
+    <span class="coverage-percent">{{ m.coverage_percent }}</span>
+  </li>
+{% endfor %}
+  <li class="summary {{ summary.css }}">
+    <span class="module-name">Summary:</span>
+    <span class="total-lines">{{ summary.total_lines }}</span>
+    <span class="executed-lines">{{ summary.executed_lines }}</span>
+    <span class="excluded-lines">{{ summary.excluded_lines }}</span>
+    <span class="coverage-percent">{{ summary.coverage_percent }}</span>
+  </li>
+</ul>
+</div class="footer"><p>Report created {{ creation_time }} by {{ report_generator }}<p></div>
+{% endblock %}

File django_coverage/templates/coverage_errors.html

+{% extends "coverage_list.html" %}
+{% block title %}Error list{% endblock %}
+{% block contenttitle %}Error list{% endblock %}

File django_coverage/templates/coverage_excludes.html

+{% extends "coverage_list.html" %}
+{% block title %}Ignored list{% endblock %}
+{% block contenttitle %}Ignored list{% endblock %}

File django_coverage/templates/coverage_list.html

+{% extends "base.html" %}
+{% block content %}
+<div class="heade"><h1>{% block contenttitle %}{% endblock %}</h1></div>
+<ul class="modules_list">
+  {% for title in list %}<li>{{ title }}</li>{% endfor %}
+</ul>
+{% endblock %}

File django_coverage/templates/coverage_module.html

+{% extends "base.html" %}
+{% block title %}Coverage details for {{ module_name }}{% endblock %}
+{% block content %}
+<h1>Coverage details for {{ module.name }}</h1>
+<dl class="coverage-summary {{ module.css }}">
+  <dt>Total lines:</dt><dd>{{ module.total_lines }}</dd>
+  <dt>Covered lines:</dt><dd>{{ module.executed_lines }}</dd>
+  <dt>Uncovered</dt><dd>{{ module.excluded_lines }}</dd>
+  <dt>% coverage</dt><dd>{{ module.coverage_percent }}</dd>
+</dl>
+<ol class="sources">
+  {% for line in module.source_lines %}<li class="{{ line.status }}"><code>{{ line.line }}</code></li>{% endfor %}
+</ol>
+{% endblock %}
+
+
+
+

File django_coverage/templates/index.html

+{% extends "base.html" %}
+{% block title %}Test results - Index page{% endblock %}
+{% block content %}
+<div class="heade"><h1>Test results</h1></div>
+<div class="coverage {{ summary.css }}">
+<h1>Tests coverage info</h1>
+<ul class="links">
+  <a href="coverage/index.html">Covered modules</a>
+  <a href="coverage/errors.html">Coverage errors</a>
+  <a href="coverage/excludes.html">Coverage excludes</a>
+</ul>
+<dl class="coverage-summary {{ coverage_summary.css }}">
+  <dt>Total lines:</dt><dd>{{ coverage_summary.total_lines }}</dd>
+  <dt>Covered lines:</dt><dd>{{ coverage_summary.executed_lines }}</dd>
+  <dt>Uncovered</dt><dd>{{ coverage_summary.excluded_lines }}</dd>
+  <dt>% coverage</dt><dd>{{ coverage_summary.coverage_percent }}</dd>
+</dl>
+</div>
+<p>There are {{ summary.testsRun }} in {{ TestTime }}</p>
+<ol class="results">
+{% for group in result_list %}
+  <li class="{{ group.classes }}"><div class="group">{{ group.name }}</div>
+  <ol>{% for item in group.items %}
+    <li class="{{ item.result_class }}">{{ item.name }}
+    {% if item.data %}<a href="#{{ item.class_name }}-{{ item.name }}">details</a><pre id="{{ item.class_name }}-{{ item.name }}">{{ item.data|escape }}</pre>{% endif %}
+    </li>{% endfor %}</ol>
+  </li>
+{% endfor %}
+</ol>
+</div class="footer"><p>Report created {{ creation_time }} by {{ report_generator }}<p></div>
+{% endblock %}

File django_coverage/utils/result_processor.py

+from datetime import time, datetime
+
+class TestGroup():
+  def __init__(self, name):
+    self.name = name
+    self.count = 0
+    self.classes = ''
+    self._classes = []
+    self.items = []
+
+  def appendTestInfo(self, tInfo):
+    self.items.append( tInfo )
+    if not tInfo.result_class in self.classes:
+      self._classes.append( tInfo.result_class )
+      self.classes = " ".join( self._classes )
+    self.count += 1
+
+class TestInfo():
+  def __init__(self, test, result_class, data):
+    self.module_name = test.__class__.__module__
+    self.class_name = test.__class__.__name__
+    self.name = test.id().split('.')[-1]
+    self.group = self.module_name + '.' + self.class_name
+    self.result_class = result_class
+    self.data = data
+
+  def stackTrace(self):
+    return ''
+
+class Summary():
+  def __init__(self, result):
+    fails = errors = 0
+    try:
+      results = map(len, ( result.failures, result.errors )  )
+      fails, errors = results
+    except AttributeError:
+      pass
+    expectedFails = unexpectedSuccesses = skipped = 0
+    try:
+      results = map(len, (result.expectedFailures,
+                          result.unexpectedSuccesses,
+                          result.skipped))
+      expectedFails, unexpectedSuccesses, skipped = results
+    except AttributeError:
+      pass
+    self.testsRun = result.testsRun
+    self.successes = result.testsRun - errors - fails
+    self.errors = errors
+    self.fails = fails
+    self.skips = skipped
+    self.unexpected_successes = unexpectedSuccesses
+    self.expected_fails = expectedFails
+
+
+class ResultProcessor():
+  def __init__(self, result, coverage_result):
+    self.result = result
+    self.coverage_result = coverage_result
+
+  def context(self):
+    tests = []
+    for test_info in self.result.all_tests.values():
+      tests.append( test_info )
+    return { 'summary': Summary( self.result )
+            , 'coverage_summary': self.coverage_result
+            , 'TestTime': self.result.timeTaken
+            , 'result_list': tests
+            , 'report_generator': 'HTMLTestRunner'
+            , 'creation_time': datetime.now().strftime( '%a %Y-%m-%d %H:%M %Z' ) }
+
+class CoverageProcessor():
+  def __init__(self, results):
+    self.result = results
+
+  def context(self):
+    return {}