diff --git a/etc/ORG-NEWS b/etc/ORG-NEWS index 5d421172f..4f9523cf3 100644 --- a/etc/ORG-NEWS +++ b/etc/ORG-NEWS @@ -162,6 +162,17 @@ bibliography format requires them to be written in title-case. # This also includes changes in function behavior from Elisp perspective. +*** ob-matlab: fixed MATLAB support + +Fixed MATLAB babel code blocks processing. MATLAB code blocks, ~#+begin_src matlab~, with ~:results +verbatim~, ~:results output~, ~:results output latex~, or ~:results file graphics~ now work. To use +MATLAB with org, you need https://github.com/MathWorks/Emacs-MATLAB-Mode. + +*** ob-matlab: MATLAB behavior change + +MATLAB code blocks now reuse the ~MATLAB*~ buffer created by ~M-x matlab-shell~ by default. To +change this behavior, customize ~org-babel-default-header-args:matlab~. + *** ob-sqlite: Added ability to open a database in readonly mode Added option :readonly to ob-sqlite. diff --git a/lisp/ob-comint.el b/lisp/ob-comint.el index b88ac445a..2246badc2 100644 --- a/lisp/ob-comint.el +++ b/lisp/ob-comint.el @@ -101,15 +101,28 @@ PROMPT-REGEXP defaults to `comint-prompt-regexp'." (setq string (substring string (match-end 0)))) string) +(defun org-babel-comint--strip-regexps (result strip-regexps) + "STRIP-REGEXPS from RESULT list of strings." + (dolist (strip-regexp strip-regexps) + (let ((new-result '())) + (dolist (str result) + (setq str (replace-regexp-in-string strip-regexp "" str)) + (when (not (string= str "")) + (push str new-result))) + (setq result (nreverse new-result)))) + result) + (defmacro org-babel-comint-with-output (meta &rest body) "Evaluate BODY in BUFFER and return process output. Will wait until EOE-INDICATOR appears in the output, then return all process output. If REMOVE-ECHO and FULL-BODY are present and -non-nil, then strip echo'd body from the returned output. META -should be a list containing the following where the last two -elements are optional. +non-nil, then strip echoed body from the returned output. If +optional STRIP-REGEXPS, a list of regular expressions, is +present, then all matches will be removed from the returned +output. META should be a list containing the following where the +last three elements are optional. - (BUFFER EOE-INDICATOR REMOVE-ECHO FULL-BODY) + (BUFFER EOE-INDICATOR REMOVE-ECHO FULL-BODY STRIP-REGEXPS) This macro ensures that the filter is removed in case of an error or user `keyboard-quit' during execution of body." @@ -117,7 +130,8 @@ or user `keyboard-quit' during execution of body." (let ((buffer (nth 0 meta)) (eoe-indicator (nth 1 meta)) (remove-echo (nth 2 meta)) - (full-body (nth 3 meta))) + (full-body (nth 3 meta)) + (strip-regexps (nth 4 meta))) `(org-babel-comint-in-buffer ,buffer (let* ((string-buffer "") (comint-output-filter-functions @@ -165,8 +179,12 @@ or user `keyboard-quit' during execution of body." (and ,remove-echo ,full-body (setq string-buffer (org-babel-comint--echo-filter string-buffer ,full-body))) - ;; Filter out prompts. - (org-babel-comint--prompt-filter string-buffer))))) + ;; Filter out prompts from returned output result. + (let ((result (org-babel-comint--prompt-filter string-buffer))) + ;; Remove all matches of STRIP-REGEXPS in returned output result. + (when ,strip-regexps + (setq result (org-babel-comint--strip-regexps result ,strip-regexps))) + result))))) (defun org-babel-comint-input-command (buffer cmd) "Pass CMD to BUFFER. diff --git a/lisp/ob-matlab.el b/lisp/ob-matlab.el index de8deadbe..083dcdec3 100644 --- a/lisp/ob-matlab.el +++ b/lisp/ob-matlab.el @@ -28,11 +28,10 @@ ;;; Requirements: -;; Matlab - -;; matlab.el required for interactive emacs sessions and matlab-mode -;; major mode for source code editing buffer -;; https://matlab-emacs.sourceforge.net/ +;; 1) MATLAB +;; 2) https://github.com/mathworks/Emacs-MATLAB-Mode +;; For matlab-shell to run MATLAB within Emacs and matlab-mode +;; major mode for source code editing buffer ;;; Code: diff --git a/lisp/ob-octave.el b/lisp/ob-octave.el index 005990f20..5e622df9a 100644 --- a/lisp/ob-octave.el +++ b/lisp/ob-octave.el @@ -1,8 +1,8 @@ -;;; ob-octave.el --- Babel Functions for Octave and Matlab -*- lexical-binding: t; -*- +;;; ob-octave.el --- Babel Functions for Octave and MATLAB -*- lexical-binding: t; -*- ;; Copyright (C) 2010-2024 Free Software Foundation, Inc. -;; Author: Dan Davison +;; Author: Dan Davison (Octave), John Ciolfi (MATLAB) ;; Keywords: literate programming, reproducible research ;; URL: https://orgmode.org @@ -30,6 +30,8 @@ ;;; Code: +(require 'cl-seq) + (require 'org-macs) (org-assert-version) @@ -39,7 +41,11 @@ (declare-function matlab-shell "ext:matlab-mode") (declare-function matlab-shell-run-region "ext:matlab-mode") -(defvar org-babel-default-header-args:matlab '()) +(defvar org-babel-default-header-args:matlab '((:session . "*MATLAB*")) + "Reuse the matlab-shell buffer for code block evaluations. +Add the MATLAB clear command to your code block to clear the +MATLAB workspace.") + (defvar org-babel-default-header-args:octave '()) (defvar org-babel-matlab-shell-command "matlab -nosplash" @@ -47,18 +53,42 @@ (defvar org-babel-octave-shell-command "octave -q" "Shell command to run octave as an external process.") -(defvar org-babel-matlab-with-emacs-link nil - "If non-nil use matlab-shell-run-region for session evaluation. -This will use EmacsLink if (matlab-with-emacs-link) evaluates -to a non-nil value.") - -(defvar org-babel-matlab-emacs-link-wrapper-method - "%s -if ischar(ans), fid = fopen('%s', 'w'); fprintf(fid, '%%s\\n', ans); fclose(fid); -else, save -ascii %s ans -end -delete('%s') +(make-obsolete-variable 'org-babel-matlab-with-emacs-link + "MATLAB removed EmacsLink in R2009a." "9.8") + +(make-obsolete-variable 'org-babel-matlab-emacs-link-wrapper-method + "MATLAB removed EmacsLink in R2009a." "9.8") + +(defvar org-babel-matlab-print "print(\"-dpng\", %S);\nans=%S;" + ;; MATLAB command-function duality requires that the file name be specified + ;; without quotes. Using: print -dpng "file.png", would produce a file with + ;; the quotes in the file name on disk. Therefore, use the functional form + ;; to handle files with spaces, print("-dpng", "file.png"). + ;; Example: + ;; #+begin_src matlab :file "sine wave.png" :results file graphics + ;; t = [0 : 0.1 : 2*pi]; + ;; y = sin(t); + ;; plot(t, y); + ;; set(gcf, 'PaperUnits', 'inches', 'PaperPosition', [0 0 4 3]) % Set the size to 4" x 3" + ;; #+end_src + ;; + ;; #+RESULTS: + ;; [[file:sine wave.png]] + "MATLAB format specifier to print current figure to a file.") + +(defvar org-babel-octave-print "print -dpng %S\nans=%S" + "Octave format specifier to print current figure to a file.") + +(defvar org-babel-matlab-wrapper-method + (concat "\ +cd('%s'); +%s +if ~exist('ans', 'var') ans = ''; end; \ +writematrix(ans, '%s', 'Delimiter', 'tab'); ") + "Format specifier used when evaluating MATLAB code blocks. +Arguments are the `default-directory', the MATLAB code, and a result file.txt.") + (defvar org-babel-octave-wrapper-method "%s if ischar(ans), fid = fopen('%s', 'w'); fdisp(fid, ans); fclose(fid); @@ -92,7 +122,10 @@ When MATLABP is non-nil, execute Matlab. Otherwise, execute Octave." (list "set (0, \"defaultfigurevisible\", \"off\");" full-body - (format "print -dpng %S\nans=%S" gfx-file gfx-file)) + (format (if matlabp + org-babel-matlab-print + org-babel-octave-print) + gfx-file gfx-file)) "\n") full-body) result-type matlabp))) @@ -153,6 +186,19 @@ If there is not a current inferior-process-buffer in SESSION then create. Return the initialized session. PARAMS are src block parameters." (org-babel-octave-initiate-session session params 'matlab)) +(defun org-babel-matlab-shell () + "Start and/or wait for MATLAB shell." + (require 'matlab-shell) ;; make `matlab-shell-busy-checker' available + (if (fboundp 'matlab-shell-busy-checker) + (progn + ;; Start the shell if needed. `matlab-shell' will be reused if it is already running. + (matlab-shell) + ;; If we just started the matlab-shell, wait for the prompt. If we do not + ;; wait, then the startup messages will show up in the evaluation results. + (matlab-shell-busy-checker 'wait-for-prompt)) + (user-error "A newer version of matlab-mode is required, see \ +https://github.com/mathworks/Emacs-MATLAB-Mode\n"))) + (defun org-babel-octave-initiate-session (&optional session _params matlabp) "Create an octave inferior process buffer. If there is not a current inferior-process-buffer in SESSION then @@ -165,9 +211,15 @@ Octave session, unless MATLABP is non-nil." (unless (string= session "none") (let ((session (or session (if matlabp "*Inferior Matlab*" "*Inferior Octave*")))) - (if (org-babel-comint-buffer-livep session) session + (if (org-babel-comint-buffer-livep session) + (progn + (when (and matlabp (fboundp 'matlab-shell-busy-checker)) + ;; Can't evaluate if the matlab-shell is currently running code + (matlab-shell-busy-checker 'error-if-busy)) + session) (save-window-excursion - (if matlabp (unless org-babel-matlab-with-emacs-link (matlab-shell)) + (if matlabp + (org-babel-matlab-shell) (run-octave)) (rename-buffer (if (bufferp session) (buffer-name session) (if (stringp session) session (buffer-name)))) @@ -183,79 +235,125 @@ value of the last statement in BODY, as elisp." (org-babel-octave-evaluate-session session body result-type matlabp) (org-babel-octave-evaluate-external-process body result-type matlabp))) +(defun org-babel-octave-wrapper-tmp-file (matlabp) + "Return a local tmp file with name adjusted for MATLABP." + (if matlabp + ;; writematrix requires a file ending with '.txt' + (org-babel-temp-file "matlab-" ".txt") + (org-babel-temp-file "octave-"))) + +(defun org-babel-octave-get-code-to-eval (body tmp-file matlabp) + "Format BODY of the code block for evaluation saving results to TMP-FILE. +If MATLABP, format for MATLAB, else format for Octave." + (if matlabp + (format org-babel-matlab-wrapper-method default-directory body tmp-file) + (format org-babel-octave-wrapper-method body tmp-file tmp-file))) + (defun org-babel-octave-evaluate-external-process (body result-type matlabp) - "Evaluate BODY in an external Octave or Matalab process. + "Evaluate BODY in an external Octave or MATLAB process. Process the result as RESULT-TYPE. Use Octave, unless MATLABP is non-nil." (let ((cmd (if matlabp org-babel-matlab-shell-command org-babel-octave-shell-command))) (pcase result-type (`output (org-babel-eval cmd body)) - (`value (let ((tmp-file (org-babel-temp-file "octave-"))) + (`value (let ((tmp-file (org-babel-process-file-name + (org-babel-octave-wrapper-tmp-file matlabp) + 'noquote))) (org-babel-eval cmd - (format org-babel-octave-wrapper-method body - (org-babel-process-file-name tmp-file 'noquote) - (org-babel-process-file-name tmp-file 'noquote))) + (org-babel-octave-get-code-to-eval body tmp-file matlabp)) (org-babel-octave-import-elisp-from-file tmp-file)))))) +(defvar org-babel-octave--matlab-line-indicator " %-\n" + "Comment appended to each code line being evaluated.") + +(defvar org-babel-octave--matlab-error-indicator-re "\r?\n?" + "MATLAB shell error indicators.") + +(defun org-babel-octave-body-for-output (body matlabp) + "If MATLABP, fix up BODY for MATLAB output result-type." + (when matlabp + ;; MATLAB does not echo the whole BODY all at once and instead mixes input + ;; with the output. Therefore, when we send multi-line input to + ;; `matlab-shell', we'll see the "body" code lines echoed in the output + ;; which is not what one would expect. To remove these unwanted lines, + ;; we append `org-babel-octave--matlab-line-indicator' to each line in the BODY. + ;; We leverage this indicator to remove the unwanted lines. + ;; Example of desired behavior: + ;; #+begin_src matlab :results output + ;; disp('The results are:') + ;; a = [1, 2; 3, 4] + ;; b = a * 2 + ;; #+end_src + ;; + ;; #+RESULTS: + ;; #+begin_example + ;; The results are: + ;; + ;; a = + ;; + ;; 1 2 + ;; 3 4 + ;; + ;; b = + ;; + ;; 2 4 + ;; 6 8 + ;; #+end_example + (setq body (replace-regexp-in-string "\r" "" body)) ;; CRLF => LF + (setq body (concat (string-trim-right body) "\n")) ;; Ensure a single final newline + (setq body (replace-regexp-in-string "\n" org-babel-octave--matlab-line-indicator body))) + body) + (defun org-babel-octave-evaluate-session (session body result-type &optional matlabp) "Evaluate BODY in SESSION." - (let* ((tmp-file (org-babel-temp-file (if matlabp "matlab-" "octave-"))) - (wait-file (org-babel-temp-file "matlab-emacs-link-wait-signal-")) + (let* ((tmp-file (org-babel-octave-wrapper-tmp-file matlabp)) (full-body (pcase result-type (`output (mapconcat #'org-babel-chomp - (list body org-babel-octave-eoe-indicator) "\n")) + (list (org-babel-octave-body-for-output body matlabp) + org-babel-octave-eoe-indicator) + "\n")) (`value - (if (and matlabp org-babel-matlab-with-emacs-link) - (concat - (format org-babel-matlab-emacs-link-wrapper-method - body - (org-babel-process-file-name tmp-file 'noquote) - (org-babel-process-file-name tmp-file 'noquote) wait-file) "\n") - (mapconcat - #'org-babel-chomp - (list (format org-babel-octave-wrapper-method - body - (org-babel-process-file-name tmp-file 'noquote) - (org-babel-process-file-name tmp-file 'noquote)) - org-babel-octave-eoe-indicator) "\n"))))) - (raw (if (and matlabp org-babel-matlab-with-emacs-link) - (save-window-excursion - (with-temp-buffer - (insert full-body) - (write-region "" 'ignored wait-file nil nil nil 'excl) - (matlab-shell-run-region (point-min) (point-max)) - (message "Waiting for Matlab Emacs Link") - (while (file-exists-p wait-file) (sit-for 0.01)) - "")) ;; matlab-shell-run-region doesn't seem to - ;; make *matlab* buffer contents easily - ;; available, so :results output currently - ;; won't work - (org-babel-comint-with-output - (session - (if matlabp - org-babel-octave-eoe-indicator - org-babel-octave-eoe-output) - t full-body) - (insert full-body) (comint-send-input nil t)))) - results) + (mapconcat + #'org-babel-chomp + (list (org-babel-octave-get-code-to-eval body tmp-file matlabp) + org-babel-octave-eoe-indicator) + "\n")))) + (raw-results + (org-babel-comint-with-output + (session + (if matlabp + org-babel-octave-eoe-indicator + org-babel-octave-eoe-output) + t full-body ;; Remove echo'd full-body from result + (when matlabp + `(;; MATLAB echo's input interleaved w/output, so strip inputs + ,(concat "^.*" org-babel-octave--matlab-line-indicator) + ;; Strip matlab-shell error indicators + ,org-babel-octave--matlab-error-indicator-re))) + (insert full-body) (comint-send-input nil t)))) (pcase result-type (`value (org-babel-octave-import-elisp-from-file tmp-file)) (`output - (setq results - (if matlabp - (cdr (reverse (delete "" (mapcar #'org-strip-quotes - (mapcar #'org-trim raw))))) - (cdr (member org-babel-octave-eoe-output - (reverse (mapcar #'org-strip-quotes - (mapcar #'org-trim raw))))))) - (mapconcat #'identity (reverse results) "\n"))))) + (if matlabp + (let* ((stripped (delete "" (mapcar #'org-strip-quotes raw-results))) + ;; Trim extra newline, keeping MATLAB's newlines + (trimmed (mapcar (lambda (str) + (replace-regexp-in-string "\n\n\\'" "\n" str)) + stripped)) + (reversed (cdr (reverse trimmed)))) + (concat (string-trim (mapconcat #'identity (reverse reversed)) "[\r\n]+") + "\n")) + (let ((reversed (cdr (member org-babel-octave-eoe-output + (reverse (mapcar #'org-strip-quotes + (mapcar #'org-trim raw-results))))))) + (mapconcat #'identity (reverse reversed)))))))) (defun org-babel-octave-import-elisp-from-file (file-name) "Import data from FILE-NAME. diff --git a/lisp/org.el b/lisp/org.el index 819a82eb9..1e46d238e 100644 --- a/lisp/org.el +++ b/lisp/org.el @@ -332,9 +332,10 @@ requirement." (const :tag "Lisp" lisp) (const :tag "Lua" lua) (const :tag "Makefile" makefile) + (const :tag "MATLAB" matlab) (const :tag "Maxima" maxima) (const :tag "OCaml" ocaml) - (const :tag "Octave and MatLab" octave) + (const :tag "Octave" octave) (const :tag "Org" org) (const :tag "Perl" perl) (const :tag "Processing" processing) diff --git a/mk/default.mk b/mk/default.mk index c5ea1ba01..2635d3a98 100644 --- a/mk/default.mk +++ b/mk/default.mk @@ -53,7 +53,7 @@ BTEST_POST = # -L /ert # needed for Emacs23, Emacs24 has ert built in # -L /ess # needed for running R tests # -L /htmlize # need at least version 1.34 for source code formatting -BTEST_OB_LANGUAGES = awk C fortran maxima lilypond octave perl python java sqlite eshell calc +BTEST_OB_LANGUAGES = awk C fortran matlab maxima lilypond octave perl python java sqlite eshell calc # R # requires ESS to be installed and configured # ruby # requires inf-ruby to be installed and configured # extra packages to require for testing diff --git a/testing/examples/ob-matlab-test.org b/testing/examples/ob-matlab-test.org new file mode 100644 index 000000000..63500f223 --- /dev/null +++ b/testing/examples/ob-matlab-test.org @@ -0,0 +1,114 @@ +#+Title: A collection of examples for ob-matlab tests +#+OPTIONS: ^:nil + +* Test MATLAB results output +:PROPERTIES: +:ID: 99332277-5e0e-4834-a3ad-ebcaddb855ab +:END: + +#+begin_src matlab :results output + disp('The results are:') + a = [1, 2; 3, 4] + b = a * 2 +#+end_src + +#+RESULTS: +#+begin_example +The results are: + +a = + + 1 2 + 3 4 + +b = + + 2 4 + 6 8 +#+end_example + +Following validates whitespace is preserved + +#+begin_src matlab :results output + disp(' +---+') + disp(' |one|') + disp(' +---+') + disp(newline) + disp([' ', num2str(1)]); + disp(newline) + disp(' +---+') + disp(' |two|') + disp(' +---+') + disp(newline) + disp([' ', num2str(2)]); + disp(newline) +#+end_src + +* Test reuse of MATLAB buffer with results output +:PROPERTIES: +:ID: a68db9ff-efdf-42b9-85fc-174015cd1f77 +:END: + +#+begin_src matlab :results output + a = 123 +#+end_src + +#+RESULTS: +: a = +: +: 123 + +#+begin_src matlab :exports both :results output + b = a + 1000 +#+end_src + +#+RESULTS: +: b = +: +: 1123 + +Following should give: Unrecognized function or variable 'b'. + +#+begin_src matlab :results output + clear + c = b * 2 +#+end_src + +#+RESULTS: +: Unrecognized function or variable 'b'. + +* Test results verbatim +:PROPERTIES: +:ID: 278047b6-4b87-4852-9050-e3e99fcaabb8 +:END: + +#+begin_src matlab :results verbatim + a = 2 + 3; + ans = magic(a); +#+end_src + +* Test results output latex +:PROPERTIES: +:ID: 7a8190be-d674-4944-864e-6fdaa7362585 +:END: + +#+begin_src matlab :results output latex + m = [4*pi, 3*pi; 2*pi, pi]; + result = latex(sym(m)); + disp(result) +#+end_src + +* Test results file graphics +:PROPERTIES: +:ID: 5bee8841-a898-4135-b44b-f1bd5465ceed +:END: + +#+begin_src matlab :results file graphics :file NAME.png + t = [0 : 0.1 : 2*pi]; + y = sin(t); + plot(t, y); + set(gcf, 'PaperUnits', 'inches', 'PaperPosition', [0 0 4 3]) % Set the size to 4" x 3" +#+end_src + + +# LocalWords: ebcaddb efdf fc fcaabb fdaa ceed sinewave diff --git a/testing/lisp/test-ob-matlab.el b/testing/lisp/test-ob-matlab.el new file mode 100644 index 000000000..476e5b1e4 --- /dev/null +++ b/testing/lisp/test-ob-matlab.el @@ -0,0 +1,191 @@ +;;; test-ob-matlab.el --- tests for ob-matlab.el -*- lexical-binding: t; -*- + +;;; Commentary: + +;; Copyright 2024 Free Software Foundation +;; Authors: John Ciolfi + +;; This file is not part of GNU Emacs. + +;; This program is free software; you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with this program. If not, see . + +;; ---------- +;; Code blocks reside in: ../examples/ob-matlab-test.org +;; To run test-ob-matlab.el using latest matlab-mode and org-mode: +;; 1. Download and build latest org-mode, https://orgmode.org/worg/org-contribute.html +;; Example: +;; cd ~/github +;; git clone https://git.sv.gnu.org/git/emacs/org-mode.git +;; make -C org-mode compile -j32 && make prefix=~/github/org-mode-install install +;; 2. Download and build latest matlab-mode, https://github.com/MathWorks/Emacs-MATLAB-Mode +;; Example: +;; cd ~/github +;; git clone https://github.com/mathworks/Emacs-MATLAB-Mode.git +;; make -C Emacs-MATLAB-Mode -j32 +;; 3. Test uses matlab-shell which requires that "matlab" is on the PATH: +;; Example: +;; cd ~/github/org-mode +;; env PATH=/path/to/MATLAB-INSTALL/bin:$PATH \ +;; make -j32 test \ +;; BTEST_POST="-L ~/github/Emacs-MATLAB-Mode -l ~/Emacs-MATLAB-Mode/matlab-autoload.el" + +;;; Code: + +(require 'ob-core) + +(org-test-for-executable "matlab") + +(unless (fboundp 'matlab-shell) + (signal 'missing-test-dependency '("Support for MATLAB code blocks"))) + +;;-------------------------------;; +;; Basic ":results output" tests ;; +;;-------------------------------;; +(ert-deftest ob-matlab/results-output () + "Test matlab :results output code block." + (let ((expected "\ +The results are: + +a = + + 1 2 + 3 4 + +b = + + 2 4 + 6 8 +")) + (org-test-at-id "99332277-5e0e-4834-a3ad-ebcaddb855ab" + (org-babel-next-src-block) + (should (equal expected (org-babel-execute-src-block)))))) + +(ert-deftest ob-matlab/results-output-preserve-whitespace () + "Test matlab :results output code block preserves whitespace." + (let ((expected "\ + +---+ + |one| + +---+ + + 1 + + +---+ + |two| + +---+ + + 2 +")) + (org-test-at-id "99332277-5e0e-4834-a3ad-ebcaddb855ab" + (org-babel-next-src-block 2) + (should (equal expected (org-babel-execute-src-block)))))) + +;;------------------------------------------------;; +;; MATLAB workspace reuse tests (:results output) ;; +;;------------------------------------------------;; + +(ert-deftest ob-matlab/results-output-reuse-a () + "Test matlab :results output defining variable, a." + (let ((expected "\ +a = + + 123 +")) + (org-test-at-id "a68db9ff-efdf-42b9-85fc-174015cd1f77" + (org-babel-next-src-block) + (should (equal expected (org-babel-execute-src-block)))))) + +(ert-deftest ob-matlab/results-output-reuse-b () + "Test matlab :results output reusing variable, a, to compute variable, b." + (let ((expected "\ +b = + + 1123 +")) + (org-test-at-id "a68db9ff-efdf-42b9-85fc-174015cd1f77" + (org-babel-next-src-block 2) + (should (equal expected (org-babel-execute-src-block)))))) + +(ert-deftest ob-matlab/results-output-reuse-clear () + "Test matlab :results output with clear resulting in error. +Also validates that we strip the matlab-shell indicators." + (let ((expected "\ +Unrecognized function or variable 'b'. +")) + (org-test-at-id "a68db9ff-efdf-42b9-85fc-174015cd1f77" + (org-babel-next-src-block 3) + (should (equal expected (org-babel-execute-src-block)))))) + +;;---------------------------;; +;; ":results verbatim" tests ;; +;;---------------------------;; +(ert-deftest ob-matlab/results-verbatim () + "Test matlab :results verbatim." + (let ((expected '((17 24 1 8 15) + (23 5 7 14 16) + (4 6 13 20 22) + (10 12 19 21 3) + (11 18 25 2 9)))) + (org-test-at-id "278047b6-4b87-4852-9050-e3e99fcaabb8" + (org-babel-next-src-block) + (should (equal expected (org-babel-execute-src-block)))))) + +;;-------------------------------;; +;; ":results output latex" tests ;; +;;-------------------------------;; + +(ert-deftest ob-matlab/results-output-latex () + "Test matlab :results output latex." + (let ((expected "\ +\\left(\\begin{array}{cc} 4\\,\\pi & 3\\,\\pi \\\\ 2\\,\\pi & \\pi \\end{array}\\right) +")) + (org-test-at-id "7a8190be-d674-4944-864e-6fdaa7362585" + (org-babel-next-src-block) + (let ((got (org-babel-execute-src-block))) + (should (equal expected got)))))) + + +;;--------------------------------;; +;; ":results file graphics" tests ;; +;;--------------------------------;; + +(ert-deftest ob-matlab/results-file-graphics () + "Test matlab :results file graphics." + (let ((code-block (org-test-get-code-block "5bee8841-a898-4135-b44b-f1bd5465ceed")) + (temp-file-png (make-temp-file "test-ob-matlab-" nil ".png"))) + (setq code-block (replace-regexp-in-string "NAME\\.png" temp-file-png code-block)) + (unwind-protect + (org-test-with-temp-text + code-block + (org-babel-execute-src-block) + (should (search-forward (format "[[file:%s]]" temp-file-png) nil nil)) + (should (file-readable-p temp-file-png))) + (delete-file temp-file-png)))) + +(ert-deftest ob-matlab/results-file-graphics-with-space () + "Test matlab :results file graphics using a file name with a space." + (let ((code-block (org-test-get-code-block "5bee8841-a898-4135-b44b-f1bd5465ceed")) + (temp-file-png (make-temp-file "test ob-matlab-" nil ".png"))) + (setq code-block (replace-regexp-in-string "NAME\\.png" temp-file-png code-block)) + (unwind-protect + (org-test-with-temp-text + code-block + (org-babel-execute-src-block) + (should (search-forward (format "[[file:%s]]" temp-file-png) nil nil)) + (should (file-readable-p temp-file-png))) + (delete-file temp-file-png)))) + +(provide 'test-ob-matlab) +;;; test-ob-matlab.el ends here (emacs-lisp-checkdoc) + +;; LocalWords: fboundp ebcaddb efdf fc ERRORTXT fcaabb fdaa ceed setq env BTEST diff --git a/testing/org-test.el b/testing/org-test.el index 52d38c3fd..93dfa46fc 100644 --- a/testing/org-test.el +++ b/testing/org-test.el @@ -133,6 +133,27 @@ currently executed.") (unless (or visited-p (not to-be-removed)) (kill-buffer to-be-removed))))) +(defun org-test-get-code-block (id &optional count) + "Get the COUNT, defaulting to first, code block after ID." + (let* ((id-location (org-id-find id)) + (id-file (car id-location)) + (visited-p (get-file-buffer id-file)) + code-block + to-be-removed) + (unwind-protect + (save-window-excursion + (save-match-data + (org-id-goto id) + (org-babel-next-src-block count) + (beginning-of-line) + (setq code-block (buffer-substring-no-properties + (point) + (re-search-forward "^[ \t]*#\\+end_src[ \t\r]*\n"))) + (setq to-be-removed (current-buffer)))) + (unless (or visited-p (not to-be-removed)) + (kill-buffer to-be-removed))) + code-block)) + (defmacro org-test-in-example-file (file &rest body) "Execute body in the Org example file." (declare (indent 1) (debug t))