aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-05-10 14:17:25 -0500
committerCraig Jennings <c@cjennings.net>2026-05-10 14:17:25 -0500
commit57e558ce13fae77eed07095638a753ba75af55d4 (patch)
tree840a83d80c834efb808ebcf464715e85c89ea8b3
parentf1e8f0898244bd2d834baf7541d10e5eff351d34 (diff)
downloaddotemacs-57e558ce13fae77eed07095638a753ba75af55d4.tar.gz
dotemacs-57e558ce13fae77eed07095638a753ba75af55d4.zip
refactor(system-lib): extract cj/process-output-or-error and cj/git-output-or-error from coverage-core
Phase 2.3 of utility-consolidation. `cj/--coverage-git-string' was a generic argv-based runner ("run program, return stdout, raise user-error on non-zero with status+output in the message") trapped inside coverage-core. Lift the generic shape into `cj/process-output-or-error' and add `cj/git-output-or-error' as a one-line wrapper that supplies "git" as the program. Both live in system-lib.el. Future callers I have in mind: reconcile-open-repos shell-style git calls (the high-priority data-safety task), vc-config clipboard cloning, mail integrations that touch git for commit signatures. Six Normal/Boundary/Error tests cover success/no-args/non-zero-exit for the generic runner, the user-error message content (program name, exit status, trimmed output), and the git wrapper's program argument routing. Migrate coverage-core's `cj/--coverage-git-merge-base' and `cj/--coverage-git-diff' to call the new git wrapper. Drop the local `cj/--coverage-git-string' definition. Add `(require \='system-lib)' to coverage-core.el per the Phase 2 exit criterion.
-rw-r--r--modules/coverage-core.el18
-rw-r--r--modules/system-lib.el26
-rw-r--r--tests/test-system-lib-process-output-or-error.el91
3 files changed, 120 insertions, 15 deletions
diff --git a/modules/coverage-core.el b/modules/coverage-core.el
index 93979530..47e891e6 100644
--- a/modules/coverage-core.el
+++ b/modules/coverage-core.el
@@ -14,6 +14,7 @@
(require 'seq)
(require 'subr-x)
+(require 'system-lib)
(defvar cj/coverage-backends nil
"Registry of coverage backends in priority order.
@@ -197,30 +198,17 @@ empty hash table. Malformed hunk headers are skipped silently."
(forward-line 1)))
result))
-(defun cj/--coverage-git-string (&rest args)
- "Run git with ARGS and return its stdout as a string.
-Signals `user-error' when git exits non-zero."
- (with-temp-buffer
- (let ((status (apply #'process-file "git" nil (current-buffer) nil args))
- (output (buffer-string)))
- (unless (zerop status)
- (user-error "git %s failed with status %s: %s"
- (string-join args " ")
- status
- (string-trim output)))
- output)))
-
(defun cj/--coverage-git-merge-base (base)
"Return the merge-base between HEAD and BASE."
(let ((merge-base (string-trim
- (cj/--coverage-git-string "merge-base" "HEAD" base))))
+ (cj/git-output-or-error "merge-base" "HEAD" base))))
(unless (not (string-empty-p merge-base))
(user-error "git merge-base HEAD %s returned no commit" base))
merge-base))
(defun cj/--coverage-git-diff (&rest args)
"Return git diff output for ARGS plus --unified=0."
- (apply #'cj/--coverage-git-string
+ (apply #'cj/git-output-or-error
(append (list "diff") args (list "--unified=0"))))
(defun cj/--coverage-changed-lines (scope &optional base)
diff --git a/modules/system-lib.el b/modules/system-lib.el
index dc1f8316..3ccec06b 100644
--- a/modules/system-lib.el
+++ b/modules/system-lib.el
@@ -54,6 +54,32 @@ interpolate."
argument
(shell-quote-argument argument)))
+(defun cj/process-output-or-error (program &rest args)
+ "Run PROGRAM with ARGS via `process-file' and return stdout, or signal error.
+
+On zero exit, returns the program's stdout as a string (including any
+trailing newline -- callers that need a trimmed value should call
+`string-trim' themselves). On non-zero exit, signals `user-error' with
+a message naming the program, the exit status, and the (trimmed) output
+so a user inspecting *Messages* can see what went wrong."
+ (with-temp-buffer
+ (let ((status (apply #'process-file program nil (current-buffer) nil args))
+ (output (buffer-string)))
+ (unless (zerop status)
+ (user-error "%s %s failed with status %s: %s"
+ program
+ (string-join args " ")
+ status
+ (string-trim output)))
+ output)))
+
+(defun cj/git-output-or-error (&rest args)
+ "Run git with ARGS and return stdout, or signal `user-error' on failure.
+
+Thin wrapper around `cj/process-output-or-error' with `git' as the
+program."
+ (apply #'cj/process-output-or-error "git" args))
+
(defun cj/log-silently (format-string &rest args)
"Append formatted message (FORMAT-STRING with ARGS) to *Messages* buffer.
This does so without echoing in the minibuffer."
diff --git a/tests/test-system-lib-process-output-or-error.el b/tests/test-system-lib-process-output-or-error.el
new file mode 100644
index 00000000..26b70abb
--- /dev/null
+++ b/tests/test-system-lib-process-output-or-error.el
@@ -0,0 +1,91 @@
+;;; test-system-lib-process-output-or-error.el --- Tests for cj/process-output-or-error and cj/git-output-or-error -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; `cj/process-output-or-error' is the generic argv-based runner: take
+;; a program name and its argument list, run it via `process-file', and
+;; return stdout on success or signal a clear `user-error' on non-zero
+;; exit (with program/status/output in the message).
+;;
+;; `cj/git-output-or-error' is the thin wrapper that supplies "git" as
+;; the program. Both helpers stay deterministic in tests by stubbing
+;; `process-file'.
+
+;;; Code:
+
+(require 'ert)
+(require 'cl-lib)
+
+(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
+(require 'system-lib)
+
+(defmacro test-system-lib-runner--with-stub (status output &rest body)
+ "Run BODY with `process-file' stubbed to return STATUS and write OUTPUT."
+ (declare (indent 2))
+ `(cl-letf (((symbol-function 'process-file)
+ (lambda (_program _infile destination _display &rest _args)
+ (when destination
+ (let ((buf (cond
+ ((eq destination t) (current-buffer))
+ ((bufferp destination) destination)
+ ((consp destination) (car destination)))))
+ (when (bufferp buf)
+ (with-current-buffer buf
+ (insert ,output)))))
+ ,status)))
+ ,@body))
+
+(ert-deftest test-cj-process-output-or-error-success-returns-stdout ()
+ "Normal: zero exit returns stdout."
+ (test-system-lib-runner--with-stub 0 "hello world\n"
+ (should (equal (cj/process-output-or-error "echo" "hello" "world")
+ "hello world\n"))))
+
+(ert-deftest test-cj-process-output-or-error-no-args-runs ()
+ "Boundary: no args is a valid invocation."
+ (test-system-lib-runner--with-stub 0 "ok\n"
+ (should (equal (cj/process-output-or-error "true") "ok\n"))))
+
+(ert-deftest test-cj-process-output-or-error-non-zero-signals-user-error ()
+ "Error: non-zero exit signals user-error."
+ (test-system-lib-runner--with-stub 1 "boom\n"
+ (should-error (cj/process-output-or-error "false")
+ :type 'user-error)))
+
+(ert-deftest test-cj-process-output-or-error-message-names-program-and-status ()
+ "Error: the user-error message names the program, the exit status,
+and the (trimmed) output so the user can see what went wrong."
+ (test-system-lib-runner--with-stub 128 "fatal: not a git repo\n"
+ (condition-case err
+ (cj/process-output-or-error "git" "status")
+ (user-error
+ (let ((message (error-message-string err)))
+ (should (string-match-p "git" message))
+ (should (string-match-p "128" message))
+ (should (string-match-p "fatal: not a git repo" message)))))))
+
+(ert-deftest test-cj-git-output-or-error-uses-git-program ()
+ "Normal: git wrapper passes its args through with `git' as the program."
+ (let (captured-program captured-args)
+ (cl-letf (((symbol-function 'process-file)
+ (lambda (program _infile destination _display &rest args)
+ (setq captured-program program
+ captured-args args)
+ (when destination
+ (let ((buf (cond ((eq destination t) (current-buffer))
+ ((bufferp destination) destination)
+ ((consp destination) (car destination)))))
+ (when (bufferp buf)
+ (with-current-buffer buf (insert "abcdef\n")))))
+ 0)))
+ (should (equal (cj/git-output-or-error "rev-parse" "HEAD") "abcdef\n"))
+ (should (equal captured-program "git"))
+ (should (equal captured-args '("rev-parse" "HEAD"))))))
+
+(ert-deftest test-cj-git-output-or-error-non-zero-signals-user-error ()
+ "Error: git wrapper raises the same user-error shape on failure."
+ (test-system-lib-runner--with-stub 1 "fatal: bad ref\n"
+ (should-error (cj/git-output-or-error "rev-parse" "bogus")
+ :type 'user-error)))
+
+(provide 'test-system-lib-process-output-or-error)
+;;; test-system-lib-process-output-or-error.el ends here