24 from coverage.exceptions import ConfigError, CoverageException, CoverageWarning, PluginError |
26 from coverage.exceptions import ConfigError, CoverageException, CoverageWarning, PluginError |
25 from coverage.files import PathAliases, abs_file, relative_filename, set_relative_directory |
27 from coverage.files import PathAliases, abs_file, relative_filename, set_relative_directory |
26 from coverage.html import HtmlReporter |
28 from coverage.html import HtmlReporter |
27 from coverage.inorout import InOrOut |
29 from coverage.inorout import InOrOut |
28 from coverage.jsonreport import JsonReporter |
30 from coverage.jsonreport import JsonReporter |
29 from coverage.misc import bool_or_none, join_regex, human_sorted, human_sorted_items |
31 from coverage.lcovreport import LcovReporter |
|
32 from coverage.misc import bool_or_none, join_regex, human_sorted |
30 from coverage.misc import DefaultValue, ensure_dir_for_file, isolate_module |
33 from coverage.misc import DefaultValue, ensure_dir_for_file, isolate_module |
31 from coverage.plugin import FileReporter |
34 from coverage.plugin import FileReporter |
32 from coverage.plugin_support import Plugins |
35 from coverage.plugin_support import Plugins |
33 from coverage.python import PythonFileReporter |
36 from coverage.python import PythonFileReporter |
34 from coverage.report import render_report |
37 from coverage.report import render_report |
99 return cls._instances[-1] |
103 return cls._instances[-1] |
100 else: |
104 else: |
101 return None |
105 return None |
102 |
106 |
103 def __init__( |
107 def __init__( |
104 self, data_file=_DEFAULT_DATAFILE, data_suffix=None, cover_pylib=None, |
108 self, data_file=DEFAULT_DATAFILE, data_suffix=None, cover_pylib=None, |
105 auto_data=False, timid=None, branch=None, config_file=True, |
109 auto_data=False, timid=None, branch=None, config_file=True, |
106 source=None, source_pkgs=None, omit=None, include=None, debug=None, |
110 source=None, source_pkgs=None, omit=None, include=None, debug=None, |
107 concurrency=None, check_preimported=False, context=None, |
111 concurrency=None, check_preimported=False, context=None, |
108 messages=False, |
112 messages=False, |
109 ): # pylint: disable=too-many-arguments |
113 ): # pylint: disable=too-many-arguments |
196 |
200 |
197 """ |
201 """ |
198 # data_file=None means no disk file at all. data_file missing means |
202 # data_file=None means no disk file at all. data_file missing means |
199 # use the value from the config file. |
203 # use the value from the config file. |
200 self._no_disk = data_file is None |
204 self._no_disk = data_file is None |
201 if data_file is _DEFAULT_DATAFILE: |
205 if data_file is DEFAULT_DATAFILE: |
202 data_file = None |
206 data_file = None |
203 |
207 |
204 self.config = None |
208 self.config = None |
205 |
209 |
206 # This is injectable by tests. |
210 # This is injectable by tests. |
225 self._inorout = None |
229 self._inorout = None |
226 self._data_suffix = self._run_suffix = None |
230 self._data_suffix = self._run_suffix = None |
227 self._exclude_re = None |
231 self._exclude_re = None |
228 self._debug = None |
232 self._debug = None |
229 self._file_mapper = None |
233 self._file_mapper = None |
|
234 self._old_sigterm = None |
230 |
235 |
231 # State machine variables: |
236 # State machine variables: |
232 # Have we initialized everything? |
237 # Have we initialized everything? |
233 self._inited = False |
238 self._inited = False |
234 self._inited_for_start = False |
239 self._inited_for_start = False |
308 |
313 |
309 def _write_startup_debug(self): |
314 def _write_startup_debug(self): |
310 """Write out debug info at startup if needed.""" |
315 """Write out debug info at startup if needed.""" |
311 wrote_any = False |
316 wrote_any = False |
312 with self._debug.without_callers(): |
317 with self._debug.without_callers(): |
313 if self._debug.should('config'): |
318 if self._debug.should("config"): |
314 config_info = human_sorted_items(self.config.__dict__.items()) |
319 config_info = self.config.debug_info() |
315 config_info = [(k, v) for k, v in config_info if not k.startswith('_')] |
320 write_formatted_info(self._debug.write, "config", config_info) |
316 write_formatted_info(self._debug, "config", config_info) |
|
317 wrote_any = True |
321 wrote_any = True |
318 |
322 |
319 if self._debug.should('sys'): |
323 if self._debug.should("sys"): |
320 write_formatted_info(self._debug, "sys", self.sys_info()) |
324 write_formatted_info(self._debug.write, "sys", self.sys_info()) |
321 for plugin in self._plugins: |
325 for plugin in self._plugins: |
322 header = "sys: " + plugin._coverage_plugin_name |
326 header = "sys: " + plugin._coverage_plugin_name |
323 info = plugin.sys_info() |
327 info = plugin.sys_info() |
324 write_formatted_info(self._debug, header, info) |
328 write_formatted_info(self._debug.write, header, info) |
325 wrote_any = True |
329 wrote_any = True |
326 |
330 |
|
331 if self._debug.should("pybehave"): |
|
332 write_formatted_info(self._debug.write, "pybehave", env.debug_info()) |
|
333 wrote_any = True |
|
334 |
327 if wrote_any: |
335 if wrote_any: |
328 write_formatted_info(self._debug, "end", ()) |
336 write_formatted_info(self._debug.write, "end", ()) |
329 |
337 |
330 def _should_trace(self, filename, frame): |
338 def _should_trace(self, filename, frame): |
331 """Decide whether to trace execution in `filename`. |
339 """Decide whether to trace execution in `filename`. |
332 |
340 |
333 Calls `_should_trace_internal`, and returns the FileDisposition. |
341 Calls `_should_trace_internal`, and returns the FileDisposition. |
452 if "multiprocessing" in concurrency: |
460 if "multiprocessing" in concurrency: |
453 if not patch_multiprocessing: |
461 if not patch_multiprocessing: |
454 raise ConfigError( # pragma: only jython |
462 raise ConfigError( # pragma: only jython |
455 "multiprocessing is not supported on this Python" |
463 "multiprocessing is not supported on this Python" |
456 ) |
464 ) |
|
465 if self.config.config_file is None: |
|
466 raise ConfigError("multiprocessing requires a configuration file") |
457 patch_multiprocessing(rcfile=self.config.config_file) |
467 patch_multiprocessing(rcfile=self.config.config_file) |
458 |
468 |
459 dycon = self.config.dynamic_context |
469 dycon = self.config.dynamic_context |
460 if not dycon or dycon == "none": |
470 if not dycon or dycon == "none": |
461 context_switchers = [] |
471 context_switchers = [] |
522 self._inorout.disp_class = self._collector.file_disposition_class |
532 self._inorout.disp_class = self._collector.file_disposition_class |
523 |
533 |
524 # It's useful to write debug info after initing for start. |
534 # It's useful to write debug info after initing for start. |
525 self._should_write_debug = True |
535 self._should_write_debug = True |
526 |
536 |
|
537 # Register our clean-up handlers. |
527 atexit.register(self._atexit) |
538 atexit.register(self._atexit) |
|
539 is_main = (threading.current_thread() == threading.main_thread()) |
|
540 if is_main and not env.WINDOWS: |
|
541 # The Python docs seem to imply that SIGTERM works uniformly even |
|
542 # on Windows, but that's not my experience, and this agrees: |
|
543 # https://stackoverflow.com/questions/35772001/x/35792192#35792192 |
|
544 self._old_sigterm = signal.signal(signal.SIGTERM, self._on_sigterm) |
528 |
545 |
529 def _init_data(self, suffix): |
546 def _init_data(self, suffix): |
530 """Create a data file if we don't have one yet.""" |
547 """Create a data file if we don't have one yet.""" |
531 if self._data is None: |
548 if self._data is None: |
532 # Create the data file. We do this at construction time so that the |
549 # Create the data file. We do this at construction time so that the |
580 self._instances.pop() |
597 self._instances.pop() |
581 if self._started: |
598 if self._started: |
582 self._collector.stop() |
599 self._collector.stop() |
583 self._started = False |
600 self._started = False |
584 |
601 |
585 def _atexit(self): |
602 def _atexit(self, event="atexit"): |
586 """Clean up on process shutdown.""" |
603 """Clean up on process shutdown.""" |
587 if self._debug.should("process"): |
604 if self._debug.should("process"): |
588 self._debug.write(f"atexit: pid: {os.getpid()}, instance: {self!r}") |
605 self._debug.write(f"{event}: pid: {os.getpid()}, instance: {self!r}") |
589 if self._started: |
606 if self._started: |
590 self.stop() |
607 self.stop() |
591 if self._auto_save: |
608 if self._auto_save: |
592 self.save() |
609 self.save() |
|
610 |
|
611 def _on_sigterm(self, signum_unused, frame_unused): |
|
612 """A handler for signal.SIGTERM.""" |
|
613 self._atexit("sigterm") |
|
614 # Statements after here won't be seen by metacov because we just wrote |
|
615 # the data, and are about to kill the process. |
|
616 signal.signal(signal.SIGTERM, self._old_sigterm) # pragma: not covered |
|
617 os.kill(os.getpid(), signal.SIGTERM) # pragma: not covered |
593 |
618 |
594 def erase(self): |
619 def erase(self): |
595 """Erase previously collected coverage data. |
620 """Erase previously collected coverage data. |
596 |
621 |
597 This removes the in-memory data collected in this session as well as |
622 This removes the in-memory data collected in this session as well as |
1046 ignore_errors=ignore_errors, report_omit=omit, report_include=include, |
1071 ignore_errors=ignore_errors, report_omit=omit, report_include=include, |
1047 json_output=outfile, report_contexts=contexts, json_pretty_print=pretty_print, |
1072 json_output=outfile, report_contexts=contexts, json_pretty_print=pretty_print, |
1048 json_show_contexts=show_contexts |
1073 json_show_contexts=show_contexts |
1049 ): |
1074 ): |
1050 return render_report(self.config.json_output, JsonReporter(self), morfs, self._message) |
1075 return render_report(self.config.json_output, JsonReporter(self), morfs, self._message) |
|
1076 |
|
1077 def lcov_report( |
|
1078 self, morfs=None, outfile=None, ignore_errors=None, |
|
1079 omit=None, include=None, contexts=None, |
|
1080 ): |
|
1081 """Generate an LCOV report of coverage results. |
|
1082 |
|
1083 Each module in 'morfs' is included in the report. 'outfile' is the |
|
1084 path to write the file to, "-" will write to stdout. |
|
1085 |
|
1086 See :meth 'report' for other arguments. |
|
1087 |
|
1088 .. versionadded:: 6.3 |
|
1089 """ |
|
1090 with override_config(self, |
|
1091 ignore_errors=ignore_errors, report_omit=omit, report_include=include, |
|
1092 lcov_output=outfile, report_contexts=contexts, |
|
1093 ): |
|
1094 return render_report(self.config.lcov_output, LcovReporter(self), morfs, self._message) |
1051 |
1095 |
1052 def sys_info(self): |
1096 def sys_info(self): |
1053 """Return a list of (key, value) pairs showing internal information.""" |
1097 """Return a list of (key, value) pairs showing internal information.""" |
1054 |
1098 |
1055 import coverage as covmod |
1099 import coverage as covmod |