aboutsummaryrefslogtreecommitdiff
path: root/tests/test-init-module-headers.el
blob: 2680a19c9535f6bf442c301ee4f9d52936aaad8d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
;;; test-init-module-headers.el --- Validate load-graph header contracts -*- lexical-binding: t; -*-

;;; Commentary:
;; Enforces the module load-graph header standard from
;; docs/design/init-load-graph.org against every module that has been
;; classified so far.  Classification proceeds in batches; a module joins
;; `test-init-header--classified-modules' once its header declares the
;; contract.  When that list reaches parity with the modules required by
;; init.el, the project's Phase 1 exit criterion is met.
;;
;; The contract is seven commentary lines after `;;; Commentary:':
;;   Layer:, Category:, Load shape:, Eager reason: (only when eager),
;;   Top-level side effects:, Runtime requires:, Direct test load:.
;;
;; This test reads module files directly; it does not load them, so it adds
;; no startup or package dependencies of its own.

;;; Code:

(require 'ert)
(require 'seq)

(defconst test-init-header--classified-modules
  '(;; Batch 1 — Foundation (Layer 1)
    "system-lib"
    "user-constants"
    "host-environment"
    "system-defaults"
    "keyboard-compat"
    "keybindings"
    "config-utilities"
    ;; Batch 2 — Text/editing command modules (Layer 2)
    "custom-case"
    "custom-comments"
    "custom-datetime"
    "custom-buffer-file"
    "custom-line-paragraph"
    "custom-misc"
    "custom-ordering"
    "custom-text-enclose"
    "custom-whitespace"
    ;; Batch 3 — Core libraries and command modules (Layer 1-3)
    "external-open"
    "media-utils"
    "auth-config"
    "keyboard-macros"
    "system-utils"
    "text-config"
    "undead-buffers"
    ;; Batch 4 — UI / core-UX modules (Layer 2)
    "auto-dim-config"
    "ui-config"
    "ui-theme"
    "ui-navigation"
    "font-config"
    "selection-framework"
    "modeline-config"
    "mousetrap-mode"
    "dashboard-config"
    "nerd-icons-config"
    ;; Batch 5 — Dev entry-points, diff, help, lint, VC (Layer 2)
    "coverage-core"
    "coverage-elisp"
    "dev-fkeys"
    "diff-config"
    "help-config"
    "help-utils"
    "flycheck-config"
    "test-runner"
    "vc-config"
    ;; Batch 6 — Programming modules (Layer 2-4)
    "prog-general"
    "prog-c"
    "prog-go"
    "prog-lisp"
    "prog-python"
    "prog-webdev"
    "prog-json"
    "prog-yaml"
    "prog-shell"
    "prog-training"
    ;; Batch 7 — Org modules (Layer 3-4)
    "org-config"
    "org-agenda-config"
    "org-babel-config"
    "org-capture-config"
    "org-contacts-config"
    "org-drill-config"
    "org-export-config"
    "org-noter-config"
    "org-refile-config"
    "org-reveal-config"
    "org-roam-config"
    "org-webclipper"
    "hugo-config"
    ;; Batch 8 — Domain / integration / optional modules (Layer 2-4)
    "ai-config"
    "ai-term"
    "browser-config"
    "calendar-sync"
    "calibredb-epub-config"
    "chrono-tools"
    "dirvish-config"
    "dwim-shell-config"
    "elfeed-config"
    "erc-config"
    "eshell-config"
    "eww-config"
    "flyspell-and-abbrev"
    "games-config"
    "gloss-config"
    "httpd-config"
    "jumper"
    "latex-config"
    ;; Batch 9 — Remaining domain / integration / optional modules (Layer 2-4)
    "linear-config"
    "local-repository"
    "lorem-optimum"
    "mail-config"
    "markdown-config"
    "music-config"
    "pdf-config"
    "quick-video-capture"
    "reconcile-open-repos"
    "restclient-config"
    "slack-config"
    "system-commands"
    "telega-config"
    "tramp-config"
    "transcription-config"
    "video-audio-recording"
    "term-config"
    "weather-config"
    "wrap-up")
  "Modules annotated with the load-graph header contract.
Grows one batch at a time.  Parity with the init.el require set is the
Phase 1 exit criterion.")

(defconst test-init-header--required-labels
  '("Layer:"
    "Category:"
    "Load shape:"
    "Top-level side effects:"
    "Runtime requires:"
    "Direct test load:")
  "Header labels every classified module must declare.
`Eager reason:' is required additionally, but only when the load shape is
eager; it is checked separately.")

(defun test-init-header--header-text (module)
  "Return MODULE's commentary header text (everything before `;;; Code:')."
  (let ((file (expand-file-name (concat module ".el")
                                (expand-file-name "modules" user-emacs-directory))))
    (with-temp-buffer
      (insert-file-contents file)
      (buffer-substring-no-properties
       (point-min)
       (or (save-excursion
             (goto-char (point-min))
             (when (re-search-forward "^;;; Code:" nil t)
               (match-beginning 0)))
           (point-max))))))

(defun test-init-header--missing-labels (header)
  "Return the list of required labels absent from HEADER.
Includes `Eager reason:' when HEADER declares an eager load shape but
omits the reason."
  (let ((missing
         (seq-remove (lambda (label) (string-match-p (regexp-quote label) header))
                     test-init-header--required-labels)))
    (when (and (string-match-p "Load shape:[ \t]*eager" header)
               (not (string-match-p "Eager reason:" header)))
      (setq missing (append missing '("Eager reason:"))))
    missing))

(ert-deftest test-init-header-classified-modules-declare-contract ()
  "Normal: every classified module declares all required header lines."
  (let (failures)
    (dolist (module test-init-header--classified-modules)
      (let ((missing (test-init-header--missing-labels
                      (test-init-header--header-text module))))
        (when missing
          (push (format "%s missing: %s" module (string-join missing ", "))
                failures))))
    (should-not failures)))

(ert-deftest test-init-header-detects-single-missing-line ()
  "Boundary: a header missing exactly one line is reported by that line's name."
  (let ((header (concat ";; Layer: 1 (Foundation).\n"
                        ";; Category: F.\n"
                        ";; Load shape: command.\n"
                        ";; Top-level side effects: none.\n"
                        ";; Runtime requires: none.\n")))
    ;; Missing only `Direct test load:'.
    (should (equal '("Direct test load:")
                   (test-init-header--missing-labels header)))))

(ert-deftest test-init-header-eager-requires-reason ()
  "Error: an eager load shape with no `Eager reason:' flags the omission."
  (let ((header (concat ";; Layer: 1 (Foundation).\n"
                        ";; Category: F.\n"
                        ";; Load shape: eager.\n"
                        ";; Top-level side effects: none.\n"
                        ";; Runtime requires: none.\n"
                        ";; Direct test load: yes.\n")))
    (should (member "Eager reason:" (test-init-header--missing-labels header)))))

(ert-deftest test-init-header-scoping-only-checks-allowlist ()
  "Error: only allowlisted modules are enforced, and the list stays clean.
A name never added to the allowlist stands in for any not-yet-classified
module: the validator checks allowlist members only. The duplicate guard
keeps the list honest as it grows batch by batch."
  (should-not (member "not-a-classified-module" test-init-header--classified-modules))
  (should (equal (length test-init-header--classified-modules)
                 (length (delete-dups (copy-sequence test-init-header--classified-modules))))))

(provide 'test-init-module-headers)
;;; test-init-module-headers.el ends here