aboutsummaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-07-18 18:09:04 -0500
committerCraig Jennings <c@cjennings.net>2026-07-18 18:09:04 -0500
commitfbd69990f7e3731a6df4ed992f1aefaef3748750 (patch)
treeaea5e905e9a9a1101358e3683b410a64475f3306 /tests
parent125e28ee1073a4b83a9c933d747ecdbba4c17b4b (diff)
downloaddotemacs-fbd69990f7e3731a6df4ed992f1aefaef3748750.tar.gz
dotemacs-fbd69990f7e3731a6df4ed992f1aefaef3748750.zip
fix(dev): repair hunk jump, F4 hook leak, async clone, and nil guards
- vc-config: C-; v d matched literal +/- source text via consult-line, not gutter hunks. It now completes over git-gutter's hunk list and jumps to the chosen line. - vc-config: clipboard clone ran git synchronously, freezing every frame for the whole clone. It now runs async with a sentinel that opens the clone on success and surfaces the process buffer on failure. - vc-config: magit-blame bound D and S to the same command. D is now difftastic-magit-diff, matching the transient. - vc-config: dropped the :commands autoload for git-timemachine-show-selected-revision, a function the package never defined. The phantom appeared in M-x and errored. - dev-fkeys: both F4 chained-compile handlers armed a hook on the global compilation-finish-functions before their compile ran, so a quit left it live and the next unrelated compile fired the chain. The one-shot hook now installs buffer-locally in the compilation buffer, same shape as the projectile cache-revert hooks. - test-runner: outside a project with no global test directory, three commands crashed with wrong-type-argument on a nil path. Discovery now returns nil and the commands signal user-error. - diff-config: removed the global "-w" ediff default, which made every session ignore whitespace (indentation-only changes compared as identical). Whitespace-ignore stays available as ediff's per-session toggle. - restclient-config: the C-; R bindings went through raw global-set-key, silently depending on keybindings.el loading first. They now use a prefix keymap registered like the other C-; prefixes. - httpd-config: simple-httpd loaded on a 1s timer and created www/ on every startup. It now defers until impatient-mode needs it, and the doc root is created at package load.
Diffstat (limited to 'tests')
-rw-r--r--tests/test-dev-fkeys--f4-clean-rebuild-impl.el94
-rw-r--r--tests/test-dev-fkeys--f4-compile-and-run-impl.el101
-rw-r--r--tests/test-dev-fkeys--f4-make-once-hook.el15
-rw-r--r--tests/test-diff-config--ediff-options.el27
-rw-r--r--tests/test-httpd-config--defer.el34
-rw-r--r--tests/test-restclient-config--keymap.el29
-rw-r--r--tests/test-test-runner--nil-global-directory.el51
-rw-r--r--tests/test-vc-config--git-clone.el135
-rw-r--r--tests/test-vc-config--gutter-hunk-candidates.el69
-rw-r--r--tests/test-vc-config--timemachine-commands.el36
10 files changed, 505 insertions, 86 deletions
diff --git a/tests/test-dev-fkeys--f4-clean-rebuild-impl.el b/tests/test-dev-fkeys--f4-clean-rebuild-impl.el
index 27c7c56a..bed51d79 100644
--- a/tests/test-dev-fkeys--f4-clean-rebuild-impl.el
+++ b/tests/test-dev-fkeys--f4-clean-rebuild-impl.el
@@ -3,7 +3,11 @@
;;; Commentary:
;; Tests for the "Clean + Rebuild" action handler. Runs the heuristic clean
;; command via `compile' from the project root, then chains
-;; `projectile-compile-project' on success via the one-shot finish hook.
+;; `projectile-compile-project' on success via a one-shot finish hook
+;; installed buffer-locally in the compilation buffer `compile' returns.
+;; The global `compilation-finish-functions' is never touched, so a quit
+;; before the compile starts or an unrelated concurrent compile can never
+;; fire the chained rebuild.
;;; Code:
@@ -24,6 +28,18 @@ Bind the dir path to ROOT in BODY. Cleans up on exit."
,@body)
(delete-directory root t))))
+(defmacro test-dev-fkeys-cr--with-compilation-buffer (buf &rest body)
+ "Run BODY with BUF bound to a temp buffer standing in for a compilation buffer."
+ (declare (indent 1))
+ `(let ((,buf (generate-new-buffer " *test-compilation*")))
+ (unwind-protect
+ (progn ,@body)
+ (kill-buffer ,buf))))
+
+(defun test-dev-fkeys-cr--local-hooks (buf)
+ "Return the buffer-local finish hooks of BUF, without the t marker."
+ (remq t (buffer-local-value 'compilation-finish-functions buf)))
+
;;; Normal Cases
(ert-deftest test-dev-fkeys-clean-rebuild-impl-runs-derived-clean-cmd ()
@@ -34,48 +50,50 @@ Components integrated:
- `cj/--f4-clean-rebuild-impl' (unit under test)
- `cj/--f4-derive-clean-cmd' (real)
- `compile' (MOCKED — captures the command string)
-- `projectile-compile-project' (MOCKED — no-op)
-- `compilation-finish-functions' (real, scoped via let)"
+- `projectile-compile-project' (MOCKED — no-op)"
(test-dev-fkeys-cr--with-project '("Makefile")
- (let ((compile-calls nil)
- (compilation-finish-functions nil))
+ (let ((compile-calls nil))
(cl-letf (((symbol-function 'compile)
- (lambda (cmd) (push cmd compile-calls)))
+ (lambda (cmd) (push cmd compile-calls) nil))
((symbol-function 'projectile-compile-project)
(lambda (_arg) nil)))
(cj/--f4-clean-rebuild-impl root)
(should (equal compile-calls '("make clean")))))))
-(ert-deftest test-dev-fkeys-clean-rebuild-impl-installs-finish-hook ()
- "Normal: handler installs exactly one hook in `compilation-finish-functions'."
+(ert-deftest test-dev-fkeys-clean-rebuild-impl-installs-hook-in-compilation-buffer ()
+ "Normal: the one-shot hook lands buffer-locally in the buffer `compile'
+returns; the global `compilation-finish-functions' stays untouched."
(test-dev-fkeys-cr--with-project '("go.mod")
- (let ((compilation-finish-functions nil))
- (cl-letf (((symbol-function 'compile) (lambda (_cmd) nil))
- ((symbol-function 'projectile-compile-project)
- (lambda (_arg) nil)))
- (cj/--f4-clean-rebuild-impl root)
- (should (= (length compilation-finish-functions) 1))))))
+ (test-dev-fkeys-cr--with-compilation-buffer buf
+ (let ((compilation-finish-functions nil))
+ (cl-letf (((symbol-function 'compile) (lambda (_cmd) buf))
+ ((symbol-function 'projectile-compile-project)
+ (lambda (_arg) nil)))
+ (cj/--f4-clean-rebuild-impl root)
+ (should (null compilation-finish-functions))
+ (should (= 1 (length (test-dev-fkeys-cr--local-hooks buf)))))))))
(ert-deftest test-dev-fkeys-clean-rebuild-impl-hook-runs-projectile-compile-on-success ()
- "Normal: when the clean step finishes successfully, the installed hook
+ "Normal: when the clean step finishes successfully, the buffer-local hook
calls `projectile-compile-project' to do the rebuild."
(test-dev-fkeys-cr--with-project '("Cargo.toml")
- (let ((compile-calls 0)
- (compilation-finish-functions nil))
- (cl-letf (((symbol-function 'compile) (lambda (_cmd) nil))
- ((symbol-function 'projectile-compile-project)
- (lambda (_arg) (cl-incf compile-calls))))
- (cj/--f4-clean-rebuild-impl root)
- (run-hook-with-args 'compilation-finish-functions nil "finished\n")
- (should (= compile-calls 1))))))
+ (test-dev-fkeys-cr--with-compilation-buffer buf
+ (let ((compile-calls 0)
+ (compilation-finish-functions nil))
+ (cl-letf (((symbol-function 'compile) (lambda (_cmd) buf))
+ ((symbol-function 'projectile-compile-project)
+ (lambda (_arg) (cl-incf compile-calls))))
+ (cj/--f4-clean-rebuild-impl root)
+ (with-current-buffer buf
+ (run-hook-with-args 'compilation-finish-functions buf "finished\n"))
+ (should (= compile-calls 1)))))))
(ert-deftest test-dev-fkeys-clean-rebuild-impl-runs-clean-from-project-root ()
"Normal: the clean compile runs with default-directory bound to ROOT."
(test-dev-fkeys-cr--with-project '("Eask")
- (let ((seen-dir nil)
- (compilation-finish-functions nil))
+ (let ((seen-dir nil))
(cl-letf (((symbol-function 'compile)
- (lambda (_cmd) (setq seen-dir default-directory)))
+ (lambda (_cmd) (setq seen-dir default-directory) nil))
((symbol-function 'projectile-compile-project)
(lambda (_arg) nil)))
(cj/--f4-clean-rebuild-impl root)
@@ -87,14 +105,28 @@ calls `projectile-compile-project' to do the rebuild."
(ert-deftest test-dev-fkeys-clean-rebuild-impl-hook-skips-rebuild-on-failure ()
"Boundary: when the clean step fails, projectile-compile-project does not run."
(test-dev-fkeys-cr--with-project '("Makefile")
- (let ((compile-calls 0)
- (compilation-finish-functions nil))
+ (test-dev-fkeys-cr--with-compilation-buffer buf
+ (let ((compile-calls 0)
+ (compilation-finish-functions nil))
+ (cl-letf (((symbol-function 'compile) (lambda (_cmd) buf))
+ ((symbol-function 'projectile-compile-project)
+ (lambda (_arg) (cl-incf compile-calls))))
+ (cj/--f4-clean-rebuild-impl root)
+ (with-current-buffer buf
+ (run-hook-with-args 'compilation-finish-functions
+ buf "exited abnormally\n"))
+ (should (= compile-calls 0)))))))
+
+(ert-deftest test-dev-fkeys-clean-rebuild-impl-dead-compile-buffer-no-global-hook ()
+ "Boundary: when `compile' returns no live buffer, nothing is installed
+anywhere — the global hook list stays empty."
+ (test-dev-fkeys-cr--with-project '("Makefile")
+ (let ((compilation-finish-functions nil))
(cl-letf (((symbol-function 'compile) (lambda (_cmd) nil))
((symbol-function 'projectile-compile-project)
- (lambda (_arg) (cl-incf compile-calls))))
+ (lambda (_arg) nil)))
(cj/--f4-clean-rebuild-impl root)
- (run-hook-with-args 'compilation-finish-functions nil "exited abnormally\n")
- (should (= compile-calls 0))))))
+ (should (null compilation-finish-functions))))))
;;; Error Cases
diff --git a/tests/test-dev-fkeys--f4-compile-and-run-impl.el b/tests/test-dev-fkeys--f4-compile-and-run-impl.el
index d59a6cd6..34e5bdf3 100644
--- a/tests/test-dev-fkeys--f4-compile-and-run-impl.el
+++ b/tests/test-dev-fkeys--f4-compile-and-run-impl.el
@@ -2,8 +2,11 @@
;;; Commentary:
;; Tests for the "Compile + Run" action handler. After kicking off the
-;; compile, attaches a one-shot `compilation-finish-functions' hook that
-;; runs the project on success.
+;; compile, attaches a one-shot finish hook buffer-locally in the
+;; compilation buffer projectile returns, so the global
+;; `compilation-finish-functions' is never touched. A quit at
+;; projectile's compile prompt therefore can never leave an armed hook
+;; that a later unrelated compile would fire.
;;; Code:
@@ -12,6 +15,14 @@
(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
(require 'dev-fkeys)
+(defmacro test-dev-fkeys-car--with-buffer (buf &rest body)
+ "Run BODY with BUF bound to a temp buffer standing in for a compilation buffer."
+ (declare (indent 1))
+ `(let ((,buf (generate-new-buffer " *test-compilation*")))
+ (unwind-protect
+ (progn ,@body)
+ (kill-buffer ,buf))))
+
;;; Normal Cases
(ert-deftest test-dev-fkeys-compile-and-run-impl-invokes-projectile-compile ()
@@ -19,57 +30,77 @@
Components integrated:
- `cj/--f4-compile-and-run-impl' (unit under test)
-- `projectile-compile-project' (MOCKED via cl-letf)
-- `compilation-finish-functions' (real, scoped via let)"
- (let ((compile-calls 0)
- (compilation-finish-functions nil))
+- `projectile-compile-project' (MOCKED via cl-letf)"
+ (let ((compile-calls 0))
(cl-letf (((symbol-function 'projectile-compile-project)
- (lambda (_arg) (cl-incf compile-calls))))
+ (lambda (_arg) (cl-incf compile-calls) nil)))
(cj/--f4-compile-and-run-impl)
(should (= compile-calls 1)))))
-(ert-deftest test-dev-fkeys-compile-and-run-impl-installs-finish-hook ()
- "Normal: handler installs exactly one hook in `compilation-finish-functions'."
- (let ((compilation-finish-functions nil))
- (cl-letf (((symbol-function 'projectile-compile-project)
- (lambda (_arg) nil)))
- (cj/--f4-compile-and-run-impl)
- (should (= (length compilation-finish-functions) 1)))))
+(ert-deftest test-dev-fkeys-compile-and-run-impl-installs-hook-in-compilation-buffer ()
+ "Normal: the one-shot hook lands buffer-locally in the compilation buffer;
+the global `compilation-finish-functions' stays untouched."
+ (test-dev-fkeys-car--with-buffer buf
+ (let ((compilation-finish-functions nil))
+ (cl-letf (((symbol-function 'projectile-compile-project)
+ (lambda (_arg) buf)))
+ (cj/--f4-compile-and-run-impl)
+ (should (null compilation-finish-functions))
+ (should (= 1 (length (remq t (buffer-local-value
+ 'compilation-finish-functions buf)))))))))
(ert-deftest test-dev-fkeys-compile-and-run-impl-hook-runs-projectile-run-on-success ()
- "Normal: when the compile finishes successfully, the installed hook calls
-`projectile-run-project'.
+ "Normal: when the compile finishes successfully, the buffer-local hook
+calls `projectile-run-project'.
Components integrated:
- `cj/--f4-compile-and-run-impl' (unit under test)
-- `projectile-compile-project' (MOCKED — no-op)
+- `projectile-compile-project' (MOCKED — returns the compilation buffer)
- `projectile-run-project' (MOCKED — counts calls)
-- `compilation-finish-functions' (real)
+- `compilation-finish-functions' (real, buffer-local)
- `run-hook-with-args' (real — simulates compile.el firing the hook)"
- (let ((run-calls 0)
- (compilation-finish-functions nil))
- (cl-letf (((symbol-function 'projectile-compile-project)
- (lambda (_arg) nil))
- ((symbol-function 'projectile-run-project)
- (lambda (_arg) (cl-incf run-calls))))
- (cj/--f4-compile-and-run-impl)
- (run-hook-with-args 'compilation-finish-functions nil "finished\n")
- (should (= run-calls 1)))))
+ (test-dev-fkeys-car--with-buffer buf
+ (let ((run-calls 0)
+ (compilation-finish-functions nil))
+ (cl-letf (((symbol-function 'projectile-compile-project)
+ (lambda (_arg) buf))
+ ((symbol-function 'projectile-run-project)
+ (lambda (_arg) (cl-incf run-calls))))
+ (cj/--f4-compile-and-run-impl)
+ (with-current-buffer buf
+ (run-hook-with-args 'compilation-finish-functions buf "finished\n"))
+ (should (= run-calls 1))))))
;;; Boundary Cases
(ert-deftest test-dev-fkeys-compile-and-run-impl-hook-skips-projectile-run-on-failure ()
"Boundary: when the compile fails, projectile-run-project must not run.
The hook still self-removes (covered in the make-once-hook tests)."
- (let ((run-calls 0)
- (compilation-finish-functions nil))
+ (test-dev-fkeys-car--with-buffer buf
+ (let ((run-calls 0)
+ (compilation-finish-functions nil))
+ (cl-letf (((symbol-function 'projectile-compile-project)
+ (lambda (_arg) buf))
+ ((symbol-function 'projectile-run-project)
+ (lambda (_arg) (cl-incf run-calls))))
+ (cj/--f4-compile-and-run-impl)
+ (with-current-buffer buf
+ (run-hook-with-args 'compilation-finish-functions
+ buf "exited abnormally\n"))
+ (should (= run-calls 0))))))
+
+(ert-deftest test-dev-fkeys-compile-and-run-impl-quit-leaves-no-global-hook ()
+ "Boundary: a quit at projectile's prompt leaves no armed hook anywhere.
+This is the regression the buffer-local install exists to prevent: the
+old shape armed a global hook before the prompt, so C-g left it live and
+the next unrelated compile fired the chained run."
+ (let ((compilation-finish-functions nil))
(cl-letf (((symbol-function 'projectile-compile-project)
- (lambda (_arg) nil))
- ((symbol-function 'projectile-run-project)
- (lambda (_arg) (cl-incf run-calls))))
- (cj/--f4-compile-and-run-impl)
- (run-hook-with-args 'compilation-finish-functions nil "exited abnormally\n")
- (should (= run-calls 0)))))
+ (lambda (_arg) (signal 'quit nil))))
+ (condition-case nil
+ (cj/--f4-compile-and-run-impl)
+ (quit nil))
+ (should (null compilation-finish-functions)))))
(provide 'test-dev-fkeys--f4-compile-and-run-impl)
;;; test-dev-fkeys--f4-compile-and-run-impl.el ends here
diff --git a/tests/test-dev-fkeys--f4-make-once-hook.el b/tests/test-dev-fkeys--f4-make-once-hook.el
index b6c71dd7..4fc84e63 100644
--- a/tests/test-dev-fkeys--f4-make-once-hook.el
+++ b/tests/test-dev-fkeys--f4-make-once-hook.el
@@ -95,6 +95,21 @@ hook exactly once per compile, so the practical contract is one-shot."
(funcall hook nil "interrupt\n"))
(should (= called 0))))
+(ert-deftest test-dev-fkeys-make-once-hook-removes-itself-buffer-locally ()
+ "Boundary: a hook installed buffer-locally removes its local entry when
+run in that buffer — the shape used by the F4 chained-compile handlers."
+ (let ((buf (generate-new-buffer " *test-once-hook*"))
+ (called 0))
+ (unwind-protect
+ (let ((hook (cj/--f4-make-once-hook (lambda () (cl-incf called)))))
+ (with-current-buffer buf
+ (add-hook 'compilation-finish-functions hook nil t)
+ (funcall hook buf "finished\n")
+ (should-not (memq hook (buffer-local-value
+ 'compilation-finish-functions buf))))
+ (should (= called 1)))
+ (kill-buffer buf))))
+
;;; Error Cases
(ert-deftest test-dev-fkeys-make-once-hook-then-fn-error-still-removes-hook ()
diff --git a/tests/test-diff-config--ediff-options.el b/tests/test-diff-config--ediff-options.el
new file mode 100644
index 00000000..a43637d9
--- /dev/null
+++ b/tests/test-diff-config--ediff-options.el
@@ -0,0 +1,27 @@
+;;; test-diff-config--ediff-options.el --- Tests for ediff diff options -*- lexical-binding: t -*-
+
+;;; Commentary:
+;; Pins the removal of the global "-w" default for `ediff-diff-options'.
+;; With "-w", every ediff session ignores ALL whitespace, so
+;; indentation-only changes (significant in Python, Makefiles, YAML)
+;; compare as identical. Whitespace-ignoring is a per-session toggle
+;; (ediff's `##'), not a global default.
+
+;;; Code:
+
+(require 'ert)
+
+(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
+(require 'diff-config)
+
+;;; Normal Cases
+
+(ert-deftest test-diff-config-ediff-options-no-global-whitespace-ignore ()
+ "Normal: after ediff loads, no global -w sits in ediff-diff-options.
+The use-package :custom values apply when the deferred package loads,
+so the assertion must run with ediff actually loaded."
+ (require 'ediff)
+ (should (not (string-match-p "-w" (or ediff-diff-options "")))))
+
+(provide 'test-diff-config--ediff-options)
+;;; test-diff-config--ediff-options.el ends here
diff --git a/tests/test-httpd-config--defer.el b/tests/test-httpd-config--defer.el
new file mode 100644
index 00000000..1a1fbbed
--- /dev/null
+++ b/tests/test-httpd-config--defer.el
@@ -0,0 +1,34 @@
+;;; test-httpd-config--defer.el --- Tests for httpd-config lazy loading -*- lexical-binding: t -*-
+
+;;; Commentary:
+;; Pins httpd-config's load-time behavior: merely loading the module must
+;; not create the www directory (that belongs to the moment simple-httpd
+;; actually loads) and must not pull in simple-httpd itself —
+;; impatient-mode requires it on demand.
+
+;;; Code:
+
+(require 'ert)
+
+(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
+
+;;; Boundary Cases
+
+(ert-deftest test-httpd-config-load-creates-no-www-dir ()
+ "Boundary: loading httpd-config does not create www/ in user-emacs-directory."
+ (let* ((sandbox (make-temp-file "httpd-config-test-" t))
+ (user-emacs-directory (file-name-as-directory sandbox)))
+ (unwind-protect
+ (progn
+ (require 'httpd-config)
+ (should-not (file-directory-p
+ (expand-file-name "www" user-emacs-directory))))
+ (delete-directory sandbox t))))
+
+(ert-deftest test-httpd-config-load-does-not-load-simple-httpd ()
+ "Boundary: loading httpd-config leaves simple-httpd unloaded."
+ (require 'httpd-config)
+ (should-not (featurep 'simple-httpd)))
+
+(provide 'test-httpd-config--defer)
+;;; test-httpd-config--defer.el ends here
diff --git a/tests/test-restclient-config--keymap.el b/tests/test-restclient-config--keymap.el
new file mode 100644
index 00000000..7265c4f3
--- /dev/null
+++ b/tests/test-restclient-config--keymap.el
@@ -0,0 +1,29 @@
+;;; test-restclient-config--keymap.el --- Tests for the restclient prefix keymap -*- lexical-binding: t -*-
+
+;;; Commentary:
+;; Pins the C-; R prefix wiring. The bindings must go through
+;; `cj/restclient-map' + `cj/register-prefix-map' (like the other C-;
+;; prefixes), not raw `global-set-key' calls that silently depend on
+;; keybindings.el having installed C-; first.
+
+;;; Code:
+
+(require 'ert)
+
+(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
+(require 'restclient-config)
+
+;;; Normal Cases
+
+(ert-deftest test-restclient-keymap-registered-under-custom-prefix ()
+ "Normal: cj/restclient-map is bound at R inside cj/custom-keymap."
+ (should (boundp 'cj/restclient-map))
+ (should (eq cj/restclient-map (keymap-lookup cj/custom-keymap "R"))))
+
+(ert-deftest test-restclient-keymap-binds-new-buffer-and-open-file ()
+ "Normal: the prefix map carries the two restclient commands."
+ (should (eq #'cj/restclient-new-buffer (keymap-lookup cj/restclient-map "n")))
+ (should (eq #'cj/restclient-open-file (keymap-lookup cj/restclient-map "o"))))
+
+(provide 'test-restclient-config--keymap)
+;;; test-restclient-config--keymap.el ends here
diff --git a/tests/test-test-runner--nil-global-directory.el b/tests/test-test-runner--nil-global-directory.el
new file mode 100644
index 00000000..c70de5bd
--- /dev/null
+++ b/tests/test-test-runner--nil-global-directory.el
@@ -0,0 +1,51 @@
+;;; test-test-runner--nil-global-directory.el --- Tests for the no-test-directory path -*- lexical-binding: t -*-
+
+;;; Commentary:
+;; Outside a Projectile project, `cj/test--get-test-directory' falls back
+;; to `cj/test-global-directory', which defaults to nil. These tests pin
+;; the contract for that nil case: discovery helpers return nil instead
+;; of crashing on (file-directory-p nil), and the interactive commands
+;; signal `user-error' instead of wrong-type-argument.
+
+;;; Code:
+
+(require 'ert)
+(require 'cl-lib)
+
+(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
+(require 'test-runner)
+
+(defmacro test-runner-nil-dir--outside-project (&rest body)
+ "Run BODY with no project root and a nil `cj/test-global-directory'."
+ (declare (indent 0))
+ `(let ((cj/test-global-directory nil))
+ (cl-letf (((symbol-function 'cj/test--project-root)
+ (lambda () nil)))
+ ,@body)))
+
+;;; Boundary Cases
+
+(ert-deftest test-test-runner-nil-dir-get-test-directory-returns-nil ()
+ "Boundary: no project and no global dir yields nil, not an error."
+ (test-runner-nil-dir--outside-project
+ (should (null (cj/test--get-test-directory)))))
+
+(ert-deftest test-test-runner-nil-dir-get-test-files-returns-nil ()
+ "Boundary: file discovery returns nil instead of crashing on a nil dir."
+ (test-runner-nil-dir--outside-project
+ (should (null (cj/test--get-test-files)))))
+
+;;; Error Cases
+
+(ert-deftest test-test-runner-nil-dir-load-all-signals-user-error ()
+ "Error: `cj/test-load-all' signals `user-error', not wrong-type-argument."
+ (test-runner-nil-dir--outside-project
+ (should-error (cj/test-load-all) :type 'user-error)))
+
+(ert-deftest test-test-runner-nil-dir-focus-add-signals-user-error ()
+ "Error: `cj/test-focus-add' signals `user-error', not wrong-type-argument."
+ (test-runner-nil-dir--outside-project
+ (should-error (cj/test-focus-add) :type 'user-error)))
+
+(provide 'test-test-runner--nil-global-directory)
+;;; test-test-runner--nil-global-directory.el ends here
diff --git a/tests/test-vc-config--git-clone.el b/tests/test-vc-config--git-clone.el
index 3b39ece2..46ce3d40 100644
--- a/tests/test-vc-config--git-clone.el
+++ b/tests/test-vc-config--git-clone.el
@@ -2,9 +2,11 @@
;;; Commentary:
;; Unit tests for cj/--git-clone-dir-name (robust repo-dir derivation across
-;; HTTPS, scp-style SSH, ssh:// and local URLs) and for cj/git-clone-clipboard-url
-;; reporting a failed clone from the process exit status instead of silently
-;; assuming the directory appeared.
+;; HTTPS, scp-style SSH, ssh:// and local URLs), for the async clone process
+;; wiring in cj/git-clone-clipboard-url (make-process argv, no shell), and
+;; for the sentinel built by cj/--git-clone-make-sentinel (open on success,
+;; surface the process buffer on failure). Sentinel tests drive real
+;; short-lived processes rather than mocking process primitives.
;;; Code:
@@ -14,6 +16,18 @@
(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
(require 'vc-config)
+(defun test-vc-clone--run-sentinel (command sentinel buffer)
+ "Run COMMAND with SENTINEL and BUFFER; wait for process exit."
+ (let ((proc (make-process :name "test-vc-clone"
+ :buffer buffer
+ :command command
+ :sentinel sentinel)))
+ (while (process-live-p proc)
+ (accept-process-output proc 0.05))
+ ;; Give the sentinel a chance to run after exit.
+ (accept-process-output nil 0.05)
+ proc))
+
;;; cj/--git-clone-dir-name — Normal Cases
(ert-deftest test-vc-git-clone-dir-name-https-with-git-suffix ()
@@ -36,7 +50,7 @@
(should (equal "repo"
(cj/--git-clone-dir-name "ssh://git@example.com/user/repo.git"))))
-;;; Boundary Cases
+;;; cj/--git-clone-dir-name — Boundary Cases
(ert-deftest test-vc-git-clone-dir-name-ssh-scp-without-user ()
"Boundary: scp-style SSH with no user path (host:repo.git) still works.
@@ -60,29 +74,110 @@ since there is no `/' separator."
(should (equal "repo"
(cj/--git-clone-dir-name " https://example.com/user/repo.git\n"))))
-;;; cj/git-clone-clipboard-url — Error Cases
+;;; cj/--git-clone-open — Normal / Boundary Cases
-(ert-deftest test-vc-git-clone-clipboard-url-reports-clone-failure ()
- "Error: a nonzero git exit status surfaces a user-error, not silence.
-Uses a real writable temp dir as the target (so the file predicates run
-for real) and mocks only the clone process to fail."
- (let ((target (make-temp-file "cj-clone-fail-" t)))
+(ert-deftest test-vc-git-clone-open-finds-readme ()
+ "Normal: a README in the clone is opened."
+ (let ((dir (make-temp-file "cj-clone-open-" t))
+ (opened nil))
+ (unwind-protect
+ (progn
+ (with-temp-file (expand-file-name "README.md" dir) (insert "hi"))
+ (cl-letf (((symbol-function 'find-file)
+ (lambda (f &rest _) (setq opened f)))
+ ((symbol-function 'dired) #'ignore))
+ (cj/--git-clone-open dir)
+ (should (equal (expand-file-name "README.md" dir) opened))))
+ (delete-directory dir t))))
+
+(ert-deftest test-vc-git-clone-open-no-readme-dires ()
+ "Boundary: with no README, the clone directory is dired."
+ (let ((dir (make-temp-file "cj-clone-open-" t))
+ (dired-dir nil))
+ (unwind-protect
+ (cl-letf (((symbol-function 'find-file)
+ (lambda (&rest _) (error "find-file should not run")))
+ ((symbol-function 'dired)
+ (lambda (d &rest _) (setq dired-dir d))))
+ (cj/--git-clone-open dir)
+ (should (equal dir dired-dir)))
+ (delete-directory dir t))))
+
+;;; cj/--git-clone-make-sentinel — Normal / Error Cases
+
+(ert-deftest test-vc-git-clone-sentinel-success-opens-clone ()
+ "Normal: a zero-exit clone process opens the clone directory."
+ (let ((buffer (generate-new-buffer " *test-clone-ok*"))
+ (opened nil))
+ (unwind-protect
+ (cl-letf (((symbol-function 'cj/--git-clone-open)
+ (lambda (d) (setq opened d)))
+ ((symbol-function 'message) (lambda (&rest _) nil)))
+ (test-vc-clone--run-sentinel
+ '("true") (cj/--git-clone-make-sentinel "url" "/tmp/clone-dst") buffer)
+ (should (equal "/tmp/clone-dst" opened)))
+ (kill-buffer buffer))))
+
+(ert-deftest test-vc-git-clone-sentinel-failure-pops-process-buffer ()
+ "Error: a nonzero exit surfaces the process buffer, never opens the clone."
+ (let ((buffer (generate-new-buffer " *test-clone-fail*"))
+ (opened nil)
+ (popped nil))
(unwind-protect
- (cl-letf (((symbol-function 'call-process) (lambda (&rest _) 128))
- ((symbol-function 'pop-to-buffer) #'ignore)
- ((symbol-function 'message) #'ignore))
- (should-error
- (cj/git-clone-clipboard-url "https://example.com/user/repo.git" target)
- :type 'user-error))
+ (cl-letf (((symbol-function 'cj/--git-clone-open)
+ (lambda (d) (setq opened d)))
+ ((symbol-function 'pop-to-buffer)
+ (lambda (b &rest _) (setq popped b)))
+ ((symbol-function 'message) (lambda (&rest _) nil)))
+ (test-vc-clone--run-sentinel
+ '("false") (cj/--git-clone-make-sentinel "url" "/tmp/clone-dst") buffer)
+ (should-not opened)
+ (should (eq buffer popped)))
+ (kill-buffer buffer))))
+
+;;; cj/git-clone-clipboard-url — Normal / Error Cases
+
+(ert-deftest test-vc-git-clone-clipboard-url-spawns-async-argv ()
+ "Normal: the clone runs as an async process with a plain argv, no shell.
+The `--' separator must precede the URL so a leading-dash URL cannot be
+read as a git flag."
+ (let ((target (make-temp-file "cj-clone-async-" t))
+ (spawned nil))
+ (unwind-protect
+ (cl-letf (((symbol-function 'make-process)
+ (lambda (&rest args)
+ (setq spawned (plist-get args :command))
+ nil))
+ ((symbol-function 'message) (lambda (&rest _) nil)))
+ (cj/git-clone-clipboard-url "https://example.com/user/repo.git" target)
+ (should (equal (list "git" "clone" "--"
+ "https://example.com/user/repo.git"
+ (expand-file-name "repo" target))
+ spawned)))
(delete-directory target t))))
(ert-deftest test-vc-git-clone-clipboard-url-empty-clipboard-errors ()
"Error: an empty clipboard URL aborts before any clone attempt."
- (let ((cloned nil))
- (cl-letf (((symbol-function 'call-process)
- (lambda (&rest _) (setq cloned t) 0)))
+ (let ((spawned nil))
+ (cl-letf (((symbol-function 'make-process)
+ (lambda (&rest _) (setq spawned t) nil)))
(should-error (cj/git-clone-clipboard-url " " "/tmp") :type 'user-error))
- (should-not cloned)))
+ (should-not spawned)))
+
+(ert-deftest test-vc-git-clone-clipboard-url-existing-destination-errors ()
+ "Error: an existing clone destination aborts before any clone attempt."
+ (let ((target (make-temp-file "cj-clone-exists-" t))
+ (spawned nil))
+ (unwind-protect
+ (progn
+ (make-directory (expand-file-name "repo" target))
+ (cl-letf (((symbol-function 'make-process)
+ (lambda (&rest _) (setq spawned t) nil)))
+ (should-error
+ (cj/git-clone-clipboard-url "https://example.com/user/repo.git" target)
+ :type 'user-error))
+ (should-not spawned))
+ (delete-directory target t))))
(provide 'test-vc-config--git-clone)
;;; test-vc-config--git-clone.el ends here
diff --git a/tests/test-vc-config--gutter-hunk-candidates.el b/tests/test-vc-config--gutter-hunk-candidates.el
new file mode 100644
index 00000000..65db0c3c
--- /dev/null
+++ b/tests/test-vc-config--gutter-hunk-candidates.el
@@ -0,0 +1,69 @@
+;;; test-vc-config--gutter-hunk-candidates.el --- Tests for cj/--git-gutter-hunk-candidates -*- lexical-binding: t -*-
+
+;;; Commentary:
+;; Unit tests for cj/--git-gutter-hunk-candidates, the pure helper that
+;; builds completion candidates (label . line) from git-gutter hunk start
+;; lines against the current buffer's text. The interactive wrapper
+;; cj/goto-git-gutter-diff-hunks maps git-gutter:diffinfos onto start
+;; lines and delegates here.
+
+;;; Code:
+
+(require 'ert)
+(require 'cl-lib)
+
+(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
+(require 'vc-config)
+
+;;; Normal Cases
+
+(ert-deftest test-vc-gutter-hunk-candidates-labels-carry-line-text ()
+ "Normal: each candidate label contains the hunk line's text."
+ (with-temp-buffer
+ (insert "alpha\nbravo\ncharlie\n")
+ (let ((candidates (cj/--git-gutter-hunk-candidates '(2 3))))
+ (should (= 2 (length candidates)))
+ (should (string-match-p "bravo" (car (nth 0 candidates))))
+ (should (string-match-p "charlie" (car (nth 1 candidates)))))))
+
+(ert-deftest test-vc-gutter-hunk-candidates-cdr-is-start-line ()
+ "Normal: each candidate's cdr is the hunk's start line number."
+ (with-temp-buffer
+ (insert "alpha\nbravo\ncharlie\n")
+ (let ((candidates (cj/--git-gutter-hunk-candidates '(1 3))))
+ (should (equal '(1 3) (mapcar #'cdr candidates))))))
+
+;;; Boundary Cases
+
+(ert-deftest test-vc-gutter-hunk-candidates-empty-input-returns-nil ()
+ "Boundary: no hunks produce no candidates."
+ (with-temp-buffer
+ (insert "alpha\n")
+ (should (null (cj/--git-gutter-hunk-candidates '())))))
+
+(ert-deftest test-vc-gutter-hunk-candidates-first-line ()
+ "Boundary: a hunk on line 1 resolves to the first line's text."
+ (with-temp-buffer
+ (insert "alpha\nbravo\n")
+ (let ((candidates (cj/--git-gutter-hunk-candidates '(1))))
+ (should (string-match-p "alpha" (caar candidates)))
+ (should (= 1 (cdar candidates))))))
+
+(ert-deftest test-vc-gutter-hunk-candidates-unicode-line-text ()
+ "Boundary: line text with unicode survives into the label."
+ (with-temp-buffer
+ (insert "naïve — 日本語\n")
+ (let ((candidates (cj/--git-gutter-hunk-candidates '(1))))
+ (should (string-match-p "日本語" (caar candidates))))))
+
+;;; Error Cases
+
+(ert-deftest test-vc-goto-git-gutter-diff-hunks-no-hunks-user-error ()
+ "Error: the command signals `user-error' when the buffer has no hunks."
+ (with-temp-buffer
+ (setq-local git-gutter:diffinfos nil)
+ (cl-letf (((symbol-function 'require) (lambda (&rest _) nil)))
+ (should-error (cj/goto-git-gutter-diff-hunks) :type 'user-error))))
+
+(provide 'test-vc-config--gutter-hunk-candidates)
+;;; test-vc-config--gutter-hunk-candidates.el ends here
diff --git a/tests/test-vc-config--timemachine-commands.el b/tests/test-vc-config--timemachine-commands.el
new file mode 100644
index 00000000..36a71695
--- /dev/null
+++ b/tests/test-vc-config--timemachine-commands.el
@@ -0,0 +1,36 @@
+;;; test-vc-config--timemachine-commands.el --- Tests for git-timemachine command wiring -*- lexical-binding: t -*-
+
+;;; Commentary:
+;; Guards the git-timemachine autoload surface in vc-config.el. The
+;; upstream package defines no `git-timemachine-show-selected-revision';
+;; an autoload for it in :commands creates a phantom M-x command that
+;; errors after loading the package. The real selector lives in the
+;; cj/ namespace.
+
+;;; Code:
+
+(require 'ert)
+
+(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
+(require 'vc-config)
+
+;;; Normal Cases
+
+(ert-deftest test-vc-timemachine-selected-revision-cj-command-defined ()
+ "Normal: the cj/ selected-revision selector is defined by vc-config."
+ (should (fboundp 'cj/git-timemachine-show-selected-revision)))
+
+(ert-deftest test-vc-timemachine-entry-command-defined ()
+ "Normal: the cj/git-timemachine entry command is defined."
+ (should (commandp 'cj/git-timemachine)))
+
+;;; Error Cases
+
+(ert-deftest test-vc-timemachine-no-phantom-package-autoload ()
+ "Error: no autoload exists for a function the package never defines.
+An autoload stub for `git-timemachine-show-selected-revision' would
+surface in M-x and signal void-function after the package loads."
+ (should-not (fboundp 'git-timemachine-show-selected-revision)))
+
+(provide 'test-vc-config--timemachine-commands)
+;;; test-vc-config--timemachine-commands.el ends here