blob: b6c71dd7a5dd84aa9705decf2f958fc9d44953d5 (
plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
|
;;; test-dev-fkeys--f4-make-once-hook.el --- Tests for cj/--f4-make-once-hook -*- lexical-binding: t -*-
;;; Commentary:
;; Tests for the one-shot `compilation-finish-functions' hook builder used by
;; "Compile + Run" and "Clean + Rebuild" to chain a follow-up command after
;; a successful compile. The returned lambda:
;;
;; - removes itself from `compilation-finish-functions' on first invocation
;; regardless of status (so it never lingers across compiles)
;; - invokes THEN-FN only when the status string indicates success — i.e.
;; `string-prefix-p \"finished\"' matches.
;;
;; The status conventions come from the compile.el infrastructure: a
;; successful compile passes \"finished\\n\", a failed compile passes
;; something starting with \"exited\".
;;; Code:
(require 'ert)
(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
(require 'dev-fkeys)
;;; Normal Cases
(ert-deftest test-dev-fkeys-make-once-hook-success-status-calls-then-fn ()
"Normal: status starting with 'finished' invokes THEN-FN once."
(let* ((called 0)
(hook (cj/--f4-make-once-hook (lambda () (cl-incf called)))))
(let ((compilation-finish-functions nil))
(add-hook 'compilation-finish-functions hook)
(funcall hook nil "finished\n"))
(should (= called 1))))
(ert-deftest test-dev-fkeys-make-once-hook-failure-status-skips-then-fn ()
"Normal: status string starting with 'exited abnormally' does not invoke THEN-FN."
(let* ((called 0)
(hook (cj/--f4-make-once-hook (lambda () (cl-incf called)))))
(let ((compilation-finish-functions nil))
(add-hook 'compilation-finish-functions hook)
(funcall hook nil "exited abnormally with code 1\n"))
(should (= called 0))))
(ert-deftest test-dev-fkeys-make-once-hook-success-removes-itself ()
"Normal: hook removes itself from `compilation-finish-functions' on success."
(let* ((hook (cj/--f4-make-once-hook (lambda () nil)))
(compilation-finish-functions (list hook)))
(should (memq hook compilation-finish-functions))
(funcall hook nil "finished\n")
(should-not (memq hook compilation-finish-functions))))
;;; Boundary Cases
(ert-deftest test-dev-fkeys-make-once-hook-failure-also-removes-itself ()
"Boundary: hook removes itself even on failure, so it never fires on the
next compile. THEN-FN is not invoked, but the hook is still cleaned up."
(let* ((called 0)
(hook (cj/--f4-make-once-hook (lambda () (cl-incf called))))
(compilation-finish-functions (list hook)))
(funcall hook nil "exited abnormally with code 1\n")
(should (= called 0))
(should-not (memq hook compilation-finish-functions))))
(ert-deftest test-dev-fkeys-make-once-hook-only-fires-then-fn-once ()
"Boundary: re-invoking the hook lambda after it self-removes does not
re-trigger THEN-FN — the hook has been removed from the hook list and only
external mistakes (calling the lambda directly twice) could trigger it. We
guard against that case anyway by checking the hook removed itself, so a
second direct funcall sees the (now-gone) hook still calls THEN-FN. This
test documents the contract: the hook does not gate on its own state, only
on the hook list. So the SECOND direct funcall WILL call THEN-FN. The
guarantee in production is that `compilation-finish-functions' calls each
hook exactly once per compile, so the practical contract is one-shot."
(let* ((called 0)
(hook (cj/--f4-make-once-hook (lambda () (cl-incf called))))
(compilation-finish-functions (list hook)))
(funcall hook nil "finished\n")
(funcall hook nil "finished\n")
(should (= called 2))))
(ert-deftest test-dev-fkeys-make-once-hook-empty-status-skips-then-fn ()
"Boundary: empty status string does not invoke THEN-FN."
(let* ((called 0)
(hook (cj/--f4-make-once-hook (lambda () (cl-incf called)))))
(let ((compilation-finish-functions nil))
(add-hook 'compilation-finish-functions hook)
(funcall hook nil ""))
(should (= called 0))))
(ert-deftest test-dev-fkeys-make-once-hook-interrupt-status-skips-then-fn ()
"Boundary: an 'interrupt' status (process killed) does not invoke THEN-FN."
(let* ((called 0)
(hook (cj/--f4-make-once-hook (lambda () (cl-incf called)))))
(let ((compilation-finish-functions nil))
(add-hook 'compilation-finish-functions hook)
(funcall hook nil "interrupt\n"))
(should (= called 0))))
;;; Error Cases
(ert-deftest test-dev-fkeys-make-once-hook-then-fn-error-still-removes-hook ()
"Error: if THEN-FN raises, the hook is still removed first so the
follow-up doesn't run twice on the next compile.
Components integrated:
- `cj/--f4-make-once-hook' (the unit under test)
- `compilation-finish-functions' (real, mutated via add-hook/remove-hook)
- A then-fn that signals an error"
(let* ((hook (cj/--f4-make-once-hook (lambda () (error "boom"))))
(compilation-finish-functions (list hook)))
;; Hook signals through the error from THEN-FN; remove-hook ran first.
(should-error (funcall hook nil "finished\n"))
(should-not (memq hook compilation-finish-functions))))
(provide 'test-dev-fkeys--f4-make-once-hook)
;;; test-dev-fkeys--f4-make-once-hook.el ends here
|