aboutsummaryrefslogtreecommitdiff
path: root/tests/test-init-module-headers.el
blob: 7ab99448a57baf672458f16176730e1810e217b8 (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
;;; 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)
    "ui-config"
    "ui-theme"
    "ui-navigation"
    "font-config"
    "selection-framework"
    "modeline-config"
    "mousetrap-mode"
    "popper-config"
    "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")
  "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-excludes-unclassified ()
  "Error: an unclassified module is not enforced (scoping proof)."
  ;; games-config is required by init.el but not yet classified; it must be
  ;; absent from the allowlist so the suite stays green during migration.
  (should-not (member "games-config" test-init-header--classified-modules)))

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