docs and tasks
This commit is contained in:
296
tasks/04-error-handling.md
Normal file
296
tasks/04-error-handling.md
Normal file
@@ -0,0 +1,296 @@
|
||||
# Task 04: Error Handling
|
||||
|
||||
**Phase**: 1 - Core (MVP)
|
||||
**Priority**: High
|
||||
**Estimated Time**: 1 hour
|
||||
**Dependencies**: Task 02 (Basic Execution)
|
||||
|
||||
## Objective
|
||||
|
||||
Implement proper error detection and reporting for Elixir code execution, so users get meaningful feedback when their code fails.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Task 02 completed
|
||||
- Basic execution working
|
||||
|
||||
## Background
|
||||
|
||||
Currently, Elixir errors are returned as raw output. We need to:
|
||||
1. Detect when Elixir reports an error
|
||||
2. Extract useful error information
|
||||
3. Present errors clearly to the user
|
||||
4. Optionally signal Emacs error conditions
|
||||
|
||||
## Steps
|
||||
|
||||
### Step 1: Define error patterns
|
||||
|
||||
Add to `ob-elixir.el`:
|
||||
|
||||
```elisp
|
||||
;;; 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.")
|
||||
```
|
||||
|
||||
### Step 2: Define custom error types
|
||||
|
||||
```elisp
|
||||
(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)
|
||||
```
|
||||
|
||||
### Step 3: Implement error detection
|
||||
|
||||
```elisp
|
||||
(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)))
|
||||
```
|
||||
|
||||
### Step 4: Implement error formatting
|
||||
|
||||
```elisp
|
||||
(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)))
|
||||
|
||||
(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)
|
||||
```
|
||||
|
||||
### Step 5: Update the execute function
|
||||
|
||||
Modify `ob-elixir--execute`:
|
||||
|
||||
```elisp
|
||||
(defun ob-elixir--execute (body result-type)
|
||||
"Execute BODY as Elixir code.
|
||||
|
||||
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.
|
||||
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))
|
||||
"")))
|
||||
(ob-elixir--process-result result))))
|
||||
|
||||
(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)))
|
||||
```
|
||||
|
||||
### Step 6: Handle warnings
|
||||
|
||||
```elisp
|
||||
(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)
|
||||
|
||||
(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"))))
|
||||
```
|
||||
|
||||
### Step 7: Add tests
|
||||
|
||||
Add to `test/test-ob-elixir.el`:
|
||||
|
||||
```elisp
|
||||
;;; 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)))))
|
||||
```
|
||||
|
||||
### Step 8: Test in an org buffer
|
||||
|
||||
Add to `test.org`:
|
||||
|
||||
```org
|
||||
* Error Handling Tests
|
||||
|
||||
** Runtime Error
|
||||
|
||||
#+BEGIN_SRC elixir
|
||||
raise "This is a test error"
|
||||
#+END_SRC
|
||||
|
||||
** Compile Error
|
||||
|
||||
#+BEGIN_SRC elixir
|
||||
def incomplete_function(
|
||||
#+END_SRC
|
||||
|
||||
** Undefined Function
|
||||
|
||||
#+BEGIN_SRC elixir
|
||||
this_function_does_not_exist()
|
||||
#+END_SRC
|
||||
|
||||
** Warning (should still execute)
|
||||
|
||||
#+BEGIN_SRC elixir
|
||||
x = 1
|
||||
y = 2
|
||||
x # y is unused, may generate warning
|
||||
#+END_SRC
|
||||
```
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] Runtime errors are detected (e.g., `raise "error"`)
|
||||
- [ ] Compile errors are detected (e.g., syntax errors)
|
||||
- [ ] Errors are formatted with type and message
|
||||
- [ ] `ob-elixir-signal-errors` controls error behavior
|
||||
- [ ] Warnings are handled according to `ob-elixir-show-warnings`
|
||||
- [ ] Valid output is not mistakenly detected as errors
|
||||
- [ ] All tests pass: `make test`
|
||||
|
||||
## Error Types to Handle
|
||||
|
||||
| Error Type | Example | Detection |
|
||||
|------------|---------|-----------|
|
||||
| RuntimeError | `raise "msg"` | `** (RuntimeError)` |
|
||||
| ArgumentError | Bad function arg | `** (ArgumentError)` |
|
||||
| ArithmeticError | Division by zero | `** (ArithmeticError)` |
|
||||
| CompileError | Syntax error | `** (CompileError)` |
|
||||
| SyntaxError | Invalid syntax | `** (SyntaxError)` |
|
||||
| TokenMissingError | Missing end | `** (TokenMissingError)` |
|
||||
| UndefinedFunctionError | Unknown function | `** (UndefinedFunctionError)` |
|
||||
|
||||
## Files Modified
|
||||
|
||||
- `ob-elixir.el` - Add error handling functions
|
||||
- `test/test-ob-elixir.el` - Add error handling tests
|
||||
|
||||
## References
|
||||
|
||||
- [docs/04-elixir-integration-strategies.md](../docs/04-elixir-integration-strategies.md) - Error Handling section
|
||||
Reference in New Issue
Block a user