krz/orgstar

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

Tests/OrgCoreTests/TangleTests.swift

15f6b0709d88971fb62ed432c3e5b8032643670a
orgstar/Tests/OrgCoreTests/TangleTests.swift history · blame · raw

454 lines · 17715 bytes

  1import Foundation
  2import Testing
  3@testable import OrgCore
  4
  5/// `org-babel-tangle` against Emacs: every file written, with its mode.
  6struct TangleTests {
  7    static let text = #"""
  8        #+PROPERTY: header-args:python :tangle script.py
  9
 10        * Setup
 11        #+begin_src sh :tangle yes
 12        echo one
 13        #+end_src
 14
 15        #+begin_src sh :tangle yes :padline no
 16          echo two
 17            indented
 18
 19        #+end_src
 20
 21        #+begin_src python
 22        x = 1
 23        #+end_src
 24
 25        * COMMENT Skipped
 26        #+begin_src sh :tangle yes
 27        echo skipped
 28        #+end_src
 29
 30        * Archived                                                          :ARCHIVE:
 31        #+begin_src sh :tangle yes
 32        echo archived
 33        #+end_src
 34
 35        * Tools
 36        :PROPERTIES:
 37        :header-args: :tangle bin/tool.sh :mkdirp yes :shebang "#!/bin/sh"
 38        :END:
 39        #+begin_src sh :var name="Ann's" n=3
 40        echo "$name $n"
 41        #+end_src
 42
 43        #+begin_src sh -r
 44        grep x file   (ref:grep)
 45        #+end_src
 46
 47        #+name: helper
 48        #+begin_src elisp :tangle no
 49        (message "hi")
 50        #+end_src
 51
 52        #+begin_src elisp :tangle init.el :noweb yes :var greeting="hello" count=2
 53        <<helper>>
 54        (print greeting)
 55        #+end_src
 56
 57        #+begin_src python :noweb strip-tangle :var items=3.5 label="a\"b"
 58        print(1) <<helper>>
 59        #+end_src
 60
 61        #+begin_src text :tangle notes.txt :tangle-mode o600 :prologue "start" :epilogue "end"
 62        middle
 63        #+end_src
 64
 65        #+begin_src conf :tangle ~/../../tmp/../tmp/orgstar-never/x.conf :tangle-mode u+x,g-r
 66        never written: its folder doesn't exist
 67        #+end_src
 68
 69        #+begin_src sh :tangle no
 70        echo never
 71        #+end_src
 72
 73        """#
 74
 75    static func describe(_ output: Tangle.Output, root: String) -> String {
 76        output.files.sorted { $0.path < $1.path }.map { file in
 77            let relative = file.path.hasPrefix(root + "/") ? String(file.path.dropFirst(root.count + 1)) : file.path
 78            return "== \(relative) \(String(file.mode ?? 0o644, radix: 8))\n" + file.contents
 79        }.joined()
 80    }
 81
 82    @Test func tanglesLikeOrg() throws {
 83        // The conf block's folder doesn't exist, so Emacs stops there; tangling it alone shows
 84        // the rest, and the error is checked apart.
 85        let text = Self.text.replacingOccurrences(of: ":tangle ~/../../tmp/../tmp/orgstar-never/x.conf", with: ":tangle no")
 86        let ns = text as NSString
 87        let cases: [(offset: Int, arg: String, scope: Tangle.Scope)] = [
 88            (0, "nil", .all),
 89            (ns.range(of: "echo two").location, "'(4)", .block(ns.range(of: "echo two").location)),
 90            (ns.range(of: "grep x").location, "'(16)", .target(ns.range(of: "grep x").location)),
 91            (ns.range(of: "echo never").location, "'(4)", .block(ns.range(of: "echo never").location)),
 92        ]
 93        let form = { (arg: String) in """
 94            (let* ((dir (file-truename (make-temp-file "orgstar-tangle" t)))
 95                   (src (expand-file-name "notes.org" dir))
 96                   (text (buffer-string)) (pt (point)) out)
 97              (require 'ob-shell) (require 'ob-python)
 98              (with-temp-file src (insert text))
 99              (with-current-buffer (find-file-noselect src)
100                (goto-char pt)
101                (let ((files (org-babel-tangle \(arg))))
102                  (setq out (mapconcat (lambda (f) (format "== %s %o\\n%s" (file-relative-name f dir) (file-modes f)
103                                                           (with-temp-buffer (insert-file-contents f) (buffer-string))))
104                                       (sort (copy-sequence files) #'string<) ""))))
105              (erase-buffer) (insert out))
106            """ }
107        let results = try EmacsOracle.run(cases.map { EmacsOracle.Case(text: text, point: EmacsOracle.point($0.offset, in: text), form: form($0.arg)) })
108        for (c, theirs) in zip(cases, results) {
109            let ours = try Tangle.run(text: text, path: "/oracle/notes.org", scope: c.scope).get()
110            #expect(Self.describe(ours, root: "/oracle") == theirs.text, "\(c.arg)\nours:\n\(Self.describe(ours, root: "/oracle"))\nemacs:\n\(theirs.text)\n\(theirs.error)")
111        }
112    }
113
114    static let comments = """
115        #+PROPERTY: header-args :comments link
116        Before any heading.
117        #+begin_src sh :tangle top.sh
118        echo top
119        #+end_src
120        * Setup section :tag:
121        Some prose before the block.
122          Indented prose.
123        #+begin_src emacs-lisp :tangle out.el
124        (setq x 1)
125        #+end_src
126        Between text.
127        #+NAME: named-py
128        #+begin_src python :tangle out.py :comments both
129        print(1)
130        #+end_src
131        * TODO [#A] Other [1/2]
132        #+begin_src sh :tangle sub/out.sh :comments org :mkdirp yes
133        echo hi
134        #+end_src
135        C prose one.
136        C prose two.
137        #+begin_src c :tangle out.c :comments both
138        int x;
139        #+end_src
140        #+begin_src js :tangle out.js
141        let y;
142        #+end_src
143        #+begin_src conf :tangle out.conf :comments noweb :noweb yes
144        a=<<named-py>>
145        #+end_src
146        #+begin_src sh :noweb-ref setup
147        echo one
148        #+end_src
149        #+begin_src sh :noweb-ref setup
150        echo two
151        #+end_src
152        #+begin_src sh :tangle refs.sh :noweb yes :comments noweb
153        <<setup>>
154        echo main
155        #+end_src
156        #+begin_src lua :tangle out.lua
157        x = 1
158        #+end_src
159        #+begin_src sh :tangle out.el :comments no
160        not commented
161        #+end_src
162        ** Child
163        :PROPERTIES:
164        :CUSTOM_ID: kid
165        :END:
166        #+begin_src ruby :tangle out.rb
167        puts 1
168        #+end_src
169        #+begin_src sql :tangle out.sql
170        select 1;
171        #+end_src
172        #+begin_src css :tangle out.css
173        a {}
174        #+end_src
175        #+begin_src java :tangle out.java
176        class A {}
177        #+end_src
178        #+begin_src perl :tangle out.pl
179        print 1;
180        #+end_src
181        #+begin_src latex :tangle out.tex
182        \\relax
183        #+end_src
184        #+begin_src scheme :tangle out.scm
185        (display 1)
186        #+end_src
187        #+begin_src makefile :tangle Makefile
188        all:
189        #+end_src
190        #+begin_src toml :tangle out.toml
191        a = 1
192        #+end_src
193        #+begin_src cpp :tangle out.cpp
194        int y;
195        #+end_src
196        #+begin_src html :tangle out.html
197        <p>hi</p>
198        #+end_src
199        #+begin_src octave :tangle out.m
200        x = 1
201        #+end_src
202        #+begin_src pascal :tangle out.pas
203        begin end.
204        #+end_src
205        #+begin_src tex :tangle out.tex2
206        \\relax
207        #+end_src
208        #+begin_src f90 :tangle out.f90
209        print *, 1
210        #+end_src
211        #+begin_src asm :tangle out.s
212        nop
213        #+end_src
214        #+begin_src verilog :tangle out.v
215        module m; endmodule
216        #+end_src
217        #+begin_src nroff :tangle out.1
218        .TH x
219        #+end_src
220        #+begin_src bat :tangle out.bat
221        echo hi
222        #+end_src
223        #+begin_src bash :tangle out.bash
224        echo b
225        #+end_src
226        """ + "\n"
227
228    @Test func commentsLikeOrg() throws {
229        let form = """
230            (let* ((dir (file-truename (make-temp-file "orgstar-tangle" t)))
231                   (src (expand-file-name "notes.org" dir))
232                   (text (buffer-string)) out)
233              (require 'ob-shell) (require 'ob-python) (require 'ob-C) (require 'ob-java)
234              (with-temp-file src (insert text))
235              (with-current-buffer (find-file-noselect src)
236                (let ((files (org-babel-tangle)))
237                  (setq out (mapconcat (lambda (f) (format "== %s %o\\n%s" (file-relative-name f dir) (file-modes f)
238                                                           (with-temp-buffer (insert-file-contents f) (buffer-string))))
239                                       (sort (copy-sequence files) #'string<) ""))))
240              (erase-buffer) (insert out))
241            """
242        let theirs = try EmacsOracle.run([EmacsOracle.Case(text: Self.comments, point: 1, form: form)])[0]
243        let ours = try Tangle.run(text: Self.comments, path: "/oracle/notes.org").get()
244        #expect(Self.describe(ours, root: "/oracle") == theirs.text, "ours:\n\(Self.describe(ours, root: "/oracle"))\nemacs:\n\(theirs.text)\n\(theirs.error)")
245    }
246
247    static let lisp = """
248        #+begin_src sh :tangle (concat "joined" ".sh")
249        echo joined
250        #+end_src
251        #+begin_src sh :tangle (if (eq system-type 'darwin) "mac.sh" "other.sh")
252        echo mac
253        #+end_src
254        #+begin_src sh :tangle (concat (file-name-directory buffer-file-name) "beside.sh")
255        echo beside
256        #+end_src
257        #+begin_src sh :tangle (expand-file-name "sub/deep.sh") :mkdirp yes
258        echo deep
259        #+end_src
260        #+begin_src sh :tangle (file-name-sans-extension (file-name-nondirectory buffer-file-name))
261        echo named after the file
262        #+end_src
263        #+begin_src sh :tangle quoted.sh :prologue "(cd /tmp && ls)"
264        echo quoted
265        #+end_src
266        #+begin_src python -r -l "#(ref:%s)" :tangle refs.py
267        x = 1  #(ref:one)
268        y = 2 (ref:kept)
269        #+end_src
270        #+begin_src python -r :tangle refs.py
271        z = 3 (ref:gone)
272        #+end_src
273        """ + "\n"
274
275    @Test func evaluatesLispHeadersLikeOrg() throws {
276        let form = """
277            (let* ((dir (file-truename (make-temp-file "orgstar-tangle" t)))
278                   (src (expand-file-name "notes.org" dir))
279                   (text (buffer-string)) out)
280              (require 'ob-shell) (require 'ob-python)
281              (with-temp-file src (insert text))
282              (with-current-buffer (find-file-noselect src)
283                (let ((files (org-babel-tangle)))
284                  (setq out (mapconcat (lambda (f) (format "== %s %o\\n%s" (file-relative-name f dir) (file-modes f)
285                                                           (with-temp-buffer (insert-file-contents f) (buffer-string))))
286                                       (sort (copy-sequence files) #'string<) ""))))
287              (erase-buffer) (insert out))
288            """
289        let theirs = try EmacsOracle.run([EmacsOracle.Case(text: Self.lisp, point: 1, form: form)])[0]
290        let ours = try Tangle.run(text: Self.lisp, path: "/oracle/notes.org").get()
291        #expect(Self.describe(ours, root: "/oracle") == theirs.text, "ours:\n\(Self.describe(ours, root: "/oracle"))\nemacs:\n\(theirs.text)\n\(theirs.error)")
292    }
293
294    /// The file names only; `expandsBodiesLikeOrg` checks the bodies.
295    @Test func extensionsLikeOrg() throws {
296        let languages = ["python", "ruby", "perl", "C", "C++", "D", "awk", "sed", "lua", "haskell", "java", "latex", "LilyPond",
297                         "fortran", "clojure", "ocaml", "julia", "sh", "text"]
298        let text = languages.map { "#+begin_src \($0) :tangle yes\n\($0)\n#+end_src\n" }.joined()
299        let features = ["ob-python", "ob-ruby", "ob-perl", "ob-C", "ob-awk", "ob-sed", "ob-lua", "ob-haskell", "ob-java", "ob-latex",
300                        "ob-lilypond", "ob-fortran", "ob-clojure", "ob-ocaml", "ob-julia", "ob-shell"]
301        let form = """
302            (let* ((dir (file-truename (make-temp-file "orgstar-tangle" t)))
303                   (src (expand-file-name "notes.org" dir))
304                   (text (buffer-string)) out)
305              \(features.map { "(require '\($0))" }.joined(separator: " ")) (setq org-babel-clojure-backend 'babashka)
306              (with-temp-file src (insert text))
307              (with-current-buffer (find-file-noselect src)
308                (let ((files (org-babel-tangle)))
309                  (setq out (mapconcat (lambda (f) (concat (file-relative-name f dir) "\\n")) (sort (copy-sequence files) #'string<) ""))))
310              (erase-buffer) (insert out))
311            """
312        let theirs = try EmacsOracle.run([EmacsOracle.Case(text: text, point: 1, form: form)])[0]
313        let ours = try Tangle.run(text: text, path: "/oracle/notes.org").get()
314        let names = ours.files.map { ($0.path as NSString).lastPathComponent + "\n" }.sorted().joined()
315        #expect(names == theirs.text, "ours:\n\(names)\nemacs:\n\(theirs.text)\n\(theirs.error)")
316    }
317
318    static let expanded = #"""
319        #+NAME: tbl
320        | a | b   |
321        |---+-----|
322        | 1 | 2.5 |
323        | 3 | 4   |
324
325        #+NAME: items
326        - x
327        - y
328
329        #+NAME: ints
330        | 1 | 2 |
331        | 3 | 4 |
332
333        * C
334        :PROPERTIES:
335        :includes: <never.h>
336        :imports: never.d
337        :END:
338        #+begin_src C :tangle out.c
339        printf("hi\n");
340        #+end_src
341        #+begin_src C :tangle out.c :includes <stdio.h> <math.h> :defines N 5 M 6 :var n=3 x=2.5 s="str" l=items
342        printf("%d\n", n);
343        #+end_src
344        #+begin_src C :tangle out.c :includes '(<stdio.h> "local.h") :defines '("A 1") :var t=tbl
345        int main() { return t_h(0, "a"); }
346        #+end_src
347        #+begin_src C :tangle out.c :main no :prologue "// pro" :epilogue "// epi" :var m=ints
348        int f(void) { return 1; }
349        #+end_src
350        #+begin_src C++ :tangle out.cpp :namespaces std :includes <iostream>
351        cout << 1;
352        #+end_src
353        #+begin_src cpp :tangle out.cpp
354          INT main(void) {}
355        #+end_src
356        #+begin_src D :tangle out.d :imports std.math :var n=3 s="d" l=items t=tbl
357        writeln(n);
358        #+end_src
359        #+begin_src D :tangle out.d :main no
360        void f() {}
361        #+end_src
362        * Java
363        #+begin_src java :tangle Main.java
364        System.out.println(1);
365            if (true) {
366                System.out.println(2);
367            }
368        #+end_src
369        #+begin_src java :tangle Foo.java :classname com.x.Foo :imports java.util.List java.util.Map :var n=3 x=2.5 s="s" l=items t=ints
370        System.out.println(n);
371        #+end_src
372        #+begin_src java :tangle Pkg.java
373        package org.y;
374        import java.util.List;
375
376        public class Pkg {
377            public static void main(String[] args) {
378                System.out.println(1);
379            }
380        }
381        #+end_src
382        #+begin_src java :tangle Methods.java :var n=1
383        static int twice(int x) {
384            return 2 * x;
385        }
386        #+end_src
387        #+begin_src java :tangle Pro.java :prologue "int a = 1;" :epilogue "int b = 2;"
388        int c = 3;
389        #+end_src
390        * Fortran
391        #+begin_src fortran :tangle out.F90
392        print *, 1
393        #+end_src
394        #+begin_src fortran :tangle out.F90 :var n=3 x=2.5 s="str" l='(1 2) m=ints :includes "a.h" :defines N 5 :prologue "! pro"
395        print *, n
396        #+end_src
397        #+begin_src fortran :tangle out.F90
398        PROGRAM hello
399        print *, 2
400        end program hello
401        #+end_src
402        #+begin_src fortran :tangle out.F90 :main no :prologue "! pro" :epilogue "! epi" :var n=1
403        subroutine s
404        end subroutine s
405        #+end_src
406        * Clojure
407        #+begin_src clojure :tangle out.clj
408        (+ 1 2)
409        #+end_src
410        #+begin_src clojure :tangle out.clj :var n=3 s="a\"b" l=items t=tbl
411        ;; a comment
412        (+ n 1)
413        #+end_src
414        #+begin_src clojure :tangle out.clj :ns my.ns :results output
415          (println 1)
416        #+end_src
417        #+begin_src clojure :tangle out.clj :results pp
418        {:a 1}
419        #+end_src
420        * Python
421        #+begin_src python :tangle out.py :var t=tbl
422        print(t)
423        #+end_src
424        """# + "\n"
425
426    @Test func expandsBodiesLikeOrg() throws {
427        let form = """
428            (let* ((dir (file-truename (make-temp-file "orgstar-tangle" t)))
429                   (src (expand-file-name "notes.org" dir))
430                   (text (buffer-string)) out)
431              (require 'ob-C) (require 'ob-java) (require 'ob-fortran) (require 'ob-clojure) (require 'ob-python)
432              (setq org-babel-clojure-backend 'babashka)
433              (with-temp-file src (insert text))
434              (with-current-buffer (find-file-noselect src)
435                (let ((files (org-babel-tangle)))
436                  (setq out (mapconcat (lambda (f) (format "== %s %o\\n%s" (file-relative-name f dir) (file-modes f)
437                                                           (with-temp-buffer (insert-file-contents f) (buffer-string))))
438                                       (sort (copy-sequence files) #'string<) ""))))
439              (erase-buffer) (insert out))
440            """
441        let theirs = try EmacsOracle.run([EmacsOracle.Case(text: Self.expanded, point: 1, form: form)])[0]
442        let ours = try Tangle.run(text: Self.expanded, path: "/oracle/notes.org").get()
443        #expect(Self.describe(ours, root: "/oracle") == theirs.text, "ours:\n\(Self.describe(ours, root: "/oracle"))\nemacs:\n\(theirs.text)\n\(theirs.error)")
444    }
445
446    @Test func refusesWhatItCantDo() {
447        for (language, header) in [("haskell", ":comments link"), ("sh", ":tangle (concat user-emacs-directory \"a.sh\")"), ("sh", ":tangle-mode go+q")] {
448            let text = "#+begin_src \(language) :tangle yes \(header)\necho\n#+end_src\n"
449            guard case .failure = Tangle.run(text: text, path: "/x/a.org") else { Issue.record("tangled with \(header)"); continue }
450        }
451        let modes = ["(identity #o755)": 0o755, "o700": 0o700, "rw-r-----": 0o640, "u+x": 0o744, "a=r,u+w": 0o644, "go-r": 0o600]
452        for (value, mode) in modes { #expect((try? Tangle.fileMode(value)) == mode, "\(value)") }
453    }
454}