Imported Upstream version 1.1.2
[platform/upstream/python-nose.git] / nose / plugins / cover.py
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
9 variable.
10
11 .. _coverage: http://www.nedbatchelder.com/code/modules/coverage.html
12 """
13 import logging
14 import os
15 import re
16 import sys
17 from nose.plugins.base import Plugin
18 from nose.util import src, tolist
19
20 log =  logging.getLogger(__name__)
21
22 COVERAGE_TEMPLATE = '''<html>
23 <head>
24 %(title)s
25 </head>
26 <body>
27 %(header)s
28 <style>
29 .coverage pre {float: left; margin: 0px 1em; border: none;
30                padding: 0px; }
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}
35 </style>
36 <div class="stats">
37 %(stats)s
38 </div>
39 <div class="coverage">
40 %(body)s
41 </div>
42 </body>
43 </html>
44 '''
45
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/>
50 '''
51
52
53 class Coverage(Plugin):
54     """
55     Activate a coverage report using Ned Batchelder's coverage module.
56     """
57     coverTests = False
58     coverPackages = None
59     _coverInstance = None
60     score = 200
61     status = {}
62
63     def coverInstance(self):
64         if not self._coverInstance:
65             import coverage
66             try:
67                 self._coverInstance = coverage.coverage()
68             except coverage.CoverageException:
69                 self._coverInstance = coverage
70         return self._coverInstance
71     coverInstance = property(coverInstance)
72
73     def options(self, parser, env):
74         """
75         Add options to command line.
76         """
77         Plugin.options(self, parser, env)
78         parser.add_option("--cover-package", action="append",
79                           default=env.get('NOSE_COVER_PACKAGE'),
80                           metavar="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'),
86                           dest="cover_erase",
87                           help="Erase previously collected coverage "
88                           "statistics before run")
89         parser.add_option("--cover-tests", action="store_true",
90                           dest="cover_tests",
91                           default=env.get('NOSE_COVER_TESTS'),
92                           help="Include test modules in coverage report "
93                           "[NOSE_COVER_TESTS]")
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'),
104                           dest='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',
109                           metavar='DIR',
110                           help='Produce HTML coverage information in dir')
111
112     def configure(self, options, config):
113         """
114         Configure plugin.
115         """
116         try:
117             self.status.pop('active')
118         except KeyError:
119             pass
120         Plugin.configure(self, options, config)
121         if config.worker:
122             return
123         if self.enabled:
124             try:
125                 import coverage
126             except ImportError:
127                 log.error("Coverage not available: "
128                           "unable to import coverage module")
129                 self.enabled = False
130                 return
131         self.conf = config
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",
141                      self.coverPackages)
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)
146         if self.enabled:
147             self.status['active'] = True
148
149     def begin(self):
150         """
151         Begin recording coverage information.
152         """
153         log.debug("Coverage begin")
154         self.skipModules = sys.modules.keys()[:]
155         if self.coverErase:
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()
160
161     def report(self, stream):
162         """
163         Output code coverage report.
164         """
165         log.debug("Coverage report")
166         self.coverInstance.stop()
167         self.coverInstance.save()
168         modules = [ module
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)
177             else:
178                 self.report_html(modules)
179
180     def report_html(self, modules):
181         if not os.path.exists(self.coverHtmlDir):
182             os.makedirs(self.coverHtmlDir)
183         files = {}
184         for m in modules:
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}
189         file_list = []
190         for m, f in files.iteritems():
191             if f.endswith('pyc'):
192                 f = f[:-1]
193             coverfile = f+',cover'
194             outfile, stats = self.htmlAnnotate(m, f, coverfile,
195                                                self.coverHtmlDir)
196             for field in ('covered', 'missed', 'skipped'):
197                 global_stats[field] += stats[field]
198             file_list.append((stats['percent'], m, outfile, stats))
199             os.unlink(coverfile)
200         file_list.sort()
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>'
206                     '<body><p>')
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')
216         index.close()
217
218     def htmlAnnotate(self, name, file, coverfile, outputDir):
219         log.debug('Name: %s file: %s' % (name, file, ))
220         rows = []
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):
225             lineno += 1
226             if line:
227                 status = line[0]
228                 line = line[2:]
229             else:
230                 status = ''
231                 line = ''
232             lineno = (' ' * (padding - len(str(lineno)))) + str(lineno)
233             for old, new in (('&', '&amp;'), ('<', '&lt;'), ('>', '&gt;'),
234                              ('"', '&quot;'), ):
235                 line = line.replace(old, new)
236             if status == '!':
237                 rows.append('<div class="nocov"><span class="num"><pre>'
238                             '%s</pre></span><pre>%s</pre></div>' % (lineno,
239                                                                     line))
240                 stats['missed'] += 1
241             elif status == '>':
242                 rows.append('<div class="cov"><span class="num"><pre>%s</pre>'
243                             '</span><pre>%s</pre></div>' % (lineno, line))
244                 stats['covered'] += 1
245             else:
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'],
250                                                stats['missed'])
251         html = COVERAGE_TEMPLATE % {'title': '<title>%s</title>' % name,
252                                     'header': name,
253                                     'body': '\n'.join(rows),
254                                     'stats': COVERAGE_STATS_TEMPLATE % stats,
255                                    }
256         outfilename = name + '.html'
257         outfile = open(os.path.join(outputDir, outfilename), 'w')
258         outfile.write(html)
259         outfile.close()
260         return outfilename, stats
261
262     def computePercent(self, covered, missed):
263         if covered + missed == 0:
264             percent = 1
265         else:
266             percent = covered/(covered+missed+0.0)
267         return int(percent * 100)
268
269     def wantModuleCoverage(self, name, module):
270         if not hasattr(module, '__file__'):
271             log.debug("no coverage of %s: no __file__", name)
272             return False
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)
276             return False
277         if self.coverPackages:
278             for package in self.coverPackages:
279                 if (re.findall(r'^%s\b' % re.escape(package), name)
280                     and (self.coverTests
281                          or not self.conf.testMatch.search(name))):
282                     log.debug("coverage for %s", name)
283                     return True
284         if name in self.skipModules:
285             log.debug("no coverage for %s: loaded before coverage start",
286                       name)
287             return False
288         if self.conf.testMatch.search(name) and not self.coverTests:
289             log.debug("no coverage for %s: is a test", name)
290             return False
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
295
296     def wantFile(self, file, package=None):
297         """If inclusive coverage enabled, return true for all source files
298         in wanted packages.
299         """
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):
305                             return True
306                 else:
307                     return True
308         return None