tasks/04-error-handling.md done
This commit is contained in:
121
ob-elixir.el
121
ob-elixir.el
@@ -36,6 +36,7 @@
|
|||||||
|
|
||||||
(require 'ob)
|
(require 'ob)
|
||||||
(require 'ob-eval)
|
(require 'ob-eval)
|
||||||
|
(require 'cl-lib)
|
||||||
|
|
||||||
;;; Customization
|
;;; Customization
|
||||||
|
|
||||||
@@ -51,6 +52,22 @@ Can be a full path or command name if in PATH."
|
|||||||
:group 'ob-elixir
|
:group 'ob-elixir
|
||||||
:safe #'stringp)
|
: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
|
;;; Header Arguments
|
||||||
|
|
||||||
(defvar org-babel-default-header-args:elixir
|
(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
|
(with-eval-after-load 'org-src
|
||||||
(add-to-list 'org-src-lang-modes '("elixir" . elixir)))
|
(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
|
;;; Type Conversion
|
||||||
|
|
||||||
(defun ob-elixir--escape-string (str)
|
(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 `value', wraps code to capture return value.
|
||||||
For `output', captures stdout directly.
|
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"))
|
(let* ((tmp-file (org-babel-temp-file "ob-elixir-" ".exs"))
|
||||||
(code (if (eq result-type 'value)
|
(code (if (eq result-type 'value)
|
||||||
(ob-elixir--wrap-for-value body)
|
(ob-elixir--wrap-for-value body)
|
||||||
body)))
|
body)))
|
||||||
(with-temp-file tmp-file
|
(with-temp-file tmp-file
|
||||||
(insert code))
|
(insert code))
|
||||||
(let ((result (org-babel-eval
|
(let ((result (with-temp-buffer
|
||||||
(format "%s %s"
|
(call-process ob-elixir-command nil t nil
|
||||||
ob-elixir-command
|
(org-babel-process-file-name tmp-file))
|
||||||
(org-babel-process-file-name tmp-file))
|
;; Capture both stdout and stderr
|
||||||
"")))
|
(buffer-string))))
|
||||||
(string-trim result))))
|
(ob-elixir--process-result result))))
|
||||||
|
|
||||||
(defun org-babel-execute:elixir (body params)
|
(defun org-babel-execute:elixir (body params)
|
||||||
"Execute a block of Elixir code with org-babel.
|
"Execute a block of Elixir code with org-babel.
|
||||||
|
|||||||
@@ -131,5 +131,50 @@
|
|||||||
"\nEnum.sum(data)")))
|
"\nEnum.sum(data)")))
|
||||||
(should (equal "6" (ob-elixir--execute full-body 'value)))))
|
(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)
|
(provide 'test-ob-elixir)
|
||||||
;;; test-ob-elixir.el ends here
|
;;; test-ob-elixir.el ends here
|
||||||
|
|||||||
Reference in New Issue
Block a user