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