--- a/eric7/DebugClients/Python/coverage/control.py Sun Mar 20 17:26:35 2022 +0100 +++ b/eric7/DebugClients/Python/coverage/control.py Sun Mar 20 17:49:44 2022 +0100 @@ -9,7 +9,9 @@ import os import os.path import platform +import signal import sys +import threading import time import warnings @@ -26,7 +28,8 @@ from coverage.html import HtmlReporter from coverage.inorout import InOrOut from coverage.jsonreport import JsonReporter -from coverage.misc import bool_or_none, join_regex, human_sorted, human_sorted_items +from coverage.lcovreport import LcovReporter +from coverage.misc import bool_or_none, join_regex, human_sorted from coverage.misc import DefaultValue, ensure_dir_for_file, isolate_module from coverage.plugin import FileReporter from coverage.plugin_support import Plugins @@ -60,7 +63,8 @@ cov.config = original_config -_DEFAULT_DATAFILE = DefaultValue("MISSING") +DEFAULT_DATAFILE = DefaultValue("MISSING") +_DEFAULT_DATAFILE = DEFAULT_DATAFILE # Just in case, for backwards compatibility class Coverage: """Programmatic access to coverage.py. @@ -101,7 +105,7 @@ return None def __init__( - self, data_file=_DEFAULT_DATAFILE, data_suffix=None, cover_pylib=None, + self, data_file=DEFAULT_DATAFILE, data_suffix=None, cover_pylib=None, auto_data=False, timid=None, branch=None, config_file=True, source=None, source_pkgs=None, omit=None, include=None, debug=None, concurrency=None, check_preimported=False, context=None, @@ -198,7 +202,7 @@ # data_file=None means no disk file at all. data_file missing means # use the value from the config file. self._no_disk = data_file is None - if data_file is _DEFAULT_DATAFILE: + if data_file is DEFAULT_DATAFILE: data_file = None self.config = None @@ -227,6 +231,7 @@ self._exclude_re = None self._debug = None self._file_mapper = None + self._old_sigterm = None # State machine variables: # Have we initialized everything? @@ -310,22 +315,25 @@ """Write out debug info at startup if needed.""" wrote_any = False with self._debug.without_callers(): - if self._debug.should('config'): - config_info = human_sorted_items(self.config.__dict__.items()) - config_info = [(k, v) for k, v in config_info if not k.startswith('_')] - write_formatted_info(self._debug, "config", config_info) + if self._debug.should("config"): + config_info = self.config.debug_info() + write_formatted_info(self._debug.write, "config", config_info) wrote_any = True - if self._debug.should('sys'): - write_formatted_info(self._debug, "sys", self.sys_info()) + if self._debug.should("sys"): + write_formatted_info(self._debug.write, "sys", self.sys_info()) for plugin in self._plugins: header = "sys: " + plugin._coverage_plugin_name info = plugin.sys_info() - write_formatted_info(self._debug, header, info) + write_formatted_info(self._debug.write, header, info) + wrote_any = True + + if self._debug.should("pybehave"): + write_formatted_info(self._debug.write, "pybehave", env.debug_info()) wrote_any = True if wrote_any: - write_formatted_info(self._debug, "end", ()) + write_formatted_info(self._debug.write, "end", ()) def _should_trace(self, filename, frame): """Decide whether to trace execution in `filename`. @@ -454,6 +462,8 @@ raise ConfigError( # pragma: only jython "multiprocessing is not supported on this Python" ) + if self.config.config_file is None: + raise ConfigError("multiprocessing requires a configuration file") patch_multiprocessing(rcfile=self.config.config_file) dycon = self.config.dynamic_context @@ -524,7 +534,14 @@ # It's useful to write debug info after initing for start. self._should_write_debug = True + # Register our clean-up handlers. atexit.register(self._atexit) + is_main = (threading.current_thread() == threading.main_thread()) + if is_main and not env.WINDOWS: + # The Python docs seem to imply that SIGTERM works uniformly even + # on Windows, but that's not my experience, and this agrees: + # https://stackoverflow.com/questions/35772001/x/35792192#35792192 + self._old_sigterm = signal.signal(signal.SIGTERM, self._on_sigterm) def _init_data(self, suffix): """Create a data file if we don't have one yet.""" @@ -582,15 +599,23 @@ self._collector.stop() self._started = False - def _atexit(self): + def _atexit(self, event="atexit"): """Clean up on process shutdown.""" if self._debug.should("process"): - self._debug.write(f"atexit: pid: {os.getpid()}, instance: {self!r}") + self._debug.write(f"{event}: pid: {os.getpid()}, instance: {self!r}") if self._started: self.stop() if self._auto_save: self.save() + def _on_sigterm(self, signum_unused, frame_unused): + """A handler for signal.SIGTERM.""" + self._atexit("sigterm") + # Statements after here won't be seen by metacov because we just wrote + # the data, and are about to kill the process. + signal.signal(signal.SIGTERM, self._old_sigterm) # pragma: not covered + os.kill(os.getpid(), signal.SIGTERM) # pragma: not covered + def erase(self): """Erase previously collected coverage data. @@ -1049,6 +1074,25 @@ ): return render_report(self.config.json_output, JsonReporter(self), morfs, self._message) + def lcov_report( + self, morfs=None, outfile=None, ignore_errors=None, + omit=None, include=None, contexts=None, + ): + """Generate an LCOV report of coverage results. + + Each module in 'morfs' is included in the report. 'outfile' is the + path to write the file to, "-" will write to stdout. + + See :meth 'report' for other arguments. + + .. versionadded:: 6.3 + """ + with override_config(self, + ignore_errors=ignore_errors, report_omit=omit, report_include=include, + lcov_output=outfile, report_contexts=contexts, + ): + return render_report(self.config.lcov_output, LcovReporter(self), morfs, self._message) + def sys_info(self): """Return a list of (key, value) pairs showing internal information."""