~mwinter4/maus/ckov_0_9_3

« back to all changes in this revision

Viewing changes to third_party/nose-0.11.3/lib/python/nose/plugins/cover.py

  • Committer: tunnell
  • Date: 2010-09-30 13:56:05 UTC
  • Revision ID: tunnell@itchy-20100930135605-wxbkfgy75p0sndk3
add third party

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
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 sys
 
16
from nose.plugins.base import Plugin
 
17
from nose.util import src, tolist
 
18
 
 
19
log =  logging.getLogger(__name__)
 
20
 
 
21
COVERAGE_TEMPLATE = '''<html>
 
22
<head>
 
23
%(title)s
 
24
</head>
 
25
<body>
 
26
%(header)s
 
27
<style>
 
28
.coverage pre {float: left; margin: 0px 1em; border: none;
 
29
               padding: 0px; }
 
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}
 
34
</style>
 
35
<div class="stats">
 
36
%(stats)s
 
37
</div>
 
38
<div class="coverage">
 
39
%(body)s
 
40
</div>
 
41
</body>
 
42
</html>
 
43
'''
 
44
 
 
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/>
 
49
'''
 
50
 
 
51
 
 
52
class Coverage(Plugin):
 
53
    """
 
54
    Activate a coverage report using Ned Batchelder's coverage module.
 
55
    """
 
56
    coverTests = False
 
57
    coverPackages = None
 
58
    score = 200
 
59
    status = {}
 
60
 
 
61
    def options(self, parser, env):
 
62
        """
 
63
        Add options to command line.
 
64
        """
 
65
        Plugin.options(self, parser, env)
 
66
        parser.add_option("--cover-package", action="append",
 
67
                          default=env.get('NOSE_COVER_PACKAGE'),
 
68
                          metavar="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'),
 
74
                          dest="cover_erase",
 
75
                          help="Erase previously collected coverage "
 
76
                          "statistics before run")
 
77
        parser.add_option("--cover-tests", action="store_true",
 
78
                          dest="cover_tests",
 
79
                          default=env.get('NOSE_COVER_TESTS'),
 
80
                          help="Include test modules in coverage report "
 
81
                          "[NOSE_COVER_TESTS]")
 
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'),
 
92
                          dest='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',
 
97
                          metavar='DIR',
 
98
                          help='Produce HTML coverage information in dir')
 
99
 
 
100
    def configure(self, options, config):
 
101
        """
 
102
        Configure plugin.
 
103
        """
 
104
        try:
 
105
            self.status.pop('active')
 
106
        except KeyError:
 
107
            pass
 
108
        Plugin.configure(self, options, config)
 
109
        if config.worker:
 
110
            return
 
111
        if self.enabled:
 
112
            try:
 
113
                import coverage
 
114
            except ImportError:
 
115
                log.error("Coverage not available: "
 
116
                          "unable to import coverage module")
 
117
                self.enabled = False
 
118
                return
 
119
        self.conf = config
 
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",
 
129
                     self.coverPackages)
 
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)
 
134
        if self.enabled:
 
135
            self.status['active'] = True
 
136
 
 
137
    def begin(self):
 
138
        """
 
139
        Begin recording coverage information.
 
140
        """
 
141
        log.debug("Coverage begin")
 
142
        import coverage
 
143
        self.skipModules = sys.modules.keys()[:]
 
144
        if self.coverErase:
 
145
            log.debug("Clearing previously collected coverage statistics")
 
146
            coverage.erase()
 
147
        coverage.exclude('#pragma[: ]+[nN][oO] [cC][oO][vV][eE][rR]')
 
148
        coverage.start()
 
149
 
 
150
    def report(self, stream):
 
151
        """
 
152
        Output code coverage report.
 
153
        """
 
154
        log.debug("Coverage report")
 
155
        import coverage
 
156
        coverage.stop()
 
157
        modules = [ module
 
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")
 
166
            files = {}
 
167
            for m in modules:
 
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}
 
172
            file_list = []
 
173
            for m, f in files.iteritems():
 
174
                if f.endswith('pyc'):
 
175
                    f = f[:-1]
 
176
                coverfile = f+',cover'
 
177
                outfile, stats = self.htmlAnnotate(m, f, coverfile,
 
178
                                                   self.coverHtmlDir)
 
179
                for field in ('covered', 'missed', 'skipped'):
 
180
                    global_stats[field] += stats[field]
 
181
                file_list.append((stats['percent'], m, outfile, stats))
 
182
                os.unlink(coverfile)
 
183
            file_list.sort()
 
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>'
 
189
                        '<body><p>')
 
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')
 
199
            index.close()
 
200
 
 
201
    def htmlAnnotate(self, name, file, coverfile, outputDir):
 
202
        log.debug('Name: %s file: %s' % (name, file, ))
 
203
        rows = []
 
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):
 
208
            lineno += 1
 
209
            if line:
 
210
                status = line[0]
 
211
                line = line[2:]
 
212
            else:
 
213
                status = ''
 
214
                line = ''
 
215
            lineno = (' ' * (padding - len(str(lineno)))) + str(lineno)
 
216
            for old, new in (('&', '&amp;'), ('<', '&lt;'), ('>', '&gt;'),
 
217
                             ('"', '&quot;'), ):
 
218
                line = line.replace(old, new)
 
219
            if status == '!':
 
220
                rows.append('<div class="nocov"><span class="num"><pre>'
 
221
                            '%s</pre></span><pre>%s</pre></div>' % (lineno,
 
222
                                                                    line))
 
223
                stats['missed'] += 1
 
224
            elif status == '>':
 
225
                rows.append('<div class="cov"><span class="num"><pre>%s</pre>'
 
226
                            '</span><pre>%s</pre></div>' % (lineno, line))
 
227
                stats['covered'] += 1
 
228
            else:
 
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'],
 
233
                                               stats['missed'])
 
234
        html = COVERAGE_TEMPLATE % {'title': '<title>%s</title>' % name,
 
235
                                    'header': name,
 
236
                                    'body': '\n'.join(rows),
 
237
                                    'stats': COVERAGE_STATS_TEMPLATE % stats,
 
238
                                   }
 
239
        outfilename = name + '.html'
 
240
        outfile = open(os.path.join(outputDir, outfilename), 'w')
 
241
        outfile.write(html)
 
242
        outfile.close()
 
243
        return outfilename, stats
 
244
 
 
245
    def computePercent(self, covered, missed):
 
246
        if covered + missed == 0:
 
247
            percent = 1
 
248
        else:
 
249
            percent = covered/(covered+missed+0.0)
 
250
        return int(percent * 100)
 
251
 
 
252
    def wantModuleCoverage(self, name, module):
 
253
        if not hasattr(module, '__file__'):
 
254
            log.debug("no coverage of %s: no __file__", name)
 
255
            return False
 
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)
 
259
            return False
 
260
        if self.coverPackages:
 
261
            for package in self.coverPackages:
 
262
                if (name.startswith(package)
 
263
                    and (self.coverTests
 
264
                         or not self.conf.testMatch.search(name))):
 
265
                    log.debug("coverage for %s", name)
 
266
                    return True
 
267
        if name in self.skipModules:
 
268
            log.debug("no coverage for %s: loaded before coverage start",
 
269
                      name)
 
270
            return False
 
271
        if self.conf.testMatch.search(name) and not self.coverTests:
 
272
            log.debug("no coverage for %s: is a test", name)
 
273
            return False
 
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
 
278
 
 
279
    def wantFile(self, file, package=None):
 
280
        """If inclusive coverage enabled, return true for all source files
 
281
        in wanted packages.
 
282
        """
 
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):
 
288
                            return True
 
289
                else:
 
290
                    return True
 
291
        return None