From f04500c7211ff79dbe344cc340346b21cc758387 Mon Sep 17 00:00:00 2001 From: Lewis Gaul Date: Mon, 3 Apr 2023 00:39:46 +0100 Subject: [PATCH 1/3] Add test that reproduces the issue --- tests/test_concurrency.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/tests/test_concurrency.py b/tests/test_concurrency.py index 9f12e77ec..8d5bff554 100644 --- a/tests/test_concurrency.py +++ b/tests/test_concurrency.py @@ -705,7 +705,7 @@ class SigtermTest(CoverageTest): """Tests of our handling of SIGTERM.""" @pytest.mark.parametrize("sigterm", [False, True]) - def test_sigterm_saves_data(self, sigterm: bool) -> None: + def test_sigterm_multiprocessing_saves_data(self, sigterm: bool) -> None: # A terminated process should save its coverage data. self.make_file("clobbered.py", """\ import multiprocessing @@ -751,6 +751,28 @@ def subproc(x): expected = "clobbered.py 17 5 71% 5-10" assert self.squeezed_lines(out)[2] == expected + def test_sigterm_threading_saves_data(self) -> None: + # A terminated process should save its coverage data. + self.make_file("handler.py", """\ + import os, signal + + print("START", flush=True) + print("SIGTERM", flush=True) + os.kill(os.getpid(), signal.SIGTERM) + print("NOT HERE", flush=True) + """) + self.make_file(".coveragerc", """\ + [run] + # The default concurrency option. + concurrency = thread + sigterm = true + """) + out = self.run_command("coverage run handler.py") + assert out == "START\nSIGTERM\nTerminated\n" + out = self.run_command("coverage report -m") + expected = "handler.py 5 1 80% 6" + assert self.squeezed_lines(out)[2] == expected + def test_sigterm_still_runs(self) -> None: # A terminated process still runs its own SIGTERM handler. self.make_file("handler.py", """\ From 2908b78aa0add29cb597839e637f8ef8bb115946 Mon Sep 17 00:00:00 2001 From: Lewis Gaul Date: Mon, 3 Apr 2023 00:40:47 +0100 Subject: [PATCH 2/3] Suggested fix - always save data in sigterm exit flow --- coverage/control.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coverage/control.py b/coverage/control.py index acce622d3..e405a5bf4 100644 --- a/coverage/control.py +++ b/coverage/control.py @@ -653,7 +653,7 @@ def _atexit(self, event: str = "atexit") -> None: self._debug.write(f"{event}: pid: {os.getpid()}, instance: {self!r}") if self._started: self.stop() - if self._auto_save: + if self._auto_save or event == "sigterm": self.save() def _on_sigterm(self, signum_unused: int, frame_unused: Optional[FrameType]) -> None: From ba8c6c06c330aa1a5ba77a82a9be268c0c3a1044 Mon Sep 17 00:00:00 2001 From: Lewis Gaul Date: Mon, 3 Apr 2023 00:58:42 +0100 Subject: [PATCH 3/3] Address test failures on MacOS due to lack of 'Terminated' output on SIGTERM --- tests/test_concurrency.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/test_concurrency.py b/tests/test_concurrency.py index 8d5bff554..a9b64d158 100644 --- a/tests/test_concurrency.py +++ b/tests/test_concurrency.py @@ -768,7 +768,10 @@ def test_sigterm_threading_saves_data(self) -> None: sigterm = true """) out = self.run_command("coverage run handler.py") - assert out == "START\nSIGTERM\nTerminated\n" + if env.LINUX: + assert out == "START\nSIGTERM\nTerminated\n" + else: + assert out == "START\nSIGTERM\n" out = self.run_command("coverage report -m") expected = "handler.py 5 1 80% 6" assert self.squeezed_lines(out)[2] == expected