Tests/OrgCoreTests/BabelCompiledTests.swift
225 lines · 17842 bytes
6 symbols in this file
1import Foundation
2import Testing
3@testable import OrgCore
4
5/// C, C++, D, Java, Fortran and Clojure blocks (ob-C, ob-java, ob-fortran, ob-clojure).
6struct BabelCompiledTests {
7 static func installed(_ program: String) -> Bool {
8 (ProcessInfo.processInfo.environment["PATH"] ?? "").split(separator: ":").contains {
9 FileManager.default.isExecutableFile(atPath: "\($0)/\(program)")
10 }
11 }
12
13 static let table = "#+NAME: tbl\n| a | b |\n|---+---|\n| 1 | 2 |\n| 3 | 4 |\n\n"
14
15 /// Compiled and run here and in Emacs.
16 static let cases: [String] = [
17 "#+begin_src C :includes <stdio.h>\nprintf(\"hello\\n\");\n#+end_src\n",
18 "#+begin_src C :includes <stdio.h> :results output\nprintf(\"a b\\nc d\\n\");\n#+end_src\n",
19 "#+begin_src C :includes <stdio.h>\nprintf(\"a b\\nc d\\n\");\n#+end_src\n",
20 "#+begin_src C :includes <stdio.h>\nprintf(\"1\\t2\\n3\\t4\\n\");\n#+end_src\n",
21 "#+begin_src C :includes <stdio.h>\nprintf(\" x\\n y\\n\");\n#+end_src\n",
22 "#+begin_src C :includes <stdio.h> :results output\nprintf(\" x\\n y\\n\");\n#+end_src\n",
23 "#+begin_src C :includes <stdio.h> :results verbatim\nprintf(\"007\\n\");\n#+end_src\n",
24 "#+begin_src C :includes <stdio.h> :results raw\nprintf(\"*bold*\\n\");\n#+end_src\n",
25 "#+begin_src C :includes '(<stdio.h> <string.h>) :var n=3 x=2.5 s=\"hi\"\nprintf(\"%d %.1f %s %zu\\n\", n, x, s, strlen(s));\n#+end_src\n",
26 "#+begin_src C :includes <stdio.h> :var l='(1 2 3)\nint s = 0; for (int i = 0; i < l_cols; i++) s += l[i]; printf(\"%d\\n\", s);\n#+end_src\n",
27 table + "#+begin_src C :var t=tbl :includes <stdio.h>\nfor (int i = 0; i < t_rows; i++) printf(\"%d %d\\n\", t[i][0], t_h(i, \"b\"));\n#+end_src\n",
28 table + "#+begin_src C :var t=tbl :includes <stdio.h> :colnames yes\nfor (int i = 0; i < t_rows; i++) printf(\"%d %d\\n\", t[i][1], t[i][0]);\n#+end_src\n",
29 "#+NAME: rt\n| r1 | 1 |\n| r2 | 2 |\n\n#+begin_src C :var t=rt :includes <stdio.h> :rownames yes\nfor (int i = 0; i < t_rows; i++) printf(\"%d\\n\", t[i][0] * 10);\n#+end_src\n",
30 "#+begin_src C :includes <stdio.h> :main no\nint main() { printf(\"own main\\n\"); return 0; }\n#+end_src\n",
31 "#+begin_src C :includes <stdio.h> :main no\nint main(int argc, char **argv) { printf(\"%d\\n\", argc); return 0; }\n#+end_src\n",
32 "#+begin_src C :includes <stdio.h> :cmdline one \"two words\" :results output\nint main(int argc, char **argv) { for (int i = 1; i < argc; i++) printf(\"[%s]\\n\", argv[i]); return 0; }\n#+end_src\n",
33 "#+begin_src C :includes <stdio.h> :flags -DN=5 -O2\nprintf(\"%d\\n\", N * 2);\n#+end_src\n",
34 "#+begin_src C :includes <stdio.h> <math.h> :libs -lm\nprintf(\"%.3f\\n\", sqrt(2.0));\n#+end_src\n",
35 "#+begin_src C :includes <stdio.h> :defines N 3 M 4\nprintf(\"%d\\n\", N * M);\n#+end_src\n",
36 "#+begin_src C :includes <stdio.h> :prologue int k = 6; :epilogue printf(\"%d\\n\", k);\nk *= 7;\n#+end_src\n",
37 "#+begin_src C :includes <stdio.h> <unistd.h> :dir /tmp :results output\nchar b[1024]; getcwd(b, sizeof b); printf(\"%s\\n\", b);\n#+end_src\n",
38 "#+begin_src C :includes <stdio.h>\nint c = getchar(); printf(\"%d\\n\", c);\n#+end_src\n",
39 "#+begin_src C :includes <stdio.h> :results output\nprintf(\"out\\n\"); fprintf(stderr, \"err\\n\"); return 3;\n#+end_src\n",
40 "#+begin_src C\nsyntax error\n#+end_src\n",
41 "#+begin_src C :includes <stdio.h> :session s\nprintf(\"no session\\n\");\n#+end_src\n",
42 "#+begin_src C++ :includes <iostream> :namespaces std\ncout << \"c++ \" << 6 * 7 << endl;\n#+end_src\n",
43 "#+begin_src cpp :includes <iostream> :var v='(1.5 2.5)\nstd::cout << v[0] + v[1] << std::endl;\n#+end_src\n",
44 "#+begin_src C++ :includes <iostream> <vector> :results output :wrap example\nstd::vector<int> v{1, 2, 3}; for (int x : v) std::cout << x << \"\\n\";\n#+end_src\n",
45 "* Heading\n:PROPERTIES:\n:libs: -lm\n:END:\n#+begin_src C :includes <stdio.h> <math.h>\nprintf(\"%.0f\\n\", pow(2, 10));\n#+end_src\n",
46 "#+NAME: words\n| x | yy |\n| zzz | w |\n\n#+begin_src C :var t=words :includes <stdio.h> <string.h>\nprintf(\"%s %zu\\n\", t[1][0], strlen(t[0][1]));\n#+end_src\n",
47 "#+begin_src C :includes <stdio.h> :results output list\nprintf(\"a\\nb\\n\");\n#+end_src\n",
48 "#+begin_src C :includes <stdio.h> :var x=gen\nprintf(\"%d\\n\", x + 1);\n#+end_src\n\n#+NAME: gen\n#+begin_src C :includes <stdio.h>\nprintf(\"41\\n\");\n#+end_src\n",
49 ]
50
51 static let emacsSetup = """
52 (require 'ob-C) (require 'ob-java) (require 'ob-fortran) (require 'ob-clojure)
53 (setq org-confirm-babel-evaluate nil org-babel-clojure-backend 'babashka)
54 """
55
56 @Test(.enabled(if: ProcessInfo.processInfo.environment["ORGSTAR_SKIP_ORACLE"] == nil && installed("gcc") && installed("g++")))
57 func compiledMatchEmacs() throws {
58 let folder = FileManager.default.temporaryDirectory
59 var emacsCases: [EmacsOracle.Case] = []
60 var ours: [(text: String, failure: String?)] = []
61 for text in Self.cases {
62 let caret = (text as NSString).range(of: "#+begin_src").location + 2
63 ours.append(try TestBabelRunner.execute(text, caret: caret, directory: folder))
64 emacsCases.append(EmacsOracle.Case(text: text, point: EmacsOracle.point(caret, in: text), form: """
65 (progn \(Self.emacsSetup)
66 (setq default-directory temporary-file-directory)
67 (org-babel-execute-src-block))
68 """))
69 }
70 let emacs = try EmacsOracle.run(emacsCases)
71 for (i, (mine, theirs)) in zip(ours, emacs).enumerated() {
72 #expect(mine.text == theirs.text, "case \(i):\n\(Self.cases[i])\nours (\(mine.failure ?? "")):\n\(mine.text)\nemacs (\(theirs.error)):\n\(theirs.text)")
73 }
74 }
75
76 /// Blocks whose toolchain may not be installed, with the output the program would give:
77 /// the source each writes and the result each inserts, compared with Emacs's.
78 static let canned: [(text: String, output: String)] = [
79 ("#+begin_src D :var n=3\nwriteln(n);\n#+end_src\n", "3\n"),
80 ("#+begin_src D :imports std.math :var x=2.0\nwriteln(sqrt(x));\n#+end_src\n", "1.41421\n"),
81 (table + "#+begin_src D :var t=tbl\nforeach (r; t) writeln(r[0], \" \", t_h(0, \"b\"));\n#+end_src\n", "1 2\n3 2\n"),
82 ("* H\n:PROPERTIES:\n:IMPORTS: std.range\n:END:\n#+begin_src D :cmdline x y :flags -O\nwriteln(iota(3));\n#+end_src\n", "[0, 1, 2]\n"),
83 ("#+begin_src java\nSystem.out.println(\"hi\");\n#+end_src\n", "hi\n"),
84 ("#+begin_src java :results output\nSystem.out.println(\"a b\");\nSystem.out.println(\"c d\");\n#+end_src\n", "a b\nc d\n"),
85 ("#+begin_src java :results value\nreturn 42;\n#+end_src\n", "42"),
86 ("#+begin_src java :results value\nreturn \"hello\";\n#+end_src\n", "\"hello\""),
87 ("#+begin_src java :results value\nreturn Arrays.asList(1, 2, 3);\n#+end_src\n", "[1,2,3]"),
88 ("#+begin_src java :results value\nreturn Arrays.asList(Arrays.asList(1, 2), Arrays.asList(3, 4));\n#+end_src\n", "[[1,2],[3,4]]"),
89 ("#+begin_src java :results value\nreturn new Object[] {1, null};\n#+end_src\n", "[1,null]"),
90 ("#+begin_src java :results value\nreturn new Object[] {new Object[] {\"x\", 1}, null, new Object[] {\"y\", 2}};\n#+end_src\n", "[[\"x\",1],null,[\"y\",2]]"),
91 ("#+begin_src java :results value verbatim\nreturn Arrays.asList(1, 2);\n#+end_src\n", "[1,2]"),
92 (table + "#+begin_src java :var t=tbl :results value\nreturn t.get(1).get(0);\n#+end_src\n", "3"),
93 ("#+begin_src java :var n=3 s=\"x\" x=1.5 :imports java.util.ArrayList\nSystem.out.println(n + s + x);\n#+end_src\n", "3x1.5\n"),
94 ("#+begin_src java :classname pkg.Hello\nSystem.out.println(\"pkg\");\n#+end_src\n", "pkg\n"),
95 ("#+begin_src java\npackage org.ex;\n\npublic class Thing {\n public static void main(String[] args) {\n System.out.println(\"thing\");\n }\n}\n#+end_src\n", "thing\n"),
96 ("#+begin_src java :results value\npublic class Calc {\n static int twice(int x) {\n return x * 2;\n }\n public static void main(String[] args) {\n return twice(21);\n }\n}\n#+end_src\n", "42"),
97 ("#+begin_src java\npublic class NoMain {\n static int x = 1;\n}\n#+end_src\n", "success"),
98 ("#+begin_src java :dir /tmp :cmdline -ea :cmdargs one two :cmpflag -g\nSystem.out.println(args.length);\n#+end_src\n", "2\n"),
99 ("#+begin_src fortran\nprint *, \"hello\"\n#+end_src\n", " hello\n"),
100 ("#+begin_src fortran :var n=3\nprint *, n * 14\n#+end_src\n", " 42\n"),
101 ("#+begin_src fortran :results output\nprint *, 1, 2\nprint *, 3, 4\n#+end_src\n", " 1 2\n 3 4\n"),
102 ("#+begin_src fortran\nprint *, 1, 2\nprint *, 3, 4\n#+end_src\n", " 1 2\n 3 4\n"),
103 ("#+begin_src fortran :results verbatim\nprint *, \"007\"\n#+end_src\n", " 007\n"),
104 ("#+begin_src fortran :var x=2.5 s=\"ab\" :flags -O2 :cmdline q\nprint *, x, s\n#+end_src\n", " 2.50000000 ab\n"),
105 (table + "#+begin_src fortran :var t=tbl\nprint *, t(2,1)\n#+end_src\n", " 3.00000000 \n"),
106 ("#+begin_src fortran :main no\nprogram p\nprint *, 'p'\nend program p\n#+end_src\n", " p\n"),
107 ("#+begin_src fortran :includes \"x.inc\" :defines N 2\nprint *, N\n#+end_src\n", " 2\n"),
108 ("* H\n:PROPERTIES:\n:INCLUDES: \"y.inc\"\n:END:\n#+begin_src fortran\nprint *, 1\n#+end_src\n", " 1\n"),
109 ("#+begin_src clojure\n(+ 40 2)\n#+end_src\n", "42\n"),
110 ("#+begin_src clojure\n(str \"a\" \"b\")\n#+end_src\n", "\"ab\"\n"),
111 ("#+begin_src clojure\n[1 2 3]\n#+end_src\n", "[1 2 3]\n"),
112 ("#+begin_src clojure\n'(1 2)\n#+end_src\n", "(1 2)\n"),
113 ("#+begin_src clojure\n{:a 1}\n#+end_src\n", "{:a 1}\n"),
114 ("#+begin_src clojure\nnil\n#+end_src\n", "nil\n"),
115 ("#+begin_src clojure :results output\n(println \"a\")\n(println \"b\")\n#+end_src\n", "a\nb\n"),
116 ("#+begin_src clojure :var x=3 l='(1 2)\n;; comment\n(apply + x l)\n#+end_src\n", "6\n"),
117 ("#+begin_src clojure :results pp\n{:a 1 :b 2}\n#+end_src\n", "{:a 1, :b 2}\n"),
118 ("#+begin_src clojure :ns demo :results verbatim\n(str *ns*)\n#+end_src\n", "\"demo\"\n"),
119 (table + "#+begin_src clojure :var t=tbl\n(count t)\n#+end_src\n", "2\n"),
120 ]
121
122 static let sourceMark = "\n-----SOURCE-----\n"
123
124 @Test(.enabled(if: ProcessInfo.processInfo.environment["ORGSTAR_SKIP_ORACLE"] == nil))
125 func cannedMatchEmacs() throws {
126 let folder = FileManager.default.temporaryDirectory.appendingPathComponent("orgstar-compiled-\(UUID().uuidString)")
127 try FileManager.default.createDirectory(at: folder, withIntermediateDirectories: true)
128 defer { try? FileManager.default.removeItem(at: folder) }
129 var emacsCases: [EmacsOracle.Case] = []
130 var ours: [String] = []
131 for (text, output) in Self.canned {
132 let caret = (text as NSString).range(of: "#+begin_src").location + 2
133 let context = EditContext(revision: 0, text: text, tree: OrgParser.parse(text), selection: [caret..<caret])
134 switch Babel.plan(context) {
135 case .failure(.message(let message)):
136 ours.append("failed: \(message)")
137 case .success(let job):
138 guard case .success(let edits) = Babel.finish(job, output: BabelOutput(stdout: output, resultFile: output), text: text) else {
139 ours.append("not finished")
140 break
141 }
142 let new = edits.sorted { $0.range.lowerBound > $1.range.lowerBound }.reduce(text) { $1.apply(to: $0) }
143 ours.append(new + Self.sourceMark + job.input)
144 }
145 emacsCases.append(EmacsOracle.Case(text: text, point: EmacsOracle.point(caret, in: text), form: """
146 (let (source)
147 \(Self.emacsSetup)
148 (setq default-directory \(Babel.lispString(folder.path + "/")))
149 (advice-add 'org-babel-eval :override (lambda (&rest _) \(Babel.lispString(output))) '((name . canned)))
150 (advice-add 'org-babel-eval-read-file :override (lambda (&rest _) \(Babel.lispString(output))) '((name . canned)))
151 (advice-add 'write-region :before
152 (lambda (start end file &rest _)
153 (when (string-match-p "\\\\(C-src-\\\\|fortran-src-\\\\|clojure-cmd-script-\\\\|\\\\.java\\\\'\\\\)" file)
154 (setq source (cond ((stringp start) start) (start (buffer-substring-no-properties start end)) (t (buffer-string))))))
155 '((name . capture)))
156 (unwind-protect (org-babel-execute-src-block)
157 (advice-remove 'org-babel-eval 'canned)
158 (advice-remove 'org-babel-eval-read-file 'canned)
159 (advice-remove 'write-region 'capture))
160 (goto-char (point-max))
161 (insert \(Babel.lispString(Self.sourceMark))
162 (replace-regexp-in-string "FileWriter(\\"[^\\"]*\\")" \(Babel.lispString("FileWriter(\"\(Babel.resultFileToken)\")")) (or source "") t t)))
163 """))
164 }
165 let emacs = try EmacsOracle.run(emacsCases)
166 for (i, (mine, theirs)) in zip(ours, emacs).enumerated() {
167 #expect(mine == theirs.text, "case \(i):\n\(Self.canned[i].text)\nours:\n\(mine)\nemacs (\(theirs.error)):\n\(theirs.text)")
168 }
169 }
170
171 static func job(_ text: String) -> BabelJob? {
172 let context = EditContext(revision: 0, text: text, tree: OrgParser.parse(text), selection: [2..<2])
173 guard case .success(let job) = Babel.plan(context) else { return nil }
174 return job
175 }
176
177 @Test func plansCompileAndRun() throws {
178 let c = try #require(Self.job("#+begin_src C :flags -O2 :libs -lm :cmdline a b\nreturn 0;\n#+end_src\n"))
179 #expect(c.runner == .process(executable: "/bin/sh", arguments: [Babel.scriptFileToken]))
180 #expect(c.programs == [["gcc"]])
181 #expect(c.script == "d=$(mktemp -d) || exit 1\ncat > \"$d/C-src.c\"\necho | gcc -o \"$d/C-bin\" -O2 \"$d/C-src.c\" -lm > /dev/null\necho | \"$d/C-bin\" a b\ns=$?\nrm -rf \"$d\"\nexit $s\n")
182 #expect(c.input == "\n\n\n\n\n\n\nint main() {\nreturn 0;\nreturn 0;\n}\n\n\n")
183 let cpp = try #require(Self.job("#+begin_src C++\nreturn 0;\n#+end_src\n"))
184 #expect(cpp.programs == [["g++"]])
185 #expect(cpp.script?.contains("echo | g++ -o \"$d/C-bin\" \"$d/C-src.cpp\" > /dev/null\n") == true)
186
187 let d = try #require(Self.job("#+begin_src D :flags -O :cmdline x\nwriteln(1);\n#+end_src\n"))
188 #expect(d.programs == [["rdmd"]])
189 #expect(d.script == "d=$(mktemp -d) || exit 1\ncat > \"$d/C-src.d\"\necho | rdmd -O \"$d/C-src.d\" x\ns=$?\nrm -rf \"$d\"\nexit $s\n")
190
191 let fortran = try #require(Self.job("#+begin_src fortran :flags -O2 :cmdline q\nprint *, 1\n#+end_src\n"))
192 #expect(fortran.programs == [["gfortran"]])
193 #expect(fortran.script == "d=$(mktemp -d) || exit 1\ncat > \"$d/fortran-src.F90\"\necho | gfortran -o \"$d/fortran-bin\" -O2 \"$d/fortran-src.F90\" > /dev/null\n"
194 + "echo | \"$d/fortran-bin\" q\ns=$?\nrm -rf \"$d\"\nexit $s\n")
195
196 // Java's default `:dir .`: compiled in the run's folder, under the package's folders.
197 let java = try #require(Self.job("#+begin_src java :classname org.ex.Hello :cmpflag -g :cmdline -ea :cmdargs 1 2\nSystem.out.println(1);\n#+end_src\n"))
198 #expect(java.programs == [["javac"], ["java"]])
199 #expect(java.directory == ".")
200 #expect(java.finish.output)
201 #expect(java.script == "mkdir -p 'org/ex/'\ncat > 'org/ex/Hello.java'\necho | { javac -g 'org/ex/Hello.java' && java -cp \"$PWD/\" -ea org.ex.Hello 1 2; }\n")
202 #expect(java.input.hasPrefix("package org.ex;\n"))
203 let value = try #require(Self.job("#+begin_src java :results value :javac javac -Xlint :java /opt/java -server\nreturn 1;\n#+end_src\n"))
204 #expect(value.programs == [["javac"], ["/opt/java"]])
205 #expect(value.input.contains("new FileWriter(\"\(Babel.resultFileToken)\")"))
206 #expect(value.input.contains("public static Object _main(String[] args) {"))
207 #expect(value.script?.contains("echo | { javac -Xlint 'Main.java' && /opt/java -server -cp \"$PWD/\" Main ; }") == true)
208
209 let clojure = try #require(Self.job("#+begin_src clojure\n(+ 1 2)\n#+end_src\n"))
210 #expect(clojure.programs == [["bb", "clojure"]])
211 #expect(clojure.input == "(prn (binding [*out* (java.io.StringWriter.)](+ 1 2)))")
212 #expect(clojure.script == "d=$(mktemp -d) || exit 1\ncat > \"$d/clojure-cmd-script.clj\"\n"
213 + "if command -v bb > /dev/null 2>&1; then echo | bb \"$d/clojure-cmd-script.clj\"; else echo | clojure -M \"$d/clojure-cmd-script.clj\"; fi\n"
214 + "s=$?\nrm -rf \"$d\"\nexit $s\n")
215 let cli = try #require(Self.job("#+begin_src clojure :backend clojure-cli\n1\n#+end_src\n"))
216 #expect(cli.programs == [["clojure"]])
217 #expect(cli.script?.contains("echo | clojure -M \"$d/clojure-cmd-script.clj\"\n") == true)
218 let cider = "#+begin_src clojure :backend cider\n1\n#+end_src\n"
219 guard case .failure(.message(let message)) = Babel.plan(EditContext(revision: 0, text: cider, tree: OrgParser.parse(cider), selection: [2..<2])) else {
220 Issue.record("planned cider")
221 return
222 }
223 #expect(message == "The cider backend for clojure needs Emacs; Orgstar runs babashka or clojure-cli.")
224 }
225}