aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-05-25 19:06:13 -0500
committerCraig Jennings <c@cjennings.net>2026-05-25 19:06:13 -0500
commit423ca0e8e502661343a8aa02d5c58c5029e40b03 (patch)
treeb166d0026243ef57d225a15aa1dae0540be8bfd3
parentd09de2adf2c727410743c373230cd466baa9d170 (diff)
downloaddotemacs-423ca0e8e502661343a8aa02d5c58c5029e40b03.tar.gz
dotemacs-423ca0e8e502661343a8aa02d5c58c5029e40b03.zip
refactor(user-constants): move filesystem creation out of module load
(require 'user-constants) created ~8 directories and ~10 org/calendar files at load time, via a top-level dolist for the calendar stubs and a top-level call to cj/initialize-user-directories-and-files. That meant any bare require — tests, byte-compile, batch tools — wrote to disk. It's why a stray sync/org/ tree kept appearing in the repo during test runs. I removed both top-level forms and folded the gcal/pcal/dcal creation into the initializer. The path defconsts stay exactly as they were, so every consumer that just reads a path is unaffected. init.el now calls the initializer right after requiring the module, guarded by (unless noninteractive), so interactive and daemon startup create everything in the same order as before while a bare require stays side-effect-free. Added tests/test-user-constants.el: loading the module creates nothing, and the initializer creates the backbone dirs and the configured files. Updated the module header — top-level side effects are now none and it's safe to load in tests.
-rw-r--r--init.el2
-rw-r--r--modules/user-constants.el35
-rw-r--r--tests/test-user-constants.el81
3 files changed, 102 insertions, 16 deletions
diff --git a/init.el b/init.el
index f65e6085..c0de8b62 100644
--- a/init.el
+++ b/init.el
@@ -21,6 +21,8 @@
(require 'system-lib) ;; low-level system utility functions
(require 'config-utilities) ;; enable for extra Emacs config debug helpers
(require 'user-constants) ;; paths for files referenced in this config
+(unless noninteractive
+ (cj/initialize-user-directories-and-files)) ;; create configured dirs/files on real startup
(require 'host-environment) ;; convenience functions re: host environment
(require 'keyboard-compat) ;; terminal/GUI keyboard compatibility
(require 'system-defaults) ;; native comp; log; unicode, backup, exec path
diff --git a/modules/user-constants.el b/modules/user-constants.el
index 293bc806..02a500d6 100644
--- a/modules/user-constants.el
+++ b/modules/user-constants.el
@@ -5,15 +5,16 @@
;; Layer: 1 (Foundation).
;; Category: F.
;; Load shape: eager.
-;; Eager reason: defines the path constants referenced across the config and
-;; creates the required directories/files before other modules load.
-;; Top-level side effects: file writes — creates configured directories and
-;; stub files via `cj/initialize-user-directories-and-files' at load.
+;; Eager reason: defines the path constants referenced across the config; other
+;; modules read them at their own load time.
+;; Top-level side effects: none — only path definitions. Filesystem creation
+;; lives in `cj/initialize-user-directories-and-files', which init.el calls on
+;; real startup (not at module load), so a bare require is side-effect-free.
;; Runtime requires: none.
-;; Direct test load: conditional (touches the filesystem on load).
+;; Direct test load: yes.
;;
;; This module defines important file and directory paths used throughout the
-;; Emacs configuration, and ensures they exist during startup.
+;; Emacs configuration, and provides a command to create them on startup.
;;
;; WHY THIS EXISTS:
;; 1. Centralizes all path definitions for easy reference and maintenance
@@ -22,7 +23,8 @@
;;
;; The module first defines constants and variables for directories and files,
;; then provides functions that verify their existence, creating them if needed.
-;; This happens automatically when the module loads.
+;; init.el calls `cj/initialize-user-directories-and-files' after requiring this
+;; module so the paths exist before the modules that depend on them load.
;;
;; The paths are designed with a hierarchical structure, allowing child paths
;; to reference their parents (e.g., roam-dir is inside org-dir) for better
@@ -170,12 +172,6 @@ Stored in .emacs.d/data/ so each machine syncs independently from Proton Calenda
"The location of the org file containing DeepSat Calendar information.
Stored in .emacs.d/data/ so each machine syncs independently from Google Calendar.")
-;; Ensure calendar data files exist so org-agenda-list doesn't hang
-;; prompting for missing files (calendar-sync populates them on first sync)
-(dolist (f (list gcal-file pcal-file dcal-file))
- (unless (file-exists-p f)
- (make-empty-file f t)))
-
(defvar reference-file (expand-file-name "reference.org" org-dir)
"The location of the org file containing reference information.")
@@ -241,7 +237,12 @@ and portable across different machines."
video-recordings-dir
audio-recordings-dir
org-dir))
- (mapc 'cj/verify-or-create-file (list schedule-file
+ ;; 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
article-archive
reading-notes-file
@@ -249,8 +250,10 @@ and portable across different machines."
webclipped-file
reference-file)))
-;; Initialize directories and files when this module is loaded
-(cj/initialize-user-directories-and-files)
+;; Creation is deferred to startup: init.el calls
+;; `cj/initialize-user-directories-and-files' after requiring this module, so a
+;; bare `(require 'user-constants)' (tests, byte-compile, batch) stays
+;; side-effect-free.
(provide 'user-constants)
;;; user-constants.el ends here
diff --git a/tests/test-user-constants.el b/tests/test-user-constants.el
new file mode 100644
index 00000000..5246e7ea
--- /dev/null
+++ b/tests/test-user-constants.el
@@ -0,0 +1,81 @@
+;;; test-user-constants.el --- Tests for user-constants path init -*- lexical-binding: t; -*-
+
+;;; Commentary:
+
+;; user-constants defines the config's path constants and creates the
+;; configured directories/files. After the split, loading the module is
+;; side-effect-free: creation happens only when
+;; cj/initialize-user-directories-and-files is called (from init.el on real
+;; startup). These tests sandbox user-emacs-directory and user-home-dir so the
+;; path defconsts resolve into temp dirs, then check that loading creates
+;; nothing, the initializer creates what it should, and the verify-or-create
+;; helpers report required vs optional failures differently.
+
+;;; Code:
+
+(require 'ert)
+(require 'cl-lib)
+
+;; Declare special so the macro's let-bindings are dynamic — the module
+;; `defvar's user-home-dir at load, which errors if the var is lexically bound.
+;; user-emacs-directory is already special (built-in).
+(defvar user-home-dir)
+
+(defconst test-user-constants--repo-root
+ (file-name-directory
+ (directory-file-name
+ (file-name-directory (or load-file-name buffer-file-name))))
+ "Repository root, derived from this file's location under tests/.")
+
+(defun test-user-constants--load ()
+ "Load user-constants.el fresh from the repo."
+ (load (expand-file-name "modules/user-constants.el"
+ test-user-constants--repo-root)
+ nil t))
+
+(defmacro test-user-constants--with-sandbox (&rest body)
+ "Load user-constants with paths redirected to temp dirs, run BODY, restore.
+The path defconsts are recomputed against temp `user-home-dir' /
+`user-emacs-directory' so BODY can create into the sandbox. Afterwards the
+module is reloaded against the real paths so the globals are not left pointing
+at deleted temp directories."
+ (declare (indent 0))
+ `(let ((home (file-name-as-directory (make-temp-file "uc-home-" t)))
+ (emacs (file-name-as-directory (make-temp-file "uc-emacs-" t))))
+ (unwind-protect
+ (let ((user-home-dir home)
+ (user-emacs-directory emacs))
+ (test-user-constants--load)
+ ,@body)
+ (test-user-constants--load)
+ (ignore-errors (delete-directory home t))
+ (ignore-errors (delete-directory emacs t)))))
+
+;;; Loading is side-effect-free
+
+(ert-deftest test-user-constants-loading-creates-no-files ()
+ "Normal: loading the module creates no directories or files.
+The whole point of the split — a bare require must not touch the filesystem."
+ (test-user-constants--with-sandbox
+ (should-not (file-exists-p (expand-file-name "sync" home)))
+ (should-not (file-exists-p (expand-file-name "org" sync-dir)))
+ (should-not (file-exists-p gcal-file))
+ (should-not (file-exists-p schedule-file))))
+
+;;; The initializer creates the configured paths
+
+(ert-deftest test-user-constants-initialize-creates-dirs-and-files ()
+ "Normal: the initializer creates the backbone dirs and the configured files."
+ (test-user-constants--with-sandbox
+ (cj/initialize-user-directories-and-files)
+ (should (file-directory-p sync-dir))
+ (should (file-directory-p org-dir))
+ (should (file-directory-p roam-dir))
+ (should (file-exists-p gcal-file))
+ (should (file-exists-p pcal-file))
+ (should (file-exists-p dcal-file))
+ (should (file-exists-p schedule-file))
+ (should (file-exists-p inbox-file))))
+
+(provide 'test-user-constants)
+;;; test-user-constants.el ends here