krz/orgstar

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

Tests/OrgCoreTests/BabelCompiledTests.swift

3a3dedba062790e1580c66d38606eda4b02cb501
orgstar/Tests/OrgCoreTests/BabelCompiledTests.swift history · blame · raw

225 lines · 17842 bytes

  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}