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 .annotate import AnnotateReporter |
6 from .codeunit import code_unit_factory |
6 from .backward import string_class # pylint: disable-msg=W0622 |
|
7 from .codeunit import code_unit_factory, CodeUnit |
7 from .collector import Collector |
8 from .collector import Collector |
8 from .data import CoverageData |
9 from .data import CoverageData |
9 from .files import FileLocator |
10 from .files import FileLocator |
10 from .html import HtmlReporter |
11 from .html import HtmlReporter |
11 from .misc import format_lines, CoverageException |
12 from .results import Analysis |
12 from .summary import SummaryReporter |
13 from .summary import SummaryReporter |
13 |
14 from .xmlreport import XmlReporter |
14 class coverage: |
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, str): |
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): |
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 |