aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-05-25 19:11:00 -0500
committerCraig Jennings <c@cjennings.net>2026-05-25 19:11:00 -0500
commita09398f27f1e93bfbe84e1dd185e70b391e82888 (patch)
treecf4e344a4970749249183cfb58e8484c265adbbf
parent423ca0e8e502661343a8aa02d5c58c5029e40b03 (diff)
downloaddotemacs-a09398f27f1e93bfbe84e1dd185e70b391e82888.tar.gz
dotemacs-a09398f27f1e93bfbe84e1dd185e70b391e82888.zip
feat(user-constants): make required-path init failures actionable
cj/verify-or-create-dir and cj/verify-or-create-file caught every creation failure and only messaged it, so a broken environment for a path the config actually needs stayed quiet until some later module failed in a more confusing way. I gave both an optional required flag and routed failures through a shared cj/--report-path-failure: a required failure raises a prominent display-warning, an optional one is still just logged so it never blocks startup. The initializer now groups its paths by that distinction. Required: the backbone directories (sync, org, roam) and the calendar stubs (gcal/pcal/dcal), since org-agenda-list hangs prompting for those when they're missing. Optional: the secondary dirs and the content files, each populated by its own workflow. I went with a warning rather than a user-error for required failures so a directory hiccup surfaces loudly without aborting init. Added error-path tests: an optional failure logs and never warns, and a required dir or file failure raises a user-constants warning.
-rw-r--r--modules/user-constants.el57
-rw-r--r--tests/test-user-constants.el43
2 files changed, 81 insertions, 19 deletions
diff --git a/modules/user-constants.el b/modules/user-constants.el
index 02a500d6..43a23d79 100644
--- a/modules/user-constants.el
+++ b/modules/user-constants.el
@@ -205,23 +205,39 @@ For more information, see org webclipper section of org-capture-config.el")
(and (file-directory-p dir)
(file-writable-p dir)))
-(defun cj/verify-or-create-dir (dir)
- "Verify the directory DIR exists; create it if it doesn't."
+(defun cj/--report-path-failure (kind path err required)
+ "Report a failure to create the KIND (a string) at PATH from ERR.
+A REQUIRED failure raises a prominent `display-warning' so a broken
+environment is hard to miss; an optional one is only logged, so it does not
+block startup."
+ (if required
+ (display-warning 'user-constants
+ (format "Failed to create required %s %s: %s"
+ kind path (error-message-string err))
+ :error)
+ (message "Error creating %s %s: %s" kind path (error-message-string err))))
+
+(defun cj/verify-or-create-dir (dir &optional required)
+ "Verify the directory DIR exists; create it if it doesn't.
+With REQUIRED non-nil, a creation failure raises a prominent warning instead
+of being logged quietly."
(condition-case err
(unless (file-directory-p dir)
(make-directory dir t)
(message "Created directory: %s" dir))
- (error (message "Error creating directory %s: %s" dir (error-message-string err)))))
+ (error (cj/--report-path-failure "directory" dir err required))))
-(defun cj/verify-or-create-file (file)
- "Verify the file FILE exists; create it if it doesn't."
+(defun cj/verify-or-create-file (file &optional required)
+ "Verify the file FILE exists; create it if it doesn't.
+With REQUIRED non-nil, a creation failure raises a prominent warning instead
+of being logged quietly. The parent directory inherits REQUIRED."
(condition-case err
(let ((dir (file-name-directory file)))
- (when dir (cj/verify-or-create-dir dir))
+ (when dir (cj/verify-or-create-dir dir required))
(unless (file-exists-p file)
(write-region "" nil file)
(message "Created file: %s" file)))
- (error (message "Error creating file %s: %s" file (error-message-string err)))))
+ (error (cj/--report-path-failure "file" file err required))))
(defun cj/initialize-user-directories-and-files ()
"Initialize all necessary directories and files.
@@ -229,21 +245,24 @@ This ensures that all directories and files required by the Emacs configuration
exist, creating them if necessary. This makes the configuration more robust
and portable across different machines."
(interactive)
- (mapc 'cj/verify-or-create-dir (list sync-dir
- drill-dir
+ ;; Required: the backbone directories everything else hangs off. If these
+ ;; can't be created the config is broken, so failure warns prominently.
+ (dolist (d (list sync-dir org-dir roam-dir))
+ (cj/verify-or-create-dir d t))
+ ;; Optional: secondary directories whose absence degrades one feature, not
+ ;; startup.
+ (mapc 'cj/verify-or-create-dir (list drill-dir
journals-dir
- roam-dir
snippets-dir
video-recordings-dir
- audio-recordings-dir
- org-dir))
- ;; gcal/pcal/dcal exist so org-agenda-list doesn't hang on missing files
- ;; (calendar-sync populates them on first sync).
- (mapc 'cj/verify-or-create-file (list gcal-file
- pcal-file
- dcal-file
- schedule-file
- inbox-file
+ audio-recordings-dir))
+ ;; Required: the calendar stubs — org-agenda-list hangs prompting for these
+ ;; if they're missing (calendar-sync populates them on first sync).
+ (dolist (f (list gcal-file pcal-file dcal-file))
+ (cj/verify-or-create-file f t))
+ ;; Optional: content files each populated by their own workflow.
+ (mapc 'cj/verify-or-create-file (list schedule-file
+ inbox-file
article-archive
reading-notes-file
contacts-file
diff --git a/tests/test-user-constants.el b/tests/test-user-constants.el
index 5246e7ea..8dd9284f 100644
--- a/tests/test-user-constants.el
+++ b/tests/test-user-constants.el
@@ -77,5 +77,48 @@ The whole point of the split — a bare require must not touch the filesystem."
(should (file-exists-p schedule-file))
(should (file-exists-p inbox-file))))
+;;; verify-or-create failure reporting (required vs optional)
+
+;; A path under a nonexistent root makes file-directory-p / file-exists-p return
+;; nil naturally, so the only stub needed is make-directory failing — no need to
+;; redefine core predicates (which trips native-comp trampolines).
+
+(ert-deftest test-user-constants-verify-dir-optional-failure-logs ()
+ "Error: an optional directory failure is logged, never warned or signalled."
+ (test-user-constants--load)
+ (let ((warned nil) (messaged nil))
+ (cl-letf (((symbol-function 'make-directory) (lambda (&rest _) (error "boom")))
+ ((symbol-function 'display-warning) (lambda (&rest _) (setq warned t)))
+ ((symbol-function 'message) (lambda (&rest _) (setq messaged t))))
+ (cj/verify-or-create-dir "/nonexistent-uc-test/optional")
+ (should messaged)
+ (should-not warned))))
+
+(ert-deftest test-user-constants-verify-dir-required-failure-warns ()
+ "Error: a required directory failure raises a prominent user-constants warning."
+ (test-user-constants--load)
+ (let ((warn-args nil))
+ (cl-letf (((symbol-function 'make-directory) (lambda (&rest _) (error "boom")))
+ ((symbol-function 'display-warning)
+ (lambda (group _msg &optional level) (setq warn-args (list group level)))))
+ (cj/verify-or-create-dir "/nonexistent-uc-test/required" t)
+ (should (eq (nth 0 warn-args) 'user-constants))
+ (should (eq (nth 1 warn-args) :error)))))
+
+(ert-deftest test-user-constants-verify-file-required-failure-warns ()
+ "Error: a required file failure raises a prominent user-constants warning."
+ (test-user-constants--load)
+ (let ((dir (make-temp-file "uc-reqfile-" t))
+ (warn-args nil))
+ (unwind-protect
+ (cl-letf (((symbol-function 'write-region) (lambda (&rest _) (error "boom")))
+ ((symbol-function 'display-warning)
+ (lambda (group _msg &optional level) (setq warn-args (list group level)))))
+ ;; dir exists so the failure is the write, not the parent directory
+ (cj/verify-or-create-file (expand-file-name "required.org" dir) t)
+ (should (eq (nth 0 warn-args) 'user-constants))
+ (should (eq (nth 1 warn-args) :error)))
+ (delete-directory dir t))))
+
(provide 'test-user-constants)
;;; test-user-constants.el ends here