Tests/OrgCoreTests/BabelTests.swift
481 lines · 30557 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 silent\necho quiet\n#+end_src\n",
112 "#+begin_src sh :results output code\necho 'x=1'\n#+end_src\n",
113 "#+begin_src sh :wrap example\necho wrapped\n#+end_src\n",
114 "#+begin_src sh\necho new\n#+end_src\n\n#+RESULTS:\n: old\n\nAfter.\n",
115 "#+begin_src sh\necho new\n#+end_src\n#+RESULTS:\n| old | table |\n| a | b |\nAfter.\n",
116 "#+begin_src sh :results append\necho more\n#+end_src\n\n#+RESULTS:\n: first\n",
117 "#+NAME: greet\n#+begin_src sh\necho named\n#+end_src\n\nText.\n\n#+RESULTS: greet\n: stale\n",
118 "#+NAME: fresh\n#+begin_src sh\necho named\n#+end_src\n",
119 "- item\n #+begin_src sh\n echo indented\n #+end_src\n",
120 "#+begin_src sh :var x=5 :var y=\"two words\"\necho $x $y\n#+end_src\n",
121 "#+begin_src bash\necho ${BASH_VERSION:+bash}\n#+end_src\n",
122 "#+begin_src sh :results output\n#+end_src\n",
123 "#+begin_src sh\nprintf ''\n#+end_src\n",
124 "#+begin_src python\nreturn 42\n#+end_src\n",
125 "#+begin_src python\nreturn 'hello'\n#+end_src\n",
126 "#+begin_src python\nreturn [[1, 2], [3, 4]]\n#+end_src\n",
127 "#+begin_src python\nreturn [1, 'a', 2.5]\n#+end_src\n",
128 "#+begin_src python\nreturn 1/3\n#+end_src\n",
129 "#+begin_src python\nx = 2\nif x:\n return x * 21\n#+end_src\n",
130 "#+begin_src python :results output\nprint('one')\nprint('two')\n#+end_src\n",
131 "#+begin_src python :results output\nfor i in range(12): print(i)\n#+end_src\n",
132 "#+begin_src python :var n=3 :var s=\"hi\"\nreturn s * n\n#+end_src\n",
133 "#+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",
134 "#+begin_src python :results list\nreturn ['a', 'b']\n#+end_src\n",
135 "#+begin_src python :results verbatim\nreturn [1, 2]\n#+end_src\n",
136 "#+begin_src python\ns = '''multi\n line'''\nreturn s\n#+end_src\n",
137 "#+begin_src python\nreturn None\n#+end_src\n",
138 "#+begin_src emacs-lisp\n(+ 1 2)\n#+end_src\n",
139 "#+begin_src emacs-lisp\n'((1 2) (3 4))\n#+end_src\n",
140 "#+begin_src elisp\n(concat \"a\" \"b\")\n#+end_src\n",
141 "#+begin_src emacs-lisp :results verbatim\n\"quoted\"\n#+end_src\n",
142 "#+begin_src emacs-lisp :results output\n(princ \"printed\")\n#+end_src\n",
143 "#+begin_src emacs-lisp :var x=4\n(* x x)\n#+end_src\n",
144 "#+PROPERTY: header-args :results output\n#+begin_src sh\nprintf 'a b\\nc d\\n'\n#+end_src\n",
145 "* Heading\n:PROPERTIES:\n:header-args:sh: :results verbatim\n:END:\n#+begin_src sh\necho 1 2\n#+end_src\n",
146 "* Heading\n:PROPERTIES:\n:header-args:sh: :results verbatim\n:END:\n#+begin_src python\nreturn [[1, 2]]\n#+end_src\n",
147 "#+HEADER: :results output\n#+begin_src sh\nprintf 'x y\\nz w\\n'\n#+end_src\n",
148 // `:cmdline`, `:stdin` and `:shebang`.
149 "#+begin_src sh :cmdline one \"two words\" :results output\nfor a in \"$@\"; do echo \"[$a]\"; done\n#+end_src\n",
150 "#+begin_src sh :cmdline 3\necho $(( $1 * 2 ))\n#+end_src\n",
151 "#+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",
152 "#+NAME: items\n- x\n- y\n\n#+begin_src sh :stdin items\nwc -l | tr -d ' '\n#+end_src\n",
153 "#+begin_src bash :shebang #!/bin/bash :results output\necho ${BASH_VERSION:+bash}\n#+end_src\n",
154 "#+begin_src sh :cmdline a b :padline no :var x=5 :results output\necho $x $#\n#+end_src\n",
155 "#+NAME: in\n| hello |\n\n#+begin_src sh :results value :stdin in\ncat\nfalse\n#+end_src\n",
156 "#+begin_src sh :results value :cmdline x\necho one\n#+end_src\n",
157 "#+begin_src bash :shebang #!/bin/bash :cmdline \"a b\" c :results output\necho $#\n#+end_src\n",
158 "#+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",
159 // `:stdin` and `:cmdline` run a script rather than the session.
160 "#+begin_src sh :session s1 :cmdline a b :results output\necho $# $1\n#+end_src\n",
161 "#+NAME: in\n| hello |\n\n#+begin_src sh :session s2 :stdin in :results output\ncat\n#+end_src\n",
162 "#+begin_src bash :session s3 :shebang #!/bin/bash :cmdline x :var v=3\necho $v $1 ${BASH_VERSION:+bash}\n#+end_src\n",
163 // `:var` from other blocks' results.
164 "#+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",
165 "#+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",
166 "#+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",
167 "#+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",
168 "#+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",
169 "#+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",
170 "#+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",
171 // Tables with rules, `:colnames`, `:rownames` and `:hlines`.
172 "TBL#+begin_src python :var t=tbl\nreturn t\n#+end_src\n",
173 "TBL#+begin_src python :var t=tbl :colnames yes\nreturn t\n#+end_src\n",
174 "TBL#+begin_src python :var t=tbl :colnames no\nreturn t\n#+end_src\n",
175 "TBL#+begin_src python :var t=tbl :rownames yes\nreturn t\n#+end_src\n",
176 "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",
177 "TBL#+begin_src python :var t=tbl :colnames yes\nreturn [r + [9] for r in t]\n#+end_src\n",
178 "MULTI#+begin_src python :var t=multi :hlines yes\nreturn t\n#+end_src\n",
179 "MULTI#+begin_src python :var t=multi\nreturn t\n#+end_src\n",
180 "MULTI#+begin_src python :var t=multi :colnames yes\nreturn t\n#+end_src\n",
181 "TBL#+begin_src emacs-lisp :var tb=tbl\ntb\n#+end_src\n",
182 "TBL#+begin_src emacs-lisp :var tb=tbl :colnames yes\n(mapcar #'reverse tb)\n#+end_src\n",
183 "MULTI#+begin_src emacs-lisp :var tb=multi :hlines yes\ntb\n#+end_src\n",
184 "MULTI#+begin_src sh :stdin multi :results output\ncat\n#+end_src\n",
185 "#+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",
186 // Tables and lists in shell variables.
187 "TBL#+begin_src sh :var t=tbl :results output\necho \"$t\"\n#+end_src\n",
188 "MULTI#+begin_src sh :var t=multi :hlines yes :separator , :results output\necho \"$t\"\n#+end_src\n",
189 "MULTI#+begin_src sh :var t=multi :hlines yes :hline-string -- :results output\necho \"$t\"\n#+end_src\n",
190 "#+NAME: items\n- one\n- it's\n\n#+begin_src sh :var l=items :results output\necho \"$l\"\n#+end_src\n",
191 "#+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",
192 "#+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",
193 "#+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",
194 "#+NAME: col\n| 5 |\n| 6 |\n\n#+begin_src bash :var t=col :results output\necho ${t[@]}\n#+end_src\n",
195 "#+begin_src bash :var l='(1 \"two\" 3) :results output\necho ${l[2]} ${l[1]}\n#+end_src\n",
196 "#+NAME: items\n- one\n- two\n\n#+begin_src zsh :var l=items :results output\necho \"$l\"\n#+end_src\n",
197 "TBL#+begin_src shell :var t=tbl :results output\necho \"$t\"\n#+end_src\n",
198 "TBL#+begin_src python :var t=tbl :colnames '(\"x\" \"y\")\nreturn t\n#+end_src\n",
199 "TBL#+begin_src python :var t=tbl :rownames '(\"r1\" \"r2\")\nreturn t\n#+end_src\n",
200 "#+begin_src sh :dir (concat \"/\" \"usr\") :results output\npwd\n#+end_src\n",
201 "#+begin_src sh :dir /tmp :results output\npwd\n#+end_src\n",
202 "#+NAME: items\n- one\n- two\n\n#+begin_src shell :var l=items :results output\necho \"$l\"\n#+end_src\n",
203 // Lisp values.
204 "#+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",
205 "#+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",
206 "#+begin_src sh :var n=(* 6 7)\necho $n\n#+end_src\n",
207 ]
208
209 @Test(.enabled(if: ProcessInfo.processInfo.environment["ORGSTAR_SKIP_ORACLE"] == nil))
210 func matchesEmacs() throws {
211 let folder = FileManager.default.temporaryDirectory
212 var emacsCases: [EmacsOracle.Case] = []
213 var ours: [(text: String, failure: String?)] = []
214 for text in Self.cases {
215 let caret = (text as NSString).range(of: "#+begin_src").location + 2
216 ours.append(try TestBabelRunner.execute(text, caret: caret, directory: folder))
217 emacsCases.append(EmacsOracle.Case(text: text, point: EmacsOracle.point(caret, in: text), form: """
218 (progn
219 (require 'ob-shell)
220 (require 'ob-python)
221 (require 'ob-emacs-lisp)
222 (setq org-confirm-babel-evaluate nil
223 org-babel-python-command "python3"
224 default-directory temporary-file-directory)
225 ;; No shell session in batch; the cases with one don't use it.
226 (advice-add 'org-babel-sh-initiate-session :override (lambda (s &rest _) (unless (member s '(nil "none")) s)))
227 (org-babel-execute-src-block))
228 """))
229 }
230 let emacs = try EmacsOracle.run(emacsCases)
231 for (i, (mine, theirs)) in zip(ours, emacs).enumerated() {
232 #expect(mine.text == theirs.text, "case \(i):\n\(Self.cases[i])\nours (\(mine.failure ?? "")):\n\(mine.text)\nemacs (\(theirs.error)):\n\(theirs.text)")
233 }
234 }
235
236 static let lispCases = [
237 "#+begin_src emacs-lisp\n(list 1 2.5 \"three\" 'four)\n#+end_src\n",
238 "#+begin_src emacs-lisp\n(list '(a 1) 'hline '(b 2))\n#+end_src\n",
239 "#+begin_src emacs-lisp\n(format \"%05.2f|%s\" 3.14159 'sym)\n#+end_src\n",
240 "#+begin_src emacs-lisp\n\"line one\\nline \\\"two\\\"\"\n#+end_src\n",
241 "#+begin_src emacs-lisp :results verbatim\n\"with \\\"quotes\\\" and \\\\ slash\"\n#+end_src\n",
242 "#+begin_src emacs-lisp\nnil\n#+end_src\n",
243 "#+begin_src emacs-lisp\nt\n#+end_src\n",
244 "#+begin_src emacs-lisp\n[1 2 3]\n#+end_src\n",
245 "#+begin_src emacs-lisp\n(cons 1 2)\n#+end_src\n",
246 "#+begin_src emacs-lisp\n(/ 10 4.0)\n#+end_src\n",
247 "#+begin_src emacs-lisp\n(let ((s 0)) (dotimes (i 5) (setq s (+ s i))) s)\n#+end_src\n",
248 "#+begin_src emacs-lisp\n(mapcar (lambda (x) (* x x)) '(1 2 3))\n#+end_src\n",
249 "#+begin_src emacs-lisp :results output\n(princ \"a\")\n(terpri)\n(prin1 \"b\")\n(print 'c)\n#+end_src\n",
250 "#+begin_src emacs-lisp :results value scalar\n'(1 2)\n#+end_src\n",
251 "#+begin_src emacs-lisp :results list\n'(\"x\" \"y\")\n#+end_src\n",
252 "#+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",
253 "#+begin_src emacs-lisp :var n=6 :var s=\"hi\"\n(concat s (number-to-string (* n 7)))\n#+end_src\n",
254 "#+begin_src emacs-lisp\n(car 1)\n#+end_src\n",
255 "#+begin_src emacs-lisp :results raw\n\"*bold*\"\n#+end_src\n",
256 "#+begin_src emacs-lisp :wrap example\n(upcase \"wrapped\")\n#+end_src\n",
257 ]
258
259 @Test(.enabled(if: ProcessInfo.processInfo.environment["ORGSTAR_SKIP_ORACLE"] == nil))
260 func lispRunsNativelyLikeEmacs() throws {
261 let folder = FileManager.default.temporaryDirectory
262 let texts = Self.lispCases + Self.cases.filter { ($0.contains("emacs-lisp") || $0.contains("begin_src elisp")) && !$0.contains("begin_src sh") }
263 var emacsCases: [EmacsOracle.Case] = []
264 var ours: [(text: String, failure: String?)] = []
265 for text in texts {
266 let caret = (text as NSString).range(of: "#+begin_src").location + 2
267 do {
268 ours.append(try TestBabelRunner.execute(text, caret: caret, directory: folder, native: true))
269 } catch is NativeUnsupported {
270 Issue.record("not run natively:\n\(text)")
271 ours.append((text, "unsupported"))
272 }
273 emacsCases.append(EmacsOracle.Case(text: text, point: EmacsOracle.point(caret, in: text), form: """
274 (progn
275 (setq org-confirm-babel-evaluate nil)
276 (org-babel-execute-src-block))
277 """))
278 }
279 let emacs = try EmacsOracle.run(emacsCases)
280 for (i, (mine, theirs)) in zip(ours, emacs).enumerated() {
281 #expect(mine.text == theirs.text, "case \(i):\n\(texts[i])\nours (\(mine.failure ?? "")):\n\(mine.text)\nemacs (\(theirs.error)):\n\(theirs.text)")
282 }
283 }
284
285 @Test func referencedQueryBlocksAskEveryTime() {
286 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"
287 guard case .success(let job) = Babel.plan(EditContext(revision: 0, text: text, tree: OrgParser.parse(text), selection: [2..<2])) else {
288 Issue.record("not planned")
289 return
290 }
291 #expect(job.alwaysAsk)
292 }
293
294 @Test func aChangedBufferLeavesTheApprovedBlocks() throws {
295 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"
296 guard case .success(let job) = Babel.plan(EditContext(revision: 0, text: text, tree: OrgParser.parse(text), selection: [2..<2])),
297 let need = job.needs.first else {
298 Issue.record("not planned")
299 return
300 }
301 #expect(job.approved.contains(need.job.blockText))
302 // Another block now sits where the approved one was.
303 let changed = "#+begin_src sh\nrm -rf nothing\n#+end_src\n" + text
304 guard case .success(let next) = Babel.resume(job, need: need, output: BabelOutput(stdout: "1\n"), text: changed, tree: OrgParser.parse(changed)) else {
305 Issue.record("not resumed")
306 return
307 }
308 #expect(!job.approved.contains(next.blockText))
309 }
310
311 @Test func asksOnceForEveryReferencedBlock() throws {
312 let text = BabelTests.cases.first { $0.contains(":var a=outer") }!
313 guard case .success(let job) = Babel.plan(EditContext(revision: 0, text: text, tree: OrgParser.parse(text), selection: [2..<2])) else {
314 Issue.record("not planned")
315 return
316 }
317 #expect(job.needs.first?.reference == "inner")
318 #expect(job.blockText.contains("return a * 2") && job.blockText.contains("return b + 1") && job.blockText.contains("return 20"))
319 }
320
321 @Test func refusesWhatWouldRunDifferently() {
322 for (text, message) in [
323 ("#+begin_src sh :eval never\necho\n#+end_src\n", "Evaluation of this sh code block is disabled."),
324 ("#+begin_src ruby :session s\nputs 1\n#+end_src\n", ":session isn't supported for ruby yet; nothing was run."),
325 ("#+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."),
326 ("#+begin_src python :cmdline -x\nreturn 1\n#+end_src\n", ":cmdline isn't supported for python yet; nothing was run."),
327 ("#+begin_src cobol\nDISPLAY\n#+end_src\n", "No way to run cobol blocks yet."),
328 ("#+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"),
329 ] {
330 let context = EditContext(revision: 0, text: text, tree: OrgParser.parse(text), selection: [2..<2])
331 guard case .failure(.message(let got)) = Babel.plan(context) else {
332 Issue.record("ran: \(text)")
333 continue
334 }
335 #expect(got == message)
336 }
337 }
338
339 @Test func noExportStillRunsInteractively() {
340 for eval in ["never-export", "no-export"] {
341 let text = "#+begin_src sh :eval \(eval)\necho 1\n#+end_src\n"
342 let context = EditContext(revision: 0, text: text, tree: OrgParser.parse(text), selection: [2..<2])
343 guard case .success = Babel.plan(context) else {
344 Issue.record("refused :eval \(eval)")
345 continue
346 }
347 }
348 }
349}
350
351struct BabelCallTests {
352 static let named = "#+NAME: double\n#+begin_src sh :var n=2\necho $((n*2))\n#+end_src\n\n"
353 static let cases: [(String, String)] = [
354 (named + "#+CALL: double(n=5)\n", "#+CALL"),
355 (named + "#+CALL: double()\n", "#+CALL"),
356 (named + "#+CALL: double(n=5)\n\n#+RESULTS:\n: 99\n\nAfter.\n", "#+CALL"),
357 (named + "#+CALL: double[:results raw](n=7)\n", "#+CALL"),
358 (named + "#+CALL: double(n=3) :results verbatim\n", "#+CALL"),
359 (named + "#+NAME: six\n#+CALL: double(n=3)\n", "#+CALL"),
360 ("Text src_sh{echo hi} end.\n", "src_sh"),
361 ("Text src_sh{echo hi} {{{results(=old=)}}} end.\n", "src_sh"),
362 ("Text src_sh[:results raw]{echo raw} end.\n", "src_sh"),
363 ("Text src_python{return 6*7} end.\n", "src_python"),
364 ("Text src_sh[:var x=3]{echo $x} end.\n", "src_sh"),
365 (named + "Inline call_double(n=6) here.\n", "call_"),
366 (named + "Inline call_double() here.\n", "call_"),
367 (named + "Inline call_double(n=6)[:results raw] here.\n", "call_"),
368 (named + "Inline call_double[:results verbatim](n=1) here.\n", "call_"),
369 (named + "Inline call_double(n=2) {{{results(=0=)}}} here.\n", "call_"),
370 (named + "Inline call_double(n=2) and call_double(n=3) here.\n", "call_double(n=3"),
371 // Properties where the call is, not where the block is.
372 (underHeadings + "#+CALL: double()\n", "#+CALL"),
373 (underHeadings + "#+CALL: double() :var n=1\n", "#+CALL"),
374 (underHeadings + "Inline call_double() here.\n", "call_"),
375 ]
376 static let underHeadings = """
377 * Library
378 :PROPERTIES:
379 :header-args: :results verbatim
380 :END:
381 #+NAME: double
382 #+begin_src sh :var n=2
383 echo $((n*2))
384 #+end_src
385 * Use
386 :PROPERTIES:
387 :header-args:sh: :var n=7
388 :header-args: :results raw
389 :END:
390
391 """
392
393 @Test(.enabled(if: ProcessInfo.processInfo.environment["ORGSTAR_SKIP_ORACLE"] == nil))
394 func matchesEmacs() throws {
395 let folder = FileManager.default.temporaryDirectory
396 var emacsCases: [EmacsOracle.Case] = []
397 var ours: [(text: String, failure: String?)] = []
398 for (text, at) in Self.cases {
399 let caret = (text as NSString).range(of: at).location + 2
400 ours.append(try TestBabelRunner.execute(text, caret: caret, directory: folder))
401 emacsCases.append(EmacsOracle.Case(text: text, point: EmacsOracle.point(caret, in: text), form: """
402 (progn
403 (require 'ob-shell)
404 (require 'ob-python)
405 (setq org-confirm-babel-evaluate nil
406 org-babel-python-command "python3"
407 default-directory temporary-file-directory)
408 (org-ctrl-c-ctrl-c))
409 """))
410 }
411 let emacs = try EmacsOracle.run(emacsCases)
412 for (i, (mine, theirs)) in zip(ours, emacs).enumerated() {
413 #expect(mine.text == theirs.text && (mine.failure != nil) == !theirs.error.isEmpty,
414 "case \(i):\n\(Self.cases[i].0)\nours (\(mine.failure ?? "")):\n\(mine.text)\nemacs (\(theirs.error)):\n\(theirs.text)")
415 }
416 }
417}
418
419extension BabelCallTests {
420 @Test func ctrlCCtrlCRunsInlineCalls() {
421 let text = Self.named + "Inline call_double(n=6) here.\n"
422 let caret = (text as NSString).range(of: "call_").location + 2
423 let context = EditContext(revision: 0, text: text, tree: OrgParser.parse(text), selection: [caret..<caret])
424 #expect(ExecuteSrcBlock().applies(in: context))
425 guard case .external(.babel(let job)) = CtrlCCtrlC().run(in: context) else {
426 Issue.record("inline call not run")
427 return
428 }
429 #expect((job.script ?? job.input).contains("echo $((n*2))"))
430 }
431}
432
433struct BabelFileTests {
434 static let cases = [
435 "#+begin_src sh :results file :file out.txt\necho hello\necho there\n#+end_src\n",
436 "#+begin_src sh :results output file :file x.txt :file-desc Desc\necho out\n#+end_src\n",
437 "#+begin_src sh :results file :file y.txt :file-desc\necho y\n#+end_src\n",
438 "#+begin_src python :results file :file p.txt\nreturn [[1,2],[3,4]]\n#+end_src\n",
439 "#+begin_src sh :results file\necho no-file.txt\n#+end_src\n",
440 "#+begin_src sh :results file :file w.txt :output-dir outd\necho w\n#+end_src\n",
441 "#+begin_src sh :file plain.txt\necho not to a file\n#+end_src\n",
442 ]
443
444 @Test(.enabled(if: ProcessInfo.processInfo.environment["ORGSTAR_SKIP_ORACLE"] == nil))
445 func matchesEmacs() throws {
446 for (i, text) in Self.cases.enumerated() {
447 let ours = FileManager.default.temporaryDirectory.appendingPathComponent("orgstar-file-ours-\(UUID().uuidString)")
448 let theirs = FileManager.default.temporaryDirectory.appendingPathComponent("orgstar-file-emacs-\(UUID().uuidString)")
449 for folder in [ours, theirs] { try FileManager.default.createDirectory(at: folder.appendingPathComponent("outd"), withIntermediateDirectories: true) }
450 defer { for folder in [ours, theirs] { try? FileManager.default.removeItem(at: folder) } }
451 let mine = try TestBabelRunner.execute(text, caret: 2, directory: ours)
452 let emacs = try EmacsOracle.run([EmacsOracle.Case(text: text, point: 3, form: """
453 (progn (require 'ob-shell) (require 'ob-python)
454 (setq org-confirm-babel-evaluate nil org-babel-python-command "python3" default-directory \(lispString(theirs.path + "/")))
455 (org-babel-execute-src-block))
456 """)])[0]
457 #expect(mine.text == emacs.text, "case \(i)\nours: \(mine.text) \(mine.failure ?? "")\nemacs: \(emacs.text) \(emacs.error)")
458 for name in ["out.txt", "x.txt", "y.txt", "p.txt", "outd/w.txt", "plain.txt"] {
459 let a = try? String(contentsOf: ours.appendingPathComponent(name), encoding: .utf8)
460 let b = try? String(contentsOf: theirs.appendingPathComponent(name), encoding: .utf8)
461 #expect(a == b, "case \(i) \(name): \(a.debugDescription) vs \(b.debugDescription)")
462 }
463 }
464 }
465
466 @Test func plansDiagrams() throws {
467 let text = "#+begin_src dot :file g.svg\ndigraph { a -> b }\n#+end_src\n"
468 guard case .success(let job) = Babel.plan(EditContext(revision: 0, text: text, tree: OrgParser.parse(text), selection: [2..<2])) else {
469 Issue.record("not planned")
470 return
471 }
472 #expect(job.runner == .process(executable: "/bin/sh", arguments: ["-c", "dot -Tsvg -o 'g.svg'"]))
473 guard case .success(let edits) = Babel.finish(job, output: BabelOutput(stdout: ""), text: text) else { return }
474 let new = edits.sorted { $0.range.lowerBound > $1.range.lowerBound }.reduce(text) { $1.apply(to: $0) }
475 #expect(new.hasSuffix("#+RESULTS:\n[[file:g.svg]]\n"))
476 #expect(Babel.fileResult(job, output: BabelOutput(stdout: "")) == nil)
477 let uml = "#+begin_src plantuml :file u.png\nA -> B\n#+end_src\n"
478 guard case .success(let umlJob) = Babel.plan(EditContext(revision: 0, text: uml, tree: OrgParser.parse(uml), selection: [2..<2])) else { return }
479 #expect(umlJob.input == "@startuml\nA -> B\n@enduml\n")
480 }
481}