eric7/DebugClients/Python/coverage/control.py

branch
eric7
changeset 8991
2fc945191992
parent 8929
fcca2fa618bf
child 9099
0e511e0e94a3
diff -r ca8e477c590c -r 2fc945191992 eric7/DebugClients/Python/coverage/control.py
--- 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."""
 

eric ide

mercurial