diff --git a/ob-elixir.el b/ob-elixir.el index e4152e6..e7affad 100644 --- a/ob-elixir.el +++ b/ob-elixir.el @@ -36,6 +36,7 @@ (require 'ob) (require 'ob-eval) +(require 'cl-lib) ;;; Customization @@ -51,6 +52,22 @@ Can be a full path or command name if in PATH." :group 'ob-elixir :safe #'stringp) +(defcustom ob-elixir-signal-errors t + "Whether to signal Emacs errors on Elixir execution failure. + +When non-nil, Elixir errors will be signaled as Emacs errors. +When nil, errors are returned as the result string." + :type 'boolean + :group 'ob-elixir) + +(defcustom ob-elixir-show-warnings t + "Whether to include warnings in output. + +When non-nil, Elixir warnings are included in the result. +When nil, warnings are stripped from the output." + :type 'boolean + :group 'ob-elixir) + ;;; Header Arguments (defvar org-babel-default-header-args:elixir @@ -67,6 +84,94 @@ Can be a full path or command name if in PATH." (with-eval-after-load 'org-src (add-to-list 'org-src-lang-modes '("elixir" . elixir))) +;;; Error Handling + +(defconst ob-elixir--error-regexp + "^\\*\\* (\\([A-Za-z.]+Error\\))\\(.*\\)" + "Regexp matching Elixir runtime errors. +Group 1 is the error type, group 2 is the message.") + +(defconst ob-elixir--compile-error-regexp + "^\\*\\* (\\(CompileError\\|TokenMissingError\\|SyntaxError\\))\\(.*\\)" + "Regexp matching Elixir compile-time errors.") + +(defconst ob-elixir--warning-regexp + "^warning: \\(.*\\)" + "Regexp matching Elixir warnings.") + +(define-error 'ob-elixir-error + "Elixir evaluation error") + +(define-error 'ob-elixir-compile-error + "Elixir compilation error" + 'ob-elixir-error) + +(define-error 'ob-elixir-runtime-error + "Elixir runtime error" + 'ob-elixir-error) + +(defun ob-elixir--detect-error (output) + "Check OUTPUT for Elixir errors. + +Returns a plist with :type, :message, and :line if an error is found. +Returns nil if no error detected." + (cond + ;; Compile-time error + ((string-match ob-elixir--compile-error-regexp output) + (list :type 'compile + :error-type (match-string 1 output) + :message (string-trim (match-string 2 output)) + :full-output output)) + + ;; Runtime error + ((string-match ob-elixir--error-regexp output) + (list :type 'runtime + :error-type (match-string 1 output) + :message (string-trim (match-string 2 output)) + :full-output output)) + + ;; No error + (t nil))) + +(defun ob-elixir--format-error (error-info) + "Format ERROR-INFO into a user-friendly message." + (let ((type (plist-get error-info :type)) + (error-type (plist-get error-info :error-type)) + (message (plist-get error-info :message))) + (format "Elixir %s: (%s) %s" + (if (eq type 'compile) "Compile Error" "Runtime Error") + error-type + message))) + +(defun ob-elixir--strip-warnings (output) + "Remove warning lines from OUTPUT if configured." + (if ob-elixir-show-warnings + output + (let ((lines (split-string output "\n"))) + (mapconcat #'identity + (cl-remove-if (lambda (line) + (string-match-p ob-elixir--warning-regexp line)) + lines) + "\n")))) + +(defun ob-elixir--process-result (result) + "Process RESULT from Elixir execution. + +Checks for errors and handles them according to `ob-elixir-signal-errors'. +Returns the cleaned result string." + (let ((trimmed (string-trim result)) + (error-info (ob-elixir--detect-error result))) + (if error-info + (if ob-elixir-signal-errors + (signal (if (eq (plist-get error-info :type) 'compile) + 'ob-elixir-compile-error + 'ob-elixir-runtime-error) + (list (ob-elixir--format-error error-info))) + ;; Return error as result + (plist-get error-info :full-output)) + ;; No error, return trimmed result + trimmed))) + ;;; Type Conversion (defun ob-elixir--escape-string (str) @@ -181,19 +286,21 @@ RESULT-TYPE is either `value' or `output'. For `value', wraps code to capture return value. For `output', captures stdout directly. -Returns the result as a string." +Returns the result as a string. +May signal `ob-elixir-error' if execution fails and +`ob-elixir-signal-errors' is non-nil." (let* ((tmp-file (org-babel-temp-file "ob-elixir-" ".exs")) (code (if (eq result-type 'value) (ob-elixir--wrap-for-value body) body))) (with-temp-file tmp-file (insert code)) - (let ((result (org-babel-eval - (format "%s %s" - ob-elixir-command - (org-babel-process-file-name tmp-file)) - ""))) - (string-trim result)))) + (let ((result (with-temp-buffer + (call-process ob-elixir-command nil t nil + (org-babel-process-file-name tmp-file)) + ;; Capture both stdout and stderr + (buffer-string)))) + (ob-elixir--process-result result)))) (defun org-babel-execute:elixir (body params) "Execute a block of Elixir code with org-babel. diff --git a/test/test-ob-elixir.el b/test/test-ob-elixir.el index 773e338..4ca8a3c 100644 --- a/test/test-ob-elixir.el +++ b/test/test-ob-elixir.el @@ -131,5 +131,50 @@ "\nEnum.sum(data)"))) (should (equal "6" (ob-elixir--execute full-body 'value))))) +;;; Error Handling Tests + +(ert-deftest ob-elixir-test-detect-runtime-error () + "Test runtime error detection." + (let ((output "** (RuntimeError) something went wrong")) + (let ((error-info (ob-elixir--detect-error output))) + (should error-info) + (should (eq 'runtime (plist-get error-info :type))) + (should (equal "RuntimeError" (plist-get error-info :error-type)))))) + +(ert-deftest ob-elixir-test-detect-compile-error () + "Test compile error detection." + (let ((output "** (CompileError) test.exs:1: undefined function foo/0")) + (let ((error-info (ob-elixir--detect-error output))) + (should error-info) + (should (eq 'compile (plist-get error-info :type))) + (should (equal "CompileError" (plist-get error-info :error-type)))))) + +(ert-deftest ob-elixir-test-no-error () + "Test that valid output is not detected as error." + (should-not (ob-elixir--detect-error "42")) + (should-not (ob-elixir--detect-error "[1, 2, 3]")) + (should-not (ob-elixir--detect-error "\"hello\""))) + +(ert-deftest ob-elixir-test-error-execution () + "Test that errors are properly handled during execution." + (skip-unless (executable-find ob-elixir-command)) + (let ((ob-elixir-signal-errors nil)) + (let ((result (ob-elixir--execute "raise \"test error\"" 'value))) + (should (string-match-p "RuntimeError" result))))) + +(ert-deftest ob-elixir-test-error-signaling () + "Test that errors are signaled when configured." + (skip-unless (executable-find ob-elixir-command)) + (let ((ob-elixir-signal-errors t)) + (should-error (ob-elixir--execute "raise \"test error\"" 'value) + :type 'ob-elixir-runtime-error))) + +(ert-deftest ob-elixir-test-undefined-function () + "Test handling of undefined function error." + (skip-unless (executable-find ob-elixir-command)) + (let ((ob-elixir-signal-errors nil)) + (let ((result (ob-elixir--execute "undefined_function()" 'value))) + (should (string-match-p "\\(UndefinedFunctionError\\|CompileError\\)" result))))) + (provide 'test-ob-elixir) ;;; test-ob-elixir.el ends here