Tests/OrgCoreTests/RefileTests.swift
126 lines · 7564 bytes
6 symbols in this file
1import Foundation
2import Testing
3@testable import OrgCore
4
5struct RefileTests {
6 @Test func withinFile() {
7 let text = "* a\nx\n* b\n** c\n"
8 #expect(runCommand(RefileWithinFile(), text, caret: 0, answers: ["refile": "6"]).text == "* b\n** c\n** a\nx\n")
9 #expect(runCommand(RefileWithinFile(), text, caret: 0, answers: ["refile": "0"]).failure != nil)
10 #expect(runCommand(RefileWithinFile(), "* a\n* b\n", caret: 0, answers: ["refile": ""]).text == "* b\n* a\n")
11 }
12
13 @Test func toAnotherFile() throws {
14 let source = "* a :t:\nbody\n* b\n"
15 let context = EditContext(revision: 0, text: source, tree: OrgParser.parse(source), selection: [0..<0])
16 let result = try #require(Refile.toOtherFile(source: context, target: "* inbox\n", under: 0))
17 #expect(result.target.apply(to: "* inbox\n").hasPrefix("* inbox\n** a"))
18 #expect(result.source.edits.first?.apply(to: source) == "* b\n")
19 }
20
21 static let variants = [
22 "* a\nbody\n* b\n** c\n* d\n",
23 "* a\n\n* b\n\n** c\n\n* d\n",
24 "intro\n* TODO a :x:\n** sub\n* b\n",
25 "* a\n** b\n*** c\n* d",
26 ]
27
28 /// Emacs needs files to refile; each case writes the text to a temporary file and refiles there.
29 static func sameFileForm(_ target: Int?) -> String {
30 """
31 (let* ((f (make-temp-file "orgstar" nil ".org")) (p (point)) s)
32 (write-region (point-min) (point-max) f)
33 (with-current-buffer (find-file-noselect f)
34 (org-fold-show-all) (goto-char p)
35 (org-refile nil nil (list "t" f nil \(target.map { String($0 + 1) } ?? "nil")))
36 (setq p (point) s (buffer-string))
37 (set-buffer-modified-p nil) (kill-buffer))
38 (delete-file f)
39 (erase-buffer) (insert s) (goto-char p))
40 """
41 }
42
43 @Test(.enabled(if: ProcessInfo.processInfo.environment["ORGSTAR_SKIP_ORACLE"] == nil))
44 func refileWithinFileMatchesEmacs() throws {
45 try #require(EmacsOracle.isAvailable)
46 var cases: [EmacsOracle.Case] = []
47 var ours: [(label: String, text: String, caret: Int, failed: Bool)] = []
48 for text in Self.variants {
49 let tree = OrgParser.parse(text)
50 let targets: [Int?] = [nil] + tree.root.descendants().filter { $0.kind == .heading }.map(\.range.lowerBound)
51 for caret in EmacsOracle.positions(text) where entryHeading(at: caret, in: tree) != nil {
52 for target in targets {
53 cases.append(EmacsOracle.Case(text: text, point: EmacsOracle.point(caret, in: text), form: Self.sameFileForm(target)))
54 let result = runCommand(RefileWithinFile(), text, caret: caret, answers: ["refile": target.map(String.init) ?? ""])
55 ours.append(("to \(target.map(String.init) ?? "end") at \(caret) in \(text.debugDescription)", result.text, result.caret, result.failure != nil))
56 }
57 }
58 }
59 var mismatches = 0
60 for (mine, emacs) in zip(ours, try EmacsOracle.run(cases)) {
61 let failed = !emacs.error.isEmpty
62 let caret = EmacsOracle.offset(emacs.point, in: emacs.text)
63 guard mine.failed != failed || (!failed && (mine.text != emacs.text || mine.caret != caret)) else { continue }
64 mismatches += 1
65 if mismatches <= 10 {
66 Issue.record("\(mine.label)\n ours: \(mine.failed ? "failed" : "\(mine.text.debugDescription) @\(mine.caret)")\n emacs: \(failed ? emacs.error : "\(emacs.text.debugDescription) @\(caret)")")
67 }
68 }
69 #expect(mismatches == 0, "\(mismatches) of \(ours.count) differ")
70 }
71
72 static let separator = "\n@@@@\n"
73
74 @Test(.enabled(if: ProcessInfo.processInfo.environment["ORGSTAR_SKIP_ORACLE"] == nil))
75 func refileToAnotherFileMatchesEmacs() throws {
76 try #require(EmacsOracle.isAvailable)
77 let targetTexts = ["* inbox\n", "* p\n** q\n* r :t:\n", "", "text only\n", "* p\n\n* r\n"]
78 var cases: [EmacsOracle.Case] = []
79 var ours: [(label: String, text: String, caret: Int, failed: Bool)] = []
80 for source in Self.variants {
81 let tree = OrgParser.parse(source)
82 for caret in EmacsOracle.positions(source) where entryHeading(at: caret, in: tree) != nil {
83 for targetText in targetTexts {
84 let targetTree = OrgParser.parse(targetText)
85 let headings: [Int?] = [nil] + targetTree.root.descendants().filter { $0.kind == .heading }.map(\.range.lowerBound)
86 for heading in headings {
87 let escaped = targetText.replacingOccurrences(of: "\\", with: "\\\\").replacingOccurrences(of: "\"", with: "\\\"")
88 let form = """
89 (let* ((f1 (make-temp-file "orgstar" nil ".org")) (f2 (make-temp-file "orgstar" nil ".org" "\(escaped)")) (p (point)) s1 s2)
90 (write-region (point-min) (point-max) f1)
91 (let ((b2 (find-file-noselect f2)))
92 (with-current-buffer (find-file-noselect f1)
93 (org-fold-show-all) (goto-char p)
94 (org-refile nil nil (list "t" f2 nil \(heading.map { String($0 + 1) } ?? "nil")))
95 (setq p (point) s1 (buffer-string))
96 (set-buffer-modified-p nil) (kill-buffer))
97 (with-current-buffer b2 (setq s2 (buffer-string)) (set-buffer-modified-p nil) (kill-buffer)))
98 (delete-file f1) (delete-file f2)
99 (erase-buffer) (insert s1 "\(Self.separator.replacingOccurrences(of: "\n", with: "\\n"))" s2) (goto-char p))
100 """
101 cases.append(EmacsOracle.Case(text: source, point: EmacsOracle.point(caret, in: source), form: form))
102 let context = EditContext(revision: 0, text: source, tree: tree, selection: [caret..<caret])
103 if let result = Refile.toOtherFile(source: context, target: targetText, under: heading) {
104 let newSource = result.source.edits.reduce(source) { $1.apply(to: $0) }
105 let caretAfter = result.source.selection?.first?.lowerBound ?? caret
106 ours.append(("to \(heading.map(String.init) ?? "end") of \(targetText.debugDescription) at \(caret) in \(source.debugDescription)", newSource + Self.separator + result.target.apply(to: targetText), caretAfter, false))
107 } else {
108 ours.append(("failed", "", 0, true))
109 }
110 }
111 }
112 }
113 }
114 var mismatches = 0
115 for (mine, emacs) in zip(ours, try EmacsOracle.run(cases)) {
116 let failed = !emacs.error.isEmpty
117 let caret = EmacsOracle.offset(emacs.point, in: emacs.text)
118 guard mine.failed != failed || (!failed && (mine.text != emacs.text || mine.caret != caret)) else { continue }
119 mismatches += 1
120 if mismatches <= 10 {
121 Issue.record("\(mine.label)\n ours: \(mine.failed ? "failed" : "\(mine.text.debugDescription) @\(mine.caret)")\n emacs: \(failed ? emacs.error : "\(emacs.text.debugDescription) @\(caret)")")
122 }
123 }
124 #expect(mismatches == 0, "\(mismatches) of \(ours.count) differ")
125 }
126}