Tests/OrgCoreTests/TodoLoggingTests.swift
90 lines · 4969 bytes
1import Foundation
2import Testing
3@testable import OrgCore
4
5struct TodoLoggingTests {
6 static let texts = [
7 "#+TODO: TODO(t) WAIT(w@/!) | DONE(d!) CANCELED(c@)\n* TODO Task\nBody\n",
8 "* TODO Task\nSCHEDULED: <2026-10-05 Mon>\n:PROPERTIES:\n:X: 1\n:END:\nBody\n",
9 "* TODO Task\n:LOGBOOK:\n- State \"TODO\" from [2026-01-01 Thu 10:00]\n:END:\n",
10 "* DONE Task\nCLOSED: [2026-01-01 Thu 10:00]\n",
11 "#+STARTUP: lognotedone logdrawer\n* TODO Task\n",
12 "#+begin_example\n#+STARTUP: lognotedone logdrawer\n#+end_example\n* TODO Task\n",
13 "* TODO Task\n:PROPERTIES:\n:LOGGING: DONE(!) logrepeat\n:LOG_INTO_DRAWER: NOTES\n:END:\n",
14 "* TODO Task\nSCHEDULED: <2026-10-05 Mon +1w>\n",
15 "* TODO Task\n- item one\n",
16 "* TODO Task\n** Child\n",
17 "* TODO Task",
18 ]
19
20 static let options: [(LogHow?, String?)] = [(nil, nil), (.time, nil), (.time, "LOGBOOK"), (.note, nil), (.note, "LOGBOOK")]
21
22 static let note = "Called back\nsecond line"
23
24 @Test(.enabled(if: ProcessInfo.processInfo.environment["ORGSTAR_SKIP_ORACLE"] == nil))
25 func logsLikeOrgTodo() throws {
26 let changes: [(any OrgCommand, String)] = [
27 (TodoCycle(), "(org-todo)"),
28 (SetTodo(keyword: "DONE"), "(org-todo \"DONE\")"),
29 (SetTodo(keyword: "TODO"), "(org-todo \"TODO\")"),
30 (SetTodo(keyword: nil), "(org-todo 'none)"),
31 (SetTodo(keyword: "WAIT"), "(org-todo \"WAIT\")"),
32 (SetTodo(keyword: "CANCELED"), "(org-todo \"CANCELED\")"),
33 ]
34 var utc = Calendar(identifier: .gregorian)
35 utc.timeZone = TimeZone(identifier: "UTC")!
36 let now = Date(timeIntervalSince1970: 1_789_000_000)
37 var cases: [EmacsOracle.Case] = []
38 var ours: [String] = []
39 for text in Self.texts {
40 let heading = (text as NSString).range(of: "Task").location
41 for (done, drawer) in Self.options {
42 let options = EditingOptions(logDone: done, logIntoDrawer: drawer, fastTodoSelection: false)
43 for (command, form) in changes {
44 let lisp = """
45 (let ((b (current-buffer)) (org-use-fast-todo-selection nil))
46 ;; As the user's settings, under the file's #+STARTUP.
47 (setq-default org-log-done \(done.map { "'" + $0.rawValue } ?? "nil") org-log-into-drawer \(drawer.map { "\"\($0)\"" } ?? "nil"))
48 (org-mode)
49 \(form)
50 (run-hooks 'post-command-hook)
51 (when (get-buffer "*Org Note*")
52 (with-current-buffer "*Org Note*" (goto-char (point-max)) (insert \(lispString(Self.note))) (org-store-log-note)))
53 (set-buffer b))
54 """
55 cases.append(EmacsOracle.Case(text: text, point: EmacsOracle.point(heading, in: text), form: frozenClock(1_789_000_000, lisp)))
56 var answers: [String: String] = [:]
57 var result = text
58 while true {
59 let context = EditContext(revision: 0, text: text, tree: OrgParser.parse(text), selection: [heading..<heading], now: now, calendar: utc, answers: answers, options: options)
60 switch command.run(in: context) {
61 case .commit(let r):
62 for edit in r.edits.sorted(by: { $0.range.lowerBound > $1.range.lowerBound }) { result = edit.apply(to: result) }
63 case .prompt(let prompt) where prompt.key == "note" && answers["note"] == nil:
64 answers["note"] = Self.note
65 continue
66 default: break
67 }
68 break
69 }
70 ours.append(result)
71 }
72 }
73 }
74 let emacs = try EmacsOracle.run(cases)
75 for (i, (mine, theirs)) in zip(ours, emacs).enumerated() {
76 #expect(mine == theirs.text, "\(cases[i].form.split(separator: "\n")[1...2].joined()) on \(cases[i].text.debugDescription)\nours: \(mine.debugDescription)\nemacs: \(theirs.text.debugDescription) \(theirs.error)")
77 }
78 }
79
80 @Test func asksForTheNote() {
81 let text = "#+TODO: TODO(t) WAIT(w@/!) | DONE(d!)\n* TODO Task\n"
82 let heading = (text as NSString).range(of: "Task").location
83 let context = EditContext(revision: 0, text: text, tree: OrgParser.parse(text), selection: [heading..<heading])
84 #expect(SetTodo(keyword: "WAIT").run(in: context) == .prompt(Prompt(key: "note", message: "Note for state change from \"TODO\" to \"WAIT\":")))
85 }
86}
87
88func lispString(_ s: String) -> String {
89 "\"" + s.replacingOccurrences(of: "\\", with: "\\\\").replacingOccurrences(of: "\"", with: "\\\"") + "\""
90}