Tests/OrgAppTests/EmacsImportTests.swift
184 lines · 11478 bytes
1import Foundation
2import OrgCore
3import Testing
4@testable import OrgApp
5
6struct EmacsImportTests {
7 static let config = #"""
8 (setq user-full-name "Someone")
9 (setq doom-theme 'doom-homage-black)
10 (setq doom-font (font-spec :family "JetBrains Mono" :size 14))
11 (setq display-line-numbers-type t)
12 (setq org-support-shift-select t)
13 (setq org-directory "~/notes/")
14 (setq org-roam-directory (file-name-concat org-directory "roam"))
15 (after! org
16 (setq org-hide-emphasis-markers t
17 org-pretty-entities t
18 org-agenda-files '("~/notes/" "~/work/plan.org")
19 org-tags-column -80))
20 (setq org-tags-column 4)
21 (setq org-log-done 'note org-log-into-drawer t org-log-reschedule 'time)
22 (setq org-agenda-prefix-format '((agenda . " %i %-12:c%?-12t% s%b") (todo . " %-8:c")))
23 (setq fill-column (+ 70 2))
24 (setq org-capture-templates
25 '(("t" "Todo" entry (file+headline "~/notes/todo.org" "Inbox") "* TODO %?\n%a")
26 ("j" "Journal" entry (file+olp+datetree "~/notes/journal.org") "* %?" :tree-type week :empty-lines 1 :time-prompt t)
27 ("c" "Clocked" item (clock) "%?" :immediate-finish t)
28 ("x" "Function" entry (function ignore) "* %?")
29 ("w" "Work")
30 ("wn" "Work note" plain (file "~/work/notes.org") "%?" :prepend t)))
31 (map! :leader :desc "Report" "z t" #'tr/report
32 :localleader "x" #'org-toggle-checkbox)
33 (map! :n "g s" #'save-buffer)
34 (global-set-key (kbd "C-c a") #'org-agenda)
35 (with-eval-after-load 'org (define-key org-mode-map (kbd "C-c l") (lambda () (interactive))))
36 """#
37
38 static let doomOrg = #"""
39 (defun +org-init-appearance-h ()
40 (setq org-tags-column 0
41 org-agenda-span 10
42 org-agenda-start-day "-3d"
43 org-clock-idle-time 15
44 org-clock-history-length 10
45 org-todo-keywords '((sequence "TODO(t)" "WAIT(w)" "|" "DONE(d)") (sequence "[ ](T)" "|" "[X](D)") (sequence "BUG" "|" "FIXED"))))
46 (after! org (setq org-M-RET-may-split-line nil org-list-allow-alphabetical t))
47 (map! :map org-mode-map "C-c q" #'org-set-tags-command)
48 """#
49
50 func scan() throws -> EmacsImport {
51 let folder = FileManager.default.temporaryDirectory.appendingPathComponent("orgstar-import-\(UUID().uuidString)")
52 try FileManager.default.createDirectory(at: folder, withIntermediateDirectories: true)
53 defer { try? FileManager.default.removeItem(at: folder) }
54 try "(doom! :editor (evil +everywhere) :lang (org +pretty))".write(to: folder.appendingPathComponent("init.el"), atomically: true, encoding: .utf8)
55 try Self.config.write(to: folder.appendingPathComponent("config.el"), atomically: true, encoding: .utf8)
56 let doom = folder.appendingPathComponent("doom-org.el")
57 try Self.doomOrg.write(to: doom, atomically: true, encoding: .utf8)
58 return EmacsImport.scan(EmacsImport.files(in: folder), doomDefaults: [doom])
59 }
60
61 func settings(_ result: EmacsImport) -> [String: TOML.Value] {
62 var out: [String: TOML.Value] = [:]
63 for item in result.items { if case .setting(let key, let value) = item.change { out[key] = value } }
64 return out
65 }
66
67 @Test func readsSettingsWithDoomDefaultsUnderneath() throws {
68 let result = try scan()
69 #expect(result.isDoom)
70 let s = settings(result)
71 #expect(s["keymap"] == .string("doom"))
72 #expect(s["display-line-numbers-type"] == .bool(true))
73 #expect(s["org-hide-emphasis-markers"] == .bool(true))
74 #expect(s["org-pretty-entities"] == .bool(true))
75 #expect(s["org-log-done"] == .string("note"))
76 #expect(s["org-log-reschedule"] == .string("time"))
77 #expect(s["org-log-into-drawer"] == .string("LOGBOOK"))
78 #expect(s["org-agenda-prefix-format.agenda"] == .string(" %i %-12:c%?-12t% s%b"))
79 #expect(s["org-agenda-prefix-format.todo"] == .string(" %-8:c"))
80 // after! org runs after the top-level setq.
81 #expect(s["org-tags-column"] == .integer(-80))
82 #expect(s["org-agenda-span"] == .integer(10))
83 #expect(s["org-agenda-start-day"] == .string("-3d"))
84 #expect(s["org-clock-idle-time"] == .integer(15))
85 #expect(s["org-clock-history-length"] == .integer(10))
86 #expect(s["org-M-RET-may-split-line"] == .bool(false))
87 #expect(s["org-todo-keywords"] == .string("TODO(t) WAIT(w) | DONE(d)\nBUG | FIXED"))
88 #expect(s["fill-column"] == nil)
89 #expect(s["font"] == .string("JetBrains Mono") && s["font-size"] == .integer(14))
90 let folders = result.items.compactMap { if case .folder(let f) = $0.change { f } else { nil } }
91 #expect(folders == ["~/notes/", "~/work"])
92 #expect(result.unreadable.map(\.name) == ["fill-column"])
93 #expect(Set(result.unsupported.map(\.name)) == ["org-support-shift-select", "doom-theme", "org-roam-directory",
94 "capture \"j\"", "capture \"x\"", "SPC z t → tr/report", "C-c l"])
95 #expect(result.skipped == 1)
96 #expect(result.doomDefaultsLeftOut == 1)
97 }
98
99 @Test func readsCaptureTemplatesAndBindings() throws {
100 let result = try scan()
101 let captures = result.items.compactMap { if case .capture(let key, let toml) = $0.change { (key, toml) } else { nil } }
102 #expect(captures.map(\.0) == ["t", "j", "c", "wn"])
103 #expect(captures[1].1 == "[[template]]\nkey = \"j\"\nname = \"Journal\"\ntype = \"entry\"\nfile = \"~/notes/journal.org\"\ndatetree = true\ntree-type = \"week\"\nempty-lines = 1\ntemplate = \"* %?\"\n")
104 #expect(captures[2].1.contains("clock = true\n") && captures[2].1.contains("immediate-finish = true\n"))
105 #expect(result.unsupported.contains { $0.name == "capture \"j\"" && $0.reason == ":time-prompt left out" })
106 #expect(captures[0].1 == "[[template]]\nkey = \"t\"\nname = \"Todo\"\ntype = \"entry\"\nfile = \"~/notes/todo.org\"\nheadline = \"Inbox\"\ntemplate = \"* TODO %?\\n%a\"\n")
107 #expect(captures[3].1.contains("prepend = true\n") && captures[3].1.contains("type = \"plain\""))
108
109 let bindings = result.items.compactMap { if case .binding(let keys, let command, let mode) = $0.change { "\(keys) \(command) \(mode ?? "-")" } else { nil } }
110 #expect(bindings == ["SPC m x org.checkbox.toggle normal", "g s app.save normal", "C-c a app.agenda normal"])
111 #expect(result.unsupported.contains { $0.name == "SPC z t → tr/report" })
112 #expect(result.unsupported.contains { $0.name == "C-c l" && $0.reason == "bound to code, not a command" })
113 }
114
115 @Test func readsCaptureTargetsFromVariablesAndForms() throws {
116 let folder = FileManager.default.temporaryDirectory.appendingPathComponent("orgstar-import-\(UUID().uuidString)")
117 try FileManager.default.createDirectory(at: folder, withIntermediateDirectories: true)
118 defer { try? FileManager.default.removeItem(at: folder) }
119 try #"""
120 (defvar my-inbox "~/old.org")
121 (setq org-directory "~/org"
122 org-default-notes-file (expand-file-name "notes.org" org-directory)
123 my-inbox (concat org-directory "/inbox.org"))
124 (setq org-capture-templates
125 `(("n" "Note" entry (file+headline org-default-notes-file "Notes") "* %?")
126 ("i" "Inbox" entry (file my-inbox) "* TODO %?")
127 ("w" "Work" entry (file ,(concat org-directory "/work.org")) "* %?")
128 ("j" "Journal" entry (file+olp+datetree (concat org-directory "/journal.org")) "* %?")))
129 """#.write(to: folder.appendingPathComponent("init.el"), atomically: true, encoding: .utf8)
130 let result = EmacsImport.scan(EmacsImport.files(in: folder))
131 let files = result.items.compactMap { item -> String? in
132 guard case .capture(let key, let toml) = item.change else { return nil }
133 return key + " " + (toml.firstMatch(of: /file = "([^"]*)"/).map { String($0.1) } ?? "-")
134 }
135 #expect(files == ["n ~/org/notes.org", "i ~/org/inbox.org", "w ~/org/work.org", "j ~/org/journal.org"])
136 }
137
138 /// The user's own Doom configuration, when there is one.
139 @Test(.enabled(if: FileManager.default.fileExists(atPath: NSHomeDirectory() + "/.config/doom/config.el")))
140 func readsTheInstalledDoomConfig() {
141 let result = EmacsImport.scan(EmacsImport.files(in: URL(fileURLWithPath: NSHomeDirectory() + "/.config/doom")))
142 #expect(result.problems.isEmpty, "\(result.problems)")
143 let s = settings(result)
144 #expect(s["keymap"] == .string("doom"))
145 #expect(s["org-agenda-start-day"] == .string("-3d"))
146 #expect(s["org-tags-column"] == .integer(0))
147 let report = result.items.map { "\($0.name) | \($0.summary) | \($0.source)" }
148 + result.unsupported.map { "NO \($0.name): \($0.reason) | \($0.source)" }
149 + result.unreadable.map { "?? \($0.name): \($0.reason) | \($0.source)" }
150 print(report.joined(separator: "\n"))
151 }
152
153 @MainActor
154 @Test func appliesToDefaultsAndFiles() async throws {
155 let result = try scan()
156 let folder = FileManager.default.temporaryDirectory.appendingPathComponent("orgstar-import-out-\(UUID().uuidString)")
157 defer { try? FileManager.default.removeItem(at: folder) }
158 try FileManager.default.createDirectory(at: folder, withIntermediateDirectories: true)
159 try "[[template]]\nkey = \"t\"\nfile = \"x.org\"\ntemplate = \"* %?\"\n".write(to: folder.appendingPathComponent("capture.toml"), atomically: true, encoding: .utf8)
160 let name = "orgstar-import-\(UUID().uuidString)"
161 let defaults = UserDefaults(suiteName: name)!
162 defer { defaults.removePersistentDomain(forName: name) }
163 let summary = await EmacsImport.apply(result.items, defaults: defaults, workspace: nil, directory: folder)
164 #expect(defaults.string(forKey: "keymap") == "doom")
165 #expect(defaults.integer(forKey: "tagsColumn") == -80)
166 #expect(defaults.integer(forKey: "agendaStartOffset") == -3)
167 #expect(defaults.string(forKey: "todoKeywords") == "TODO(t) WAIT(w) | DONE(d)\nBUG | FIXED")
168 let capture = try String(contentsOf: folder.appendingPathComponent("capture.toml"), encoding: .utf8)
169 #expect(capture.components(separatedBy: "[[template]]").count == 5)
170 #expect(CaptureTemplates.load(userFile: folder.appendingPathComponent("capture.toml")).problems.isEmpty)
171 let keymap = folder.appendingPathComponent("keymap.toml")
172 #expect(KeymapLoader.load(.doom, userFile: keymap).problems.isEmpty)
173 #expect(KeymapLoader.load(.doom, userFile: keymap).keymap.keyLabels(for: "app.agenda", modes: ["normal"]).contains("C-c a"))
174 #expect(summary.hasPrefix("Imported 19 settings, 3 capture templates, 3 key bindings."), "\(summary)")
175 #expect(defaults.string(forKey: "themeFont") == "JetBrains Mono")
176 #expect(defaults.integer(forKey: "themeFontSize") == 14)
177
178 // Importing again adds no bindings or templates.
179 let before = try String(contentsOf: keymap, encoding: .utf8)
180 let again = await EmacsImport.apply(result.items, defaults: defaults, workspace: nil, directory: folder)
181 #expect(try String(contentsOf: keymap, encoding: .utf8) == before)
182 #expect(!again.contains("key binding") && !again.contains("capture template"), "\(again)")
183 }
184}