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