krz/orgstar

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

Tests/OrgCoreTests/TableFormulaEntryTests.swift

322a87d47526d505d903e05d14e65a5464e1468f
orgstar/Tests/OrgCoreTests/TableFormulaEntryTests.swift history · blame · raw

105 lines · 7385 bytes

  1import Foundation
  2import Testing
  3@testable import OrgCore
  4
  5struct TableFormulaEntryTests {
  6    static let oracle = ProcessInfo.processInfo.environment["ORGSTAR_SKIP_ORACLE"] == nil
  7
  8    @Test(.enabled(if: oracle))
  9    func evaluatesTypedFormulasLikeOrg() throws {
 10        let plain = "| a | b |\n|---+---|\n| 1 | =$1*2 |\n| 3 |   |\nAfter\n"
 11        let named = "| a | b |\n|---+---|\n| 1 | :=$1+10 |\n| 3 |   |\nAfter\n"
 12        let stored = "| a | b | c |\n|---+---+---|\n| 1 | 2 | =$1+$2 |\n| 3 | 4 |   |\n#+TBLFM: @2$1=7::$2=$1*2\n"
 13        let replaced = "| a | b |\n| 1 | =$1*5 |\n#+TBLFM: $2=$1*2::@1$2=x\n"
 14        let reuse = "| a | b |\n| 2 | = |\n#+TBLFM: $2=$1*2\n\nText\n"
 15        let last = "| 1 | =$1+1 |\n"
 16        let marked = "|   | x | y |\n|---+---+---|\n| # | 2 |   |\n|   | 3 |   |\n#+TBLFM: $3=$2*10\n"
 17        let markedTyped = "| # | 2 | =$2+1 | |\n| # | 5 | | |\n#+TBLFM: $4=$2*2\n"
 18        let prompt = { (arg: String) in "(cl-letf (((symbol-function 'read-string) (lambda (&rest _) \"\"))) (org-table-eval-formula \(arg)))" }
 19        let cases: [(String, String, any OrgCommand, [String: String], String)] = [
 20            (plain, "*2", TableCommand(.nextField), [:], "(org-table-next-field)"),
 21            (plain, "*2", TableCommand(.nextRow), [:], "(org-table-next-row)"),
 22            (plain, "*2", CtrlCCtrlC(), [:], "(org-ctrl-c-ctrl-c)"),
 23            (named, "+10", TableCommand(.nextField), [:], "(org-table-next-field)"),
 24            (stored, "+$2", TableCommand(.nextField), [:], "(org-table-next-field)"),
 25            (replaced, "*5", TableCommand(.nextField), [:], "(org-table-next-field)"),
 26            (reuse, "= ", TableCommand(.nextField), [:], "(org-table-next-field)"),
 27            (last, "+1", CtrlCCtrlC(), [:], "(org-ctrl-c-ctrl-c)"),
 28            (stored, "| 3", TableEvalFormula(named: false), ["formula": "$1*3"], "(org-table-eval-formula nil \"$1*3\")"),
 29            (stored, "| 3", TableEvalFormula(named: true), ["formula": "99"], "(org-table-eval-formula '(4) \"99\")"),
 30            (stored, "| 1 | 2", CtrlCCtrlC(), [:], "(org-ctrl-c-ctrl-c)"),
 31            (marked, "| 2", CtrlCCtrlC(), [:], "(org-ctrl-c-ctrl-c)"),
 32            (marked, "| 3", CtrlCCtrlC(), [:], "(org-ctrl-c-ctrl-c)"),
 33            (markedTyped, "+1", CtrlCCtrlC(), [:], "(org-ctrl-c-ctrl-c)"),
 34            (stored, "| 3 | 4", TableEvalFormula(named: false), ["formula": ""], prompt("nil")),
 35            (stored, "| 1", TableEvalFormula(named: true), ["formula": ""], prompt("'(4)")),
 36        ]
 37        let emacs = try EmacsOracle.run(cases.map { c in
 38            let caret = NSMaxRange((c.0 as NSString).range(of: c.1))
 39            return EmacsOracle.Case(text: c.0, point: EmacsOracle.point(caret, in: c.0), form: c.4)
 40        })
 41        for (c, theirs) in zip(cases, emacs) {
 42            let caret = NSMaxRange((c.0 as NSString).range(of: c.1))
 43            let ours = runCommand(c.2, c.0, caret: caret, answers: c.3)
 44            let theirCaret = EmacsOracle.offset(theirs.point, in: theirs.text)
 45            #expect(ours.text == theirs.text && ours.caret == theirCaret && ours.failure == nil,
 46                    "\(c.4) at \(c.1)\nours:  \(ours.text.debugDescription) @\(ours.caret) \(ours.failure ?? "")\nemacs: \(theirs.text.debugDescription) @\(theirCaret) \(theirs.error)")
 47        }
 48    }
 49}
 50
 51extension TableFormulaEntryTests {
 52    @Test(.enabled(if: oracle))
 53    func editsFormulasLikeOrg() throws {
 54        let text = "| a | b | c |\n|---+---+---|\n| 1 | 2 | 3 |\n| 4 | 5 | 6 |\n#+TBLFM: @3$1=7::$3=$1+$2::$>=$1::@2$2=vsum(@2..@3)::$2=$1*2\n"
 55        let inTable = (text as NSString).range(of: "| 5").location + 2
 56        let onTblfm = (text as NSString).range(of: "#+TBLFM").location + 3
 57        for offset in [inTable, onTblfm] {
 58            let point = EmacsOracle.point(offset, in: text)
 59            let emacs = try EmacsOracle.evaluate(text, "(list (with-current-buffer (find-file-noselect \"oracle.org\") (goto-char \(point)) (org-table-edit-formulas) (buffer-substring-no-properties (point-min) (point-max))))")
 60            #expect(BlockEdit.formulas(at: offset, text: text)?.content == emacs.first)
 61        }
 62        let edited = "# Column Formulas\n$2 = $1 * 3\n$3 = $1+\n   $2\n\n@2$2 = 9\n$> = \n"
 63        let cases = [inTable, onTblfm].map { offset in
 64            EmacsOracle.Case(text: text, point: EmacsOracle.point(offset, in: text), form: """
 65                (let ((b (current-buffer)))
 66                  (org-table-edit-formulas) (erase-buffer) (insert \(lispString(edited)))
 67                  (cl-letf (((symbol-function 'set-window-configuration) (lambda (&rest _) (set-buffer b))) ((symbol-function 'select-window) #'ignore)) (org-table-fedit-finish))
 68                  (set-buffer b))
 69                """)
 70        }
 71        for (offset, theirs) in zip([inTable, onTblfm], try EmacsOracle.run(cases)) {
 72            let edit = try #require(BlockEdit.formulas(at: offset, text: text))
 73            let ours = runCommand(ReplaceBlockLines(edit: edit, content: edited), text, caret: offset)
 74            #expect(ours.text == theirs.text, "ours:  \(ours.text.debugDescription) \(ours.failure ?? "")\nemacs: \(theirs.text.debugDescription) \(theirs.error)")
 75        }
 76    }
 77}
 78
 79extension TableFormulaEntryTests {
 80    /// Runs a command that should commit: the new text, the caret and the message.
 81    func committed(_ command: any OrgCommand, _ text: String, after needle: String, answers: [String: String] = [:]) -> (text: String, caret: Int, message: String?)? {
 82        let caret = NSMaxRange((text as NSString).range(of: needle))
 83        let context = EditContext(revision: 0, text: text, tree: OrgParser.parse(text), selection: [caret..<caret], answers: answers)
 84        guard case .commit(let result) = command.run(in: context) else { return nil }
 85        var new = text
 86        for edit in result.edits.sorted(by: { $0.range.lowerBound > $1.range.lowerBound }) { new = edit.apply(to: new) }
 87        let message = result.effects.compactMap { effect -> String? in if case .message(let m) = effect { m } else { nil } }.first
 88        return (new, result.selection?.first?.lowerBound ?? caret, message)
 89    }
 90
 91    @Test func formulaEditsAreKeptWithTheirMessage() throws {
 92        let text = "| a | b |\n|---+---|\n| 1 | 2 |\n#+TBLFM: $2=$1*2\n"
 93        let removed = try #require(committed(TableEvalFormula(named: false), text, after: "| 1 | 2", answers: ["formula": ""]))
 94        #expect(removed.text == "| a | b |\n|---+---|\n| 1 | 2 |\n#+TBLFM: \n" && removed.message == "Formula removed", "\(removed)")
 95        // Text in a field is a Calc symbol, which needs Emacs: the formula is stored and TAB
 96        // still moves on.
 97        let symbolic = "| a | b |\n| x | =$1*2 |\n| y |   |\n"
 98        let tab = try #require(committed(TableCommand(.nextField), symbolic, after: "*2"))
 99        #expect(tab.text == "| a | b     |\n| x | =$1*2 |\n| y |       |\n#+TBLFM: $2=$1*2\n", "\(tab.text.debugDescription)")
100        #expect(tab.caret == (tab.text as NSString).range(of: "| y").location + 2)
101        #expect(tab.message?.hasPrefix("The formula was stored; recalculating it needs Emacs") == true)
102        let prompted = try #require(committed(TableEvalFormula(named: false), symbolic, after: "| x", answers: ["formula": "$1+1"]))
103        #expect(prompted.text.hasSuffix("#+TBLFM: $1=$1+1\n") && prompted.message != nil, "\(prompted)")
104    }
105}