import Foundation import Testing @testable import OrgCore /// `org-babel-tangle` against Emacs: every file written, with its mode. struct TangleTests { static let text = #""" #+PROPERTY: header-args:python :tangle script.py * Setup #+begin_src sh :tangle yes echo one #+end_src #+begin_src sh :tangle yes :padline no echo two indented #+end_src #+begin_src python x = 1 #+end_src * COMMENT Skipped #+begin_src sh :tangle yes echo skipped #+end_src * Archived :ARCHIVE: #+begin_src sh :tangle yes echo archived #+end_src * Tools :PROPERTIES: :header-args: :tangle bin/tool.sh :mkdirp yes :shebang "#!/bin/sh" :END: #+begin_src sh :var name="Ann's" n=3 echo "$name $n" #+end_src #+begin_src sh -r grep x file (ref:grep) #+end_src #+name: helper #+begin_src elisp :tangle no (message "hi") #+end_src #+begin_src elisp :tangle init.el :noweb yes :var greeting="hello" count=2 <> (print greeting) #+end_src #+begin_src python :noweb strip-tangle :var items=3.5 label="a\"b" print(1) <> #+end_src #+begin_src text :tangle notes.txt :tangle-mode o600 :prologue "start" :epilogue "end" middle #+end_src #+begin_src conf :tangle ~/../../tmp/../tmp/orgstar-never/x.conf :tangle-mode u+x,g-r never written: its folder doesn't exist #+end_src #+begin_src sh :tangle no echo never #+end_src """# static func describe(_ output: Tangle.Output, root: String) -> String { output.files.sorted { $0.path < $1.path }.map { file in let relative = file.path.hasPrefix(root + "/") ? String(file.path.dropFirst(root.count + 1)) : file.path return "== \(relative) \(String(file.mode ?? 0o644, radix: 8))\n" + file.contents }.joined() } @Test func tanglesLikeOrg() throws { // The conf block's folder doesn't exist, so Emacs stops there; tangling it alone shows // the rest, and the error is checked apart. let text = Self.text.replacingOccurrences(of: ":tangle ~/../../tmp/../tmp/orgstar-never/x.conf", with: ":tangle no") let ns = text as NSString let cases: [(offset: Int, arg: String, scope: Tangle.Scope)] = [ (0, "nil", .all), (ns.range(of: "echo two").location, "'(4)", .block(ns.range(of: "echo two").location)), (ns.range(of: "grep x").location, "'(16)", .target(ns.range(of: "grep x").location)), (ns.range(of: "echo never").location, "'(4)", .block(ns.range(of: "echo never").location)), ] let form = { (arg: String) in """ (let* ((dir (file-truename (make-temp-file "orgstar-tangle" t))) (src (expand-file-name "notes.org" dir)) (text (buffer-string)) (pt (point)) out) (require 'ob-shell) (require 'ob-python) (with-temp-file src (insert text)) (with-current-buffer (find-file-noselect src) (goto-char pt) (let ((files (org-babel-tangle \(arg)))) (setq out (mapconcat (lambda (f) (format "== %s %o\\n%s" (file-relative-name f dir) (file-modes f) (with-temp-buffer (insert-file-contents f) (buffer-string)))) (sort (copy-sequence files) #'string<) "")))) (erase-buffer) (insert out)) """ } let results = try EmacsOracle.run(cases.map { EmacsOracle.Case(text: text, point: EmacsOracle.point($0.offset, in: text), form: form($0.arg)) }) for (c, theirs) in zip(cases, results) { let ours = try Tangle.run(text: text, path: "/oracle/notes.org", scope: c.scope).get() #expect(Self.describe(ours, root: "/oracle") == theirs.text, "\(c.arg)\nours:\n\(Self.describe(ours, root: "/oracle"))\nemacs:\n\(theirs.text)\n\(theirs.error)") } } static let comments = """ #+PROPERTY: header-args :comments link Before any heading. #+begin_src sh :tangle top.sh echo top #+end_src * Setup section :tag: Some prose before the block. Indented prose. #+begin_src emacs-lisp :tangle out.el (setq x 1) #+end_src Between text. #+NAME: named-py #+begin_src python :tangle out.py :comments both print(1) #+end_src * TODO [#A] Other [1/2] #+begin_src sh :tangle sub/out.sh :comments org :mkdirp yes echo hi #+end_src C prose one. C prose two. #+begin_src c :tangle out.c :comments both int x; #+end_src #+begin_src js :tangle out.js let y; #+end_src #+begin_src conf :tangle out.conf :comments noweb :noweb yes a=<> #+end_src #+begin_src sh :noweb-ref setup echo one #+end_src #+begin_src sh :noweb-ref setup echo two #+end_src #+begin_src sh :tangle refs.sh :noweb yes :comments noweb <> echo main #+end_src #+begin_src lua :tangle out.lua x = 1 #+end_src #+begin_src sh :tangle out.el :comments no not commented #+end_src ** Child :PROPERTIES: :CUSTOM_ID: kid :END: #+begin_src ruby :tangle out.rb puts 1 #+end_src #+begin_src sql :tangle out.sql select 1; #+end_src #+begin_src css :tangle out.css a {} #+end_src #+begin_src java :tangle out.java class A {} #+end_src #+begin_src perl :tangle out.pl print 1; #+end_src #+begin_src latex :tangle out.tex \\relax #+end_src #+begin_src scheme :tangle out.scm (display 1) #+end_src #+begin_src makefile :tangle Makefile all: #+end_src #+begin_src toml :tangle out.toml a = 1 #+end_src #+begin_src cpp :tangle out.cpp int y; #+end_src #+begin_src html :tangle out.html

hi

#+end_src #+begin_src octave :tangle out.m x = 1 #+end_src #+begin_src pascal :tangle out.pas begin end. #+end_src #+begin_src tex :tangle out.tex2 \\relax #+end_src #+begin_src f90 :tangle out.f90 print *, 1 #+end_src #+begin_src asm :tangle out.s nop #+end_src #+begin_src verilog :tangle out.v module m; endmodule #+end_src #+begin_src nroff :tangle out.1 .TH x #+end_src #+begin_src bat :tangle out.bat echo hi #+end_src #+begin_src bash :tangle out.bash echo b #+end_src """ + "\n" @Test func commentsLikeOrg() throws { let form = """ (let* ((dir (file-truename (make-temp-file "orgstar-tangle" t))) (src (expand-file-name "notes.org" dir)) (text (buffer-string)) out) (require 'ob-shell) (require 'ob-python) (require 'ob-C) (require 'ob-java) (with-temp-file src (insert text)) (with-current-buffer (find-file-noselect src) (let ((files (org-babel-tangle))) (setq out (mapconcat (lambda (f) (format "== %s %o\\n%s" (file-relative-name f dir) (file-modes f) (with-temp-buffer (insert-file-contents f) (buffer-string)))) (sort (copy-sequence files) #'string<) "")))) (erase-buffer) (insert out)) """ let theirs = try EmacsOracle.run([EmacsOracle.Case(text: Self.comments, point: 1, form: form)])[0] let ours = try Tangle.run(text: Self.comments, path: "/oracle/notes.org").get() #expect(Self.describe(ours, root: "/oracle") == theirs.text, "ours:\n\(Self.describe(ours, root: "/oracle"))\nemacs:\n\(theirs.text)\n\(theirs.error)") } static let lisp = """ #+begin_src sh :tangle (concat "joined" ".sh") echo joined #+end_src #+begin_src sh :tangle (if (eq system-type 'darwin) "mac.sh" "other.sh") echo mac #+end_src #+begin_src sh :tangle (concat (file-name-directory buffer-file-name) "beside.sh") echo beside #+end_src #+begin_src sh :tangle (expand-file-name "sub/deep.sh") :mkdirp yes echo deep #+end_src #+begin_src sh :tangle (file-name-sans-extension (file-name-nondirectory buffer-file-name)) echo named after the file #+end_src #+begin_src sh :tangle quoted.sh :prologue "(cd /tmp && ls)" echo quoted #+end_src #+begin_src python -r -l "#(ref:%s)" :tangle refs.py x = 1 #(ref:one) y = 2 (ref:kept) #+end_src #+begin_src python -r :tangle refs.py z = 3 (ref:gone) #+end_src """ + "\n" @Test func evaluatesLispHeadersLikeOrg() throws { let form = """ (let* ((dir (file-truename (make-temp-file "orgstar-tangle" t))) (src (expand-file-name "notes.org" dir)) (text (buffer-string)) out) (require 'ob-shell) (require 'ob-python) (with-temp-file src (insert text)) (with-current-buffer (find-file-noselect src) (let ((files (org-babel-tangle))) (setq out (mapconcat (lambda (f) (format "== %s %o\\n%s" (file-relative-name f dir) (file-modes f) (with-temp-buffer (insert-file-contents f) (buffer-string)))) (sort (copy-sequence files) #'string<) "")))) (erase-buffer) (insert out)) """ let theirs = try EmacsOracle.run([EmacsOracle.Case(text: Self.lisp, point: 1, form: form)])[0] let ours = try Tangle.run(text: Self.lisp, path: "/oracle/notes.org").get() #expect(Self.describe(ours, root: "/oracle") == theirs.text, "ours:\n\(Self.describe(ours, root: "/oracle"))\nemacs:\n\(theirs.text)\n\(theirs.error)") } /// The file names only; `expandsBodiesLikeOrg` checks the bodies. @Test func extensionsLikeOrg() throws { let languages = ["python", "ruby", "perl", "C", "C++", "D", "awk", "sed", "lua", "haskell", "java", "latex", "LilyPond", "fortran", "clojure", "ocaml", "julia", "sh", "text"] let text = languages.map { "#+begin_src \($0) :tangle yes\n\($0)\n#+end_src\n" }.joined() let features = ["ob-python", "ob-ruby", "ob-perl", "ob-C", "ob-awk", "ob-sed", "ob-lua", "ob-haskell", "ob-java", "ob-latex", "ob-lilypond", "ob-fortran", "ob-clojure", "ob-ocaml", "ob-julia", "ob-shell"] let form = """ (let* ((dir (file-truename (make-temp-file "orgstar-tangle" t))) (src (expand-file-name "notes.org" dir)) (text (buffer-string)) out) \(features.map { "(require '\($0))" }.joined(separator: " ")) (setq org-babel-clojure-backend 'babashka) (with-temp-file src (insert text)) (with-current-buffer (find-file-noselect src) (let ((files (org-babel-tangle))) (setq out (mapconcat (lambda (f) (concat (file-relative-name f dir) "\\n")) (sort (copy-sequence files) #'string<) "")))) (erase-buffer) (insert out)) """ let theirs = try EmacsOracle.run([EmacsOracle.Case(text: text, point: 1, form: form)])[0] let ours = try Tangle.run(text: text, path: "/oracle/notes.org").get() let names = ours.files.map { ($0.path as NSString).lastPathComponent + "\n" }.sorted().joined() #expect(names == theirs.text, "ours:\n\(names)\nemacs:\n\(theirs.text)\n\(theirs.error)") } static let expanded = #""" #+NAME: tbl | a | b | |---+-----| | 1 | 2.5 | | 3 | 4 | #+NAME: items - x - y #+NAME: ints | 1 | 2 | | 3 | 4 | * C :PROPERTIES: :includes: :imports: never.d :END: #+begin_src C :tangle out.c printf("hi\n"); #+end_src #+begin_src C :tangle out.c :includes :defines N 5 M 6 :var n=3 x=2.5 s="str" l=items printf("%d\n", n); #+end_src #+begin_src C :tangle out.c :includes '( "local.h") :defines '("A 1") :var t=tbl int main() { return t_h(0, "a"); } #+end_src #+begin_src C :tangle out.c :main no :prologue "// pro" :epilogue "// epi" :var m=ints int f(void) { return 1; } #+end_src #+begin_src C++ :tangle out.cpp :namespaces std :includes cout << 1; #+end_src #+begin_src cpp :tangle out.cpp INT main(void) {} #+end_src #+begin_src D :tangle out.d :imports std.math :var n=3 s="d" l=items t=tbl writeln(n); #+end_src #+begin_src D :tangle out.d :main no void f() {} #+end_src * Java #+begin_src java :tangle Main.java System.out.println(1); if (true) { System.out.println(2); } #+end_src #+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 System.out.println(n); #+end_src #+begin_src java :tangle Pkg.java package org.y; import java.util.List; public class Pkg { public static void main(String[] args) { System.out.println(1); } } #+end_src #+begin_src java :tangle Methods.java :var n=1 static int twice(int x) { return 2 * x; } #+end_src #+begin_src java :tangle Pro.java :prologue "int a = 1;" :epilogue "int b = 2;" int c = 3; #+end_src * Fortran #+begin_src fortran :tangle out.F90 print *, 1 #+end_src #+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" print *, n #+end_src #+begin_src fortran :tangle out.F90 PROGRAM hello print *, 2 end program hello #+end_src #+begin_src fortran :tangle out.F90 :main no :prologue "! pro" :epilogue "! epi" :var n=1 subroutine s end subroutine s #+end_src * Clojure #+begin_src clojure :tangle out.clj (+ 1 2) #+end_src #+begin_src clojure :tangle out.clj :var n=3 s="a\"b" l=items t=tbl ;; a comment (+ n 1) #+end_src #+begin_src clojure :tangle out.clj :ns my.ns :results output (println 1) #+end_src #+begin_src clojure :tangle out.clj :results pp {:a 1} #+end_src * Python #+begin_src python :tangle out.py :var t=tbl print(t) #+end_src """# + "\n" @Test func expandsBodiesLikeOrg() throws { let form = """ (let* ((dir (file-truename (make-temp-file "orgstar-tangle" t))) (src (expand-file-name "notes.org" dir)) (text (buffer-string)) out) (require 'ob-C) (require 'ob-java) (require 'ob-fortran) (require 'ob-clojure) (require 'ob-python) (setq org-babel-clojure-backend 'babashka) (with-temp-file src (insert text)) (with-current-buffer (find-file-noselect src) (let ((files (org-babel-tangle))) (setq out (mapconcat (lambda (f) (format "== %s %o\\n%s" (file-relative-name f dir) (file-modes f) (with-temp-buffer (insert-file-contents f) (buffer-string)))) (sort (copy-sequence files) #'string<) "")))) (erase-buffer) (insert out)) """ let theirs = try EmacsOracle.run([EmacsOracle.Case(text: Self.expanded, point: 1, form: form)])[0] let ours = try Tangle.run(text: Self.expanded, path: "/oracle/notes.org").get() #expect(Self.describe(ours, root: "/oracle") == theirs.text, "ours:\n\(Self.describe(ours, root: "/oracle"))\nemacs:\n\(theirs.text)\n\(theirs.error)") } @Test func refusesWhatItCantDo() { for (language, header) in [("haskell", ":comments link"), ("sh", ":tangle (concat user-emacs-directory \"a.sh\")"), ("sh", ":tangle-mode go+q")] { let text = "#+begin_src \(language) :tangle yes \(header)\necho\n#+end_src\n" guard case .failure = Tangle.run(text: text, path: "/x/a.org") else { Issue.record("tangled with \(header)"); continue } } let modes = ["(identity #o755)": 0o755, "o700": 0o700, "rw-r-----": 0o640, "u+x": 0o744, "a=r,u+w": 0o644, "go-r": 0o600] for (value, mode) in modes { #expect((try? Tangle.fileMode(value)) == mode, "\(value)") } } }