krz/orgstar

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

Tests/OrgCoreTests/RefileTests.swift

2fa201330f61c801f777baa3f2943d49d8758cda
orgstar/Tests/OrgCoreTests/RefileTests.swift history · blame · raw

126 lines · 7564 bytes

  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}