krz/orgstar

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

Tests/OrgCoreTests/BabelCacheTests.swift

778058aafc7ddacee4a613b1da87c60aa0c8e113
orgstar/Tests/OrgCoreTests/BabelCacheTests.swift history · blame · raw

162 lines · 10251 bytes

  1import Foundation
  2import Testing
  3@testable import OrgCore
  4
  5struct BabelCacheTests {
  6    /// A file, and the text before which each run in turn puts the caret.
  7    struct Case {
  8        var text: String
  9        var runs: [String]
 10        /// Replacements made after the runs before the last, as the user editing.
 11        var edits: [(String, String)] = []
 12    }
 13
 14    static let cases: [Case] = [
 15        Case(text: "#+begin_src sh :cache yes\necho hi\n#+end_src\n", runs: ["#+begin_src", "#+begin_src"]),
 16        Case(text: "#+begin_src sh :cache yes\necho there\n#+end_src\n\n#+RESULTS[0000]:\n: old\n\nAfter.\n", runs: ["#+begin_src"]),
 17        Case(text: "#+begin_src sh :cache yes :results append\necho more\n#+end_src\n\n#+RESULTS[0000]:\n: first\n", runs: ["#+begin_src"]),
 18        Case(text: "#+begin_src sh :cache yes\necho new\n#+end_src\n\n#+RESULTS:\n: no hash\n", runs: ["#+begin_src"]),
 19        Case(text: "#+NAME: a\n#+begin_src sh :cache yes\necho named\n#+end_src\n\nText.\n\n#+RESULTS[abc]: a\n: stale\n", runs: ["#+begin_src", "#+begin_src"]),
 20        Case(text: "#+NAME: a\n#+begin_src sh :cache yes\necho named\n#+end_src\n", runs: ["#+begin_src", "#+begin_src"]),
 21        Case(text: "#+begin_src sh :cache yes :var x=5 :var y=\"two words\" :height 400 :width \"400\" :exports both\necho $x $y\n#+end_src\n",
 22             runs: ["#+begin_src", "#+begin_src"]),
 23        Case(text: "#+begin_src sh :cache yes :results output :exports none :wrap example\nprintf 'a\\nb\\n'\n#+end_src\n", runs: ["#+begin_src", "#+begin_src"]),
 24        Case(text: "#+NAME: tbl\n| 1 | x |\n| 2 | y |\n\n#+NAME: items\n- a\n- b\n\n#+begin_src python :cache yes :var t=tbl :var n=1.5 :var l=items\nreturn [len(t), n, l]\n#+end_src\n",
 25             runs: ["#+begin_src", "#+begin_src"]),
 26        Case(text: "#+begin_src emacs-lisp :cache yes :var x=4 :var s=\"q\\\"z\"\n(list x s)\n#+end_src\n", runs: ["#+begin_src", "#+begin_src"]),
 27        Case(text: "#+begin_src emacs-lisp :cache yes :lexical yes :results output\n(princ 7)\n#+end_src\n", runs: ["#+begin_src", "#+begin_src"]),
 28        Case(text: "#+NAME: a\n#+begin_src sh\necho hi\n#+end_src\n\n#+CALL: a() :cache yes\n", runs: ["#+CALL", "#+CALL"]),
 29        Case(text: "#+PROPERTY: header-args :cache yes\n#+begin_src sh :session none\necho prop\n#+end_src\n", runs: ["#+begin_src", "#+begin_src"]),
 30        Case(text: "#+NAME: tbl\n| a | b |\n|---+---|\n| 1 | x |\n\n#+begin_src python :cache yes :var t=tbl :colnames yes :rownames yes\nreturn t\n#+end_src\n",
 31             runs: ["#+begin_src", "#+begin_src"]),
 32        Case(text: "#+NAME: kv\n| a | 1 |\n| b | 2 |\n\n#+begin_src bash :cache yes :var t=kv :var l='(1 2)\necho ${t[a]} ${l[1]}\n#+end_src\n",
 33             runs: ["#+begin_src", "#+begin_src"]),
 34        Case(text: "#+NAME: kv\n| a | 1 |\n| b | 2 |\n\n#+begin_src sh :cache yes :var t=kv :separator ,\necho $t\n#+end_src\n",
 35             runs: ["#+begin_src", "#+begin_src"]),
 36        Case(text: "#+begin_src sh :cache yes :dir (concat \"/\" \"usr\") :foo (list 1 \"a\") :bar (+ 1 2)\npwd\n#+end_src\n",
 37             runs: ["#+begin_src", "#+begin_src"]),
 38        Case(text: "#+NAME: tbl\n| a | b |\n|---+---|\n| 1 | x |\n\n#+begin_src python :cache yes :var t=tbl :colnames '(\"x\" \"y\")\nreturn t\n#+end_src\n",
 39             runs: ["#+begin_src", "#+begin_src"]),
 40        // Edited after the first run, so it runs again.
 41        Case(text: "#+begin_src sh :cache yes\necho one\n#+end_src\n", runs: ["#+begin_src", "#+begin_src"], edits: [("echo one", "echo two")]),
 42        // A reference to a cached block takes its result without running it.
 43        Case(text: "#+begin_src sh :var x=five\necho $((x+1))\n#+end_src\n\n#+NAME: five\n#+begin_src sh :cache yes\necho 5\n#+end_src\n",
 44             runs: ["#+begin_src sh :cache", "#+begin_src sh :var"], edits: [(": 5\n", ": 50\n")]),
 45        Case(text: "#+begin_src sh :var x=five\necho $((x+1))\n#+end_src\n\n#+NAME: five\n#+begin_src sh :cache yes\necho 5\n#+end_src\n\n#+RESULTS[0000]: five\n: 50\n",
 46             runs: ["#+begin_src sh :var"]),
 47    ]
 48
 49    static func caret(_ marker: String, in text: String) -> Int {
 50        (text as NSString).range(of: marker).location + 2
 51    }
 52
 53    @Test(.enabled(if: ProcessInfo.processInfo.environment["ORGSTAR_SKIP_ORACLE"] == nil))
 54    func matchesEmacs() throws {
 55        let folder = FileManager.default.temporaryDirectory
 56        var emacsCases: [EmacsOracle.Case] = []
 57        var ours: [(text: String, failure: String?)] = []
 58        for c in Self.cases {
 59            var text = c.text
 60            var failure: String?
 61            for (i, marker) in c.runs.enumerated() {
 62                if i == c.runs.count - 1, c.runs.count > 1 { for (old, new) in c.edits { text = text.replacingOccurrences(of: old, with: new) } }
 63                (text, failure) = try TestBabelRunner.execute(text, caret: Self.caret(marker, in: text), directory: folder)
 64            }
 65            ours.append((text, failure))
 66            let steps = c.runs.enumerated().map { i, marker in
 67                let edits = i == c.runs.count - 1 && c.runs.count > 1
 68                    ? c.edits.map { "(goto-char (point-min)) (while (search-forward \(Babel.lispString($0.0)) nil t) (replace-match \(Babel.lispString($0.1)) t t))" }.joined(separator: " ")
 69                    : ""
 70                return "\(edits) (goto-char (point-min)) (search-forward \(Babel.lispString(marker))) (goto-char (+ (match-beginning 0) 2)) (org-babel-execute-maybe)"
 71            }
 72            emacsCases.append(EmacsOracle.Case(text: c.text, point: 1, form: """
 73            (progn
 74              (require 'ob-shell)
 75              (require 'ob-python)
 76              (require 'ob-emacs-lisp)
 77              (setq org-confirm-babel-evaluate nil
 78                    org-babel-python-command "python3"
 79                    default-directory temporary-file-directory)
 80              \(steps.joined(separator: "\n  ")))
 81            """))
 82        }
 83        let emacs = try EmacsOracle.run(emacsCases)
 84        for (i, (mine, theirs)) in zip(ours, emacs).enumerated() {
 85            #expect(mine.text == theirs.text, "case \(i):\n\(Self.cases[i].text)\nours (\(mine.failure ?? "")):\n\(mine.text)\nemacs (\(theirs.error)):\n\(theirs.text)")
 86        }
 87    }
 88
 89    @Test func cachedRunSaysSo() throws {
 90        let text = "#+begin_src sh :cache yes\necho hi\n#+end_src\n"
 91        let folder = FileManager.default.temporaryDirectory
 92        let first = try TestBabelRunner.execute(text, caret: 2, directory: folder)
 93        #expect(first.failure == nil)
 94        let second = try TestBabelRunner.execute(first.text, caret: 2, directory: folder)
 95        #expect(second.text == first.text)
 96        #expect(second.failure == "Cached: \"hi\"")
 97    }
 98
 99    @Test(.enabled(if: ProcessInfo.processInfo.environment["ORGSTAR_SKIP_ORACLE"] == nil))
100    func fileLispMatchesEmacs() throws {
101        let text = "#+begin_src sh :cache yes :dir (file-name-directory buffer-file-name) :foo (file-name-nondirectory buffer-file-name)\npwd\n#+end_src\n"
102        let emacs = try EmacsOracle.evaluate(text, """
103            (progn (require 'ob-shell) (goto-char (point-min))
104              (let ((info (org-babel-get-src-block-info)))
105                (setf (nth 2 info) (org-babel-process-params (nth 2 info)))
106                (list buffer-file-name (org-babel-sha1-hash info :eval))))
107            """)
108        let context = EditContext(revision: 0, text: text, tree: OrgParser.parse(text), selection: [2..<2])
109        guard case .success(let first) = Babel.plan(context) else {
110            Issue.record("not planned")
111            return
112        }
113        #expect(first.needsFile)
114        #expect(first.finish.hash == nil)
115        guard case .success(let job) = Babel.replan(first, file: emacs[0], text: text, tree: context.tree) else {
116            Issue.record("not replanned")
117            return
118        }
119        #expect(!job.needsFile)
120        #expect(job.finish.hash == emacs[1])
121        #expect(job.directory == (emacs[0] as NSString).deletingLastPathComponent + "/")
122    }
123
124    /// Languages that don't run hash the body as their `org-babel-expand-body:LANG` writes it.
125    @Test(.enabled(if: ProcessInfo.processInfo.environment["ORGSTAR_SKIP_ORACLE"] == nil))
126    func expandedLanguagesHashLikeEmacs() throws {
127        let texts = [
128            "#+NAME: tbl\n| a | b |\n|---+---|\n| 1 | 2 |\n\n#+begin_src C :cache yes :includes <stdio.h> :defines N 1 :var t=tbl n=2\nprintf(\"%d\", n);\n#+end_src\n",
129            "#+begin_src C++ :cache yes :namespaces std :main no\nint f();\n#+end_src\n",
130            "#+begin_src D :cache yes :var s=\"x\"\nwriteln(s);\n#+end_src\n",
131            "#+begin_src fortran :cache yes :var n=3\nprint *, n\n#+end_src\n",
132            "#+begin_src clojure :cache yes :var n=3\n(+ n 1)\n#+end_src\n",
133        ]
134        for text in texts {
135            let emacs = try EmacsOracle.evaluate(text, """
136                (progn (require 'ob-C) (require 'ob-fortran) (require 'ob-clojure) (setq org-babel-clojure-backend 'babashka)
137                  (goto-char (point-min)) (search-forward "#+begin_src")
138                  (list (org-babel-sha1-hash)))
139                """)
140            let cached = text + "\n#+RESULTS[\(emacs[0])]:\n: 42\n"
141            let caret = (cached as NSString).range(of: "#+begin_src").location + 2
142            let context = EditContext(revision: 0, text: cached, tree: OrgParser.parse(cached), selection: [caret..<caret])
143            guard case .failure(.message(let message)) = Babel.plan(context) else {
144                Issue.record("planned:\n\(text)")
145                continue
146            }
147            #expect(message == "Cached: 42", "\(text)")
148        }
149    }
150
151    @Test func lispHeadersAreRefused() {
152        let text = "#+begin_src sh :cache yes :dir (user-full-name)\necho hi\n#+end_src\n"
153        let context = EditContext(revision: 0, text: text, tree: OrgParser.parse(text), selection: [2..<2])
154        var options = Babel.Options()
155        options.file = "/tmp/a.org"
156        guard case .failure(.message(let message)) = Babel.plan(context, options: options) else {
157            Issue.record("planned")
158            return
159        }
160        #expect(message.hasPrefix(":cache with :dir (user-full-name)"))
161    }
162}