Tests/OrgCoreTests/ToggleCommandTests.swift
101 lines · 4828 bytes
1import Foundation
2import Testing
3@testable import OrgCore
4
5struct ToggleCommandTests {
6 static let oracle = ProcessInfo.processInfo.environment["ORGSTAR_SKIP_ORACLE"] == nil
7
8 static let text = """
9 Intro line
10 # a comment
11 * TODO [#A] Task :tag:
12 SCHEDULED: <2026-10-05 Mon>
13 :PROPERTIES:
14 :X: 1
15 :END:
16 Body text
17 indented body
18 - plain item
19 - [ ] open box
20 - [X] done box
21 - nested item
22 ** COMMENT Commented
23 *** Deep
24 1. first
25 2) second
26 * DONE Closed one
27 last
28
29 """
30
31 /// Runs `command` with `selection` (a caret when empty) and returns the text and caret.
32 static func ours(_ command: any OrgCommand, _ text: String, _ selection: Range<Int>) -> (String, Int) {
33 let context = EditContext(revision: 0, text: text, tree: OrgParser.parse(text), selection: [selection], options: .org)
34 guard case .commit(let result) = command.run(in: context) else { return (text, selection.upperBound) }
35 var new = text
36 for edit in result.edits.sorted(by: { $0.range.lowerBound > $1.range.lowerBound }) { new = edit.apply(to: new) }
37 return (new, result.selection?.first?.lowerBound ?? selection.upperBound)
38 }
39
40 static func compare(_ command: any OrgCommand, _ form: String, regions: [(String, String)] = []) throws {
41 let text = Self.text
42 let ns = text as NSString
43 var cases: [EmacsOracle.Case] = []
44 var mine: [(String, Int)] = []
45 var labels: [String] = []
46 // Each line's start and a point inside it.
47 var lineStart = 0
48 while lineStart < ns.length {
49 let line = ns.lineRange(for: NSRange(location: lineStart, length: 0))
50 for offset in Set([line.location, min(line.location + 3, NSMaxRange(line) - 1)]).sorted() {
51 cases.append(EmacsOracle.Case(text: text, point: EmacsOracle.point(offset, in: text), form: form))
52 mine.append(ours(command, text, offset..<offset))
53 labels.append("at \(offset)")
54 }
55 lineStart = NSMaxRange(line)
56 }
57 for (from, to) in regions {
58 let start = ns.range(of: from).location
59 let end = NSMaxRange(ns.range(of: to))
60 cases.append(EmacsOracle.Case(text: text, point: EmacsOracle.point(end, in: text), form: "(progn (transient-mark-mode 1) (set-mark \(EmacsOracle.point(start, in: text))) (activate-mark) \(form))"))
61 mine.append(ours(command, text, start..<end))
62 labels.append("region \(from.debugDescription)…\(to.debugDescription)")
63 }
64 let emacs = try EmacsOracle.run(cases)
65 for (i, (ours, theirs)) in zip(mine, emacs).enumerated() {
66 let expectedText = theirs.error.isEmpty ? theirs.text : text
67 #expect(ours.0 == expectedText, "\(form) \(labels[i])\nours: \(ours.0.debugDescription)\nemacs: \(theirs.text.debugDescription) \(theirs.error)")
68 if ours.0 == expectedText, theirs.error.isEmpty {
69 #expect(ours.1 == EmacsOracle.offset(theirs.point, in: theirs.text), "\(form) \(labels[i]) caret")
70 }
71 }
72 }
73
74 @Test(.enabled(if: oracle))
75 func commentsLikeOrg() throws {
76 try Self.compare(ToggleComment(), "(org-toggle-comment)")
77 }
78
79 @Test(.enabled(if: oracle))
80 func togglesHeadingsLikeOrg() throws {
81 try Self.compare(ToggleHeading(), "(org-toggle-heading)", regions: [("Body text", "indented body"), ("* TODO", "Body"), ("1. first", "2) second"), ("Intro", "comment")])
82 }
83
84 @Test(.enabled(if: oracle))
85 func togglesItemsLikeOrgCtrlCMinus() throws {
86 try Self.compare(ToggleItem(), "(org-ctrl-c-minus)", regions: [("Body text", "indented body"), ("- plain", "open box"), ("** COMMENT", "Deep"), ("* DONE", "last")])
87 }
88
89 @Test(.enabled(if: oracle))
90 func marksSubtreesLikeOrg() throws {
91 let text = Self.text
92 let offsets = [(text as NSString).range(of: "Body").location, (text as NSString).range(of: "*** Deep").location + 2]
93 let emacs = try EmacsOracle.evaluate(text, "(mapcar (lambda (p) (goto-char p) (transient-mark-mode 1) (org-mark-subtree) (format \"%d %d\" (1- (region-beginning)) (1- (region-end)))) '(\(offsets.map { String(EmacsOracle.point($0, in: text)) }.joined(separator: " "))))")
94 let ours = offsets.map { offset -> String in
95 let context = EditContext(revision: 0, text: text, tree: OrgParser.parse(text), selection: [offset..<offset])
96 guard case .commit(let result) = MarkSubtree().run(in: context), let range = result.selection?.first else { return "none" }
97 return "\(range.lowerBound) \(range.upperBound)"
98 }
99 #expect(ours == emacs)
100 }
101}