Tests/OrgCoreTests/SubtreeCommandTests.swift
137 lines · 7179 bytes
7 symbols in this file
1import Foundation
2import Testing
3@testable import OrgCore
4
5struct SubtreeCommandTests {
6 static let oracle = ProcessInfo.processInfo.environment["ORGSTAR_SKIP_ORACLE"] == nil
7
8 static let text = """
9 Preamble
10 * Banana [#B] :fruit:
11 SCHEDULED: <2026-10-07 Wed>
12 :PROPERTIES:
13 :SIZE: 3
14 :END:
15 ** Child one
16 ** Child two
17 * TODO apple 10
18 DEADLINE: <2026-10-03 Sat>
19 :LOGBOOK:
20 CLOCK: [2026-10-01 Thu 09:00]--[2026-10-01 Thu 10:30] => 1:30
21 :END:
22 * DONE Cherry 2 [#A]
23 [2026-09-01 Tue]
24 * *Date* 33
25 :PROPERTIES:
26 :SIZE: 1
27 :END:
28 <2026-09-20 Sun>
29 ** NEXT inner
30
31 """
32
33 static func offsets(_ text: String) -> [Int] {
34 let ns = text as NSString
35 var result: [Int] = []
36 var at = 0
37 while at < ns.length {
38 let line = ns.lineRange(for: NSRange(location: at, length: 0))
39 result.append(line.location)
40 result.append(min(line.location + 2, NSMaxRange(line) - 1))
41 at = NSMaxRange(line)
42 }
43 return Array(Set(result)).sorted()
44 }
45
46 static func ours(_ command: any OrgCommand, _ text: String, _ caret: Int, answers: [String: String] = [:]) -> (text: String, caret: Int, copied: String?, failed: Bool) {
47 var utc = Calendar(identifier: .gregorian)
48 utc.timeZone = TimeZone(identifier: "UTC")!
49 let context = EditContext(revision: 0, text: text, tree: OrgParser.parse(text), selection: [caret..<caret],
50 now: Date(timeIntervalSince1970: 1_789_000_000), calendar: utc, answers: answers)
51 guard case .commit(let result) = command.run(in: context) else { return (text, caret, nil, true) }
52 var new = text
53 for edit in result.edits.sorted(by: { $0.range.lowerBound > $1.range.lowerBound }) { new = edit.apply(to: new) }
54 let copied = result.effects.compactMap { if case .copy(let s) = $0 { s } else { nil } }.first
55 return (new, result.selection?.first?.lowerBound ?? caret, copied, false)
56 }
57
58 @Test(.enabled(if: oracle))
59 func cutsAndCopiesLikeOrg() throws {
60 for cut in [true, false] {
61 let offsets = Self.offsets(Self.text).filter { $0 >= 9 }
62 let forms = offsets.map { offset in
63 "(progn (erase-buffer) (insert \(lispString(Self.text))) (goto-char \(EmacsOracle.point(offset, in: Self.text))) (setq kill-ring nil) (org-\(cut ? "cut" : "copy")-subtree) (format \"%s|%d|%s\" (buffer-string) (1- (point)) (car kill-ring)))"
64 }
65 let emacs = try EmacsOracle.evaluate("", "(list \(forms.joined(separator: " ")))")
66 for (offset, expected) in zip(offsets, emacs) {
67 let mine = Self.ours(CopySubtree(cut: cut), Self.text, offset)
68 #expect("\(mine.text)|\(mine.caret)|\(mine.copied ?? "nil")" == expected, "cut \(cut) at \(offset)")
69 }
70 }
71 }
72
73 static let trees = ["* Pasted\nbody\n** Sub\n", "*** Deep\n**** Deeper", "\n** Two\n* One\n"]
74
75 @Test(.enabled(if: oracle))
76 func pastesLikeOrg() throws {
77 let offsets = Self.offsets(Self.text) + [(Self.text as NSString).length]
78 var forms: [String] = []
79 var ours: [String] = []
80 for tree in Self.trees {
81 for offset in offsets {
82 forms.append("(progn (erase-buffer) (insert \(lispString(Self.text))) (goto-char \(EmacsOracle.point(offset, in: Self.text))) (condition-case e (progn (org-paste-subtree nil \(lispString(tree))) (format \"%s|%d\" (buffer-string) (1- (point)))) (error \"error\")))")
83 let mine = Self.ours(PasteSubtree(text: tree), Self.text, offset)
84 ours.append(mine.failed ? "error" : "\(mine.text)|\(EmacsOracle.point(mine.caret, in: mine.text) - 1)")
85 }
86 }
87 let emacs = try EmacsOracle.evaluate("", "(list \(forms.joined(separator: " ")))")
88 for (i, (mine, theirs)) in zip(ours, emacs).enumerated() {
89 #expect(mine == theirs, "case \(i)\nours: \(mine.debugDescription)\nemacs: \(theirs.debugDescription)")
90 }
91 }
92
93 @Test(.enabled(if: oracle))
94 func clonesLikeOrg() throws {
95 let texts = [
96 "* Task\n:PROPERTIES:\n:ID: old\n:END:\nSCHEDULED: <2026-10-05 Mon>\n:LOGBOOK:\nCLOCK: [2026-10-01 Thu 09:00]--[2026-10-01 Thu 10:00] => 1:00\n:END:\n* Next\n",
97 "* Repeat\nSCHEDULED: <2026-10-05 Mon +1w>\n<2026-10-06 Tue 10:00>\n",
98 "* Plain\nbody\n",
99 ]
100 let runs: [(Int, String)] = [(2, "+1d"), (1, ""), (3, "-2w"), (0, "+1m")]
101 var forms: [String] = []
102 var ours: [String] = []
103 for text in texts {
104 for (count, shift) in runs {
105 forms.append("(progn (erase-buffer) (insert \(lispString(text))) (goto-char 3) (cl-letf (((symbol-function 'org-id-new) (lambda (&rest _) \"NEW-ID\")) ((symbol-function 'org-id-add-location) #'ignore)) (org-clone-subtree-with-time-shift \(count) \(lispString(shift)))) (buffer-string))")
106 let answers = ["count": String(count), "shift": shift]
107 ours.append(Self.ours(CloneSubtree(newID: { "NEW-ID" }), text, 2, answers: answers).text)
108 }
109 }
110 let emacs = try EmacsOracle.evaluate("", frozenClock(1_789_000_000, "(list \(forms.joined(separator: " ")))"), prelude: "(require 'org-id) (require 'cl-lib)")
111 for (i, (mine, theirs)) in zip(ours, emacs).enumerated() {
112 #expect(mine == theirs, "case \(i)\nours: \(mine.debugDescription)\nemacs: \(theirs.debugDescription)")
113 }
114 }
115
116 @Test(.enabled(if: oracle))
117 func sortsLikeOrgSortEntries() throws {
118 let keys = ["a", "n", "p", "o", "t", "s", "d", "c", "k", "r", "A", "N", "P", "O", "T", "R"]
119 let starts = [0, (Self.text as NSString).range(of: "* Banana").location, (Self.text as NSString).range(of: "* *Date*").location]
120 var forms: [String] = []
121 var ours: [String] = []
122 for start in starts {
123 for key in keys {
124 let property = key.lowercased() == "r" ? " nil nil \"SIZE\"" : ""
125 forms.append("(progn (erase-buffer) (insert \(lispString(Self.text))) (goto-char \(EmacsOracle.point(start, in: Self.text))) (condition-case e (progn (org-sort-entries nil ?\(key)\(property)) (format \"%s|%d\" (buffer-string) (1- (point)))) (error (error-message-string e))))")
126 var answers = ["sort": key]
127 if key.lowercased() == "r" { answers["property"] = "SIZE" }
128 let mine = Self.ours(SortEntries(), Self.text, start, answers: answers)
129 ours.append(mine.failed ? "failed" : "\(mine.text)|\(EmacsOracle.point(mine.caret, in: mine.text) - 1)")
130 }
131 }
132 let emacs = try EmacsOracle.evaluate("", frozenClock(1_789_000_000, "(list \(forms.joined(separator: " ")))"))
133 for (i, (mine, theirs)) in zip(ours, emacs).enumerated() {
134 #expect(mine == theirs, "case \(i) key \(keys[i % keys.count])\nours: \(mine.debugDescription)\nemacs: \(theirs.debugDescription)")
135 }
136 }
137}