Tests/OrgCoreTests/ClockTableTests.swift
89 lines · 4418 bytes
6 symbols in this file
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}