DebugClients/Python/coverage/control.py

changeset 31
744cd0b4b8cd
parent 0
de9c2efb9d02
child 32
01f04fbc1842
equal deleted inserted replaced
30:9513afbd57f1 31:744cd0b4b8cd
1 """Core control stuff for Coverage.""" 1 """Core control stuff for Coverage."""
2 2
3 import os, socket 3 import atexit, os, socket
4 4
5 from annotate import AnnotateReporter 5 from coverage.annotate import AnnotateReporter
6 from codeunit import code_unit_factory 6 from coverage.backward import string_class # pylint: disable-msg=W0622
7 from collector import Collector 7 from coverage.codeunit import code_unit_factory, CodeUnit
8 from data import CoverageData 8 from coverage.collector import Collector
9 from files import FileLocator 9 from coverage.data import CoverageData
10 from html import HtmlReporter 10 from coverage.files import FileLocator
11 from misc import format_lines, CoverageException 11 from coverage.html import HtmlReporter
12 from summary import SummaryReporter 12 from coverage.results import Analysis
13 13 from coverage.summary import SummaryReporter
14 class coverage: 14 from coverage.xmlreport import XmlReporter
15
16 class coverage(object):
15 """Programmatic access to Coverage. 17 """Programmatic access to Coverage.
16 18
17 To use:: 19 To use::
18 20
19 from coverage import coverage 21 from coverage import coverage
20 22
21 cov = coverage() 23 cov = coverage()
22 cov.start() 24 cov.start()
23 #.. blah blah (run your code) blah blah 25 #.. blah blah (run your code) blah blah ..
24 cov.stop() 26 cov.stop()
25 cov.html_report(directory='covhtml') 27 cov.html_report(directory='covhtml')
26 28
27 """ 29 """
30
28 def __init__(self, data_file=None, data_suffix=False, cover_pylib=False, 31 def __init__(self, data_file=None, data_suffix=False, cover_pylib=False,
29 auto_data=False): 32 auto_data=False, timid=False, branch=False):
30 """Create a new coverage measurement context. 33 """
31
32 `data_file` is the base name of the data file to use, defaulting to 34 `data_file` is the base name of the data file to use, defaulting to
33 ".coverage". `data_suffix` is appended to `data_file` to create the 35 ".coverage". `data_suffix` is appended to `data_file` to create the
34 final file name. If `data_suffix` is simply True, then a suffix is 36 final file name. If `data_suffix` is simply True, then a suffix is
35 created with the machine and process identity included. 37 created with the machine and process identity included.
36 38
37 `cover_pylib` is a boolean determining whether Python code installed 39 `cover_pylib` is a boolean determining whether Python code installed
38 with the Python interpreter is measured. This includes the Python 40 with the Python interpreter is measured. This includes the Python
39 standard library and any packages installed with the interpreter. 41 standard library and any packages installed with the interpreter.
40 42
41 If `auto_data` is true, then any existing data file will be read when 43 If `auto_data` is true, then any existing data file will be read when
42 coverage measurement starts, and data will be saved automatically when 44 coverage measurement starts, and data will be saved automatically when
43 measurement stops. 45 measurement stops.
44 46
47 If `timid` is true, then a slower and simpler trace function will be
48 used. This is important for some environments where manipulation of
49 tracing functions breaks the faster trace function.
50
51 If `branch` is true, then branch coverage will be measured in addition
52 to the usual statement coverage.
53
45 """ 54 """
46 from coverage import __version__ 55 from coverage import __version__
47 56
48 self.cover_pylib = cover_pylib 57 self.cover_pylib = cover_pylib
49 self.auto_data = auto_data 58 self.auto_data = auto_data
50 59 self.atexit_registered = False
60
51 self.exclude_re = "" 61 self.exclude_re = ""
52 self.exclude_list = [] 62 self.exclude_list = []
53 63
54 self.file_locator = FileLocator() 64 self.file_locator = FileLocator()
55 65
56 self.collector = Collector(self._should_trace) 66 # Timidity: for nose users, read an environment variable. This is a
67 # cheap hack, since the rest of the command line arguments aren't
68 # recognized, but it solves some users' problems.
69 timid = timid or ('--timid' in os.environ.get('COVERAGE_OPTIONS', ''))
70 self.collector = Collector(
71 self._should_trace, timid=timid, branch=branch
72 )
57 73
58 # Create the data file. 74 # Create the data file.
59 if data_suffix: 75 if data_suffix:
60 if not isinstance(data_suffix, basestring): 76 if not isinstance(data_suffix, string_class):
61 # if data_suffix=True, use .machinename.pid 77 # if data_suffix=True, use .machinename.pid
62 data_suffix = ".%s.%s" % (socket.gethostname(), os.getpid()) 78 data_suffix = ".%s.%s" % (socket.gethostname(), os.getpid())
63 else: 79 else:
64 data_suffix = None 80 data_suffix = None
65 81
71 # The default exclude pattern. 87 # The default exclude pattern.
72 self.exclude('# *pragma[: ]*[nN][oO] *[cC][oO][vV][eE][rR]') 88 self.exclude('# *pragma[: ]*[nN][oO] *[cC][oO][vV][eE][rR]')
73 89
74 # The prefix for files considered "installed with the interpreter". 90 # The prefix for files considered "installed with the interpreter".
75 if not self.cover_pylib: 91 if not self.cover_pylib:
92 # Look at where the "os" module is located. That's the indication
93 # for "installed with the interpreter".
76 os_file = self.file_locator.canonical_filename(os.__file__) 94 os_file = self.file_locator.canonical_filename(os.__file__)
77 self.pylib_prefix = os.path.split(os_file)[0] 95 self.pylib_prefix = os.path.split(os_file)[0]
78 96
97 # To avoid tracing the coverage code itself, we skip anything located
98 # where we are.
79 here = self.file_locator.canonical_filename(__file__) 99 here = self.file_locator.canonical_filename(__file__)
80 self.cover_prefix = os.path.split(here)[0] 100 self.cover_prefix = os.path.split(here)[0]
81 101
82 def _should_trace(self, filename, frame): 102 def _should_trace(self, filename, frame):
83 """Decide whether to trace execution in `filename` 103 """Decide whether to trace execution in `filename`
84 104
105 This function is called from the trace function. As each new file name
106 is encountered, this function determines whether it is traced or not.
107
85 Returns a canonicalized filename if it should be traced, False if it 108 Returns a canonicalized filename if it should be traced, False if it
86 should not. 109 should not.
87 110
88 """ 111 """
89 if filename == '<string>': 112 if filename == '<string>':
90 # There's no point in ever tracing string executions, we can't do 113 # There's no point in ever tracing string executions, we can't do
91 # anything with the data later anyway. 114 # anything with the data later anyway.
92 return False 115 return False
117 if canonical.startswith(self.cover_prefix): 140 if canonical.startswith(self.cover_prefix):
118 return False 141 return False
119 142
120 return canonical 143 return canonical
121 144
145 # To log what should_trace returns, change this to "if 1:"
146 if 0:
147 _real_should_trace = _should_trace
148 def _should_trace(self, filename, frame): # pylint: disable-msg=E0102
149 """A logging decorator around the real _should_trace function."""
150 ret = self._real_should_trace(filename, frame)
151 print("should_trace: %r -> %r" % (filename, ret))
152 return ret
153
122 def use_cache(self, usecache): 154 def use_cache(self, usecache):
123 """Control the use of a data file (incorrectly called a cache). 155 """Control the use of a data file (incorrectly called a cache).
124 156
125 `usecache` is true or false, whether to read and write data on disk. 157 `usecache` is true or false, whether to read and write data on disk.
126 158
127 """ 159 """
128 self.data.usefile(usecache) 160 self.data.usefile(usecache)
129 161
130 def load(self): 162 def load(self):
131 """Load previously-collected coverage data from the data file.""" 163 """Load previously-collected coverage data from the data file."""
132 self.collector.reset() 164 self.collector.reset()
133 self.data.read() 165 self.data.read()
134 166
135 def start(self): 167 def start(self):
136 """Start measuring code coverage.""" 168 """Start measuring code coverage."""
137 if self.auto_data: 169 if self.auto_data:
138 self.load() 170 self.load()
139 # Save coverage data when Python exits. 171 # Save coverage data when Python exits.
140 import atexit 172 if not self.atexit_registered:
141 atexit.register(self.save) 173 atexit.register(self.save)
174 self.atexit_registered = True
142 self.collector.start() 175 self.collector.start()
143 176
144 def stop(self): 177 def stop(self):
145 """Stop measuring code coverage.""" 178 """Stop measuring code coverage."""
146 self.collector.stop() 179 self.collector.stop()
147 self._harvest_data() 180 self._harvest_data()
148 181
149 def erase(self): 182 def erase(self):
150 """Erase previously-collected coverage data. 183 """Erase previously-collected coverage data.
151 184
152 This removes the in-memory data collected in this session as well as 185 This removes the in-memory data collected in this session as well as
153 discarding the data file. 186 discarding the data file.
154 187
155 """ 188 """
156 self.collector.reset() 189 self.collector.reset()
157 self.data.erase() 190 self.data.erase()
158 191
159 def clear_exclude(self): 192 def clear_exclude(self):
161 self.exclude_list = [] 194 self.exclude_list = []
162 self.exclude_re = "" 195 self.exclude_re = ""
163 196
164 def exclude(self, regex): 197 def exclude(self, regex):
165 """Exclude source lines from execution consideration. 198 """Exclude source lines from execution consideration.
166 199
167 `regex` is a regular expression. Lines matching this expression are 200 `regex` is a regular expression. Lines matching this expression are
168 not considered executable when reporting code coverage. A list of 201 not considered executable when reporting code coverage. A list of
169 regexes is maintained; this function adds a new regex to the list. 202 regexes is maintained; this function adds a new regex to the list.
170 Matching any of the regexes excludes a source line. 203 Matching any of the regexes excludes a source line.
171 204
172 """ 205 """
173 self.exclude_list.append(regex) 206 self.exclude_list.append(regex)
174 self.exclude_re = "(" + ")|(".join(self.exclude_list) + ")" 207 self.exclude_re = "(" + ")|(".join(self.exclude_list) + ")"
175 208
176 def get_exclude_list(self): 209 def get_exclude_list(self):
182 self._harvest_data() 215 self._harvest_data()
183 self.data.write() 216 self.data.write()
184 217
185 def combine(self): 218 def combine(self):
186 """Combine together a number of similarly-named coverage data files. 219 """Combine together a number of similarly-named coverage data files.
187 220
188 All coverage data files whose name starts with `data_file` (from the 221 All coverage data files whose name starts with `data_file` (from the
189 coverage() constructor) will be read, and combined together into the 222 coverage() constructor) will be read, and combined together into the
190 current measurements. 223 current measurements.
191 224
192 """ 225 """
193 self.data.combine_parallel_data() 226 self.data.combine_parallel_data()
194 227
195 def _harvest_data(self): 228 def _harvest_data(self):
196 """Get the collected data by filename and reset the collector.""" 229 """Get the collected data and reset the collector."""
197 self.data.add_line_data(self.collector.data_points()) 230 self.data.add_line_data(self.collector.get_line_data())
231 self.data.add_arc_data(self.collector.get_arc_data())
198 self.collector.reset() 232 self.collector.reset()
199 233
200 # Backward compatibility with version 1. 234 # Backward compatibility with version 1.
201 def analysis(self, morf): 235 def analysis(self, morf):
202 """Like `analysis2` but doesn't return excluded line numbers.""" 236 """Like `analysis2` but doesn't return excluded line numbers."""
203 f, s, _, m, mf = self.analysis2(morf) 237 f, s, _, m, mf = self.analysis2(morf)
204 return f, s, m, mf 238 return f, s, m, mf
205 239
206 def analysis2(self, morf): 240 def analysis2(self, morf):
207 """Analyze a module. 241 """Analyze a module.
208 242
209 `morf` is a module or a filename. It will be analyzed to determine 243 `morf` is a module or a filename. It will be analyzed to determine
210 its coverage statistics. The return value is a 5-tuple: 244 its coverage statistics. The return value is a 5-tuple:
211 245
212 * The filename for the module. 246 * The filename for the module.
213 * A list of line numbers of executable statements. 247 * A list of line numbers of executable statements.
214 * A list of line numbers of excluded statements. 248 * A list of line numbers of excluded statements.
215 * A list of line numbers of statements not run (missing from execution). 249 * A list of line numbers of statements not run (missing from
250 execution).
216 * A readable formatted string of the missing line numbers. 251 * A readable formatted string of the missing line numbers.
217 252
218 The analysis uses the source file itself and the current measured 253 The analysis uses the source file itself and the current measured
219 coverage data. 254 coverage data.
220 255
221 """ 256 """
222 code_unit = code_unit_factory(morf, self.file_locator)[0] 257 analysis = self._analyze(morf)
223 st, ex, m, mf = self._analyze(code_unit) 258 return (
224 return code_unit.filename, st, ex, m, mf 259 analysis.filename, analysis.statements, analysis.excluded,
225 260 analysis.missing, analysis.missing_formatted()
226 def _analyze(self, code_unit):
227 """Analyze a single code unit.
228
229 Returns a 4-tuple: (statements, excluded, missing, missing formatted).
230
231 """
232 from parser import CodeParser
233
234 filename = code_unit.filename
235 ext = os.path.splitext(filename)[1]
236 source = None
237 if ext == '.py':
238 if not os.path.exists(filename):
239 source = self.file_locator.get_zip_data(filename)
240 if not source:
241 raise CoverageException(
242 "No source for code '%s'." % code_unit.filename
243 )
244
245 parser = CodeParser()
246 statements, excluded, line_map = parser.parse_source(
247 text=source, filename=filename, exclude=self.exclude_re
248 ) 261 )
249 262
250 # Identify missing statements. 263 def _analyze(self, it):
251 missing = [] 264 """Analyze a single morf or code unit.
252 execed = self.data.executed_lines(filename) 265
253 for line in statements: 266 Returns an `Analysis` object.
254 lines = line_map.get(line) 267
255 if lines: 268 """
256 for l in range(lines[0], lines[1]+1): 269 if not isinstance(it, CodeUnit):
257 if l in execed: 270 it = code_unit_factory(it, self.file_locator)[0]
258 break 271
259 else: 272 return Analysis(self, it)
260 missing.append(line)
261 else:
262 if line not in execed:
263 missing.append(line)
264
265 return (
266 statements, excluded, missing, format_lines(statements, missing)
267 )
268 273
269 def report(self, morfs=None, show_missing=True, ignore_errors=False, 274 def report(self, morfs=None, show_missing=True, ignore_errors=False,
270 file=None, omit_prefixes=None): # pylint: disable-msg=W0622 275 file=None, omit_prefixes=None): # pylint: disable-msg=W0622
271 """Write a summary report to `file`. 276 """Write a summary report to `file`.
272 277
273 Each module in `morfs` is listed, with counts of statements, executed 278 Each module in `morfs` is listed, with counts of statements, executed
274 statements, missing statements, and a list of lines missed. 279 statements, missing statements, and a list of lines missed.
275 280
276 """ 281 """
277 reporter = SummaryReporter(self, show_missing, ignore_errors) 282 reporter = SummaryReporter(self, show_missing, ignore_errors)
278 reporter.report(morfs, outfile=file, omit_prefixes=omit_prefixes) 283 reporter.report(morfs, outfile=file, omit_prefixes=omit_prefixes)
279 284
280 def annotate(self, morfs=None, directory=None, ignore_errors=False, 285 def annotate(self, morfs=None, directory=None, ignore_errors=False,
281 omit_prefixes=None): 286 omit_prefixes=None):
282 """Annotate a list of modules. 287 """Annotate a list of modules.
283 288
284 Each module in `morfs` is annotated. The source is written to a new 289 Each module in `morfs` is annotated. The source is written to a new
285 file, named with a ",cover" suffix, with each line prefixed with a 290 file, named with a ",cover" suffix, with each line prefixed with a
286 marker to indicate the coverage of the line. Covered lines have ">", 291 marker to indicate the coverage of the line. Covered lines have ">",
287 excluded lines have "-", and missing lines have "!". 292 excluded lines have "-", and missing lines have "!".
288 293
289 """ 294 """
290 reporter = AnnotateReporter(self, ignore_errors) 295 reporter = AnnotateReporter(self, ignore_errors)
291 reporter.report( 296 reporter.report(
292 morfs, directory=directory, omit_prefixes=omit_prefixes) 297 morfs, directory=directory, omit_prefixes=omit_prefixes)
293 298
294 def html_report(self, morfs=None, directory=None, ignore_errors=False, 299 def html_report(self, morfs=None, directory=None, ignore_errors=False,
295 omit_prefixes=None): 300 omit_prefixes=None):
296 """Generate an HTML report. 301 """Generate an HTML report.
297 302
298 """ 303 """
299 reporter = HtmlReporter(self, ignore_errors) 304 reporter = HtmlReporter(self, ignore_errors)
300 reporter.report( 305 reporter.report(
301 morfs, directory=directory, omit_prefixes=omit_prefixes) 306 morfs, directory=directory, omit_prefixes=omit_prefixes)
307
308 def xml_report(self, morfs=None, outfile=None, ignore_errors=False,
309 omit_prefixes=None):
310 """Generate an XML report of coverage results.
311
312 The report is compatible with Cobertura reports.
313
314 """
315 if outfile:
316 outfile = open(outfile, "w")
317 try:
318 reporter = XmlReporter(self, ignore_errors)
319 reporter.report(
320 morfs, omit_prefixes=omit_prefixes, outfile=outfile)
321 finally:
322 outfile.close()
323
324 def sysinfo(self):
325 """Return a list of key,value pairs showing internal information."""
326
327 import coverage as covmod
328 import platform, re, sys
329
330 info = [
331 ('version', covmod.__version__),
332 ('coverage', covmod.__file__),
333 ('cover_prefix', self.cover_prefix),
334 ('pylib_prefix', self.pylib_prefix),
335 ('tracer', self.collector.tracer_name()),
336 ('data_path', self.data.filename),
337 ('python', sys.version.replace('\n', '')),
338 ('platform', platform.platform()),
339 ('cwd', os.getcwd()),
340 ('path', sys.path),
341 ('environment', [
342 ("%s = %s" % (k, v)) for k, v in os.environ.items()
343 if re.search("^COV|^PY", k)
344 ]),
345 ]
346 return info

eric ide

mercurial