Tests/OrgCoreTests/TableFormulaEntryTests.swift
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}