aboutsummaryrefslogtreecommitdiff
path: root/tests/test-chime-lifecycle.el
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-07-09 19:58:27 -0500
committerCraig Jennings <c@cjennings.net>2026-07-09 19:58:27 -0500
commite43e44cb4cef93b3e3ae68396a0af639cea1534f (patch)
tree251689657db975cd9b7e7d6bbaa5fb9a798e7860 /tests/test-chime-lifecycle.el
parent67eb495da255d0cf00c88448f8cfd232c88563d1 (diff)
downloadchime-e43e44cb4cef93b3e3ae68396a0af639cea1534f.tar.gz
chime-e43e44cb4cef93b3e3ae68396a0af639cea1534f.zip
fix: kill abandoned async children and discard their late callbacks
Three faults in one path, all made reachable by the watchdog. A callback returning just past the timeout ran against state that no longer belonged to it. It nil'd chime--process, which by then held the replacement child, so the overlap guard broke and a third child could spawn. It also reset the failure counter the watchdog had just incremented. Each spawn now captures a generation, and a result whose generation is stale is discarded whole. interrupt-process only asks. A child stuck in a blocking read ignores SIGINT and lives on as a zombie, invisible because chime--process was already cleared. The watchdog and chime--stop now call chime--kill-async-process, which silences the sentinel and calls delete-process. async.el kills a child's process buffer only on a zero exit, so every signalled child leaked one. With a persistent hang that's a buffer per chime-async-timeout until Emacs restarts. The kill path reaps the buffer itself. chime--stop also clears chime--consecutive-async-failures and chime--process-start-time, so a later chime-mode doesn't resume with the old count.
Diffstat (limited to 'tests/test-chime-lifecycle.el')
-rw-r--r--tests/test-chime-lifecycle.el31
1 files changed, 19 insertions, 12 deletions
diff --git a/tests/test-chime-lifecycle.el b/tests/test-chime-lifecycle.el
index 8ce9c13..826de4c 100644
--- a/tests/test-chime-lifecycle.el
+++ b/tests/test-chime-lifecycle.el
@@ -21,32 +21,39 @@
(require 'test-bootstrap (expand-file-name "test-bootstrap.el"))
(require 'cl-lib)
-(ert-deftest test-chime-stop-interrupts-running-process ()
- "Normal: when chime--process is set, `chime--stop' interrupts it and clears the var."
- (let ((interrupted nil)
+(ert-deftest test-chime-stop-kills-running-process ()
+ "Normal: when chime--process is set, `chime--stop' kills it and clears the var.
+The child is killed rather than interrupted, because SIGINT is a request a
+child stuck in a blocking read can ignore."
+ (let ((killed nil)
(chime--timer nil)
(chime--process 'fake-process)
+ (chime--process-generation 0)
(chime--validation-done t)
(chime--validation-retry-count 5))
- (cl-letf (((symbol-function 'interrupt-process)
- (lambda (proc) (setq interrupted proc))))
+ (cl-letf (((symbol-function 'chime--kill-async-process)
+ (lambda (proc) (setq killed proc))))
(chime--stop))
- (should (eq 'fake-process interrupted))
+ (should (eq 'fake-process killed))
(should (null chime--process))
+ ;; The abandoned child's callback is orphaned.
+ (should (= 1 chime--process-generation))
(should-not chime--validation-done)
(should (= 0 chime--validation-retry-count))))
-(ert-deftest test-chime-stop-no-process-skips-interrupt ()
- "Boundary: with chime--process nil, `interrupt-process' is never called."
- (let ((interrupted nil)
+(ert-deftest test-chime-stop-no-process-kills-nothing ()
+ "Boundary: with chime--process nil, no process is killed."
+ (let ((killed 'untouched)
(chime--timer nil)
(chime--process nil)
+ (chime--process-generation 0)
(chime--validation-done t)
(chime--validation-retry-count 5))
- (cl-letf (((symbol-function 'interrupt-process)
- (lambda (proc) (setq interrupted proc))))
+ (cl-letf (((symbol-function 'chime--kill-async-process)
+ (lambda (proc) (setq killed proc))))
(chime--stop))
- (should (null interrupted))
+ ;; chime--stop calls the helper unconditionally; it must tolerate nil.
+ (should (null killed))
(should-not chime--validation-done)
(should (= 0 chime--validation-retry-count))))