1
"""If you have Ned Batchelder's coverage_ module installed, you may activate a
2
coverage report with the ``--with-coverage`` switch or NOSE_WITH_COVERAGE
3
environment variable. The coverage report will cover any python source module
4
imported after the start of the test run, excluding modules that match
5
testMatch. If you want to include those modules too, use the ``--cover-tests``
6
switch, or set the NOSE_COVER_TESTS environment variable to a true value. To
7
restrict the coverage report to modules from a particular package or packages,
8
use the ``--cover-packages`` switch or the NOSE_COVER_PACKAGES environment
11
.. _coverage: http://www.nedbatchelder.com/code/modules/coverage.html
16
from nose.plugins.base import Plugin
17
from nose.util import src, tolist
19
log = logging.getLogger(__name__)
21
COVERAGE_TEMPLATE = '''<html>
28
.coverage pre {float: left; margin: 0px 1em; border: none;
30
.num pre { margin: 0px }
31
.nocov, .nocov pre {background-color: #faa}
32
.cov, .cov pre {background-color: #cfc}
33
div.coverage div { clear: both; height: 1.1em}
38
<div class="coverage">
45
COVERAGE_STATS_TEMPLATE = '''Covered: %(covered)s lines<br/>
46
Missed: %(missed)s lines<br/>
47
Skipped %(skipped)s lines<br/>
48
Percent: %(percent)s %%<br/>
52
class Coverage(Plugin):
54
Activate a coverage report using Ned Batchelder's coverage module.
61
def options(self, parser, env):
63
Add options to command line.
65
Plugin.options(self, parser, env)
66
parser.add_option("--cover-package", action="append",
67
default=env.get('NOSE_COVER_PACKAGE'),
69
dest="cover_packages",
70
help="Restrict coverage output to selected packages "
71
"[NOSE_COVER_PACKAGE]")
72
parser.add_option("--cover-erase", action="store_true",
73
default=env.get('NOSE_COVER_ERASE'),
75
help="Erase previously collected coverage "
76
"statistics before run")
77
parser.add_option("--cover-tests", action="store_true",
79
default=env.get('NOSE_COVER_TESTS'),
80
help="Include test modules in coverage report "
82
parser.add_option("--cover-inclusive", action="store_true",
83
dest="cover_inclusive",
84
default=env.get('NOSE_COVER_INCLUSIVE'),
85
help="Include all python files under working "
86
"directory in coverage report. Useful for "
87
"discovering holes in test coverage if not all "
88
"files are imported by the test suite. "
89
"[NOSE_COVER_INCLUSIVE]")
90
parser.add_option("--cover-html", action="store_true",
91
default=env.get('NOSE_COVER_HTML'),
93
help="Produce HTML coverage information")
94
parser.add_option('--cover-html-dir', action='store',
95
default=env.get('NOSE_COVER_HTML_DIR', 'cover'),
96
dest='cover_html_dir',
98
help='Produce HTML coverage information in dir')
100
def configure(self, options, config):
105
self.status.pop('active')
108
Plugin.configure(self, options, config)
115
log.error("Coverage not available: "
116
"unable to import coverage module")
120
self.coverErase = options.cover_erase
121
self.coverTests = options.cover_tests
122
self.coverPackages = []
123
if options.cover_packages:
124
for pkgs in [tolist(x) for x in options.cover_packages]:
125
self.coverPackages.extend(pkgs)
126
self.coverInclusive = options.cover_inclusive
127
if self.coverPackages:
128
log.info("Coverage report will include only packages: %s",
130
self.coverHtmlDir = None
131
if options.cover_html:
132
self.coverHtmlDir = options.cover_html_dir
133
log.debug('Will put HTML coverage report in %s', self.coverHtmlDir)
135
self.status['active'] = True
139
Begin recording coverage information.
141
log.debug("Coverage begin")
143
self.skipModules = sys.modules.keys()[:]
145
log.debug("Clearing previously collected coverage statistics")
147
coverage.exclude('#pragma[: ]+[nN][oO] [cC][oO][vV][eE][rR]')
150
def report(self, stream):
152
Output code coverage report.
154
log.debug("Coverage report")
158
for name, module in sys.modules.items()
159
if self.wantModuleCoverage(name, module) ]
160
log.debug("Coverage report will cover modules: %s", modules)
161
coverage.report(modules, file=stream)
162
if self.coverHtmlDir:
163
if not os.path.exists(self.coverHtmlDir):
164
os.makedirs(self.coverHtmlDir)
165
log.debug("Generating HTML coverage report")
168
if hasattr(m, '__name__') and hasattr(m, '__file__'):
169
files[m.__name__] = m.__file__
170
coverage.annotate(files.values())
171
global_stats = {'covered': 0, 'missed': 0, 'skipped': 0}
173
for m, f in files.iteritems():
174
if f.endswith('pyc'):
176
coverfile = f+',cover'
177
outfile, stats = self.htmlAnnotate(m, f, coverfile,
179
for field in ('covered', 'missed', 'skipped'):
180
global_stats[field] += stats[field]
181
file_list.append((stats['percent'], m, outfile, stats))
184
global_stats['percent'] = self.computePercent(
185
global_stats['covered'], global_stats['missed'])
186
# Now write out an index file for the coverage HTML
187
index = open(os.path.join(self.coverHtmlDir, 'index.html'), 'w')
188
index.write('<html><head><title>Coverage Index</title></head>'
190
index.write(COVERAGE_STATS_TEMPLATE % global_stats)
191
index.write('<table><tr><td>File</td><td>Covered</td><td>Missed'
192
'</td><td>Skipped</td><td>Percent</td></tr>')
193
for junk, name, outfile, stats in file_list:
194
stats['a'] = '<a href="%s">%s</a>' % (outfile, name)
195
index.write('<tr><td>%(a)s</td><td>%(covered)s</td><td>'
196
'%(missed)s</td><td>%(skipped)s</td><td>'
197
'%(percent)s %%</td></tr>' % stats)
198
index.write('</table></p></html')
201
def htmlAnnotate(self, name, file, coverfile, outputDir):
202
log.debug('Name: %s file: %s' % (name, file, ))
204
data = open(coverfile, 'r').read().split('\n')
205
padding = len(str(len(data)))
206
stats = {'covered': 0, 'missed': 0, 'skipped': 0}
207
for lineno, line in enumerate(data):
215
lineno = (' ' * (padding - len(str(lineno)))) + str(lineno)
216
for old, new in (('&', '&'), ('<', '<'), ('>', '>'),
218
line = line.replace(old, new)
220
rows.append('<div class="nocov"><span class="num"><pre>'
221
'%s</pre></span><pre>%s</pre></div>' % (lineno,
225
rows.append('<div class="cov"><span class="num"><pre>%s</pre>'
226
'</span><pre>%s</pre></div>' % (lineno, line))
227
stats['covered'] += 1
229
rows.append('<div class="skip"><span class="num"><pre>%s</pre>'
230
'</span><pre>%s</pre></div>' % (lineno, line))
231
stats['skipped'] += 1
232
stats['percent'] = self.computePercent(stats['covered'],
234
html = COVERAGE_TEMPLATE % {'title': '<title>%s</title>' % name,
236
'body': '\n'.join(rows),
237
'stats': COVERAGE_STATS_TEMPLATE % stats,
239
outfilename = name + '.html'
240
outfile = open(os.path.join(outputDir, outfilename), 'w')
243
return outfilename, stats
245
def computePercent(self, covered, missed):
246
if covered + missed == 0:
249
percent = covered/(covered+missed+0.0)
250
return int(percent * 100)
252
def wantModuleCoverage(self, name, module):
253
if not hasattr(module, '__file__'):
254
log.debug("no coverage of %s: no __file__", name)
256
module_file = src(module.__file__)
257
if not module_file or not module_file.endswith('.py'):
258
log.debug("no coverage of %s: not a python file", name)
260
if self.coverPackages:
261
for package in self.coverPackages:
262
if (name.startswith(package)
264
or not self.conf.testMatch.search(name))):
265
log.debug("coverage for %s", name)
267
if name in self.skipModules:
268
log.debug("no coverage for %s: loaded before coverage start",
271
if self.conf.testMatch.search(name) and not self.coverTests:
272
log.debug("no coverage for %s: is a test", name)
274
# accept any package that passed the previous tests, unless
275
# coverPackages is on -- in that case, if we wanted this
276
# module, we would have already returned True
277
return not self.coverPackages
279
def wantFile(self, file, package=None):
280
"""If inclusive coverage enabled, return true for all source files
283
if self.coverInclusive:
284
if file.endswith(".py"):
285
if package and self.coverPackages:
286
for want in self.coverPackages:
287
if package.startswith(want):