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
17 from nose.plugins.base import Plugin
18 from nose.util import src, tolist
20 log = logging.getLogger(__name__)
22 COVERAGE_TEMPLATE = '''<html>
29 .coverage pre {float: left; margin: 0px 1em; border: none;
31 .num pre { margin: 0px }
32 .nocov, .nocov pre {background-color: #faa}
33 .cov, .cov pre {background-color: #cfc}
34 div.coverage div { clear: both; height: 1.1em}
39 <div class="coverage">
46 COVERAGE_STATS_TEMPLATE = '''Covered: %(covered)s lines<br/>
47 Missed: %(missed)s lines<br/>
48 Skipped %(skipped)s lines<br/>
49 Percent: %(percent)s %%<br/>
53 class Coverage(Plugin):
55 Activate a coverage report using Ned Batchelder's coverage module.
63 def coverInstance(self):
64 if not self._coverInstance:
67 self._coverInstance = coverage.coverage()
68 except coverage.CoverageException:
69 self._coverInstance = coverage
70 return self._coverInstance
71 coverInstance = property(coverInstance)
73 def options(self, parser, env):
75 Add options to command line.
77 Plugin.options(self, parser, env)
78 parser.add_option("--cover-package", action="append",
79 default=env.get('NOSE_COVER_PACKAGE'),
81 dest="cover_packages",
82 help="Restrict coverage output to selected packages "
83 "[NOSE_COVER_PACKAGE]")
84 parser.add_option("--cover-erase", action="store_true",
85 default=env.get('NOSE_COVER_ERASE'),
87 help="Erase previously collected coverage "
88 "statistics before run")
89 parser.add_option("--cover-tests", action="store_true",
91 default=env.get('NOSE_COVER_TESTS'),
92 help="Include test modules in coverage report "
94 parser.add_option("--cover-inclusive", action="store_true",
95 dest="cover_inclusive",
96 default=env.get('NOSE_COVER_INCLUSIVE'),
97 help="Include all python files under working "
98 "directory in coverage report. Useful for "
99 "discovering holes in test coverage if not all "
100 "files are imported by the test suite. "
101 "[NOSE_COVER_INCLUSIVE]")
102 parser.add_option("--cover-html", action="store_true",
103 default=env.get('NOSE_COVER_HTML'),
105 help="Produce HTML coverage information")
106 parser.add_option('--cover-html-dir', action='store',
107 default=env.get('NOSE_COVER_HTML_DIR', 'cover'),
108 dest='cover_html_dir',
110 help='Produce HTML coverage information in dir')
112 def configure(self, options, config):
117 self.status.pop('active')
120 Plugin.configure(self, options, config)
127 log.error("Coverage not available: "
128 "unable to import coverage module")
132 self.coverErase = options.cover_erase
133 self.coverTests = options.cover_tests
134 self.coverPackages = []
135 if options.cover_packages:
136 for pkgs in [tolist(x) for x in options.cover_packages]:
137 self.coverPackages.extend(pkgs)
138 self.coverInclusive = options.cover_inclusive
139 if self.coverPackages:
140 log.info("Coverage report will include only packages: %s",
142 self.coverHtmlDir = None
143 if options.cover_html:
144 self.coverHtmlDir = options.cover_html_dir
145 log.debug('Will put HTML coverage report in %s', self.coverHtmlDir)
147 self.status['active'] = True
151 Begin recording coverage information.
153 log.debug("Coverage begin")
154 self.skipModules = sys.modules.keys()[:]
156 log.debug("Clearing previously collected coverage statistics")
157 self.coverInstance.erase()
158 self.coverInstance.exclude('#pragma[: ]+[nN][oO] [cC][oO][vV][eE][rR]')
159 self.coverInstance.start()
161 def report(self, stream):
163 Output code coverage report.
165 log.debug("Coverage report")
166 self.coverInstance.stop()
167 self.coverInstance.save()
169 for name, module in sys.modules.items()
170 if self.wantModuleCoverage(name, module) ]
171 log.debug("Coverage report will cover modules: %s", modules)
172 self.coverInstance.report(modules, file=stream)
173 if self.coverHtmlDir:
174 log.debug("Generating HTML coverage report")
175 if hasattr(self.coverInstance, 'html_report'):
176 self.coverInstance.html_report(modules, self.coverHtmlDir)
178 self.report_html(modules)
180 def report_html(self, modules):
181 if not os.path.exists(self.coverHtmlDir):
182 os.makedirs(self.coverHtmlDir)
185 if hasattr(m, '__name__') and hasattr(m, '__file__'):
186 files[m.__name__] = m.__file__
187 self.coverInstance.annotate(files.values())
188 global_stats = {'covered': 0, 'missed': 0, 'skipped': 0}
190 for m, f in files.iteritems():
191 if f.endswith('pyc'):
193 coverfile = f+',cover'
194 outfile, stats = self.htmlAnnotate(m, f, coverfile,
196 for field in ('covered', 'missed', 'skipped'):
197 global_stats[field] += stats[field]
198 file_list.append((stats['percent'], m, outfile, stats))
201 global_stats['percent'] = self.computePercent(
202 global_stats['covered'], global_stats['missed'])
203 # Now write out an index file for the coverage HTML
204 index = open(os.path.join(self.coverHtmlDir, 'index.html'), 'w')
205 index.write('<html><head><title>Coverage Index</title></head>'
207 index.write(COVERAGE_STATS_TEMPLATE % global_stats)
208 index.write('<table><tr><td>File</td><td>Covered</td><td>Missed'
209 '</td><td>Skipped</td><td>Percent</td></tr>')
210 for junk, name, outfile, stats in file_list:
211 stats['a'] = '<a href="%s">%s</a>' % (outfile, name)
212 index.write('<tr><td>%(a)s</td><td>%(covered)s</td><td>'
213 '%(missed)s</td><td>%(skipped)s</td><td>'
214 '%(percent)s %%</td></tr>' % stats)
215 index.write('</table></p></html')
218 def htmlAnnotate(self, name, file, coverfile, outputDir):
219 log.debug('Name: %s file: %s' % (name, file, ))
221 data = open(coverfile, 'r').read().split('\n')
222 padding = len(str(len(data)))
223 stats = {'covered': 0, 'missed': 0, 'skipped': 0}
224 for lineno, line in enumerate(data):
232 lineno = (' ' * (padding - len(str(lineno)))) + str(lineno)
233 for old, new in (('&', '&'), ('<', '<'), ('>', '>'),
235 line = line.replace(old, new)
237 rows.append('<div class="nocov"><span class="num"><pre>'
238 '%s</pre></span><pre>%s</pre></div>' % (lineno,
242 rows.append('<div class="cov"><span class="num"><pre>%s</pre>'
243 '</span><pre>%s</pre></div>' % (lineno, line))
244 stats['covered'] += 1
246 rows.append('<div class="skip"><span class="num"><pre>%s</pre>'
247 '</span><pre>%s</pre></div>' % (lineno, line))
248 stats['skipped'] += 1
249 stats['percent'] = self.computePercent(stats['covered'],
251 html = COVERAGE_TEMPLATE % {'title': '<title>%s</title>' % name,
253 'body': '\n'.join(rows),
254 'stats': COVERAGE_STATS_TEMPLATE % stats,
256 outfilename = name + '.html'
257 outfile = open(os.path.join(outputDir, outfilename), 'w')
260 return outfilename, stats
262 def computePercent(self, covered, missed):
263 if covered + missed == 0:
266 percent = covered/(covered+missed+0.0)
267 return int(percent * 100)
269 def wantModuleCoverage(self, name, module):
270 if not hasattr(module, '__file__'):
271 log.debug("no coverage of %s: no __file__", name)
273 module_file = src(module.__file__)
274 if not module_file or not module_file.endswith('.py'):
275 log.debug("no coverage of %s: not a python file", name)
277 if self.coverPackages:
278 for package in self.coverPackages:
279 if (re.findall(r'^%s\b' % re.escape(package), name)
281 or not self.conf.testMatch.search(name))):
282 log.debug("coverage for %s", name)
284 if name in self.skipModules:
285 log.debug("no coverage for %s: loaded before coverage start",
288 if self.conf.testMatch.search(name) and not self.coverTests:
289 log.debug("no coverage for %s: is a test", name)
291 # accept any package that passed the previous tests, unless
292 # coverPackages is on -- in that case, if we wanted this
293 # module, we would have already returned True
294 return not self.coverPackages
296 def wantFile(self, file, package=None):
297 """If inclusive coverage enabled, return true for all source files
300 if self.coverInclusive:
301 if file.endswith(".py"):
302 if package and self.coverPackages:
303 for want in self.coverPackages:
304 if package.startswith(want):