krz/orgstar

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

Tests/OrgCoreTests/ClockTableTests.swift

ab6ccec56eca03d0dadf2c9332aab10c4490eb62
orgstar/Tests/OrgCoreTests/ClockTableTests.swift history · blame · raw

89 lines · 4418 bytes

 1import Foundation
 2import Testing
 3@testable import OrgCore
 4
 5struct ClockTableTests {
 6    static let oracle = ProcessInfo.processInfo.environment["ORGSTAR_SKIP_ORACLE"] == nil
 7
 8    static let body = """
 9        * Project Alpha :work:
10        :PROPERTIES:
11        :Effort:   3:00
12        :END:
13        ** TODO Design the [[https://x.org][interface]] [1/2]
14        :LOGBOOK:
15        CLOCK: [2026-09-07 Mon 09:00]--[2026-09-07 Mon 11:30] =>  2:30
16        CLOCK: [2026-09-09 Wed 23:00]--[2026-09-10 Thu 01:00] =>  2:00
17        :END:
18        *** A very long heading title that goes on and on beyond the narrowing width
19        CLOCK: [2026-09-10 Thu 08:00]--[2026-09-10 Thu 08:45] =>  0:45
20        ** Write code
21        CLOCK: [2026-08-30 Sun 10:00]--[2026-08-30 Sun 12:00] =>  2:00
22        CLOCK: => 1:15
23        * COMMENT Beta
24        CLOCK: [2026-09-08 Tue 14:00]--[2026-09-09 Wed 16:00] => 26:00
25        * Untouched
26
27        """
28
29    static let params = [
30        ":scope file :maxlevel 2", ":maxlevel 3 :emphasize t", ":maxlevel 1", ":block thisweek", ":block lastweek",
31        ":block today :maxlevel 3", ":block 2026-09", ":tstart \"<2026-09-08>\" :tend \"<2026-09-10>\"",
32        ":link t :maxlevel 3", ":narrow 20! :maxlevel 3", ":tags t :level t :maxlevel 3", ":properties (\"Effort\") :formula %",
33        ":indent nil :maxlevel 3", ":block yesterday", ":block 2026-W36 :maxlevel 3", ":fileskip0 t :block 2025",
34        ":maxlevel nil",
35    ]
36
37    static func ours(_ text: String, caret: Int, command: any OrgCommand, answers: [String: String] = [:]) -> String {
38        var utc = Calendar(identifier: .gregorian)
39        utc.timeZone = TimeZone(identifier: "UTC")!
40        let context = EditContext(revision: 0, text: text, tree: OrgParser.parse(text), selection: [caret..<caret],
41                                  now: Date(timeIntervalSince1970: 1_789_000_000), calendar: utc, answers: answers)
42        switch command.run(in: context) {
43        case .commit(let result):
44            var new = text
45            for edit in result.edits.sorted(by: { $0.range.lowerBound > $1.range.lowerBound }) { new = edit.apply(to: new) }
46            return new
47        case .failed(let message): return "failed: \(message)"
48        default: return "other"
49        }
50    }
51
52    @Test(.enabled(if: oracle))
53    func updatesLikeOrgUpdateDblock() throws {
54        var cases: [EmacsOracle.Case] = []
55        var ours: [String] = []
56        for param in Self.params {
57            let text = "#+BEGIN: clocktable \(param)\nold contents\n#+END:\n" + Self.body
58            cases.append(EmacsOracle.Case(text: text, point: 1, form: frozenClock(1_789_000_000, "(org-update-dblock)")))
59            ours.append(Self.ours(text, caret: 0, command: UpdateDynamicBlock()))
60        }
61        for (i, (mine, theirs)) in zip(ours, try EmacsOracle.run(cases)).enumerated() {
62            #expect(mine == theirs.text, "\(Self.params[i])\nours:\n\(mine)\nemacs:\n\(theirs.text) \(theirs.error)")
63        }
64    }
65
66    @Test(.enabled(if: oracle))
67    func reportsLikeOrgClockReport() throws {
68        let text = "Intro\n" + Self.body
69        let ns = text as NSString
70        let points = [ns.range(of: "Intro").location + 2, ns.range(of: "** Write").location + 3, ns.range(of: "CLOCK: [2026-09-07").location]
71        let cases = points.map { EmacsOracle.Case(text: text, point: EmacsOracle.point($0, in: text), form: frozenClock(1_789_000_000, "(org-clock-report)")) }
72        let ours = points.map { Self.ours(text, caret: $0, command: ClockReportTable()) }
73        for (i, (mine, theirs)) in zip(ours, try EmacsOracle.run(cases)).enumerated() {
74            #expect(mine == theirs.text, "at \(points[i])\nours:\n\(mine)\nemacs:\n\(theirs.text) \(theirs.error)")
75        }
76    }
77
78    @Test(.enabled(if: oracle))
79    func setsEffortLikeOrgSetEffort() throws {
80        let text = "* Task\n* Other\n:PROPERTIES:\n:Effort:   1:00\n:END:\n"
81        let cases = [(0, "0:30"), (8, "2:00")].map { EmacsOracle.Case(text: text, point: $0.0 + 1, form: "(org-set-effort nil \"\($0.1)\")") }
82        let ours = [(0, "0:30"), (8, "2:00")].map { Self.ours(text, caret: $0.0, command: SetEffort(), answers: ["effort": $0.1]) }
83        #expect(ours == (try EmacsOracle.run(cases)).map(\.text))
84    }
85
86    @Test func durations() {
87        #expect(ClockTable.duration(0) == "0:00" && ClockTable.duration(95) == "1:35" && ClockTable.duration(1560) == "1d 2:00")
88    }
89}