krz/orgstar

A native macOS editor for org-mode files. editor org-mode swift

Tests/OrgCoreTests/DateCommandTests.swift

main
orgstar/Tests/OrgCoreTests/DateCommandTests.swift history · blame · raw

84 lines · 5241 bytes

5 symbols in this file
 1import Foundation
 2import Testing
 3@testable import OrgCore
 4
 5struct DateCommandTests {
 6    static let oracle = ProcessInfo.processInfo.environment["ORGSTAR_SKIP_ORACLE"] == nil
 7    static let seconds = 1_789_000_000
 8
 9    static func run(_ command: any OrgCommand, _ text: String, caret: Int, answers: [String: String], options: EditingOptions = .org, lastCommand: String? = nil) -> (text: String, message: String?) {
10        var utc = Calendar(identifier: .gregorian)
11        utc.timeZone = TimeZone(identifier: "UTC")!
12        var answers = answers
13        while true {
14            let context = EditContext(revision: 0, text: text, tree: OrgParser.parse(text), selection: [caret..<caret], now: Date(timeIntervalSince1970: Double(seconds)),
15                                      calendar: utc, answers: answers, options: options, lastCommand: lastCommand)
16            switch command.run(in: context) {
17            case .commit(let result):
18                var new = text
19                for edit in result.edits.sorted(by: { $0.range.lowerBound > $1.range.lowerBound }) { new = edit.apply(to: new) }
20                let message = result.effects.compactMap { if case .message(let m) = $0 { m } else { nil } }.first
21                return (new, message)
22            case .prompt(let prompt) where prompt.key == "note" && answers["note"] == nil:
23                answers["note"] = "moved"
24            case .failed(let message): return (text, "failed: \(message)")
25            default: return (text, "other")
26            }
27        }
28    }
29
30    @Test(.enabled(if: oracle))
31    func logsReschedulesLikeOrg() throws {
32        let texts = ["* Task\nSCHEDULED: <2026-10-05 Mon>\n", "* Task\nDEADLINE: <2026-10-05 Mon> SCHEDULED: <2026-10-01 Thu>\nbody\n", "* Task\n"]
33        let actions: [(any OrgCommand, String, [String: String])] = [
34            (SetPlanning(deadline: false), "(org-schedule nil \"2026-10-10\")", ["date": "2026-10-10"]),
35            (SetPlanning(deadline: true), "(org-deadline nil \"2026-10-12\")", ["date": "2026-10-12"]),
36            (RemovePlanning(deadline: false), "(org-schedule '(4))", [:]),
37            (RemovePlanning(deadline: true), "(org-deadline '(4))", [:]),
38        ]
39        for (how, drawer) in [(LogHow.time, nil as String?), (.note, "LOGBOOK")] {
40            var cases: [EmacsOracle.Case] = []
41            var ours: [String] = []
42            for text in texts {
43                for (command, form, answers) in actions {
44                    let lisp = """
45                        (let ((org-log-reschedule '\(how.rawValue)) (org-log-redeadline '\(how.rawValue)) (org-log-into-drawer \(drawer.map { "\"\($0)\"" } ?? "nil")) (b (current-buffer)))
46                          \(form)
47                          (run-hooks 'post-command-hook)
48                          (when (get-buffer "*Org Note*")
49                            (with-current-buffer "*Org Note*" (goto-char (point-max)) (insert "moved") (org-store-log-note)))
50                          (set-buffer b))
51                        """
52                    cases.append(EmacsOracle.Case(text: text, point: 3, form: frozenClock(Self.seconds, lisp)))
53                    let options = EditingOptions(logIntoDrawer: drawer, logReschedule: how, logRedeadline: how)
54                    ours.append(Self.run(command, text, caret: 2, answers: answers, options: options).text)
55                }
56            }
57            for (i, (mine, theirs)) in zip(ours, try EmacsOracle.run(cases)).enumerated() {
58                #expect(mine == theirs.text, "\(how) \(cases[i].form.split(separator: "\n")[2]) on \(cases[i].text.debugDescription)\nours:  \(mine.debugDescription)\nemacs: \(theirs.text.debugDescription) \(theirs.error)")
59            }
60        }
61    }
62
63    @Test(.enabled(if: oracle))
64    func insertsRangesLikeOrgTimestamp() throws {
65        let text = "Meet <2026-10-05 Mon> then\n"
66        let caret = (text as NSString).range(of: " then").location
67        let emacs = try EmacsOracle.run([EmacsOracle.Case(text: text, point: EmacsOracle.point(caret, in: text), form: frozenClock(Self.seconds, """
68            (let ((last-command 'org-timestamp) (this-command 'org-timestamp) (ord (symbol-function 'org-read-date)))
69              (cl-letf (((symbol-function 'org-read-date) (lambda (&optional with-time to-time _from prompt &rest _) (funcall ord with-time to-time "+2d" prompt))))
70                (org-timestamp nil)))
71            """))])
72        let ours = Self.run(InsertTimestamp(), text, caret: caret, answers: ["date": "+2d"], lastCommand: "org.timestamp.active")
73        #expect(ours.text == emacs[0].text)
74    }
75
76    @Test(.enabled(if: oracle))
77    func evaluatesRangesLikeOrg() throws {
78        let lines = ["<2026-10-05 Mon>--<2026-10-08 Thu>", "[2026-10-05 Mon 10:00]--[2026-10-07 Wed 12:30]", "x <2026-10-05 Mon 10:00>--<2026-10-05 Mon 10:45> y", "<2026-10-05 Mon>--<2026-10-05 Mon>"]
79        let forms = lines.map { "(progn (erase-buffer) (insert \(lispString($0))) (goto-char 1) (org-evaluate-time-range))" }
80        let emacs = try EmacsOracle.evaluate("", "(list \(forms.joined(separator: " ")))")
81        let ours = lines.map { Self.run(EvaluateTimeRange(), $0, caret: 0, answers: [:]).message ?? "nil" }
82        #expect(ours == emacs)
83    }
84}