Tests/OrgCoreTests/BabelTests.swift
484 lines · 30842 bytes
1import Foundation
2import Testing
3@testable import OrgCore
4
5/// Runs a Babel job the way the app does, for tests.
6struct NativeUnsupported: Error {}
7
8enum TestBabelRunner {
9 static func run(_ job: BabelJob, directory: URL) throws -> BabelOutput {
10 let folder = FileManager.default.temporaryDirectory.appendingPathComponent("orgstar-babel-\(UUID().uuidString)")
11 try FileManager.default.createDirectory(at: folder, withIntermediateDirectories: true)
12 defer { try? FileManager.default.removeItem(at: folder) }
13 let resultFile = folder.appendingPathComponent("result")
14 let input = job.input.replacingOccurrences(of: Babel.resultFileToken, with: resultFile.path)
15 let process = Process()
16 process.executableURL = URL(fileURLWithPath: "/usr/bin/env")
17 switch job.runner {
18 case .process(let executable, var arguments):
19 if let script = job.script {
20 let file = folder.appendingPathComponent("script")
21 try script.write(to: file, atomically: true, encoding: .utf8)
22 try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: file.path)
23 arguments = arguments.map { $0.replacingOccurrences(of: Babel.scriptFileToken, with: file.path) }
24 }
25 process.arguments = [executable] + arguments
26 case .emacs:
27 let script = folder.appendingPathComponent("babel.el")
28 try input.write(to: script, atomically: true, encoding: .utf8)
29 process.arguments = [EmacsOracle.executable, "-Q", "--batch", "-l", script.path]
30 case .session:
31 return BabelOutput(stdout: "")
32 }
33 process.currentDirectoryURL = job.directory.map { URL(fileURLWithPath: $0, relativeTo: directory).standardizedFileURL } ?? directory
34 process.environment = ProcessInfo.processInfo.environment.merging(["PWD": process.currentDirectoryURL!.path]) { $1 }
35 let stdin = Pipe()
36 let stdout = Pipe()
37 process.standardInput = stdin
38 process.standardOutput = stdout
39 process.standardError = FileHandle.nullDevice
40 try process.run()
41 if case .process = job.runner { stdin.fileHandleForWriting.write(Data(input.utf8)) }
42 try stdin.fileHandleForWriting.close()
43 let data = stdout.fileHandleForReading.readDataToEndOfFile()
44 process.waitUntilExit()
45 return BabelOutput(stdout: String(decoding: data, as: UTF8.self), resultFile: try? String(contentsOf: resultFile, encoding: .utf8))
46 }
47
48 /// Plans, runs and finishes the block at `caret`: the new text, or the failure.
49 /// `native` runs Emacs Lisp blocks with `Babel.evaluateNatively` instead of Emacs.
50 static func execute(_ text: String, caret: Int, directory: URL, native: Bool = false) throws -> (text: String, failure: String?) {
51 func run(_ job: BabelJob, directory: URL) throws -> BabelOutput {
52 guard native else { return try TestBabelRunner.run(job, directory: directory) }
53 guard let output = try Babel.evaluateNatively(job) else { throw NativeUnsupported() }
54 return output
55 }
56 let context = EditContext(revision: 0, text: text, tree: OrgParser.parse(text), selection: [caret..<caret])
57 switch Babel.plan(context) {
58 case .failure(.message(let message)): return (text, message)
59 case .success(var job):
60 while let need = job.needs.first {
61 switch Babel.resume(job, need: need, output: try run(need.job, directory: directory), text: text, tree: context.tree) {
62 case .failure(.message(let message)): return (text, message)
63 case .success(let next): job = next
64 }
65 }
66 let output: BabelOutput
67 do {
68 output = try run(job, directory: directory)
69 } catch let failure as Babel.Failure {
70 guard case .message(let message) = failure else { return (text, nil) }
71 return (text, message)
72 }
73 if let file = Babel.fileResult(job, output: output) {
74 try file.contents.write(to: directory.appendingPathComponent(file.path), atomically: true, encoding: .utf8)
75 }
76 switch Babel.finish(job, output: output, text: text) {
77 case .failure(.message(let message)): return (text, message)
78 case .success(let edits):
79 var new = text
80 for edit in edits.sorted(by: { $0.range.lowerBound > $1.range.lowerBound }) { new = edit.apply(to: new) }
81 return (new, nil)
82 }
83 }
84 }
85}
86
87struct BabelTests {
88 static let cases: [String] = rawCases.map {
89 $0.replacingOccurrences(of: "TBL#+begin_src", with: "#+NAME: tbl\n| a | b |\n|---+---|\n| 1 | x |\n| 2 | y |\n\n#+begin_src")
90 .replacingOccurrences(of: "MULTI#+begin_src", with: "#+NAME: multi\n|---+---|\n| a | b |\n|---+---|\n| 1 | x |\n|---+---|\n| 2 | y |\n\n#+begin_src")
91 }
92
93 static let rawCases: [String] = [
94 "#+begin_src sh\necho hello\n#+end_src\n",
95 "#+begin_src sh\necho hello\n#+end_src\nNext paragraph.\n",
96 "#+begin_src sh\necho hello\n#+end_src\n\nNext paragraph.\n",
97 "#+begin_src sh\necho hello\n#+end_src",
98 "#+begin_src sh\necho one; echo two\n#+end_src\n",
99 "#+begin_src sh\nprintf 'a b c\\nd e f\\n'\n#+end_src\n",
100 "#+begin_src sh\nprintf 'a\\tb\\nc\\td\\n'\n#+end_src\n",
101 "#+begin_src sh\nprintf '1,2\\n3,4\\n'\n#+end_src\n",
102 "#+begin_src sh :results output\nprintf 'a b c\\nd e f\\n'\n#+end_src\n",
103 "#+begin_src sh :results output\nseq 1 12\n#+end_src\n",
104 "#+begin_src sh :results output\nprintf '* not a heading\\n#+not a keyword\\n'\nseq 1 9\n#+end_src\n",
105 "#+begin_src sh :results value\necho hi\n#+end_src\n",
106 "#+begin_src sh :results verbatim\necho 007\n#+end_src\n",
107 "#+begin_src sh\necho 007\n#+end_src\n",
108 "#+begin_src sh :results raw\necho '*bold*'\n#+end_src\n",
109 "#+begin_src sh :results drawer\necho in a drawer\n#+end_src\n",
110 "#+begin_src sh :results list\nprintf 'a\\nb\\n'\n#+end_src\n",
111 "#+begin_src sh :results output list\nprintf 'a\\nb\\n'\n#+end_src\n",
112 "#+begin_src sh :results silent\necho quiet\n#+end_src\n",
113 "#+begin_src sh :results output code\necho 'x=1'\n#+end_src\n",
114 "#+begin_src sh :wrap example\necho wrapped\n#+end_src\n",
115 "#+begin_src sh\necho new\n#+end_src\n\n#+RESULTS:\n: old\n\nAfter.\n",
116 "#+begin_src sh\necho new\n#+end_src\n#+RESULTS:\n| old | table |\n| a | b |\nAfter.\n",
117 "#+begin_src sh :results append\necho more\n#+end_src\n\n#+RESULTS:\n: first\n",
118 "#+NAME: greet\n#+begin_src sh\necho named\n#+end_src\n\nText.\n\n#+RESULTS: greet\n: stale\n",
119 "#+NAME: fresh\n#+begin_src sh\necho named\n#+end_src\n",
120 "- item\n #+begin_src sh\n echo indented\n #+end_src\n",
121 "#+begin_src sh :var x=5 :var y=\"two words\"\necho $x $y\n#+end_src\n",
122 "#+begin_src bash\necho ${BASH_VERSION:+bash}\n#+end_src\n",
123 "#+begin_src sh :results output\n#+end_src\n",
124 "#+begin_src sh\nprintf ''\n#+end_src\n",
125 "#+begin_src python\nreturn 42\n#+end_src\n",
126 "#+begin_src python\nreturn 'hello'\n#+end_src\n",
127 "#+begin_src python\nreturn [[1, 2], [3, 4]]\n#+end_src\n",
128 "#+begin_src python\nreturn [1, 'a', 2.5]\n#+end_src\n",
129 "#+begin_src python\nreturn 1/3\n#+end_src\n",
130 "#+begin_src python\nx = 2\nif x:\n return x * 21\n#+end_src\n",
131 "#+begin_src python :results output\nprint('one')\nprint('two')\n#+end_src\n",
132 "#+begin_src python :results output\nfor i in range(12): print(i)\n#+end_src\n",
133 "#+begin_src python :var n=3 :var s=\"hi\"\nreturn s * n\n#+end_src\n",
134 "#+NAME: nums\n| 1 | 2 |\n| 3 | 4 |\n\n#+begin_src python :var t=nums\nreturn [[c * 10 for c in r] for r in t]\n#+end_src\n",
135 "#+begin_src python :results list\nreturn ['a', 'b']\n#+end_src\n",
136 "#+begin_src python :results verbatim\nreturn [1, 2]\n#+end_src\n",
137 "#+begin_src python\ns = '''multi\n line'''\nreturn s\n#+end_src\n",
138 "#+begin_src python\nreturn None\n#+end_src\n",
139 "#+begin_src emacs-lisp\n(+ 1 2)\n#+end_src\n",
140 "#+begin_src emacs-lisp\n'((1 2) (3 4))\n#+end_src\n",
141 "#+begin_src elisp\n(concat \"a\" \"b\")\n#+end_src\n",
142 "#+begin_src emacs-lisp :results verbatim\n\"quoted\"\n#+end_src\n",
143 "#+begin_src emacs-lisp :results output\n(princ \"printed\")\n#+end_src\n",
144 "#+begin_src emacs-lisp :var x=4\n(* x x)\n#+end_src\n",
145 "#+PROPERTY: header-args :results output\n#+begin_src sh\nprintf 'a b\\nc d\\n'\n#+end_src\n",
146 "* Heading\n:PROPERTIES:\n:header-args:sh: :results verbatim\n:END:\n#+begin_src sh\necho 1 2\n#+end_src\n",
147 "* Heading\n:PROPERTIES:\n:header-args:sh: :results verbatim\n:END:\n#+begin_src python\nreturn [[1, 2]]\n#+end_src\n",
148 "#+HEADER: :results output\n#+begin_src sh\nprintf 'x y\\nz w\\n'\n#+end_src\n",
149 // `:cmdline`, `:stdin` and `:shebang`.
150 "#+begin_src sh :cmdline one \"two words\" :results output\nfor a in \"$@\"; do echo \"[$a]\"; done\n#+end_src\n",
151 "#+begin_src sh :cmdline 3\necho $(( $1 * 2 ))\n#+end_src\n",
152 "#+NAME: rows\n| a | 1 |\n| b | 2 |\n\n#+begin_src sh :stdin rows :results output\nwhile IFS=$(printf '\\t') read -r k v; do echo \"$k=$v\"; done\n#+end_src\n",
153 "#+NAME: items\n- x\n- y\n\n#+begin_src sh :stdin items\nwc -l | tr -d ' '\n#+end_src\n",
154 "#+begin_src bash :shebang #!/bin/bash :results output\necho ${BASH_VERSION:+bash}\n#+end_src\n",
155 "#+begin_src sh :cmdline a b :padline no :var x=5 :results output\necho $x $#\n#+end_src\n",
156 "#+NAME: in\n| hello |\n\n#+begin_src sh :results value :stdin in\ncat\nfalse\n#+end_src\n",
157 "#+begin_src sh :results value :cmdline x\necho one\n#+end_src\n",
158 "#+begin_src bash :shebang #!/bin/bash :cmdline \"a b\" c :results output\necho $#\n#+end_src\n",
159 "#+begin_src sh :stdin src :results output\ntr a-z A-Z\n#+end_src\n\n#+NAME: src\n#+begin_src sh\necho hello\n#+end_src\n",
160 // `:stdin` and `:cmdline` run a script rather than the session.
161 "#+begin_src sh :session s1 :cmdline a b :results output\necho $# $1\n#+end_src\n",
162 "#+NAME: in\n| hello |\n\n#+begin_src sh :session s2 :stdin in :results output\ncat\n#+end_src\n",
163 "#+begin_src bash :session s3 :shebang #!/bin/bash :cmdline x :var v=3\necho $v $1 ${BASH_VERSION:+bash}\n#+end_src\n",
164 // `:var` from other blocks' results.
165 "#+begin_src sh :var x=five\necho $((x+1))\n#+end_src\n\n#+NAME: five\n#+begin_src sh\necho 5\n#+end_src\n",
166 "#+begin_src python :var t=tbl\nreturn [len(t), t[1][0]]\n#+end_src\n\n#+NAME: tbl\n#+begin_src python\nreturn [[1, 2], [3, 4]]\n#+end_src\n",
167 "#+begin_src sh :var x=double(n=4)\necho $x\n#+end_src\n\n#+NAME: double\n#+begin_src sh :var n=2\necho $((n*2))\n#+end_src\n",
168 "#+begin_src python :var a=outer\nreturn a * 2\n#+end_src\n\n#+NAME: outer\n#+begin_src python :var b=inner\nreturn b + 1\n#+end_src\n\n#+NAME: inner\n#+begin_src python\nreturn 20\n#+end_src\n",
169 "#+begin_src emacs-lisp :var s=greet\n(concat s \"!\")\n#+end_src\n\n#+NAME: greet\n#+begin_src emacs-lisp\n\"hi\"\n#+end_src\n",
170 "#+begin_src python :var s=words\nreturn s.upper()\n#+end_src\n\n#+NAME: words\n#+begin_src sh :results output\nprintf 'two words'\n#+end_src\n",
171 "#+begin_src emacs-lisp :var rows=pairs\n(length rows)\n#+end_src\n\n#+NAME: pairs\n#+begin_src sh\nprintf 'a 1\\nb 2\\nc 3\\n'\n#+end_src\n",
172 // Tables with rules, `:colnames`, `:rownames` and `:hlines`.
173 "TBL#+begin_src python :var t=tbl\nreturn t\n#+end_src\n",
174 "TBL#+begin_src python :var t=tbl :colnames yes\nreturn t\n#+end_src\n",
175 "TBL#+begin_src python :var t=tbl :colnames no\nreturn t\n#+end_src\n",
176 "TBL#+begin_src python :var t=tbl :rownames yes\nreturn t\n#+end_src\n",
177 "TBL#+begin_src python :var t=tbl :rownames yes :colnames yes\nreturn [[c * 2 for c in r] for r in t]\n#+end_src\n",
178 "TBL#+begin_src python :var t=tbl :colnames yes\nreturn [r + [9] for r in t]\n#+end_src\n",
179 "MULTI#+begin_src python :var t=multi :hlines yes\nreturn t\n#+end_src\n",
180 "MULTI#+begin_src python :var t=multi\nreturn t\n#+end_src\n",
181 "MULTI#+begin_src python :var t=multi :colnames yes\nreturn t\n#+end_src\n",
182 "TBL#+begin_src emacs-lisp :var tb=tbl\ntb\n#+end_src\n",
183 "TBL#+begin_src emacs-lisp :var tb=tbl :colnames yes\n(mapcar #'reverse tb)\n#+end_src\n",
184 "MULTI#+begin_src emacs-lisp :var tb=multi :hlines yes\ntb\n#+end_src\n",
185 "MULTI#+begin_src sh :stdin multi :results output\ncat\n#+end_src\n",
186 "#+begin_src python :var t=tb2 :colnames yes\nreturn t\n#+end_src\n\n#+NAME: tb2\n#+begin_src python\nreturn [['h1', 'h2'], None, [1, 2]]\n#+end_src\n",
187 // Tables and lists in shell variables.
188 "TBL#+begin_src sh :var t=tbl :results output\necho \"$t\"\n#+end_src\n",
189 "MULTI#+begin_src sh :var t=multi :hlines yes :separator , :results output\necho \"$t\"\n#+end_src\n",
190 "MULTI#+begin_src sh :var t=multi :hlines yes :hline-string -- :results output\necho \"$t\"\n#+end_src\n",
191 "#+NAME: items\n- one\n- it's\n\n#+begin_src sh :var l=items :results output\necho \"$l\"\n#+end_src\n",
192 "#+NAME: items\n- one\n- it's\n\n#+begin_src bash :var l=items :results output\necho ${#l[@]} \"${l[1]}\"\n#+end_src\n",
193 "#+NAME: kv\n| a | 1 |\n| b | 2 |\n\n#+begin_src bash :var t=kv :results output\necho ${t[a]} ${t[b]} ${#t[@]}\n#+end_src\n",
194 "#+NAME: kv3\n| a | 1 | x |\n| b | 2 | y |\n\n#+begin_src bash :var t=kv3 :results output\necho \"${t[b]}\"\n#+end_src\n",
195 "#+NAME: col\n| 5 |\n| 6 |\n\n#+begin_src bash :var t=col :results output\necho ${t[@]}\n#+end_src\n",
196 "#+begin_src bash :var l='(1 \"two\" 3) :results output\necho ${l[2]} ${l[1]}\n#+end_src\n",
197 "#+NAME: items\n- one\n- two\n\n#+begin_src zsh :var l=items :results output\necho \"$l\"\n#+end_src\n",
198 "TBL#+begin_src shell :var t=tbl :results output\necho \"$t\"\n#+end_src\n",
199 "TBL#+begin_src python :var t=tbl :colnames '(\"x\" \"y\")\nreturn t\n#+end_src\n",
200 "TBL#+begin_src python :var t=tbl :rownames '(\"r1\" \"r2\")\nreturn t\n#+end_src\n",
201 "#+begin_src sh :dir (concat \"/\" \"usr\") :results output\npwd\n#+end_src\n",
202 "#+begin_src sh :dir /tmp :results output\npwd\n#+end_src\n",
203 "#+NAME: items\n- one\n- two\n\n#+begin_src shell :var l=items :results output\necho \"$l\"\n#+end_src\n",
204 // The nearest heading's header-args win.
205 "* A\n:PROPERTIES:\n:header-args: :var x=1\n:END:\n** B\n:PROPERTIES:\n:header-args: :var x=2\n:END:\n*** C\n#+begin_src sh\necho $x\n#+end_src\n",
206 // Lisp values.
207 "#+begin_src python :var l='(1 2) m='((1 2) (3 4)) n=(+ 1 2) s='(\"a\" \"b\")\nreturn [l, m, n, s]\n#+end_src\n",
208 "#+begin_src emacs-lisp :var l='(1 2) h='((1 2) hline (3 4)) s=(concat \"a\" \"b\")\n(list l h s)\n#+end_src\n",
209 "#+begin_src sh :var n=(* 6 7)\necho $n\n#+end_src\n",
210 ]
211
212 @Test(.enabled(if: ProcessInfo.processInfo.environment["ORGSTAR_SKIP_ORACLE"] == nil))
213 func matchesEmacs() throws {
214 let folder = FileManager.default.temporaryDirectory
215 var emacsCases: [EmacsOracle.Case] = []
216 var ours: [(text: String, failure: String?)] = []
217 for text in Self.cases {
218 let caret = (text as NSString).range(of: "#+begin_src").location + 2
219 ours.append(try TestBabelRunner.execute(text, caret: caret, directory: folder))
220 emacsCases.append(EmacsOracle.Case(text: text, point: EmacsOracle.point(caret, in: text), form: """
221 (progn
222 (require 'ob-shell)
223 (require 'ob-python)
224 (require 'ob-emacs-lisp)
225 (setq org-confirm-babel-evaluate nil
226 org-babel-python-command "python3"
227 default-directory temporary-file-directory)
228 ;; No shell session in batch; the cases with one don't use it.
229 (advice-add 'org-babel-sh-initiate-session :override (lambda (s &rest _) (unless (member s '(nil "none")) s)))
230 (org-babel-execute-src-block))
231 """))
232 }
233 let emacs = try EmacsOracle.run(emacsCases)
234 for (i, (mine, theirs)) in zip(ours, emacs).enumerated() {
235 #expect(mine.text == theirs.text, "case \(i):\n\(Self.cases[i])\nours (\(mine.failure ?? "")):\n\(mine.text)\nemacs (\(theirs.error)):\n\(theirs.text)")
236 }
237 }
238
239 static let lispCases = [
240 "#+begin_src emacs-lisp\n(list 1 2.5 \"three\" 'four)\n#+end_src\n",
241 "#+begin_src emacs-lisp\n(list '(a 1) 'hline '(b 2))\n#+end_src\n",
242 "#+begin_src emacs-lisp\n(format \"%05.2f|%s\" 3.14159 'sym)\n#+end_src\n",
243 "#+begin_src emacs-lisp\n\"line one\\nline \\\"two\\\"\"\n#+end_src\n",
244 "#+begin_src emacs-lisp :results verbatim\n\"with \\\"quotes\\\" and \\\\ slash\"\n#+end_src\n",
245 "#+begin_src emacs-lisp\nnil\n#+end_src\n",
246 "#+begin_src emacs-lisp\nt\n#+end_src\n",
247 "#+begin_src emacs-lisp\n[1 2 3]\n#+end_src\n",
248 "#+begin_src emacs-lisp\n(cons 1 2)\n#+end_src\n",
249 "#+begin_src emacs-lisp\n(/ 10 4.0)\n#+end_src\n",
250 "#+begin_src emacs-lisp\n(let ((s 0)) (dotimes (i 5) (setq s (+ s i))) s)\n#+end_src\n",
251 "#+begin_src emacs-lisp\n(mapcar (lambda (x) (* x x)) '(1 2 3))\n#+end_src\n",
252 "#+begin_src emacs-lisp :results output\n(princ \"a\")\n(terpri)\n(prin1 \"b\")\n(print 'c)\n#+end_src\n",
253 "#+begin_src emacs-lisp :results value scalar\n'(1 2)\n#+end_src\n",
254 "#+begin_src emacs-lisp :results list\n'(\"x\" \"y\")\n#+end_src\n",
255 "#+NAME: tbl\n| 1 | 2 |\n| 3 | 4 |\n\n#+begin_src emacs-lisp :var rows=tbl\n(mapcar (lambda (r) (apply #'+ r)) rows)\n#+end_src\n",
256 "#+begin_src emacs-lisp :var n=6 :var s=\"hi\"\n(concat s (number-to-string (* n 7)))\n#+end_src\n",
257 "#+begin_src emacs-lisp\n(car 1)\n#+end_src\n",
258 "#+begin_src emacs-lisp :results raw\n\"*bold*\"\n#+end_src\n",
259 "#+begin_src emacs-lisp :wrap example\n(upcase \"wrapped\")\n#+end_src\n",
260 ]
261
262 @Test(.enabled(if: ProcessInfo.processInfo.environment["ORGSTAR_SKIP_ORACLE"] == nil))
263 func lispRunsNativelyLikeEmacs() throws {
264 let folder = FileManager.default.temporaryDirectory
265 let texts = Self.lispCases + Self.cases.filter { ($0.contains("emacs-lisp") || $0.contains("begin_src elisp")) && !$0.contains("begin_src sh") }
266 var emacsCases: [EmacsOracle.Case] = []
267 var ours: [(text: String, failure: String?)] = []
268 for text in texts {
269 let caret = (text as NSString).range(of: "#+begin_src").location + 2
270 do {
271 ours.append(try TestBabelRunner.execute(text, caret: caret, directory: folder, native: true))
272 } catch is NativeUnsupported {
273 Issue.record("not run natively:\n\(text)")
274 ours.append((text, "unsupported"))
275 }
276 emacsCases.append(EmacsOracle.Case(text: text, point: EmacsOracle.point(caret, in: text), form: """
277 (progn
278 (setq org-confirm-babel-evaluate nil)
279 (org-babel-execute-src-block))
280 """))
281 }
282 let emacs = try EmacsOracle.run(emacsCases)
283 for (i, (mine, theirs)) in zip(ours, emacs).enumerated() {
284 #expect(mine.text == theirs.text, "case \(i):\n\(texts[i])\nours (\(mine.failure ?? "")):\n\(mine.text)\nemacs (\(theirs.error)):\n\(theirs.text)")
285 }
286 }
287
288 @Test func referencedQueryBlocksAskEveryTime() {
289 let text = "#+begin_src sh :var x=gen\necho $x\n#+end_src\n\n#+NAME: gen\n#+begin_src sh :eval query\necho 1\n#+end_src\n"
290 guard case .success(let job) = Babel.plan(EditContext(revision: 0, text: text, tree: OrgParser.parse(text), selection: [2..<2])) else {
291 Issue.record("not planned")
292 return
293 }
294 #expect(job.alwaysAsk)
295 }
296
297 @Test func aChangedBufferLeavesTheApprovedBlocks() throws {
298 let text = "#+begin_src sh :var x=gen\necho $x\n#+end_src\n\n#+NAME: gen\n#+begin_src sh\necho 1\n#+end_src\n"
299 guard case .success(let job) = Babel.plan(EditContext(revision: 0, text: text, tree: OrgParser.parse(text), selection: [2..<2])),
300 let need = job.needs.first else {
301 Issue.record("not planned")
302 return
303 }
304 #expect(job.approved.contains(need.job.blockText))
305 // Another block now sits where the approved one was.
306 let changed = "#+begin_src sh\nrm -rf nothing\n#+end_src\n" + text
307 guard case .success(let next) = Babel.resume(job, need: need, output: BabelOutput(stdout: "1\n"), text: changed, tree: OrgParser.parse(changed)) else {
308 Issue.record("not resumed")
309 return
310 }
311 #expect(!job.approved.contains(next.blockText))
312 }
313
314 @Test func asksOnceForEveryReferencedBlock() throws {
315 let text = BabelTests.cases.first { $0.contains(":var a=outer") }!
316 guard case .success(let job) = Babel.plan(EditContext(revision: 0, text: text, tree: OrgParser.parse(text), selection: [2..<2])) else {
317 Issue.record("not planned")
318 return
319 }
320 #expect(job.needs.first?.reference == "inner")
321 #expect(job.blockText.contains("return a * 2") && job.blockText.contains("return b + 1") && job.blockText.contains("return 20"))
322 }
323
324 @Test func refusesWhatWouldRunDifferently() {
325 for (text, message) in [
326 ("#+begin_src sh :eval never\necho\n#+end_src\n", "Evaluation of this sh code block is disabled."),
327 ("#+begin_src ruby :session s\nputs 1\n#+end_src\n", ":session isn't supported for ruby yet; nothing was run."),
328 ("#+begin_src sh :noweb yes\n<<x()>>\n#+end_src\n", "Noweb references that run a block (x()) aren't supported yet; nothing was run."),
329 ("#+begin_src python :cmdline -x\nreturn 1\n#+end_src\n", ":cmdline isn't supported for python yet; nothing was run."),
330 ("#+begin_src cobol\nDISPLAY\n#+end_src\n", "No way to run cobol blocks yet."),
331 ("#+begin_src sh :var x=loop\necho\n#+end_src\n#+NAME: loop\n#+begin_src sh :var y=loop\necho\n#+end_src\n", "loop refers to itself through :var"),
332 ] {
333 let context = EditContext(revision: 0, text: text, tree: OrgParser.parse(text), selection: [2..<2])
334 guard case .failure(.message(let got)) = Babel.plan(context) else {
335 Issue.record("ran: \(text)")
336 continue
337 }
338 #expect(got == message)
339 }
340 }
341
342 @Test func noExportStillRunsInteractively() {
343 for eval in ["never-export", "no-export"] {
344 let text = "#+begin_src sh :eval \(eval)\necho 1\n#+end_src\n"
345 let context = EditContext(revision: 0, text: text, tree: OrgParser.parse(text), selection: [2..<2])
346 guard case .success = Babel.plan(context) else {
347 Issue.record("refused :eval \(eval)")
348 continue
349 }
350 }
351 }
352}
353
354struct BabelCallTests {
355 static let named = "#+NAME: double\n#+begin_src sh :var n=2\necho $((n*2))\n#+end_src\n\n"
356 static let cases: [(String, String)] = [
357 (named + "#+CALL: double(n=5)\n", "#+CALL"),
358 (named + "#+CALL: double()\n", "#+CALL"),
359 (named + "#+CALL: double(n=5)\n\n#+RESULTS:\n: 99\n\nAfter.\n", "#+CALL"),
360 (named + "#+CALL: double[:results raw](n=7)\n", "#+CALL"),
361 (named + "#+CALL: double(n=3) :results verbatim\n", "#+CALL"),
362 (named + "#+NAME: six\n#+CALL: double(n=3)\n", "#+CALL"),
363 ("Text src_sh{echo hi} end.\n", "src_sh"),
364 ("Text src_sh{echo hi} {{{results(=old=)}}} end.\n", "src_sh"),
365 ("Text src_sh[:results raw]{echo raw} end.\n", "src_sh"),
366 ("Text src_python{return 6*7} end.\n", "src_python"),
367 ("Text src_sh[:var x=3]{echo $x} end.\n", "src_sh"),
368 (named + "Inline call_double(n=6) here.\n", "call_"),
369 (named + "Inline call_double() here.\n", "call_"),
370 (named + "Inline call_double(n=6)[:results raw] here.\n", "call_"),
371 (named + "Inline call_double[:results verbatim](n=1) here.\n", "call_"),
372 (named + "Inline call_double(n=2) {{{results(=0=)}}} here.\n", "call_"),
373 (named + "Inline call_double(n=2) and call_double(n=3) here.\n", "call_double(n=3"),
374 // Properties where the call is, not where the block is.
375 (underHeadings + "#+CALL: double()\n", "#+CALL"),
376 (underHeadings + "#+CALL: double() :var n=1\n", "#+CALL"),
377 (underHeadings + "Inline call_double() here.\n", "call_"),
378 ]
379 static let underHeadings = """
380 * Library
381 :PROPERTIES:
382 :header-args: :results verbatim
383 :END:
384 #+NAME: double
385 #+begin_src sh :var n=2
386 echo $((n*2))
387 #+end_src
388 * Use
389 :PROPERTIES:
390 :header-args:sh: :var n=7
391 :header-args: :results raw
392 :END:
393
394 """
395
396 @Test(.enabled(if: ProcessInfo.processInfo.environment["ORGSTAR_SKIP_ORACLE"] == nil))
397 func matchesEmacs() throws {
398 let folder = FileManager.default.temporaryDirectory
399 var emacsCases: [EmacsOracle.Case] = []
400 var ours: [(text: String, failure: String?)] = []
401 for (text, at) in Self.cases {
402 let caret = (text as NSString).range(of: at).location + 2
403 ours.append(try TestBabelRunner.execute(text, caret: caret, directory: folder))
404 emacsCases.append(EmacsOracle.Case(text: text, point: EmacsOracle.point(caret, in: text), form: """
405 (progn
406 (require 'ob-shell)
407 (require 'ob-python)
408 (setq org-confirm-babel-evaluate nil
409 org-babel-python-command "python3"
410 default-directory temporary-file-directory)
411 (org-ctrl-c-ctrl-c))
412 """))
413 }
414 let emacs = try EmacsOracle.run(emacsCases)
415 for (i, (mine, theirs)) in zip(ours, emacs).enumerated() {
416 #expect(mine.text == theirs.text && (mine.failure != nil) == !theirs.error.isEmpty,
417 "case \(i):\n\(Self.cases[i].0)\nours (\(mine.failure ?? "")):\n\(mine.text)\nemacs (\(theirs.error)):\n\(theirs.text)")
418 }
419 }
420}
421
422extension BabelCallTests {
423 @Test func ctrlCCtrlCRunsInlineCalls() {
424 let text = Self.named + "Inline call_double(n=6) here.\n"
425 let caret = (text as NSString).range(of: "call_").location + 2
426 let context = EditContext(revision: 0, text: text, tree: OrgParser.parse(text), selection: [caret..<caret])
427 #expect(ExecuteSrcBlock().applies(in: context))
428 guard case .external(.babel(let job)) = CtrlCCtrlC().run(in: context) else {
429 Issue.record("inline call not run")
430 return
431 }
432 #expect((job.script ?? job.input).contains("echo $((n*2))"))
433 }
434}
435
436struct BabelFileTests {
437 static let cases = [
438 "#+begin_src sh :results file :file out.txt\necho hello\necho there\n#+end_src\n",
439 "#+begin_src sh :results output file :file x.txt :file-desc Desc\necho out\n#+end_src\n",
440 "#+begin_src sh :results file :file y.txt :file-desc\necho y\n#+end_src\n",
441 "#+begin_src python :results file :file p.txt\nreturn [[1,2],[3,4]]\n#+end_src\n",
442 "#+begin_src sh :results file\necho no-file.txt\n#+end_src\n",
443 "#+begin_src sh :results file :file w.txt :output-dir outd\necho w\n#+end_src\n",
444 "#+begin_src sh :file plain.txt\necho not to a file\n#+end_src\n",
445 ]
446
447 @Test(.enabled(if: ProcessInfo.processInfo.environment["ORGSTAR_SKIP_ORACLE"] == nil))
448 func matchesEmacs() throws {
449 for (i, text) in Self.cases.enumerated() {
450 let ours = FileManager.default.temporaryDirectory.appendingPathComponent("orgstar-file-ours-\(UUID().uuidString)")
451 let theirs = FileManager.default.temporaryDirectory.appendingPathComponent("orgstar-file-emacs-\(UUID().uuidString)")
452 for folder in [ours, theirs] { try FileManager.default.createDirectory(at: folder.appendingPathComponent("outd"), withIntermediateDirectories: true) }
453 defer { for folder in [ours, theirs] { try? FileManager.default.removeItem(at: folder) } }
454 let mine = try TestBabelRunner.execute(text, caret: 2, directory: ours)
455 let emacs = try EmacsOracle.run([EmacsOracle.Case(text: text, point: 3, form: """
456 (progn (require 'ob-shell) (require 'ob-python)
457 (setq org-confirm-babel-evaluate nil org-babel-python-command "python3" default-directory \(lispString(theirs.path + "/")))
458 (org-babel-execute-src-block))
459 """)])[0]
460 #expect(mine.text == emacs.text, "case \(i)\nours: \(mine.text) \(mine.failure ?? "")\nemacs: \(emacs.text) \(emacs.error)")
461 for name in ["out.txt", "x.txt", "y.txt", "p.txt", "outd/w.txt", "plain.txt"] {
462 let a = try? String(contentsOf: ours.appendingPathComponent(name), encoding: .utf8)
463 let b = try? String(contentsOf: theirs.appendingPathComponent(name), encoding: .utf8)
464 #expect(a == b, "case \(i) \(name): \(a.debugDescription) vs \(b.debugDescription)")
465 }
466 }
467 }
468
469 @Test func plansDiagrams() throws {
470 let text = "#+begin_src dot :file g.svg\ndigraph { a -> b }\n#+end_src\n"
471 guard case .success(let job) = Babel.plan(EditContext(revision: 0, text: text, tree: OrgParser.parse(text), selection: [2..<2])) else {
472 Issue.record("not planned")
473 return
474 }
475 #expect(job.runner == .process(executable: "/bin/sh", arguments: ["-c", "dot -Tsvg -o 'g.svg'"]))
476 guard case .success(let edits) = Babel.finish(job, output: BabelOutput(stdout: ""), text: text) else { return }
477 let new = edits.sorted { $0.range.lowerBound > $1.range.lowerBound }.reduce(text) { $1.apply(to: $0) }
478 #expect(new.hasSuffix("#+RESULTS:\n[[file:g.svg]]\n"))
479 #expect(Babel.fileResult(job, output: BabelOutput(stdout: "")) == nil)
480 let uml = "#+begin_src plantuml :file u.png\nA -> B\n#+end_src\n"
481 guard case .success(let umlJob) = Babel.plan(EditContext(revision: 0, text: uml, tree: OrgParser.parse(uml), selection: [2..<2])) else { return }
482 #expect(umlJob.input == "@startuml\nA -> B\n@enduml\n")
483 }
484}