Tests/OrgCoreTests/TangleTests.swift
454 lines · 17715 bytes
10 symbols in this file
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}