aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-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