|
1 # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 |
|
2 # For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt |
|
3 |
|
4 """Core control stuff for coverage.py.""" |
|
5 |
|
6 import atexit |
|
7 import collections |
|
8 import contextlib |
|
9 import os |
|
10 import os.path |
|
11 import platform |
|
12 import sys |
|
13 import time |
|
14 |
|
15 from coverage import env |
|
16 from coverage.annotate import AnnotateReporter |
|
17 from coverage.backward import string_class, iitems |
|
18 from coverage.collector import Collector, CTracer |
|
19 from coverage.config import read_coverage_config |
|
20 from coverage.context import should_start_context_test_function, combine_context_switchers |
|
21 from coverage.data import CoverageData, combine_parallel_data |
|
22 from coverage.debug import DebugControl, short_stack, write_formatted_info |
|
23 from coverage.disposition import disposition_debug_msg |
|
24 from coverage.files import PathAliases, abs_file, relative_filename, set_relative_directory |
|
25 from coverage.html import HtmlReporter |
|
26 from coverage.inorout import InOrOut |
|
27 from coverage.jsonreport import JsonReporter |
|
28 from coverage.misc import CoverageException, bool_or_none, join_regex |
|
29 from coverage.misc import DefaultValue, ensure_dir_for_file, isolate_module |
|
30 from coverage.plugin import FileReporter |
|
31 from coverage.plugin_support import Plugins |
|
32 from coverage.python import PythonFileReporter |
|
33 from coverage.report import render_report |
|
34 from coverage.results import Analysis, Numbers |
|
35 from coverage.summary import SummaryReporter |
|
36 from coverage.xmlreport import XmlReporter |
|
37 |
|
38 try: |
|
39 from coverage.multiproc import patch_multiprocessing |
|
40 except ImportError: # pragma: only jython |
|
41 # Jython has no multiprocessing module. |
|
42 patch_multiprocessing = None |
|
43 |
|
44 os = isolate_module(os) |
|
45 |
|
46 @contextlib.contextmanager |
|
47 def override_config(cov, **kwargs): |
|
48 """Temporarily tweak the configuration of `cov`. |
|
49 |
|
50 The arguments are applied to `cov.config` with the `from_args` method. |
|
51 At the end of the with-statement, the old configuration is restored. |
|
52 """ |
|
53 original_config = cov.config |
|
54 cov.config = cov.config.copy() |
|
55 try: |
|
56 cov.config.from_args(**kwargs) |
|
57 yield |
|
58 finally: |
|
59 cov.config = original_config |
|
60 |
|
61 |
|
62 _DEFAULT_DATAFILE = DefaultValue("MISSING") |
|
63 |
|
64 class Coverage(object): |
|
65 """Programmatic access to coverage.py. |
|
66 |
|
67 To use:: |
|
68 |
|
69 from coverage import Coverage |
|
70 |
|
71 cov = Coverage() |
|
72 cov.start() |
|
73 #.. call your code .. |
|
74 cov.stop() |
|
75 cov.html_report(directory='covhtml') |
|
76 |
|
77 Note: in keeping with Python custom, names starting with underscore are |
|
78 not part of the public API. They might stop working at any point. Please |
|
79 limit yourself to documented methods to avoid problems. |
|
80 |
|
81 """ |
|
82 |
|
83 # The stack of started Coverage instances. |
|
84 _instances = [] |
|
85 |
|
86 @classmethod |
|
87 def current(cls): |
|
88 """Get the latest started `Coverage` instance, if any. |
|
89 |
|
90 Returns: a `Coverage` instance, or None. |
|
91 |
|
92 .. versionadded:: 5.0 |
|
93 |
|
94 """ |
|
95 if cls._instances: |
|
96 return cls._instances[-1] |
|
97 else: |
|
98 return None |
|
99 |
|
100 def __init__( |
|
101 self, data_file=_DEFAULT_DATAFILE, data_suffix=None, cover_pylib=None, |
|
102 auto_data=False, timid=None, branch=None, config_file=True, |
|
103 source=None, source_pkgs=None, omit=None, include=None, debug=None, |
|
104 concurrency=None, check_preimported=False, context=None, |
|
105 ): # pylint: disable=too-many-arguments |
|
106 """ |
|
107 Many of these arguments duplicate and override values that can be |
|
108 provided in a configuration file. Parameters that are missing here |
|
109 will use values from the config file. |
|
110 |
|
111 `data_file` is the base name of the data file to use. The config value |
|
112 defaults to ".coverage". None can be provided to prevent writing a data |
|
113 file. `data_suffix` is appended (with a dot) to `data_file` to create |
|
114 the final file name. If `data_suffix` is simply True, then a suffix is |
|
115 created with the machine and process identity included. |
|
116 |
|
117 `cover_pylib` is a boolean determining whether Python code installed |
|
118 with the Python interpreter is measured. This includes the Python |
|
119 standard library and any packages installed with the interpreter. |
|
120 |
|
121 If `auto_data` is true, then any existing data file will be read when |
|
122 coverage measurement starts, and data will be saved automatically when |
|
123 measurement stops. |
|
124 |
|
125 If `timid` is true, then a slower and simpler trace function will be |
|
126 used. This is important for some environments where manipulation of |
|
127 tracing functions breaks the faster trace function. |
|
128 |
|
129 If `branch` is true, then branch coverage will be measured in addition |
|
130 to the usual statement coverage. |
|
131 |
|
132 `config_file` determines what configuration file to read: |
|
133 |
|
134 * If it is ".coveragerc", it is interpreted as if it were True, |
|
135 for backward compatibility. |
|
136 |
|
137 * If it is a string, it is the name of the file to read. If the |
|
138 file can't be read, it is an error. |
|
139 |
|
140 * If it is True, then a few standard files names are tried |
|
141 (".coveragerc", "setup.cfg", "tox.ini"). It is not an error for |
|
142 these files to not be found. |
|
143 |
|
144 * If it is False, then no configuration file is read. |
|
145 |
|
146 `source` is a list of file paths or package names. Only code located |
|
147 in the trees indicated by the file paths or package names will be |
|
148 measured. |
|
149 |
|
150 `source_pkgs` is a list of package names. It works the same as |
|
151 `source`, but can be used to name packages where the name can also be |
|
152 interpreted as a file path. |
|
153 |
|
154 `include` and `omit` are lists of file name patterns. Files that match |
|
155 `include` will be measured, files that match `omit` will not. Each |
|
156 will also accept a single string argument. |
|
157 |
|
158 `debug` is a list of strings indicating what debugging information is |
|
159 desired. |
|
160 |
|
161 `concurrency` is a string indicating the concurrency library being used |
|
162 in the measured code. Without this, coverage.py will get incorrect |
|
163 results if these libraries are in use. Valid strings are "greenlet", |
|
164 "eventlet", "gevent", "multiprocessing", or "thread" (the default). |
|
165 This can also be a list of these strings. |
|
166 |
|
167 If `check_preimported` is true, then when coverage is started, the |
|
168 already-imported files will be checked to see if they should be |
|
169 measured by coverage. Importing measured files before coverage is |
|
170 started can mean that code is missed. |
|
171 |
|
172 `context` is a string to use as the :ref:`static context |
|
173 <static_contexts>` label for collected data. |
|
174 |
|
175 .. versionadded:: 4.0 |
|
176 The `concurrency` parameter. |
|
177 |
|
178 .. versionadded:: 4.2 |
|
179 The `concurrency` parameter can now be a list of strings. |
|
180 |
|
181 .. versionadded:: 5.0 |
|
182 The `check_preimported` and `context` parameters. |
|
183 |
|
184 .. versionadded:: 5.3 |
|
185 The `source_pkgs` parameter. |
|
186 |
|
187 """ |
|
188 # data_file=None means no disk file at all. data_file missing means |
|
189 # use the value from the config file. |
|
190 self._no_disk = data_file is None |
|
191 if data_file is _DEFAULT_DATAFILE: |
|
192 data_file = None |
|
193 |
|
194 # Build our configuration from a number of sources. |
|
195 self.config = read_coverage_config( |
|
196 config_file=config_file, |
|
197 data_file=data_file, cover_pylib=cover_pylib, timid=timid, |
|
198 branch=branch, parallel=bool_or_none(data_suffix), |
|
199 source=source, source_pkgs=source_pkgs, run_omit=omit, run_include=include, debug=debug, |
|
200 report_omit=omit, report_include=include, |
|
201 concurrency=concurrency, context=context, |
|
202 ) |
|
203 |
|
204 # This is injectable by tests. |
|
205 self._debug_file = None |
|
206 |
|
207 self._auto_load = self._auto_save = auto_data |
|
208 self._data_suffix_specified = data_suffix |
|
209 |
|
210 # Is it ok for no data to be collected? |
|
211 self._warn_no_data = True |
|
212 self._warn_unimported_source = True |
|
213 self._warn_preimported_source = check_preimported |
|
214 self._no_warn_slugs = None |
|
215 |
|
216 # A record of all the warnings that have been issued. |
|
217 self._warnings = [] |
|
218 |
|
219 # Other instance attributes, set later. |
|
220 self._data = self._collector = None |
|
221 self._plugins = None |
|
222 self._inorout = None |
|
223 self._data_suffix = self._run_suffix = None |
|
224 self._exclude_re = None |
|
225 self._debug = None |
|
226 self._file_mapper = None |
|
227 |
|
228 # State machine variables: |
|
229 # Have we initialized everything? |
|
230 self._inited = False |
|
231 self._inited_for_start = False |
|
232 # Have we started collecting and not stopped it? |
|
233 self._started = False |
|
234 # Should we write the debug output? |
|
235 self._should_write_debug = True |
|
236 |
|
237 # If we have sub-process measurement happening automatically, then we |
|
238 # want any explicit creation of a Coverage object to mean, this process |
|
239 # is already coverage-aware, so don't auto-measure it. By now, the |
|
240 # auto-creation of a Coverage object has already happened. But we can |
|
241 # find it and tell it not to save its data. |
|
242 if not env.METACOV: |
|
243 _prevent_sub_process_measurement() |
|
244 |
|
245 def _init(self): |
|
246 """Set all the initial state. |
|
247 |
|
248 This is called by the public methods to initialize state. This lets us |
|
249 construct a :class:`Coverage` object, then tweak its state before this |
|
250 function is called. |
|
251 |
|
252 """ |
|
253 if self._inited: |
|
254 return |
|
255 |
|
256 self._inited = True |
|
257 |
|
258 # Create and configure the debugging controller. COVERAGE_DEBUG_FILE |
|
259 # is an environment variable, the name of a file to append debug logs |
|
260 # to. |
|
261 self._debug = DebugControl(self.config.debug, self._debug_file) |
|
262 |
|
263 if "multiprocessing" in (self.config.concurrency or ()): |
|
264 # Multi-processing uses parallel for the subprocesses, so also use |
|
265 # it for the main process. |
|
266 self.config.parallel = True |
|
267 |
|
268 # _exclude_re is a dict that maps exclusion list names to compiled regexes. |
|
269 self._exclude_re = {} |
|
270 |
|
271 set_relative_directory() |
|
272 self._file_mapper = relative_filename if self.config.relative_files else abs_file |
|
273 |
|
274 # Load plugins |
|
275 self._plugins = Plugins.load_plugins(self.config.plugins, self.config, self._debug) |
|
276 |
|
277 # Run configuring plugins. |
|
278 for plugin in self._plugins.configurers: |
|
279 # We need an object with set_option and get_option. Either self or |
|
280 # self.config will do. Choosing randomly stops people from doing |
|
281 # other things with those objects, against the public API. Yes, |
|
282 # this is a bit childish. :) |
|
283 plugin.configure([self, self.config][int(time.time()) % 2]) |
|
284 |
|
285 def _post_init(self): |
|
286 """Stuff to do after everything is initialized.""" |
|
287 if self._should_write_debug: |
|
288 self._should_write_debug = False |
|
289 self._write_startup_debug() |
|
290 |
|
291 # '[run] _crash' will raise an exception if the value is close by in |
|
292 # the call stack, for testing error handling. |
|
293 if self.config._crash and self.config._crash in short_stack(limit=4): |
|
294 raise Exception("Crashing because called by {}".format(self.config._crash)) |
|
295 |
|
296 def _write_startup_debug(self): |
|
297 """Write out debug info at startup if needed.""" |
|
298 wrote_any = False |
|
299 with self._debug.without_callers(): |
|
300 if self._debug.should('config'): |
|
301 config_info = sorted(self.config.__dict__.items()) |
|
302 config_info = [(k, v) for k, v in config_info if not k.startswith('_')] |
|
303 write_formatted_info(self._debug, "config", config_info) |
|
304 wrote_any = True |
|
305 |
|
306 if self._debug.should('sys'): |
|
307 write_formatted_info(self._debug, "sys", self.sys_info()) |
|
308 for plugin in self._plugins: |
|
309 header = "sys: " + plugin._coverage_plugin_name |
|
310 info = plugin.sys_info() |
|
311 write_formatted_info(self._debug, header, info) |
|
312 wrote_any = True |
|
313 |
|
314 if wrote_any: |
|
315 write_formatted_info(self._debug, "end", ()) |
|
316 |
|
317 def _should_trace(self, filename, frame): |
|
318 """Decide whether to trace execution in `filename`. |
|
319 |
|
320 Calls `_should_trace_internal`, and returns the FileDisposition. |
|
321 |
|
322 """ |
|
323 disp = self._inorout.should_trace(filename, frame) |
|
324 if self._debug.should('trace'): |
|
325 self._debug.write(disposition_debug_msg(disp)) |
|
326 return disp |
|
327 |
|
328 def _check_include_omit_etc(self, filename, frame): |
|
329 """Check a file name against the include/omit/etc, rules, verbosely. |
|
330 |
|
331 Returns a boolean: True if the file should be traced, False if not. |
|
332 |
|
333 """ |
|
334 reason = self._inorout.check_include_omit_etc(filename, frame) |
|
335 if self._debug.should('trace'): |
|
336 if not reason: |
|
337 msg = "Including %r" % (filename,) |
|
338 else: |
|
339 msg = "Not including %r: %s" % (filename, reason) |
|
340 self._debug.write(msg) |
|
341 |
|
342 return not reason |
|
343 |
|
344 def _warn(self, msg, slug=None, once=False): |
|
345 """Use `msg` as a warning. |
|
346 |
|
347 For warning suppression, use `slug` as the shorthand. |
|
348 |
|
349 If `once` is true, only show this warning once (determined by the |
|
350 slug.) |
|
351 |
|
352 """ |
|
353 if self._no_warn_slugs is None: |
|
354 self._no_warn_slugs = list(self.config.disable_warnings) |
|
355 |
|
356 if slug in self._no_warn_slugs: |
|
357 # Don't issue the warning |
|
358 return |
|
359 |
|
360 self._warnings.append(msg) |
|
361 if slug: |
|
362 msg = "%s (%s)" % (msg, slug) |
|
363 if self._debug.should('pid'): |
|
364 msg = "[%d] %s" % (os.getpid(), msg) |
|
365 sys.stderr.write("Coverage.py warning: %s\n" % msg) |
|
366 |
|
367 if once: |
|
368 self._no_warn_slugs.append(slug) |
|
369 |
|
370 def get_option(self, option_name): |
|
371 """Get an option from the configuration. |
|
372 |
|
373 `option_name` is a colon-separated string indicating the section and |
|
374 option name. For example, the ``branch`` option in the ``[run]`` |
|
375 section of the config file would be indicated with `"run:branch"`. |
|
376 |
|
377 Returns the value of the option. The type depends on the option |
|
378 selected. |
|
379 |
|
380 As a special case, an `option_name` of ``"paths"`` will return an |
|
381 OrderedDict with the entire ``[paths]`` section value. |
|
382 |
|
383 .. versionadded:: 4.0 |
|
384 |
|
385 """ |
|
386 return self.config.get_option(option_name) |
|
387 |
|
388 def set_option(self, option_name, value): |
|
389 """Set an option in the configuration. |
|
390 |
|
391 `option_name` is a colon-separated string indicating the section and |
|
392 option name. For example, the ``branch`` option in the ``[run]`` |
|
393 section of the config file would be indicated with ``"run:branch"``. |
|
394 |
|
395 `value` is the new value for the option. This should be an |
|
396 appropriate Python value. For example, use True for booleans, not the |
|
397 string ``"True"``. |
|
398 |
|
399 As an example, calling:: |
|
400 |
|
401 cov.set_option("run:branch", True) |
|
402 |
|
403 has the same effect as this configuration file:: |
|
404 |
|
405 [run] |
|
406 branch = True |
|
407 |
|
408 As a special case, an `option_name` of ``"paths"`` will replace the |
|
409 entire ``[paths]`` section. The value should be an OrderedDict. |
|
410 |
|
411 .. versionadded:: 4.0 |
|
412 |
|
413 """ |
|
414 self.config.set_option(option_name, value) |
|
415 |
|
416 def load(self): |
|
417 """Load previously-collected coverage data from the data file.""" |
|
418 self._init() |
|
419 if self._collector: |
|
420 self._collector.reset() |
|
421 should_skip = self.config.parallel and not os.path.exists(self.config.data_file) |
|
422 if not should_skip: |
|
423 self._init_data(suffix=None) |
|
424 self._post_init() |
|
425 if not should_skip: |
|
426 self._data.read() |
|
427 |
|
428 def _init_for_start(self): |
|
429 """Initialization for start()""" |
|
430 # Construct the collector. |
|
431 concurrency = self.config.concurrency or () |
|
432 if "multiprocessing" in concurrency: |
|
433 if not patch_multiprocessing: |
|
434 raise CoverageException( # pragma: only jython |
|
435 "multiprocessing is not supported on this Python" |
|
436 ) |
|
437 patch_multiprocessing(rcfile=self.config.config_file) |
|
438 |
|
439 dycon = self.config.dynamic_context |
|
440 if not dycon or dycon == "none": |
|
441 context_switchers = [] |
|
442 elif dycon == "test_function": |
|
443 context_switchers = [should_start_context_test_function] |
|
444 else: |
|
445 raise CoverageException( |
|
446 "Don't understand dynamic_context setting: {!r}".format(dycon) |
|
447 ) |
|
448 |
|
449 context_switchers.extend( |
|
450 plugin.dynamic_context for plugin in self._plugins.context_switchers |
|
451 ) |
|
452 |
|
453 should_start_context = combine_context_switchers(context_switchers) |
|
454 |
|
455 self._collector = Collector( |
|
456 should_trace=self._should_trace, |
|
457 check_include=self._check_include_omit_etc, |
|
458 should_start_context=should_start_context, |
|
459 file_mapper=self._file_mapper, |
|
460 timid=self.config.timid, |
|
461 branch=self.config.branch, |
|
462 warn=self._warn, |
|
463 concurrency=concurrency, |
|
464 ) |
|
465 |
|
466 suffix = self._data_suffix_specified |
|
467 if suffix or self.config.parallel: |
|
468 if not isinstance(suffix, string_class): |
|
469 # if data_suffix=True, use .machinename.pid.random |
|
470 suffix = True |
|
471 else: |
|
472 suffix = None |
|
473 |
|
474 self._init_data(suffix) |
|
475 |
|
476 self._collector.use_data(self._data, self.config.context) |
|
477 |
|
478 # Early warning if we aren't going to be able to support plugins. |
|
479 if self._plugins.file_tracers and not self._collector.supports_plugins: |
|
480 self._warn( |
|
481 "Plugin file tracers (%s) aren't supported with %s" % ( |
|
482 ", ".join( |
|
483 plugin._coverage_plugin_name |
|
484 for plugin in self._plugins.file_tracers |
|
485 ), |
|
486 self._collector.tracer_name(), |
|
487 ) |
|
488 ) |
|
489 for plugin in self._plugins.file_tracers: |
|
490 plugin._coverage_enabled = False |
|
491 |
|
492 # Create the file classifying substructure. |
|
493 self._inorout = InOrOut( |
|
494 warn=self._warn, |
|
495 debug=(self._debug if self._debug.should('trace') else None), |
|
496 ) |
|
497 self._inorout.configure(self.config) |
|
498 self._inorout.plugins = self._plugins |
|
499 self._inorout.disp_class = self._collector.file_disposition_class |
|
500 |
|
501 # It's useful to write debug info after initing for start. |
|
502 self._should_write_debug = True |
|
503 |
|
504 atexit.register(self._atexit) |
|
505 |
|
506 def _init_data(self, suffix): |
|
507 """Create a data file if we don't have one yet.""" |
|
508 if self._data is None: |
|
509 # Create the data file. We do this at construction time so that the |
|
510 # data file will be written into the directory where the process |
|
511 # started rather than wherever the process eventually chdir'd to. |
|
512 ensure_dir_for_file(self.config.data_file) |
|
513 self._data = CoverageData( |
|
514 basename=self.config.data_file, |
|
515 suffix=suffix, |
|
516 warn=self._warn, |
|
517 debug=self._debug, |
|
518 no_disk=self._no_disk, |
|
519 ) |
|
520 |
|
521 def start(self): |
|
522 """Start measuring code coverage. |
|
523 |
|
524 Coverage measurement only occurs in functions called after |
|
525 :meth:`start` is invoked. Statements in the same scope as |
|
526 :meth:`start` won't be measured. |
|
527 |
|
528 Once you invoke :meth:`start`, you must also call :meth:`stop` |
|
529 eventually, or your process might not shut down cleanly. |
|
530 |
|
531 """ |
|
532 self._init() |
|
533 if not self._inited_for_start: |
|
534 self._inited_for_start = True |
|
535 self._init_for_start() |
|
536 self._post_init() |
|
537 |
|
538 # Issue warnings for possible problems. |
|
539 self._inorout.warn_conflicting_settings() |
|
540 |
|
541 # See if we think some code that would eventually be measured has |
|
542 # already been imported. |
|
543 if self._warn_preimported_source: |
|
544 self._inorout.warn_already_imported_files() |
|
545 |
|
546 if self._auto_load: |
|
547 self.load() |
|
548 |
|
549 self._collector.start() |
|
550 self._started = True |
|
551 self._instances.append(self) |
|
552 |
|
553 def stop(self): |
|
554 """Stop measuring code coverage.""" |
|
555 if self._instances: |
|
556 if self._instances[-1] is self: |
|
557 self._instances.pop() |
|
558 if self._started: |
|
559 self._collector.stop() |
|
560 self._started = False |
|
561 |
|
562 def _atexit(self): |
|
563 """Clean up on process shutdown.""" |
|
564 if self._debug.should("process"): |
|
565 self._debug.write("atexit: pid: {}, instance: {!r}".format(os.getpid(), self)) |
|
566 if self._started: |
|
567 self.stop() |
|
568 if self._auto_save: |
|
569 self.save() |
|
570 |
|
571 def erase(self): |
|
572 """Erase previously collected coverage data. |
|
573 |
|
574 This removes the in-memory data collected in this session as well as |
|
575 discarding the data file. |
|
576 |
|
577 """ |
|
578 self._init() |
|
579 self._post_init() |
|
580 if self._collector: |
|
581 self._collector.reset() |
|
582 self._init_data(suffix=None) |
|
583 self._data.erase(parallel=self.config.parallel) |
|
584 self._data = None |
|
585 self._inited_for_start = False |
|
586 |
|
587 def switch_context(self, new_context): |
|
588 """Switch to a new dynamic context. |
|
589 |
|
590 `new_context` is a string to use as the :ref:`dynamic context |
|
591 <dynamic_contexts>` label for collected data. If a :ref:`static |
|
592 context <static_contexts>` is in use, the static and dynamic context |
|
593 labels will be joined together with a pipe character. |
|
594 |
|
595 Coverage collection must be started already. |
|
596 |
|
597 .. versionadded:: 5.0 |
|
598 |
|
599 """ |
|
600 if not self._started: # pragma: part started |
|
601 raise CoverageException( |
|
602 "Cannot switch context, coverage is not started" |
|
603 ) |
|
604 |
|
605 if self._collector.should_start_context: |
|
606 self._warn("Conflicting dynamic contexts", slug="dynamic-conflict", once=True) |
|
607 |
|
608 self._collector.switch_context(new_context) |
|
609 |
|
610 def clear_exclude(self, which='exclude'): |
|
611 """Clear the exclude list.""" |
|
612 self._init() |
|
613 setattr(self.config, which + "_list", []) |
|
614 self._exclude_regex_stale() |
|
615 |
|
616 def exclude(self, regex, which='exclude'): |
|
617 """Exclude source lines from execution consideration. |
|
618 |
|
619 A number of lists of regular expressions are maintained. Each list |
|
620 selects lines that are treated differently during reporting. |
|
621 |
|
622 `which` determines which list is modified. The "exclude" list selects |
|
623 lines that are not considered executable at all. The "partial" list |
|
624 indicates lines with branches that are not taken. |
|
625 |
|
626 `regex` is a regular expression. The regex is added to the specified |
|
627 list. If any of the regexes in the list is found in a line, the line |
|
628 is marked for special treatment during reporting. |
|
629 |
|
630 """ |
|
631 self._init() |
|
632 excl_list = getattr(self.config, which + "_list") |
|
633 excl_list.append(regex) |
|
634 self._exclude_regex_stale() |
|
635 |
|
636 def _exclude_regex_stale(self): |
|
637 """Drop all the compiled exclusion regexes, a list was modified.""" |
|
638 self._exclude_re.clear() |
|
639 |
|
640 def _exclude_regex(self, which): |
|
641 """Return a compiled regex for the given exclusion list.""" |
|
642 if which not in self._exclude_re: |
|
643 excl_list = getattr(self.config, which + "_list") |
|
644 self._exclude_re[which] = join_regex(excl_list) |
|
645 return self._exclude_re[which] |
|
646 |
|
647 def get_exclude_list(self, which='exclude'): |
|
648 """Return a list of excluded regex patterns. |
|
649 |
|
650 `which` indicates which list is desired. See :meth:`exclude` for the |
|
651 lists that are available, and their meaning. |
|
652 |
|
653 """ |
|
654 self._init() |
|
655 return getattr(self.config, which + "_list") |
|
656 |
|
657 def save(self): |
|
658 """Save the collected coverage data to the data file.""" |
|
659 data = self.get_data() |
|
660 data.write() |
|
661 |
|
662 def combine(self, data_paths=None, strict=False): |
|
663 """Combine together a number of similarly-named coverage data files. |
|
664 |
|
665 All coverage data files whose name starts with `data_file` (from the |
|
666 coverage() constructor) will be read, and combined together into the |
|
667 current measurements. |
|
668 |
|
669 `data_paths` is a list of files or directories from which data should |
|
670 be combined. If no list is passed, then the data files from the |
|
671 directory indicated by the current data file (probably the current |
|
672 directory) will be combined. |
|
673 |
|
674 If `strict` is true, then it is an error to attempt to combine when |
|
675 there are no data files to combine. |
|
676 |
|
677 .. versionadded:: 4.0 |
|
678 The `data_paths` parameter. |
|
679 |
|
680 .. versionadded:: 4.3 |
|
681 The `strict` parameter. |
|
682 |
|
683 """ |
|
684 self._init() |
|
685 self._init_data(suffix=None) |
|
686 self._post_init() |
|
687 self.get_data() |
|
688 |
|
689 aliases = None |
|
690 if self.config.paths: |
|
691 aliases = PathAliases() |
|
692 for paths in self.config.paths.values(): |
|
693 result = paths[0] |
|
694 for pattern in paths[1:]: |
|
695 aliases.add(pattern, result) |
|
696 |
|
697 combine_parallel_data(self._data, aliases=aliases, data_paths=data_paths, strict=strict) |
|
698 |
|
699 def get_data(self): |
|
700 """Get the collected data. |
|
701 |
|
702 Also warn about various problems collecting data. |
|
703 |
|
704 Returns a :class:`coverage.CoverageData`, the collected coverage data. |
|
705 |
|
706 .. versionadded:: 4.0 |
|
707 |
|
708 """ |
|
709 self._init() |
|
710 self._init_data(suffix=None) |
|
711 self._post_init() |
|
712 |
|
713 for plugin in self._plugins: |
|
714 if not plugin._coverage_enabled: |
|
715 self._collector.plugin_was_disabled(plugin) |
|
716 |
|
717 if self._collector and self._collector.flush_data(): |
|
718 self._post_save_work() |
|
719 |
|
720 return self._data |
|
721 |
|
722 def _post_save_work(self): |
|
723 """After saving data, look for warnings, post-work, etc. |
|
724 |
|
725 Warn about things that should have happened but didn't. |
|
726 Look for unexecuted files. |
|
727 |
|
728 """ |
|
729 # If there are still entries in the source_pkgs_unmatched list, |
|
730 # then we never encountered those packages. |
|
731 if self._warn_unimported_source: |
|
732 self._inorout.warn_unimported_source() |
|
733 |
|
734 # Find out if we got any data. |
|
735 if not self._data and self._warn_no_data: |
|
736 self._warn("No data was collected.", slug="no-data-collected") |
|
737 |
|
738 # Touch all the files that could have executed, so that we can |
|
739 # mark completely unexecuted files as 0% covered. |
|
740 if self._data is not None: |
|
741 file_paths = collections.defaultdict(list) |
|
742 for file_path, plugin_name in self._inorout.find_possibly_unexecuted_files(): |
|
743 file_path = self._file_mapper(file_path) |
|
744 file_paths[plugin_name].append(file_path) |
|
745 for plugin_name, paths in file_paths.items(): |
|
746 self._data.touch_files(paths, plugin_name) |
|
747 |
|
748 if self.config.note: |
|
749 self._warn("The '[run] note' setting is no longer supported.") |
|
750 |
|
751 # Backward compatibility with version 1. |
|
752 def analysis(self, morf): |
|
753 """Like `analysis2` but doesn't return excluded line numbers.""" |
|
754 f, s, _, m, mf = self.analysis2(morf) |
|
755 return f, s, m, mf |
|
756 |
|
757 def analysis2(self, morf): |
|
758 """Analyze a module. |
|
759 |
|
760 `morf` is a module or a file name. It will be analyzed to determine |
|
761 its coverage statistics. The return value is a 5-tuple: |
|
762 |
|
763 * The file name for the module. |
|
764 * A list of line numbers of executable statements. |
|
765 * A list of line numbers of excluded statements. |
|
766 * A list of line numbers of statements not run (missing from |
|
767 execution). |
|
768 * A readable formatted string of the missing line numbers. |
|
769 |
|
770 The analysis uses the source file itself and the current measured |
|
771 coverage data. |
|
772 |
|
773 """ |
|
774 analysis = self._analyze(morf) |
|
775 return ( |
|
776 analysis.filename, |
|
777 sorted(analysis.statements), |
|
778 sorted(analysis.excluded), |
|
779 sorted(analysis.missing), |
|
780 analysis.missing_formatted(), |
|
781 ) |
|
782 |
|
783 def _analyze(self, it): |
|
784 """Analyze a single morf or code unit. |
|
785 |
|
786 Returns an `Analysis` object. |
|
787 |
|
788 """ |
|
789 # All reporting comes through here, so do reporting initialization. |
|
790 self._init() |
|
791 Numbers.set_precision(self.config.precision) |
|
792 self._post_init() |
|
793 |
|
794 data = self.get_data() |
|
795 if not isinstance(it, FileReporter): |
|
796 it = self._get_file_reporter(it) |
|
797 |
|
798 return Analysis(data, it, self._file_mapper) |
|
799 |
|
800 def _get_file_reporter(self, morf): |
|
801 """Get a FileReporter for a module or file name.""" |
|
802 plugin = None |
|
803 file_reporter = "python" |
|
804 |
|
805 if isinstance(morf, string_class): |
|
806 mapped_morf = self._file_mapper(morf) |
|
807 plugin_name = self._data.file_tracer(mapped_morf) |
|
808 if plugin_name: |
|
809 plugin = self._plugins.get(plugin_name) |
|
810 |
|
811 if plugin: |
|
812 file_reporter = plugin.file_reporter(mapped_morf) |
|
813 if file_reporter is None: |
|
814 raise CoverageException( |
|
815 "Plugin %r did not provide a file reporter for %r." % ( |
|
816 plugin._coverage_plugin_name, morf |
|
817 ) |
|
818 ) |
|
819 |
|
820 if file_reporter == "python": |
|
821 file_reporter = PythonFileReporter(morf, self) |
|
822 |
|
823 return file_reporter |
|
824 |
|
825 def _get_file_reporters(self, morfs=None): |
|
826 """Get a list of FileReporters for a list of modules or file names. |
|
827 |
|
828 For each module or file name in `morfs`, find a FileReporter. Return |
|
829 the list of FileReporters. |
|
830 |
|
831 If `morfs` is a single module or file name, this returns a list of one |
|
832 FileReporter. If `morfs` is empty or None, then the list of all files |
|
833 measured is used to find the FileReporters. |
|
834 |
|
835 """ |
|
836 if not morfs: |
|
837 morfs = self._data.measured_files() |
|
838 |
|
839 # Be sure we have a collection. |
|
840 if not isinstance(morfs, (list, tuple, set)): |
|
841 morfs = [morfs] |
|
842 |
|
843 file_reporters = [self._get_file_reporter(morf) for morf in morfs] |
|
844 return file_reporters |
|
845 |
|
846 def report( |
|
847 self, morfs=None, show_missing=None, ignore_errors=None, |
|
848 file=None, omit=None, include=None, skip_covered=None, |
|
849 contexts=None, skip_empty=None, precision=None, sort=None |
|
850 ): |
|
851 """Write a textual summary report to `file`. |
|
852 |
|
853 Each module in `morfs` is listed, with counts of statements, executed |
|
854 statements, missing statements, and a list of lines missed. |
|
855 |
|
856 If `show_missing` is true, then details of which lines or branches are |
|
857 missing will be included in the report. If `ignore_errors` is true, |
|
858 then a failure while reporting a single file will not stop the entire |
|
859 report. |
|
860 |
|
861 `file` is a file-like object, suitable for writing. |
|
862 |
|
863 `include` is a list of file name patterns. Files that match will be |
|
864 included in the report. Files matching `omit` will not be included in |
|
865 the report. |
|
866 |
|
867 If `skip_covered` is true, don't report on files with 100% coverage. |
|
868 |
|
869 If `skip_empty` is true, don't report on empty files (those that have |
|
870 no statements). |
|
871 |
|
872 `contexts` is a list of regular expressions. Only data from |
|
873 :ref:`dynamic contexts <dynamic_contexts>` that match one of those |
|
874 expressions (using :func:`re.search <python:re.search>`) will be |
|
875 included in the report. |
|
876 |
|
877 `precision` is the number of digits to display after the decimal |
|
878 point for percentages. |
|
879 |
|
880 All of the arguments default to the settings read from the |
|
881 :ref:`configuration file <config>`. |
|
882 |
|
883 Returns a float, the total percentage covered. |
|
884 |
|
885 .. versionadded:: 4.0 |
|
886 The `skip_covered` parameter. |
|
887 |
|
888 .. versionadded:: 5.0 |
|
889 The `contexts` and `skip_empty` parameters. |
|
890 |
|
891 .. versionadded:: 5.2 |
|
892 The `precision` parameter. |
|
893 |
|
894 """ |
|
895 with override_config( |
|
896 self, |
|
897 ignore_errors=ignore_errors, report_omit=omit, report_include=include, |
|
898 show_missing=show_missing, skip_covered=skip_covered, |
|
899 report_contexts=contexts, skip_empty=skip_empty, precision=precision, |
|
900 sort=sort |
|
901 ): |
|
902 reporter = SummaryReporter(self) |
|
903 return reporter.report(morfs, outfile=file) |
|
904 |
|
905 def annotate( |
|
906 self, morfs=None, directory=None, ignore_errors=None, |
|
907 omit=None, include=None, contexts=None, |
|
908 ): |
|
909 """Annotate a list of modules. |
|
910 |
|
911 Each module in `morfs` is annotated. The source is written to a new |
|
912 file, named with a ",cover" suffix, with each line prefixed with a |
|
913 marker to indicate the coverage of the line. Covered lines have ">", |
|
914 excluded lines have "-", and missing lines have "!". |
|
915 |
|
916 See :meth:`report` for other arguments. |
|
917 |
|
918 """ |
|
919 with override_config(self, |
|
920 ignore_errors=ignore_errors, report_omit=omit, |
|
921 report_include=include, report_contexts=contexts, |
|
922 ): |
|
923 reporter = AnnotateReporter(self) |
|
924 reporter.report(morfs, directory=directory) |
|
925 |
|
926 def html_report( |
|
927 self, morfs=None, directory=None, ignore_errors=None, |
|
928 omit=None, include=None, extra_css=None, title=None, |
|
929 skip_covered=None, show_contexts=None, contexts=None, |
|
930 skip_empty=None, precision=None, |
|
931 ): |
|
932 """Generate an HTML report. |
|
933 |
|
934 The HTML is written to `directory`. The file "index.html" is the |
|
935 overview starting point, with links to more detailed pages for |
|
936 individual modules. |
|
937 |
|
938 `extra_css` is a path to a file of other CSS to apply on the page. |
|
939 It will be copied into the HTML directory. |
|
940 |
|
941 `title` is a text string (not HTML) to use as the title of the HTML |
|
942 report. |
|
943 |
|
944 See :meth:`report` for other arguments. |
|
945 |
|
946 Returns a float, the total percentage covered. |
|
947 |
|
948 .. note:: |
|
949 The HTML report files are generated incrementally based on the |
|
950 source files and coverage results. If you modify the report files, |
|
951 the changes will not be considered. You should be careful about |
|
952 changing the files in the report folder. |
|
953 |
|
954 """ |
|
955 with override_config(self, |
|
956 ignore_errors=ignore_errors, report_omit=omit, report_include=include, |
|
957 html_dir=directory, extra_css=extra_css, html_title=title, |
|
958 skip_covered=skip_covered, show_contexts=show_contexts, report_contexts=contexts, |
|
959 skip_empty=skip_empty, precision=precision, |
|
960 ): |
|
961 reporter = HtmlReporter(self) |
|
962 return reporter.report(morfs) |
|
963 |
|
964 def xml_report( |
|
965 self, morfs=None, outfile=None, ignore_errors=None, |
|
966 omit=None, include=None, contexts=None, skip_empty=None, |
|
967 ): |
|
968 """Generate an XML report of coverage results. |
|
969 |
|
970 The report is compatible with Cobertura reports. |
|
971 |
|
972 Each module in `morfs` is included in the report. `outfile` is the |
|
973 path to write the file to, "-" will write to stdout. |
|
974 |
|
975 See :meth:`report` for other arguments. |
|
976 |
|
977 Returns a float, the total percentage covered. |
|
978 |
|
979 """ |
|
980 with override_config(self, |
|
981 ignore_errors=ignore_errors, report_omit=omit, report_include=include, |
|
982 xml_output=outfile, report_contexts=contexts, skip_empty=skip_empty, |
|
983 ): |
|
984 return render_report(self.config.xml_output, XmlReporter(self), morfs) |
|
985 |
|
986 def json_report( |
|
987 self, morfs=None, outfile=None, ignore_errors=None, |
|
988 omit=None, include=None, contexts=None, pretty_print=None, |
|
989 show_contexts=None |
|
990 ): |
|
991 """Generate a JSON report of coverage results. |
|
992 |
|
993 Each module in `morfs` is included in the report. `outfile` is the |
|
994 path to write the file to, "-" will write to stdout. |
|
995 |
|
996 See :meth:`report` for other arguments. |
|
997 |
|
998 Returns a float, the total percentage covered. |
|
999 |
|
1000 .. versionadded:: 5.0 |
|
1001 |
|
1002 """ |
|
1003 with override_config(self, |
|
1004 ignore_errors=ignore_errors, report_omit=omit, report_include=include, |
|
1005 json_output=outfile, report_contexts=contexts, json_pretty_print=pretty_print, |
|
1006 json_show_contexts=show_contexts |
|
1007 ): |
|
1008 return render_report(self.config.json_output, JsonReporter(self), morfs) |
|
1009 |
|
1010 def sys_info(self): |
|
1011 """Return a list of (key, value) pairs showing internal information.""" |
|
1012 |
|
1013 import coverage as covmod |
|
1014 |
|
1015 self._init() |
|
1016 self._post_init() |
|
1017 |
|
1018 def plugin_info(plugins): |
|
1019 """Make an entry for the sys_info from a list of plug-ins.""" |
|
1020 entries = [] |
|
1021 for plugin in plugins: |
|
1022 entry = plugin._coverage_plugin_name |
|
1023 if not plugin._coverage_enabled: |
|
1024 entry += " (disabled)" |
|
1025 entries.append(entry) |
|
1026 return entries |
|
1027 |
|
1028 info = [ |
|
1029 ('version', covmod.__version__), |
|
1030 ('coverage', covmod.__file__), |
|
1031 ('tracer', self._collector.tracer_name() if self._collector else "-none-"), |
|
1032 ('CTracer', 'available' if CTracer else "unavailable"), |
|
1033 ('plugins.file_tracers', plugin_info(self._plugins.file_tracers)), |
|
1034 ('plugins.configurers', plugin_info(self._plugins.configurers)), |
|
1035 ('plugins.context_switchers', plugin_info(self._plugins.context_switchers)), |
|
1036 ('configs_attempted', self.config.attempted_config_files), |
|
1037 ('configs_read', self.config.config_files_read), |
|
1038 ('config_file', self.config.config_file), |
|
1039 ('config_contents', |
|
1040 repr(self.config._config_contents) |
|
1041 if self.config._config_contents |
|
1042 else '-none-' |
|
1043 ), |
|
1044 ('data_file', self._data.data_filename() if self._data is not None else "-none-"), |
|
1045 ('python', sys.version.replace('\n', '')), |
|
1046 ('platform', platform.platform()), |
|
1047 ('implementation', platform.python_implementation()), |
|
1048 ('executable', sys.executable), |
|
1049 ('def_encoding', sys.getdefaultencoding()), |
|
1050 ('fs_encoding', sys.getfilesystemencoding()), |
|
1051 ('pid', os.getpid()), |
|
1052 ('cwd', os.getcwd()), |
|
1053 ('path', sys.path), |
|
1054 ('environment', sorted( |
|
1055 ("%s = %s" % (k, v)) |
|
1056 for k, v in iitems(os.environ) |
|
1057 if any(slug in k for slug in ("COV", "PY")) |
|
1058 )), |
|
1059 ('command_line', " ".join(getattr(sys, 'argv', ['-none-']))), |
|
1060 ] |
|
1061 |
|
1062 if self._inorout: |
|
1063 info.extend(self._inorout.sys_info()) |
|
1064 |
|
1065 info.extend(CoverageData.sys_info()) |
|
1066 |
|
1067 return info |
|
1068 |
|
1069 |
|
1070 # Mega debugging... |
|
1071 # $set_env.py: COVERAGE_DEBUG_CALLS - Lots and lots of output about calls to Coverage. |
|
1072 if int(os.environ.get("COVERAGE_DEBUG_CALLS", 0)): # pragma: debugging |
|
1073 from coverage.debug import decorate_methods, show_calls |
|
1074 |
|
1075 Coverage = decorate_methods(show_calls(show_args=True), butnot=['get_data'])(Coverage) |
|
1076 |
|
1077 |
|
1078 def process_startup(): |
|
1079 """Call this at Python start-up to perhaps measure coverage. |
|
1080 |
|
1081 If the environment variable COVERAGE_PROCESS_START is defined, coverage |
|
1082 measurement is started. The value of the variable is the config file |
|
1083 to use. |
|
1084 |
|
1085 There are two ways to configure your Python installation to invoke this |
|
1086 function when Python starts: |
|
1087 |
|
1088 #. Create or append to sitecustomize.py to add these lines:: |
|
1089 |
|
1090 import coverage |
|
1091 coverage.process_startup() |
|
1092 |
|
1093 #. Create a .pth file in your Python installation containing:: |
|
1094 |
|
1095 import coverage; coverage.process_startup() |
|
1096 |
|
1097 Returns the :class:`Coverage` instance that was started, or None if it was |
|
1098 not started by this call. |
|
1099 |
|
1100 """ |
|
1101 cps = os.environ.get("COVERAGE_PROCESS_START") |
|
1102 if not cps: |
|
1103 # No request for coverage, nothing to do. |
|
1104 return None |
|
1105 |
|
1106 # This function can be called more than once in a process. This happens |
|
1107 # because some virtualenv configurations make the same directory visible |
|
1108 # twice in sys.path. This means that the .pth file will be found twice, |
|
1109 # and executed twice, executing this function twice. We set a global |
|
1110 # flag (an attribute on this function) to indicate that coverage.py has |
|
1111 # already been started, so we can avoid doing it twice. |
|
1112 # |
|
1113 # https://github.com/nedbat/coveragepy/issues/340 has more details. |
|
1114 |
|
1115 if hasattr(process_startup, "coverage"): |
|
1116 # We've annotated this function before, so we must have already |
|
1117 # started coverage.py in this process. Nothing to do. |
|
1118 return None |
|
1119 |
|
1120 cov = Coverage(config_file=cps) |
|
1121 process_startup.coverage = cov |
|
1122 cov._warn_no_data = False |
|
1123 cov._warn_unimported_source = False |
|
1124 cov._warn_preimported_source = False |
|
1125 cov._auto_save = True |
|
1126 cov.start() |
|
1127 |
|
1128 return cov |
|
1129 |
|
1130 |
|
1131 def _prevent_sub_process_measurement(): |
|
1132 """Stop any subprocess auto-measurement from writing data.""" |
|
1133 auto_created_coverage = getattr(process_startup, "coverage", None) |
|
1134 if auto_created_coverage is not None: |
|
1135 auto_created_coverage._auto_save = False |