docs and tasks
This commit is contained in:
144
tasks/00-index.md
Normal file
144
tasks/00-index.md
Normal file
@@ -0,0 +1,144 @@
|
||||
# ob-elixir Implementation Tasks
|
||||
|
||||
This directory contains step-by-step implementation tasks for building the ob-elixir package.
|
||||
|
||||
## Overview
|
||||
|
||||
The implementation is organized into 4 phases:
|
||||
|
||||
| Phase | Description | Tasks | Priority |
|
||||
|-------|-------------|-------|----------|
|
||||
| **Phase 1** | Core MVP | 01-06 | Critical |
|
||||
| **Phase 2** | Sessions | 07 | High |
|
||||
| **Phase 3** | Mix Integration | 08 | High |
|
||||
| **Phase 4** | Advanced Features | 09-10 | Medium/Low |
|
||||
|
||||
## Task List
|
||||
|
||||
### Phase 1: Core (MVP)
|
||||
|
||||
These tasks implement the minimum viable product - basic Elixir code execution in org-mode.
|
||||
|
||||
| Task | Title | Time | Status |
|
||||
|------|-------|------|--------|
|
||||
| [01](01-project-setup.md) | Project Setup | 30 min | Pending |
|
||||
| [02](02-basic-execution.md) | Basic Code Execution | 1-2 hrs | Pending |
|
||||
| [03](03-variable-injection.md) | Variable Injection | 1-2 hrs | Pending |
|
||||
| [04](04-error-handling.md) | Error Handling | 1 hr | Pending |
|
||||
| [05](05-result-formatting.md) | Result Formatting | 1-2 hrs | Pending |
|
||||
| [06](06-test-suite.md) | Comprehensive Test Suite | 2-3 hrs | Pending |
|
||||
|
||||
**Phase 1 Deliverables:**
|
||||
- Execute Elixir code with `C-c C-c`
|
||||
- `:results value` and `:results output` work
|
||||
- `:var` header arguments work
|
||||
- Errors are properly reported
|
||||
- Lists become org tables
|
||||
- Comprehensive test coverage
|
||||
|
||||
### Phase 2: Sessions
|
||||
|
||||
| Task | Title | Time | Status |
|
||||
|------|-------|------|--------|
|
||||
| [07](07-session-support.md) | IEx Session Support | 3-4 hrs | Pending |
|
||||
|
||||
**Phase 2 Deliverables:**
|
||||
- `:session name` creates persistent IEx sessions
|
||||
- Variables and modules persist across blocks
|
||||
- Session cleanup commands
|
||||
|
||||
### Phase 3: Mix Integration
|
||||
|
||||
| Task | Title | Time | Status |
|
||||
|------|-------|------|--------|
|
||||
| [08](08-mix-project-support.md) | Mix Project Support | 2-3 hrs | Pending |
|
||||
|
||||
**Phase 3 Deliverables:**
|
||||
- `:mix-project path` executes in project context
|
||||
- Auto-detection of Mix projects
|
||||
- `:mix-env` sets MIX_ENV
|
||||
- Sessions with Mix (`iex -S mix`)
|
||||
|
||||
### Phase 4: Advanced Features
|
||||
|
||||
| Task | Title | Time | Status |
|
||||
|------|-------|------|--------|
|
||||
| [09](09-remote-shell.md) | Remote Shell (remsh) | 2-3 hrs | Pending |
|
||||
| [10](10-async-execution.md) | Async Execution | 3-4 hrs | Pending |
|
||||
|
||||
**Phase 4 Deliverables:**
|
||||
- `:remsh node@host` connects to running nodes
|
||||
- `:async yes` for non-blocking execution
|
||||
|
||||
## Implementation Order
|
||||
|
||||
```
|
||||
Phase 1 (Must complete in order)
|
||||
├── 01-project-setup
|
||||
├── 02-basic-execution
|
||||
├── 03-variable-injection
|
||||
├── 04-error-handling
|
||||
├── 05-result-formatting
|
||||
└── 06-test-suite
|
||||
|
||||
Phase 2 (After Phase 1)
|
||||
└── 07-session-support
|
||||
|
||||
Phase 3 (After Phase 1, can parallel with Phase 2)
|
||||
└── 08-mix-project-support
|
||||
|
||||
Phase 4 (After relevant dependencies)
|
||||
├── 09-remote-shell (after 07)
|
||||
└── 10-async-execution (after Phase 1)
|
||||
```
|
||||
|
||||
## Time Estimates
|
||||
|
||||
| Phase | Estimated Time |
|
||||
|-------|----------------|
|
||||
| Phase 1 | 8-12 hours |
|
||||
| Phase 2 | 3-4 hours |
|
||||
| Phase 3 | 2-3 hours |
|
||||
| Phase 4 | 5-7 hours |
|
||||
| **Total** | **18-26 hours** |
|
||||
|
||||
## Getting Started
|
||||
|
||||
1. Start with [Task 01: Project Setup](01-project-setup.md)
|
||||
2. Complete Phase 1 tasks in order
|
||||
3. Phases 2-4 can be done based on your priorities
|
||||
|
||||
## Task Template
|
||||
|
||||
Each task file includes:
|
||||
|
||||
- **Objective**: What the task accomplishes
|
||||
- **Prerequisites**: What must be done first
|
||||
- **Steps**: Detailed implementation steps with code
|
||||
- **Tests**: Test cases to verify the implementation
|
||||
- **Acceptance Criteria**: Checklist of requirements
|
||||
- **Troubleshooting**: Common issues and solutions
|
||||
|
||||
## Testing
|
||||
|
||||
Run tests after each task:
|
||||
|
||||
```bash
|
||||
make test
|
||||
```
|
||||
|
||||
For integration tests with org-mode:
|
||||
|
||||
```bash
|
||||
make test-integration
|
||||
```
|
||||
|
||||
## Documentation References
|
||||
|
||||
| Document | Content |
|
||||
|----------|---------|
|
||||
| [01-emacs-elisp-best-practices.md](../docs/01-emacs-elisp-best-practices.md) | Elisp conventions |
|
||||
| [02-testing-emacs-elisp.md](../docs/02-testing-emacs-elisp.md) | Testing strategies |
|
||||
| [03-org-babel-implementation-guide.md](../docs/03-org-babel-implementation-guide.md) | Org-babel internals |
|
||||
| [04-elixir-integration-strategies.md](../docs/04-elixir-integration-strategies.md) | Elixir execution |
|
||||
| [05-existing-implementations-analysis.md](../docs/05-existing-implementations-analysis.md) | Prior art analysis |
|
||||
216
tasks/01-project-setup.md
Normal file
216
tasks/01-project-setup.md
Normal file
@@ -0,0 +1,216 @@
|
||||
# Task 01: Project Setup
|
||||
|
||||
**Phase**: 1 - Core (MVP)
|
||||
**Priority**: Critical
|
||||
**Estimated Time**: 30 minutes
|
||||
**Dependencies**: None
|
||||
|
||||
## Objective
|
||||
|
||||
Set up the project structure with proper Emacs Lisp package conventions, including file headers, licensing, and build tooling.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Emacs 27.1+ installed
|
||||
- Elixir installed and in PATH
|
||||
- Git repository initialized
|
||||
|
||||
## Steps
|
||||
|
||||
### Step 1: Create the main package file
|
||||
|
||||
Create `ob-elixir.el` with proper headers:
|
||||
|
||||
```elisp
|
||||
;;; ob-elixir.el --- Org Babel functions for Elixir -*- lexical-binding: t; -*-
|
||||
|
||||
;; Copyright (C) 2024 Your Name
|
||||
|
||||
;; Author: Your Name <your.email@example.com>
|
||||
;; URL: https://github.com/username/ob-elixir
|
||||
;; Keywords: literate programming, reproducible research, elixir
|
||||
;; Version: 0.1.0
|
||||
;; Package-Requires: ((emacs "27.1") (org "9.4"))
|
||||
|
||||
;; 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.
|
||||
|
||||
;;; Commentary:
|
||||
|
||||
;; Org Babel support for evaluating Elixir code blocks.
|
||||
;;
|
||||
;; Features:
|
||||
;; - Execute Elixir code in org-mode source blocks
|
||||
;; - Support for :results value and :results output
|
||||
;; - Variable passing with :var header argument
|
||||
;; - Mix project context support
|
||||
;;
|
||||
;; Usage:
|
||||
;; Add (elixir . t) to `org-babel-load-languages':
|
||||
;;
|
||||
;; (org-babel-do-load-languages
|
||||
;; 'org-babel-load-languages
|
||||
;; '((elixir . t)))
|
||||
|
||||
;;; Code:
|
||||
|
||||
(require 'ob)
|
||||
(require 'ob-eval)
|
||||
|
||||
(provide 'ob-elixir)
|
||||
;;; ob-elixir.el ends here
|
||||
```
|
||||
|
||||
### Step 2: Create the Eldev file for build tooling
|
||||
|
||||
Create `Eldev` file:
|
||||
|
||||
```elisp
|
||||
; -*- mode: emacs-lisp; lexical-binding: t; -*-
|
||||
|
||||
(eldev-use-package-archive 'gnu)
|
||||
(eldev-use-package-archive 'melpa)
|
||||
|
||||
;; Test dependencies
|
||||
(eldev-add-extra-dependencies 'test 'ert)
|
||||
|
||||
;; Use ERT for testing
|
||||
(setf eldev-test-framework 'ert)
|
||||
|
||||
;; Lint configuration
|
||||
(setf eldev-lint-default '(elisp package))
|
||||
```
|
||||
|
||||
### Step 3: Create test directory structure
|
||||
|
||||
```bash
|
||||
mkdir -p test
|
||||
```
|
||||
|
||||
Create `test/test-ob-elixir.el`:
|
||||
|
||||
```elisp
|
||||
;;; test-ob-elixir.el --- Tests for ob-elixir -*- lexical-binding: t; -*-
|
||||
|
||||
;;; Commentary:
|
||||
|
||||
;; Test suite for ob-elixir package.
|
||||
|
||||
;;; Code:
|
||||
|
||||
(require 'ert)
|
||||
|
||||
;; Add parent directory to load path
|
||||
(let ((dir (file-name-directory (or load-file-name buffer-file-name))))
|
||||
(add-to-list 'load-path (expand-file-name ".." dir)))
|
||||
|
||||
(require 'ob-elixir)
|
||||
|
||||
(ert-deftest ob-elixir-test-package-loads ()
|
||||
"Test that the package loads successfully."
|
||||
(should (featurep 'ob-elixir)))
|
||||
|
||||
(provide 'test-ob-elixir)
|
||||
;;; test-ob-elixir.el ends here
|
||||
```
|
||||
|
||||
### Step 4: Create Makefile
|
||||
|
||||
Create `Makefile`:
|
||||
|
||||
```makefile
|
||||
EMACS ?= emacs
|
||||
BATCH = $(EMACS) -Q -batch -L .
|
||||
|
||||
.PHONY: all compile test lint clean
|
||||
|
||||
all: compile test
|
||||
|
||||
compile:
|
||||
$(BATCH) -f batch-byte-compile ob-elixir.el
|
||||
|
||||
test:
|
||||
$(BATCH) -l ert -l test/test-ob-elixir.el \
|
||||
-f ert-run-tests-batch-and-exit
|
||||
|
||||
lint:
|
||||
$(BATCH) --eval "(require 'package)" \
|
||||
--eval "(package-initialize)" \
|
||||
--eval "(package-refresh-contents)" \
|
||||
--eval "(package-install 'package-lint)" \
|
||||
-l package-lint \
|
||||
-f package-lint-batch-and-exit ob-elixir.el
|
||||
|
||||
clean:
|
||||
rm -f *.elc test/*.elc
|
||||
```
|
||||
|
||||
### Step 5: Create .gitignore
|
||||
|
||||
Create `.gitignore`:
|
||||
|
||||
```
|
||||
# Byte-compiled files
|
||||
*.elc
|
||||
|
||||
# Eldev
|
||||
.eldev/
|
||||
Eldev-local
|
||||
|
||||
# Package archives
|
||||
/packages/
|
||||
|
||||
# Test artifacts
|
||||
/test/tmp/
|
||||
|
||||
# Editor
|
||||
*~
|
||||
\#*\#
|
||||
.#*
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
```
|
||||
|
||||
### Step 6: Verify setup
|
||||
|
||||
Run the following commands to verify:
|
||||
|
||||
```bash
|
||||
# Check Emacs version
|
||||
emacs --version
|
||||
|
||||
# Check Elixir version
|
||||
elixir --version
|
||||
|
||||
# Run tests
|
||||
make test
|
||||
|
||||
# Compile
|
||||
make compile
|
||||
```
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `ob-elixir.el` exists with proper headers
|
||||
- [ ] Package loads without errors: `(require 'ob-elixir)`
|
||||
- [ ] `make test` runs successfully
|
||||
- [ ] `make compile` produces no warnings
|
||||
- [ ] All files follow Emacs Lisp conventions
|
||||
|
||||
## Files Created
|
||||
|
||||
- `ob-elixir.el` - Main package file
|
||||
- `Eldev` - Build tool configuration
|
||||
- `Makefile` - Make targets
|
||||
- `test/test-ob-elixir.el` - Test file
|
||||
- `.gitignore` - Git ignore rules
|
||||
|
||||
## References
|
||||
|
||||
- [docs/01-emacs-elisp-best-practices.md](../docs/01-emacs-elisp-best-practices.md)
|
||||
- [docs/02-testing-emacs-elisp.md](../docs/02-testing-emacs-elisp.md)
|
||||
242
tasks/02-basic-execution.md
Normal file
242
tasks/02-basic-execution.md
Normal file
@@ -0,0 +1,242 @@
|
||||
# Task 02: Basic Code Execution
|
||||
|
||||
**Phase**: 1 - Core (MVP)
|
||||
**Priority**: Critical
|
||||
**Estimated Time**: 1-2 hours
|
||||
**Dependencies**: Task 01 (Project Setup)
|
||||
|
||||
## Objective
|
||||
|
||||
Implement the core `org-babel-execute:elixir` function that can execute Elixir code blocks using external process (one-shot execution).
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Task 01 completed
|
||||
- Elixir installed and accessible via `elixir` command
|
||||
|
||||
## Steps
|
||||
|
||||
### Step 1: Add customization group and variables
|
||||
|
||||
Add to `ob-elixir.el`:
|
||||
|
||||
```elisp
|
||||
;;; Customization
|
||||
|
||||
(defgroup ob-elixir nil
|
||||
"Org Babel support for Elixir."
|
||||
:group 'org-babel
|
||||
:prefix "ob-elixir-")
|
||||
|
||||
(defcustom ob-elixir-command "elixir"
|
||||
"Command to execute Elixir code.
|
||||
Can be a full path or command name if in PATH."
|
||||
:type 'string
|
||||
:group 'ob-elixir
|
||||
:safe #'stringp)
|
||||
```
|
||||
|
||||
### Step 2: Add default header arguments
|
||||
|
||||
```elisp
|
||||
;;; Header Arguments
|
||||
|
||||
(defvar org-babel-default-header-args:elixir
|
||||
'((:results . "value")
|
||||
(:session . "none"))
|
||||
"Default header arguments for Elixir code blocks.")
|
||||
```
|
||||
|
||||
### Step 3: Register the language
|
||||
|
||||
```elisp
|
||||
;;; Language Registration
|
||||
|
||||
;; File extension for tangling
|
||||
(add-to-list 'org-babel-tangle-lang-exts '("elixir" . "ex"))
|
||||
|
||||
;; Associate with elixir-mode for syntax highlighting (if available)
|
||||
(with-eval-after-load 'org-src
|
||||
(add-to-list 'org-src-lang-modes '("elixir" . elixir)))
|
||||
```
|
||||
|
||||
### Step 4: Implement the execute function
|
||||
|
||||
```elisp
|
||||
;;; Execution
|
||||
|
||||
(defun org-babel-execute:elixir (body params)
|
||||
"Execute a block of Elixir code with org-babel.
|
||||
|
||||
BODY is the Elixir code to execute.
|
||||
PARAMS is an alist of header arguments.
|
||||
|
||||
This function is called by `org-babel-execute-src-block'."
|
||||
(let* ((result-type (cdr (assq :result-type params)))
|
||||
(result-params (cdr (assq :result-params params)))
|
||||
(result (ob-elixir--execute body result-type)))
|
||||
(org-babel-reassemble-table
|
||||
(org-babel-result-cond result-params
|
||||
result
|
||||
(org-babel-script-escape result))
|
||||
(org-babel-pick-name (cdr (assq :colname-names params))
|
||||
(cdr (assq :colnames params)))
|
||||
(org-babel-pick-name (cdr (assq :rowname-names params))
|
||||
(cdr (assq :rownames params))))))
|
||||
```
|
||||
|
||||
### Step 5: Implement the internal execute function
|
||||
|
||||
```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."
|
||||
(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))))
|
||||
```
|
||||
|
||||
### Step 6: Implement the value wrapper
|
||||
|
||||
```elisp
|
||||
(defconst ob-elixir--value-wrapper
|
||||
"result = (
|
||||
%s
|
||||
)
|
||||
IO.puts(inspect(result, limit: :infinity, printable_limit: :infinity, charlists: :as_lists))
|
||||
"
|
||||
"Wrapper template for capturing Elixir expression value.
|
||||
%s is replaced with the user's code.")
|
||||
|
||||
(defun ob-elixir--wrap-for-value (body)
|
||||
"Wrap BODY to capture its return value.
|
||||
|
||||
The wrapper evaluates BODY, then prints the result using
|
||||
`inspect/2` with infinite limits to avoid truncation."
|
||||
(format ob-elixir--value-wrapper body))
|
||||
```
|
||||
|
||||
### Step 7: Add tests
|
||||
|
||||
Add to `test/test-ob-elixir.el`:
|
||||
|
||||
```elisp
|
||||
(ert-deftest ob-elixir-test-elixir-available ()
|
||||
"Test that Elixir is available."
|
||||
(should (executable-find ob-elixir-command)))
|
||||
|
||||
(ert-deftest ob-elixir-test-simple-value ()
|
||||
"Test simple value evaluation."
|
||||
(skip-unless (executable-find ob-elixir-command))
|
||||
(let ((result (ob-elixir--execute "1 + 1" 'value)))
|
||||
(should (equal "2" result))))
|
||||
|
||||
(ert-deftest ob-elixir-test-simple-output ()
|
||||
"Test simple output evaluation."
|
||||
(skip-unless (executable-find ob-elixir-command))
|
||||
(let ((result (ob-elixir--execute "IO.puts(\"hello\")" 'output)))
|
||||
(should (equal "hello" result))))
|
||||
|
||||
(ert-deftest ob-elixir-test-multiline-value ()
|
||||
"Test multiline code value evaluation."
|
||||
(skip-unless (executable-find ob-elixir-command))
|
||||
(let ((result (ob-elixir--execute "x = 10\ny = 20\nx + y" 'value)))
|
||||
(should (equal "30" result))))
|
||||
|
||||
(ert-deftest ob-elixir-test-list-result ()
|
||||
"Test list result."
|
||||
(skip-unless (executable-find ob-elixir-command))
|
||||
(let ((result (ob-elixir--execute "[1, 2, 3]" 'value)))
|
||||
(should (equal "[1, 2, 3]" result))))
|
||||
|
||||
(ert-deftest ob-elixir-test-map-result ()
|
||||
"Test map result."
|
||||
(skip-unless (executable-find ob-elixir-command))
|
||||
(let ((result (ob-elixir--execute "%{a: 1, b: 2}" 'value)))
|
||||
(should (string-match-p "%{a: 1, b: 2}" result))))
|
||||
```
|
||||
|
||||
### Step 8: Test in an org buffer
|
||||
|
||||
Create a test org file `test.org`:
|
||||
|
||||
```org
|
||||
* Test ob-elixir
|
||||
|
||||
** Basic arithmetic (value)
|
||||
|
||||
#+BEGIN_SRC elixir
|
||||
1 + 1
|
||||
#+END_SRC
|
||||
|
||||
** Output test
|
||||
|
||||
#+BEGIN_SRC elixir :results output
|
||||
IO.puts("Hello, World!")
|
||||
#+END_SRC
|
||||
|
||||
** List manipulation
|
||||
|
||||
#+BEGIN_SRC elixir
|
||||
Enum.map([1, 2, 3], fn x -> x * 2 end)
|
||||
#+END_SRC
|
||||
```
|
||||
|
||||
Press `C-c C-c` on each block to test.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `org-babel-execute:elixir` function exists
|
||||
- [ ] Simple expressions evaluate correctly: `1 + 1` returns `2`
|
||||
- [ ] `:results value` captures return value (default)
|
||||
- [ ] `:results output` captures stdout
|
||||
- [ ] Multiline code executes correctly
|
||||
- [ ] Lists and maps are returned in Elixir format
|
||||
- [ ] All tests pass: `make test`
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "Cannot find elixir"
|
||||
|
||||
Ensure Elixir is in PATH:
|
||||
```bash
|
||||
which elixir
|
||||
elixir --version
|
||||
```
|
||||
|
||||
Or set the full path:
|
||||
```elisp
|
||||
(setq ob-elixir-command "/usr/local/bin/elixir")
|
||||
```
|
||||
|
||||
### Results are truncated
|
||||
|
||||
The wrapper uses `limit: :infinity` to prevent truncation. If still truncated, check for very large outputs.
|
||||
|
||||
### ANSI codes in output
|
||||
|
||||
We'll handle this in a later task. For now, output should be clean with the current approach.
|
||||
|
||||
## Files Modified
|
||||
|
||||
- `ob-elixir.el` - Add execution functions
|
||||
- `test/test-ob-elixir.el` - Add execution tests
|
||||
|
||||
## References
|
||||
|
||||
- [docs/03-org-babel-implementation-guide.md](../docs/03-org-babel-implementation-guide.md)
|
||||
- [docs/04-elixir-integration-strategies.md](../docs/04-elixir-integration-strategies.md)
|
||||
334
tasks/03-variable-injection.md
Normal file
334
tasks/03-variable-injection.md
Normal file
@@ -0,0 +1,334 @@
|
||||
# Task 03: Variable Injection
|
||||
|
||||
**Phase**: 1 - Core (MVP)
|
||||
**Priority**: High
|
||||
**Estimated Time**: 1-2 hours
|
||||
**Dependencies**: Task 02 (Basic Execution)
|
||||
|
||||
## Objective
|
||||
|
||||
Implement variable injection so that `:var` header arguments work correctly, allowing data to be passed from org-mode into Elixir code blocks.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Task 02 completed
|
||||
- Basic execution working
|
||||
|
||||
## Background
|
||||
|
||||
Org-babel allows passing variables to code blocks:
|
||||
|
||||
```org
|
||||
#+BEGIN_SRC elixir :var x=5 :var name="Alice"
|
||||
"Hello, #{name}! x = #{x}"
|
||||
#+END_SRC
|
||||
```
|
||||
|
||||
We need to:
|
||||
1. Convert Elisp values to Elixir syntax
|
||||
2. Generate Elixir variable assignment statements
|
||||
3. Prepend these to the code before execution
|
||||
|
||||
## Steps
|
||||
|
||||
### Step 1: Implement Elisp to Elixir conversion
|
||||
|
||||
Add to `ob-elixir.el`:
|
||||
|
||||
```elisp
|
||||
;;; Type Conversion
|
||||
|
||||
(defun ob-elixir--elisp-to-elixir (value)
|
||||
"Convert Elisp VALUE to Elixir literal syntax.
|
||||
|
||||
Handles:
|
||||
- nil -> nil
|
||||
- t -> true
|
||||
- numbers -> numbers
|
||||
- strings -> quoted strings
|
||||
- symbols -> atoms
|
||||
- lists -> Elixir lists
|
||||
- vectors -> tuples"
|
||||
(cond
|
||||
;; nil
|
||||
((null value) "nil")
|
||||
|
||||
;; Boolean true
|
||||
((eq value t) "true")
|
||||
|
||||
;; Numbers
|
||||
((numberp value)
|
||||
(number-to-string value))
|
||||
|
||||
;; Strings
|
||||
((stringp value)
|
||||
(format "\"%s\"" (ob-elixir--escape-string value)))
|
||||
|
||||
;; Symbols become atoms (except special ones)
|
||||
((symbolp value)
|
||||
(let ((name (symbol-name value)))
|
||||
(cond
|
||||
((string= name "hline") ":hline")
|
||||
((string-match-p "^[a-z_][a-zA-Z0-9_]*[?!]?$" name)
|
||||
(format ":%s" name))
|
||||
(t (format ":\"%s\"" name)))))
|
||||
|
||||
;; Vectors become tuples
|
||||
((vectorp value)
|
||||
(format "{%s}"
|
||||
(mapconcat #'ob-elixir--elisp-to-elixir
|
||||
(append value nil) ", ")))
|
||||
|
||||
;; Lists
|
||||
((listp value)
|
||||
(format "[%s]"
|
||||
(mapconcat #'ob-elixir--elisp-to-elixir value ", ")))
|
||||
|
||||
;; Fallback
|
||||
(t (format "%S" value))))
|
||||
```
|
||||
|
||||
### Step 2: Implement string escaping
|
||||
|
||||
```elisp
|
||||
(defun ob-elixir--escape-string (str)
|
||||
"Escape special characters in STR for Elixir string literal."
|
||||
(let ((result str))
|
||||
;; Escape backslashes first
|
||||
(setq result (replace-regexp-in-string "\\\\" "\\\\\\\\" result))
|
||||
;; Escape double quotes
|
||||
(setq result (replace-regexp-in-string "\"" "\\\\\"" result))
|
||||
;; Escape newlines
|
||||
(setq result (replace-regexp-in-string "\n" "\\\\n" result))
|
||||
;; Escape tabs
|
||||
(setq result (replace-regexp-in-string "\t" "\\\\t" result))
|
||||
result))
|
||||
```
|
||||
|
||||
### Step 3: Implement variable assignments function
|
||||
|
||||
```elisp
|
||||
;;; Variable Handling
|
||||
|
||||
(defun org-babel-variable-assignments:elixir (params)
|
||||
"Return list of Elixir statements assigning variables from PARAMS.
|
||||
|
||||
Each statement has the form: var_name = value"
|
||||
(mapcar
|
||||
(lambda (pair)
|
||||
(let ((name (car pair))
|
||||
(value (cdr pair)))
|
||||
(format "%s = %s"
|
||||
(ob-elixir--var-name name)
|
||||
(ob-elixir--elisp-to-elixir value))))
|
||||
(org-babel--get-vars params)))
|
||||
|
||||
(defun ob-elixir--var-name (name)
|
||||
"Convert NAME to a valid Elixir variable name.
|
||||
|
||||
Elixir variables must start with lowercase or underscore."
|
||||
(let ((str (if (symbolp name) (symbol-name name) name)))
|
||||
;; Ensure starts with lowercase or underscore
|
||||
(if (string-match-p "^[a-z_]" str)
|
||||
str
|
||||
(concat "_" str))))
|
||||
```
|
||||
|
||||
### Step 4: Update the execute function
|
||||
|
||||
Modify `org-babel-execute:elixir` to use variable assignments:
|
||||
|
||||
```elisp
|
||||
(defun org-babel-execute:elixir (body params)
|
||||
"Execute a block of Elixir code with org-babel.
|
||||
|
||||
BODY is the Elixir code to execute.
|
||||
PARAMS is an alist of header arguments.
|
||||
|
||||
This function is called by `org-babel-execute-src-block'."
|
||||
(let* ((result-type (cdr (assq :result-type params)))
|
||||
(result-params (cdr (assq :result-params params)))
|
||||
;; Expand body with variable assignments
|
||||
(full-body (org-babel-expand-body:generic
|
||||
body params
|
||||
(org-babel-variable-assignments:elixir params)))
|
||||
(result (ob-elixir--execute full-body result-type)))
|
||||
(org-babel-reassemble-table
|
||||
(org-babel-result-cond result-params
|
||||
result
|
||||
(org-babel-script-escape result))
|
||||
(org-babel-pick-name (cdr (assq :colname-names params))
|
||||
(cdr (assq :colnames params)))
|
||||
(org-babel-pick-name (cdr (assq :rowname-names params))
|
||||
(cdr (assq :rownames params))))))
|
||||
```
|
||||
|
||||
### Step 5: Add tests for type conversion
|
||||
|
||||
Add to `test/test-ob-elixir.el`:
|
||||
|
||||
```elisp
|
||||
;;; Type Conversion Tests
|
||||
|
||||
(ert-deftest ob-elixir-test-convert-nil ()
|
||||
"Test nil conversion."
|
||||
(should (equal "nil" (ob-elixir--elisp-to-elixir nil))))
|
||||
|
||||
(ert-deftest ob-elixir-test-convert-true ()
|
||||
"Test t conversion."
|
||||
(should (equal "true" (ob-elixir--elisp-to-elixir t))))
|
||||
|
||||
(ert-deftest ob-elixir-test-convert-integer ()
|
||||
"Test integer conversion."
|
||||
(should (equal "42" (ob-elixir--elisp-to-elixir 42)))
|
||||
(should (equal "-10" (ob-elixir--elisp-to-elixir -10))))
|
||||
|
||||
(ert-deftest ob-elixir-test-convert-float ()
|
||||
"Test float conversion."
|
||||
(should (equal "3.14" (ob-elixir--elisp-to-elixir 3.14))))
|
||||
|
||||
(ert-deftest ob-elixir-test-convert-string ()
|
||||
"Test string conversion."
|
||||
(should (equal "\"hello\"" (ob-elixir--elisp-to-elixir "hello"))))
|
||||
|
||||
(ert-deftest ob-elixir-test-convert-string-escaping ()
|
||||
"Test string escaping."
|
||||
(should (equal "\"say \\\"hi\\\"\""
|
||||
(ob-elixir--elisp-to-elixir "say \"hi\"")))
|
||||
(should (equal "\"line1\\nline2\""
|
||||
(ob-elixir--elisp-to-elixir "line1\nline2"))))
|
||||
|
||||
(ert-deftest ob-elixir-test-convert-symbol ()
|
||||
"Test symbol conversion to atom."
|
||||
(should (equal ":foo" (ob-elixir--elisp-to-elixir 'foo)))
|
||||
(should (equal ":ok" (ob-elixir--elisp-to-elixir 'ok))))
|
||||
|
||||
(ert-deftest ob-elixir-test-convert-list ()
|
||||
"Test list conversion."
|
||||
(should (equal "[1, 2, 3]" (ob-elixir--elisp-to-elixir '(1 2 3))))
|
||||
(should (equal "[\"a\", \"b\"]" (ob-elixir--elisp-to-elixir '("a" "b")))))
|
||||
|
||||
(ert-deftest ob-elixir-test-convert-nested-list ()
|
||||
"Test nested list conversion."
|
||||
(should (equal "[[1, 2], [3, 4]]"
|
||||
(ob-elixir--elisp-to-elixir '((1 2) (3 4))))))
|
||||
|
||||
(ert-deftest ob-elixir-test-convert-vector ()
|
||||
"Test vector to tuple conversion."
|
||||
(should (equal "{1, 2, 3}" (ob-elixir--elisp-to-elixir [1 2 3]))))
|
||||
```
|
||||
|
||||
### Step 6: Add tests for variable injection
|
||||
|
||||
```elisp
|
||||
;;; Variable Injection Tests
|
||||
|
||||
(ert-deftest ob-elixir-test-variable-assignments ()
|
||||
"Test variable assignment generation."
|
||||
(let ((params '((:var . ("x" . 5))
|
||||
(:var . ("name" . "Alice")))))
|
||||
(let ((assignments (org-babel-variable-assignments:elixir params)))
|
||||
(should (member "x = 5" assignments))
|
||||
(should (member "name = \"Alice\"" assignments)))))
|
||||
|
||||
(ert-deftest ob-elixir-test-var-execution ()
|
||||
"Test code execution with variables."
|
||||
(skip-unless (executable-find ob-elixir-command))
|
||||
(let* ((params '((:var . ("x" . 10))))
|
||||
(var-lines (org-babel-variable-assignments:elixir params))
|
||||
(full-body (concat (mapconcat #'identity var-lines "\n")
|
||||
"\nx * 2")))
|
||||
(should (equal "20" (ob-elixir--execute full-body 'value)))))
|
||||
|
||||
(ert-deftest ob-elixir-test-var-list ()
|
||||
"Test passing list as variable."
|
||||
(skip-unless (executable-find ob-elixir-command))
|
||||
(let* ((params '((:var . ("data" . (1 2 3)))))
|
||||
(var-lines (org-babel-variable-assignments:elixir params))
|
||||
(full-body (concat (mapconcat #'identity var-lines "\n")
|
||||
"\nEnum.sum(data)")))
|
||||
(should (equal "6" (ob-elixir--execute full-body 'value)))))
|
||||
```
|
||||
|
||||
### Step 7: Test in an org buffer
|
||||
|
||||
Update `test.org`:
|
||||
|
||||
```org
|
||||
* Variable Injection Tests
|
||||
|
||||
** Simple variable
|
||||
|
||||
#+BEGIN_SRC elixir :var x=42
|
||||
x * 2
|
||||
#+END_SRC
|
||||
|
||||
#+RESULTS:
|
||||
: 84
|
||||
|
||||
** String variable
|
||||
|
||||
#+BEGIN_SRC elixir :var name="World"
|
||||
"Hello, #{name}!"
|
||||
#+END_SRC
|
||||
|
||||
#+RESULTS:
|
||||
: Hello, World!
|
||||
|
||||
** Multiple variables
|
||||
|
||||
#+BEGIN_SRC elixir :var x=10 :var y=20
|
||||
x + y
|
||||
#+END_SRC
|
||||
|
||||
#+RESULTS:
|
||||
: 30
|
||||
|
||||
** List variable
|
||||
|
||||
#+BEGIN_SRC elixir :var numbers='(1 2 3 4 5)
|
||||
Enum.sum(numbers)
|
||||
#+END_SRC
|
||||
|
||||
#+RESULTS:
|
||||
: 15
|
||||
|
||||
** Table as variable
|
||||
|
||||
#+NAME: my-data
|
||||
| a | 1 |
|
||||
| b | 2 |
|
||||
| c | 3 |
|
||||
|
||||
#+BEGIN_SRC elixir :var data=my-data
|
||||
Enum.map(data, fn [k, v] -> "#{k}=#{v}" end)
|
||||
#+END_SRC
|
||||
```
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `ob-elixir--elisp-to-elixir` correctly converts all Elisp types
|
||||
- [ ] `org-babel-variable-assignments:elixir` generates valid Elixir code
|
||||
- [ ] `:var x=5` works in org blocks
|
||||
- [ ] `:var name="string"` works with string values
|
||||
- [ ] Multiple `:var` arguments work
|
||||
- [ ] Lists and tables can be passed as variables
|
||||
- [ ] All tests pass: `make test`
|
||||
|
||||
## Edge Cases to Consider
|
||||
|
||||
1. **Variable name conflicts**: Elixir variables must start with lowercase
|
||||
2. **Special characters in strings**: Quotes, newlines, backslashes
|
||||
3. **Empty lists**: Should produce `[]`
|
||||
4. **Mixed type lists**: `[1, "two", :three]`
|
||||
5. **hline in tables**: Special symbol for table separators
|
||||
|
||||
## Files Modified
|
||||
|
||||
- `ob-elixir.el` - Add variable handling functions
|
||||
- `test/test-ob-elixir.el` - Add variable tests
|
||||
|
||||
## References
|
||||
|
||||
- [docs/03-org-babel-implementation-guide.md](../docs/03-org-babel-implementation-guide.md) - Variable Handling section
|
||||
- [docs/04-elixir-integration-strategies.md](../docs/04-elixir-integration-strategies.md) - Data Type Conversion section
|
||||
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
|
||||
330
tasks/05-result-formatting.md
Normal file
330
tasks/05-result-formatting.md
Normal file
@@ -0,0 +1,330 @@
|
||||
# Task 05: Result Formatting and Table Support
|
||||
|
||||
**Phase**: 1 - Core (MVP)
|
||||
**Priority**: Medium
|
||||
**Estimated Time**: 1-2 hours
|
||||
**Dependencies**: Task 02 (Basic Execution), Task 03 (Variable Injection)
|
||||
|
||||
## Objective
|
||||
|
||||
Implement proper result formatting so Elixir lists become org tables and results are properly parsed back into Elisp data structures.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Task 02 and 03 completed
|
||||
- Basic execution and variables working
|
||||
|
||||
## Background
|
||||
|
||||
Org-babel can display results as:
|
||||
- Scalar values (`:results scalar`)
|
||||
- Tables (`:results table`)
|
||||
- Raw org markup (`:results raw`)
|
||||
- Verbatim (`:results verbatim`)
|
||||
|
||||
When Elixir returns a list like `[[1, 2], [3, 4]]`, org should display it as a table:
|
||||
|
||||
```
|
||||
| 1 | 2 |
|
||||
| 3 | 4 |
|
||||
```
|
||||
|
||||
## Steps
|
||||
|
||||
### Step 1: Implement result parsing
|
||||
|
||||
Add to `ob-elixir.el`:
|
||||
|
||||
```elisp
|
||||
;;; Result Formatting
|
||||
|
||||
(defun ob-elixir--table-or-string (result)
|
||||
"Convert RESULT to Emacs table or string.
|
||||
|
||||
If RESULT looks like a list, parse it into an Elisp list.
|
||||
Otherwise return as string.
|
||||
|
||||
Uses `org-babel-script-escape' for parsing."
|
||||
(let ((trimmed (string-trim result)))
|
||||
(cond
|
||||
;; Empty result
|
||||
((string-empty-p trimmed) nil)
|
||||
|
||||
;; Looks like a list - try to parse
|
||||
((string-match-p "^\\[.*\\]$" trimmed)
|
||||
(condition-case nil
|
||||
(let ((parsed (org-babel-script-escape trimmed)))
|
||||
(ob-elixir--sanitize-table parsed))
|
||||
(error trimmed)))
|
||||
|
||||
;; Looks like a tuple - convert to list first
|
||||
((string-match-p "^{.*}$" trimmed)
|
||||
(condition-case nil
|
||||
(let* ((as-list (replace-regexp-in-string
|
||||
"^{\\(.*\\)}$" "[\\1]" trimmed))
|
||||
(parsed (org-babel-script-escape as-list)))
|
||||
(ob-elixir--sanitize-table parsed))
|
||||
(error trimmed)))
|
||||
|
||||
;; Scalar value
|
||||
(t trimmed))))
|
||||
```
|
||||
|
||||
### Step 2: Implement table sanitization
|
||||
|
||||
```elisp
|
||||
(defvar ob-elixir-nil-to 'hline
|
||||
"Elisp value to use for Elixir nil in table cells.
|
||||
|
||||
When nil appears in an Elixir list that becomes a table,
|
||||
it is replaced with this value. Use `hline' for org table
|
||||
horizontal lines, or nil for empty cells.")
|
||||
|
||||
(defun ob-elixir--sanitize-table (data)
|
||||
"Sanitize DATA for use as an org table.
|
||||
|
||||
Replaces nil values according to `ob-elixir-nil-to'.
|
||||
Ensures consistent structure for table rendering."
|
||||
(cond
|
||||
;; Not a list - return as-is
|
||||
((not (listp data)) data)
|
||||
|
||||
;; Empty list
|
||||
((null data) nil)
|
||||
|
||||
;; List of lists - could be table
|
||||
((and (listp (car data)) (not (null (car data))))
|
||||
(mapcar #'ob-elixir--sanitize-row data))
|
||||
|
||||
;; Simple list - single row
|
||||
(t (ob-elixir--sanitize-row data))))
|
||||
|
||||
(defun ob-elixir--sanitize-row (row)
|
||||
"Sanitize a single ROW for table display."
|
||||
(if (listp row)
|
||||
(mapcar (lambda (cell)
|
||||
(cond
|
||||
((null cell) ob-elixir-nil-to)
|
||||
((eq cell 'nil) ob-elixir-nil-to)
|
||||
(t cell)))
|
||||
row)
|
||||
row))
|
||||
```
|
||||
|
||||
### Step 3: Handle keyword lists and maps
|
||||
|
||||
```elisp
|
||||
(defun ob-elixir--parse-keyword-list (str)
|
||||
"Parse STR as Elixir keyword list into alist.
|
||||
|
||||
Handles format like: [a: 1, b: 2]"
|
||||
(when (string-match "^\\[\\(.*\\)\\]$" str)
|
||||
(let ((content (match-string 1 str)))
|
||||
(when (string-match-p "^[a-z_]+:" content)
|
||||
(let ((pairs '()))
|
||||
(dolist (part (split-string content ", "))
|
||||
(when (string-match "^\\([a-z_]+\\):\\s-*\\(.+\\)$" part)
|
||||
(push (cons (intern (match-string 1 part))
|
||||
(ob-elixir--parse-value (match-string 2 part)))
|
||||
pairs)))
|
||||
(nreverse pairs))))))
|
||||
|
||||
(defun ob-elixir--parse-value (str)
|
||||
"Parse STR as a simple Elixir value."
|
||||
(let ((trimmed (string-trim str)))
|
||||
(cond
|
||||
((string= trimmed "nil") nil)
|
||||
((string= trimmed "true") t)
|
||||
((string= trimmed "false") nil)
|
||||
((string-match-p "^[0-9]+$" trimmed)
|
||||
(string-to-number trimmed))
|
||||
((string-match-p "^[0-9]+\\.[0-9]+$" trimmed)
|
||||
(string-to-number trimmed))
|
||||
((string-match-p "^\".*\"$" trimmed)
|
||||
(substring trimmed 1 -1))
|
||||
((string-match-p "^:.*$" trimmed)
|
||||
(intern (substring trimmed 1)))
|
||||
(t trimmed))))
|
||||
```
|
||||
|
||||
### Step 4: Update the execute function
|
||||
|
||||
Ensure `org-babel-execute:elixir` uses the parsing:
|
||||
|
||||
```elisp
|
||||
(defun org-babel-execute:elixir (body params)
|
||||
"Execute a block of Elixir code with org-babel.
|
||||
|
||||
BODY is the Elixir code to execute.
|
||||
PARAMS is an alist of header arguments."
|
||||
(let* ((result-type (cdr (assq :result-type params)))
|
||||
(result-params (cdr (assq :result-params params)))
|
||||
(full-body (org-babel-expand-body:generic
|
||||
body params
|
||||
(org-babel-variable-assignments:elixir params)))
|
||||
(result (ob-elixir--execute full-body result-type)))
|
||||
(org-babel-reassemble-table
|
||||
(org-babel-result-cond result-params
|
||||
;; For output/scalar/verbatim - return as-is
|
||||
result
|
||||
;; For value - parse into Elisp data
|
||||
(ob-elixir--table-or-string result))
|
||||
(org-babel-pick-name (cdr (assq :colname-names params))
|
||||
(cdr (assq :colnames params)))
|
||||
(org-babel-pick-name (cdr (assq :rowname-names params))
|
||||
(cdr (assq :rownames params))))))
|
||||
```
|
||||
|
||||
### Step 5: Support column names
|
||||
|
||||
```elisp
|
||||
(defun ob-elixir--maybe-add-colnames (result params)
|
||||
"Add column names to RESULT if specified in PARAMS."
|
||||
(let ((colnames (cdr (assq :colnames params))))
|
||||
(if (and colnames (listp result) (listp (car result)))
|
||||
(cons (if (listp colnames) colnames (car result))
|
||||
(if (listp colnames) result (cdr result)))
|
||||
result)))
|
||||
```
|
||||
|
||||
### Step 6: Add tests
|
||||
|
||||
Add to `test/test-ob-elixir.el`:
|
||||
|
||||
```elisp
|
||||
;;; Result Formatting Tests
|
||||
|
||||
(ert-deftest ob-elixir-test-parse-simple-list ()
|
||||
"Test parsing simple list result."
|
||||
(should (equal '(1 2 3) (ob-elixir--table-or-string "[1, 2, 3]"))))
|
||||
|
||||
(ert-deftest ob-elixir-test-parse-nested-list ()
|
||||
"Test parsing nested list (table) result."
|
||||
(should (equal '((1 2) (3 4))
|
||||
(ob-elixir--table-or-string "[[1, 2], [3, 4]]"))))
|
||||
|
||||
(ert-deftest ob-elixir-test-parse-tuple ()
|
||||
"Test parsing tuple result."
|
||||
(should (equal '(1 2 3) (ob-elixir--table-or-string "{1, 2, 3}"))))
|
||||
|
||||
(ert-deftest ob-elixir-test-parse-scalar ()
|
||||
"Test that scalars are returned as strings."
|
||||
(should (equal "42" (ob-elixir--table-or-string "42")))
|
||||
(should (equal ":ok" (ob-elixir--table-or-string ":ok"))))
|
||||
|
||||
(ert-deftest ob-elixir-test-parse-string ()
|
||||
"Test parsing string result."
|
||||
(should (equal "\"hello\"" (ob-elixir--table-or-string "\"hello\""))))
|
||||
|
||||
(ert-deftest ob-elixir-test-sanitize-table-nil ()
|
||||
"Test that nil values are sanitized in tables."
|
||||
(let ((ob-elixir-nil-to 'hline))
|
||||
(should (equal '((1 hline) (hline 2))
|
||||
(ob-elixir--sanitize-table '((1 nil) (nil 2)))))))
|
||||
|
||||
(ert-deftest ob-elixir-test-execution-returns-table ()
|
||||
"Test that list results become tables."
|
||||
(skip-unless (executable-find ob-elixir-command))
|
||||
(let ((result (ob-elixir--table-or-string
|
||||
(ob-elixir--execute "[[1, 2], [3, 4]]" 'value))))
|
||||
(should (equal '((1 2) (3 4)) result))))
|
||||
|
||||
(ert-deftest ob-elixir-test-mixed-list ()
|
||||
"Test parsing mixed-type list."
|
||||
(skip-unless (executable-find ob-elixir-command))
|
||||
(let ((result (ob-elixir--table-or-string
|
||||
(ob-elixir--execute "[1, \"two\", :three]" 'value))))
|
||||
(should (listp result))
|
||||
(should (= 3 (length result)))))
|
||||
```
|
||||
|
||||
### Step 7: Test in an org buffer
|
||||
|
||||
Add to `test.org`:
|
||||
|
||||
```org
|
||||
* Result Formatting Tests
|
||||
|
||||
** Simple list as table row
|
||||
|
||||
#+BEGIN_SRC elixir
|
||||
[1, 2, 3, 4, 5]
|
||||
#+END_SRC
|
||||
|
||||
#+RESULTS:
|
||||
| 1 | 2 | 3 | 4 | 5 |
|
||||
|
||||
** Nested list as table
|
||||
|
||||
#+BEGIN_SRC elixir
|
||||
[
|
||||
["Alice", 30],
|
||||
["Bob", 25],
|
||||
["Charlie", 35]
|
||||
]
|
||||
#+END_SRC
|
||||
|
||||
#+RESULTS:
|
||||
| Alice | 30 |
|
||||
| Bob | 25 |
|
||||
| Charlie | 35 |
|
||||
|
||||
** Map result (verbatim)
|
||||
|
||||
#+BEGIN_SRC elixir :results verbatim
|
||||
%{name: "Alice", age: 30}
|
||||
#+END_SRC
|
||||
|
||||
#+RESULTS:
|
||||
: %{age: 30, name: "Alice"}
|
||||
|
||||
** Enum operations returning lists
|
||||
|
||||
#+BEGIN_SRC elixir
|
||||
Enum.map(1..5, fn x -> [x, x * x] end)
|
||||
#+END_SRC
|
||||
|
||||
#+RESULTS:
|
||||
| 1 | 1 |
|
||||
| 2 | 4 |
|
||||
| 3 | 9 |
|
||||
| 4 | 16 |
|
||||
| 5 | 25 |
|
||||
|
||||
** Tuple result
|
||||
|
||||
#+BEGIN_SRC elixir
|
||||
{:ok, "success", 123}
|
||||
#+END_SRC
|
||||
```
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] Simple lists `[1, 2, 3]` become table rows
|
||||
- [ ] Nested lists `[[1, 2], [3, 4]]` become tables
|
||||
- [ ] Tuples are handled (converted to lists)
|
||||
- [ ] Scalar values remain as strings
|
||||
- [ ] `:results verbatim` bypasses table conversion
|
||||
- [ ] nil values in tables are handled according to config
|
||||
- [ ] All tests pass: `make test`
|
||||
|
||||
## Result Format Reference
|
||||
|
||||
| Elixir Value | Org Display |
|
||||
|--------------|-------------|
|
||||
| `[1, 2, 3]` | `\| 1 \| 2 \| 3 \|` |
|
||||
| `[[1], [2]]` | Multi-row table |
|
||||
| `{:ok, 1}` | `\| ok \| 1 \|` |
|
||||
| `42` | `: 42` |
|
||||
| `"hello"` | `: "hello"` |
|
||||
| `%{a: 1}` | `: %{a: 1}` |
|
||||
|
||||
## Files Modified
|
||||
|
||||
- `ob-elixir.el` - Add result formatting functions
|
||||
- `test/test-ob-elixir.el` - Add formatting tests
|
||||
|
||||
## References
|
||||
|
||||
- [docs/03-org-babel-implementation-guide.md](../docs/03-org-babel-implementation-guide.md) - Result Handling section
|
||||
- [Org Manual - Results of Evaluation](https://orgmode.org/manual/Results-of-Evaluation.html)
|
||||
623
tasks/06-test-suite.md
Normal file
623
tasks/06-test-suite.md
Normal file
@@ -0,0 +1,623 @@
|
||||
# Task 06: Comprehensive Test Suite
|
||||
|
||||
**Phase**: 1 - Core (MVP)
|
||||
**Priority**: High
|
||||
**Estimated Time**: 2-3 hours
|
||||
**Dependencies**: Tasks 01-05 (All Core Tasks)
|
||||
|
||||
## Objective
|
||||
|
||||
Create a comprehensive test suite that covers all implemented functionality, including unit tests, integration tests, and org-buffer tests.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- All Phase 1 tasks completed
|
||||
- Basic functionality working
|
||||
|
||||
## Background
|
||||
|
||||
A good test suite should:
|
||||
1. Test each function in isolation (unit tests)
|
||||
2. Test the integration with org-mode (integration tests)
|
||||
3. Be runnable in CI/CD (batch mode tests)
|
||||
4. Provide good coverage of edge cases
|
||||
|
||||
## Steps
|
||||
|
||||
### Step 1: Organize test file structure
|
||||
|
||||
Create the test directory structure:
|
||||
|
||||
```
|
||||
test/
|
||||
├── test-ob-elixir.el # Main test file (loads all)
|
||||
├── test-ob-elixir-core.el # Core execution tests
|
||||
├── test-ob-elixir-vars.el # Variable handling tests
|
||||
├── test-ob-elixir-results.el # Result formatting tests
|
||||
├── test-ob-elixir-errors.el # Error handling tests
|
||||
└── test-ob-elixir-org.el # Org integration tests
|
||||
```
|
||||
|
||||
### Step 2: Create main test file
|
||||
|
||||
`test/test-ob-elixir.el`:
|
||||
|
||||
```elisp
|
||||
;;; test-ob-elixir.el --- Tests for ob-elixir -*- lexical-binding: t; -*-
|
||||
|
||||
;;; Commentary:
|
||||
|
||||
;; Main test file that loads all test modules.
|
||||
;; Run with: make test
|
||||
;; Or: emacs -batch -l ert -l test/test-ob-elixir.el -f ert-run-tests-batch-and-exit
|
||||
|
||||
;;; Code:
|
||||
|
||||
(require 'ert)
|
||||
|
||||
;; Add source directory to load path
|
||||
(let ((dir (file-name-directory (or load-file-name buffer-file-name))))
|
||||
(add-to-list 'load-path (expand-file-name ".." dir))
|
||||
(add-to-list 'load-path dir))
|
||||
|
||||
;; Load the package
|
||||
(require 'ob-elixir)
|
||||
|
||||
;; Load test modules
|
||||
(require 'test-ob-elixir-core)
|
||||
(require 'test-ob-elixir-vars)
|
||||
(require 'test-ob-elixir-results)
|
||||
(require 'test-ob-elixir-errors)
|
||||
(require 'test-ob-elixir-org)
|
||||
|
||||
;;; Test Helpers
|
||||
|
||||
(defvar ob-elixir-test--elixir-available
|
||||
(executable-find ob-elixir-command)
|
||||
"Non-nil if Elixir is available for testing.")
|
||||
|
||||
(defmacro ob-elixir-test-with-elixir (&rest body)
|
||||
"Execute BODY only if Elixir is available."
|
||||
`(if ob-elixir-test--elixir-available
|
||||
(progn ,@body)
|
||||
(ert-skip "Elixir not available")))
|
||||
|
||||
(defmacro ob-elixir-test-with-temp-org-buffer (&rest body)
|
||||
"Execute BODY in a temporary org-mode buffer."
|
||||
`(with-temp-buffer
|
||||
(org-mode)
|
||||
(ob-elixir--ensure-org-babel-loaded)
|
||||
,@body))
|
||||
|
||||
(defun ob-elixir--ensure-org-babel-loaded ()
|
||||
"Ensure org-babel is loaded with Elixir support."
|
||||
(require 'org)
|
||||
(require 'ob)
|
||||
(org-babel-do-load-languages
|
||||
'org-babel-load-languages
|
||||
'((elixir . t))))
|
||||
|
||||
;;; Smoke Test
|
||||
|
||||
(ert-deftest ob-elixir-test-smoke ()
|
||||
"Basic smoke test - package loads and Elixir is available."
|
||||
(should (featurep 'ob-elixir))
|
||||
(should (fboundp 'org-babel-execute:elixir))
|
||||
(should (boundp 'org-babel-default-header-args:elixir)))
|
||||
|
||||
(provide 'test-ob-elixir)
|
||||
;;; test-ob-elixir.el ends here
|
||||
```
|
||||
|
||||
### Step 3: Create core execution tests
|
||||
|
||||
`test/test-ob-elixir-core.el`:
|
||||
|
||||
```elisp
|
||||
;;; test-ob-elixir-core.el --- Core execution tests -*- lexical-binding: t; -*-
|
||||
|
||||
;;; Code:
|
||||
|
||||
(require 'ert)
|
||||
(require 'ob-elixir)
|
||||
|
||||
;;; Command Tests
|
||||
|
||||
(ert-deftest ob-elixir-test-command-exists ()
|
||||
"Test that the Elixir command is configured."
|
||||
(should (stringp ob-elixir-command))
|
||||
(should (not (string-empty-p ob-elixir-command))))
|
||||
|
||||
(ert-deftest ob-elixir-test-command-executable ()
|
||||
"Test that the Elixir command is executable."
|
||||
(skip-unless (executable-find ob-elixir-command))
|
||||
(should (executable-find ob-elixir-command)))
|
||||
|
||||
;;; Basic Execution Tests
|
||||
|
||||
(ert-deftest ob-elixir-test-execute-simple-value ()
|
||||
"Test simple value evaluation."
|
||||
(skip-unless (executable-find ob-elixir-command))
|
||||
(should (equal "2" (ob-elixir--execute "1 + 1" 'value))))
|
||||
|
||||
(ert-deftest ob-elixir-test-execute-simple-output ()
|
||||
"Test simple output evaluation."
|
||||
(skip-unless (executable-find ob-elixir-command))
|
||||
(should (equal "hello" (ob-elixir--execute "IO.puts(\"hello\")" 'output))))
|
||||
|
||||
(ert-deftest ob-elixir-test-execute-multiline ()
|
||||
"Test multiline code execution."
|
||||
(skip-unless (executable-find ob-elixir-command))
|
||||
(let ((code "x = 10\ny = 20\nx + y"))
|
||||
(should (equal "30" (ob-elixir--execute code 'value)))))
|
||||
|
||||
(ert-deftest ob-elixir-test-execute-function-def ()
|
||||
"Test function definition and call."
|
||||
(skip-unless (executable-find ob-elixir-command))
|
||||
(let ((code "
|
||||
defmodule Test do
|
||||
def double(x), do: x * 2
|
||||
end
|
||||
Test.double(21)"))
|
||||
(should (equal "42" (ob-elixir--execute code 'value)))))
|
||||
|
||||
(ert-deftest ob-elixir-test-execute-enum ()
|
||||
"Test Enum module usage."
|
||||
(skip-unless (executable-find ob-elixir-command))
|
||||
(should (equal "15"
|
||||
(ob-elixir--execute "Enum.sum([1, 2, 3, 4, 5])" 'value))))
|
||||
|
||||
(ert-deftest ob-elixir-test-execute-pipe ()
|
||||
"Test pipe operator."
|
||||
(skip-unless (executable-find ob-elixir-command))
|
||||
(let ((code "[1, 2, 3] |> Enum.map(&(&1 * 2)) |> Enum.sum()"))
|
||||
(should (equal "12" (ob-elixir--execute code 'value)))))
|
||||
|
||||
;;; Data Type Tests
|
||||
|
||||
(ert-deftest ob-elixir-test-execute-list ()
|
||||
"Test list result."
|
||||
(skip-unless (executable-find ob-elixir-command))
|
||||
(should (equal "[1, 2, 3]" (ob-elixir--execute "[1, 2, 3]" 'value))))
|
||||
|
||||
(ert-deftest ob-elixir-test-execute-tuple ()
|
||||
"Test tuple result."
|
||||
(skip-unless (executable-find ob-elixir-command))
|
||||
(should (equal "{:ok, 42}" (ob-elixir--execute "{:ok, 42}" 'value))))
|
||||
|
||||
(ert-deftest ob-elixir-test-execute-map ()
|
||||
"Test map result."
|
||||
(skip-unless (executable-find ob-elixir-command))
|
||||
(let ((result (ob-elixir--execute "%{a: 1, b: 2}" 'value)))
|
||||
(should (string-match-p "%{" result))
|
||||
(should (string-match-p "a:" result))
|
||||
(should (string-match-p "b:" result))))
|
||||
|
||||
(ert-deftest ob-elixir-test-execute-string ()
|
||||
"Test string result."
|
||||
(skip-unless (executable-find ob-elixir-command))
|
||||
(should (equal "\"hello world\""
|
||||
(ob-elixir--execute "\"hello world\"" 'value))))
|
||||
|
||||
(ert-deftest ob-elixir-test-execute-atom ()
|
||||
"Test atom result."
|
||||
(skip-unless (executable-find ob-elixir-command))
|
||||
(should (equal ":ok" (ob-elixir--execute ":ok" 'value))))
|
||||
|
||||
(ert-deftest ob-elixir-test-execute-boolean ()
|
||||
"Test boolean result."
|
||||
(skip-unless (executable-find ob-elixir-command))
|
||||
(should (equal "true" (ob-elixir--execute "true" 'value)))
|
||||
(should (equal "false" (ob-elixir--execute "false" 'value))))
|
||||
|
||||
(ert-deftest ob-elixir-test-execute-nil ()
|
||||
"Test nil result."
|
||||
(skip-unless (executable-find ob-elixir-command))
|
||||
(should (equal "nil" (ob-elixir--execute "nil" 'value))))
|
||||
|
||||
;;; Wrapper Tests
|
||||
|
||||
(ert-deftest ob-elixir-test-wrap-for-value ()
|
||||
"Test value wrapper generation."
|
||||
(let ((wrapped (ob-elixir--wrap-for-value "1 + 1")))
|
||||
(should (string-match-p "result = " wrapped))
|
||||
(should (string-match-p "IO\\.puts" wrapped))
|
||||
(should (string-match-p "inspect" wrapped))))
|
||||
|
||||
(provide 'test-ob-elixir-core)
|
||||
;;; test-ob-elixir-core.el ends here
|
||||
```
|
||||
|
||||
### Step 4: Create variable tests
|
||||
|
||||
`test/test-ob-elixir-vars.el`:
|
||||
|
||||
```elisp
|
||||
;;; test-ob-elixir-vars.el --- Variable handling tests -*- lexical-binding: t; -*-
|
||||
|
||||
;;; Code:
|
||||
|
||||
(require 'ert)
|
||||
(require 'ob-elixir)
|
||||
|
||||
;;; Type Conversion Tests
|
||||
|
||||
(ert-deftest ob-elixir-test-convert-nil ()
|
||||
(should (equal "nil" (ob-elixir--elisp-to-elixir nil))))
|
||||
|
||||
(ert-deftest ob-elixir-test-convert-true ()
|
||||
(should (equal "true" (ob-elixir--elisp-to-elixir t))))
|
||||
|
||||
(ert-deftest ob-elixir-test-convert-integer ()
|
||||
(should (equal "42" (ob-elixir--elisp-to-elixir 42)))
|
||||
(should (equal "-10" (ob-elixir--elisp-to-elixir -10)))
|
||||
(should (equal "0" (ob-elixir--elisp-to-elixir 0))))
|
||||
|
||||
(ert-deftest ob-elixir-test-convert-float ()
|
||||
(should (equal "3.14" (ob-elixir--elisp-to-elixir 3.14)))
|
||||
(should (equal "-2.5" (ob-elixir--elisp-to-elixir -2.5))))
|
||||
|
||||
(ert-deftest ob-elixir-test-convert-string ()
|
||||
(should (equal "\"hello\"" (ob-elixir--elisp-to-elixir "hello")))
|
||||
(should (equal "\"\"" (ob-elixir--elisp-to-elixir ""))))
|
||||
|
||||
(ert-deftest ob-elixir-test-convert-string-escaping ()
|
||||
(should (equal "\"say \\\"hi\\\"\""
|
||||
(ob-elixir--elisp-to-elixir "say \"hi\"")))
|
||||
(should (equal "\"line1\\nline2\""
|
||||
(ob-elixir--elisp-to-elixir "line1\nline2")))
|
||||
(should (equal "\"tab\\there\""
|
||||
(ob-elixir--elisp-to-elixir "tab\there"))))
|
||||
|
||||
(ert-deftest ob-elixir-test-convert-symbol ()
|
||||
(should (equal ":foo" (ob-elixir--elisp-to-elixir 'foo)))
|
||||
(should (equal ":ok" (ob-elixir--elisp-to-elixir 'ok)))
|
||||
(should (equal ":error" (ob-elixir--elisp-to-elixir 'error))))
|
||||
|
||||
(ert-deftest ob-elixir-test-convert-list ()
|
||||
(should (equal "[1, 2, 3]" (ob-elixir--elisp-to-elixir '(1 2 3))))
|
||||
(should (equal "[]" (ob-elixir--elisp-to-elixir '())))
|
||||
(should (equal "[\"a\", \"b\"]" (ob-elixir--elisp-to-elixir '("a" "b")))))
|
||||
|
||||
(ert-deftest ob-elixir-test-convert-nested-list ()
|
||||
(should (equal "[[1, 2], [3, 4]]"
|
||||
(ob-elixir--elisp-to-elixir '((1 2) (3 4))))))
|
||||
|
||||
(ert-deftest ob-elixir-test-convert-vector ()
|
||||
(should (equal "{1, 2, 3}" (ob-elixir--elisp-to-elixir [1 2 3]))))
|
||||
|
||||
(ert-deftest ob-elixir-test-convert-mixed ()
|
||||
(should (equal "[1, \"two\", :three]"
|
||||
(ob-elixir--elisp-to-elixir '(1 "two" three)))))
|
||||
|
||||
;;; Variable Assignment Tests
|
||||
|
||||
(ert-deftest ob-elixir-test-var-assignments-single ()
|
||||
(let ((params '((:var . ("x" . 5)))))
|
||||
(should (equal '("x = 5")
|
||||
(org-babel-variable-assignments:elixir params)))))
|
||||
|
||||
(ert-deftest ob-elixir-test-var-assignments-multiple ()
|
||||
(let ((params '((:var . ("x" . 5))
|
||||
(:var . ("y" . 10)))))
|
||||
(let ((assignments (org-babel-variable-assignments:elixir params)))
|
||||
(should (= 2 (length assignments)))
|
||||
(should (member "x = 5" assignments))
|
||||
(should (member "y = 10" assignments)))))
|
||||
|
||||
(ert-deftest ob-elixir-test-var-assignments-string ()
|
||||
(let ((params '((:var . ("name" . "Alice")))))
|
||||
(should (equal '("name = \"Alice\"")
|
||||
(org-babel-variable-assignments:elixir params)))))
|
||||
|
||||
(ert-deftest ob-elixir-test-var-assignments-list ()
|
||||
(let ((params '((:var . ("data" . (1 2 3))))))
|
||||
(should (equal '("data = [1, 2, 3]")
|
||||
(org-babel-variable-assignments:elixir params)))))
|
||||
|
||||
;;; Execution with Variables
|
||||
|
||||
(ert-deftest ob-elixir-test-execute-with-var ()
|
||||
(skip-unless (executable-find ob-elixir-command))
|
||||
(let* ((params '((:var . ("x" . 10))))
|
||||
(var-lines (org-babel-variable-assignments:elixir params))
|
||||
(full-body (concat (mapconcat #'identity var-lines "\n")
|
||||
"\nx * 2")))
|
||||
(should (equal "20" (ob-elixir--execute full-body 'value)))))
|
||||
|
||||
(ert-deftest ob-elixir-test-execute-with-list-var ()
|
||||
(skip-unless (executable-find ob-elixir-command))
|
||||
(let* ((params '((:var . ("nums" . (1 2 3 4 5)))))
|
||||
(var-lines (org-babel-variable-assignments:elixir params))
|
||||
(full-body (concat (mapconcat #'identity var-lines "\n")
|
||||
"\nEnum.sum(nums)")))
|
||||
(should (equal "15" (ob-elixir--execute full-body 'value)))))
|
||||
|
||||
(ert-deftest ob-elixir-test-execute-with-string-var ()
|
||||
(skip-unless (executable-find ob-elixir-command))
|
||||
(let* ((params '((:var . ("name" . "World"))))
|
||||
(var-lines (org-babel-variable-assignments:elixir params))
|
||||
(full-body (concat (mapconcat #'identity var-lines "\n")
|
||||
"\n\"Hello, #{name}!\"")))
|
||||
(should (equal "\"Hello, World!\""
|
||||
(ob-elixir--execute full-body 'value)))))
|
||||
|
||||
(provide 'test-ob-elixir-vars)
|
||||
;;; test-ob-elixir-vars.el ends here
|
||||
```
|
||||
|
||||
### Step 5: Create result formatting tests
|
||||
|
||||
`test/test-ob-elixir-results.el`:
|
||||
|
||||
```elisp
|
||||
;;; test-ob-elixir-results.el --- Result formatting tests -*- lexical-binding: t; -*-
|
||||
|
||||
;;; Code:
|
||||
|
||||
(require 'ert)
|
||||
(require 'ob-elixir)
|
||||
|
||||
;;; Parsing Tests
|
||||
|
||||
(ert-deftest ob-elixir-test-parse-simple-list ()
|
||||
(should (equal '(1 2 3) (ob-elixir--table-or-string "[1, 2, 3]"))))
|
||||
|
||||
(ert-deftest ob-elixir-test-parse-nested-list ()
|
||||
(should (equal '((1 2) (3 4))
|
||||
(ob-elixir--table-or-string "[[1, 2], [3, 4]]"))))
|
||||
|
||||
(ert-deftest ob-elixir-test-parse-empty-list ()
|
||||
(should (equal '() (ob-elixir--table-or-string "[]"))))
|
||||
|
||||
(ert-deftest ob-elixir-test-parse-tuple ()
|
||||
(should (equal '(1 2 3) (ob-elixir--table-or-string "{1, 2, 3}"))))
|
||||
|
||||
(ert-deftest ob-elixir-test-parse-scalar-number ()
|
||||
(should (equal "42" (ob-elixir--table-or-string "42"))))
|
||||
|
||||
(ert-deftest ob-elixir-test-parse-scalar-atom ()
|
||||
(should (equal ":ok" (ob-elixir--table-or-string ":ok"))))
|
||||
|
||||
(ert-deftest ob-elixir-test-parse-scalar-string ()
|
||||
(should (equal "\"hello\"" (ob-elixir--table-or-string "\"hello\""))))
|
||||
|
||||
(ert-deftest ob-elixir-test-parse-empty ()
|
||||
(should (null (ob-elixir--table-or-string "")))
|
||||
(should (null (ob-elixir--table-or-string " "))))
|
||||
|
||||
;;; Table Sanitization Tests
|
||||
|
||||
(ert-deftest ob-elixir-test-sanitize-nil-values ()
|
||||
(let ((ob-elixir-nil-to 'hline))
|
||||
(should (equal '((1 hline) (hline 2))
|
||||
(ob-elixir--sanitize-table '((1 nil) (nil 2)))))))
|
||||
|
||||
(ert-deftest ob-elixir-test-sanitize-nested ()
|
||||
(let ((ob-elixir-nil-to 'hline))
|
||||
(should (equal '((1 2) (3 4))
|
||||
(ob-elixir--sanitize-table '((1 2) (3 4)))))))
|
||||
|
||||
(ert-deftest ob-elixir-test-sanitize-simple ()
|
||||
(should (equal '(1 2 3)
|
||||
(ob-elixir--sanitize-table '(1 2 3)))))
|
||||
|
||||
;;; Integration Tests
|
||||
|
||||
(ert-deftest ob-elixir-test-full-result-list ()
|
||||
(skip-unless (executable-find ob-elixir-command))
|
||||
(let ((result (ob-elixir--table-or-string
|
||||
(ob-elixir--execute "[1, 2, 3]" 'value))))
|
||||
(should (equal '(1 2 3) result))))
|
||||
|
||||
(ert-deftest ob-elixir-test-full-result-table ()
|
||||
(skip-unless (executable-find ob-elixir-command))
|
||||
(let ((result (ob-elixir--table-or-string
|
||||
(ob-elixir--execute "[[1, 2], [3, 4]]" 'value))))
|
||||
(should (equal '((1 2) (3 4)) result))))
|
||||
|
||||
(provide 'test-ob-elixir-results)
|
||||
;;; test-ob-elixir-results.el ends here
|
||||
```
|
||||
|
||||
### Step 6: Create error handling tests
|
||||
|
||||
`test/test-ob-elixir-errors.el`:
|
||||
|
||||
```elisp
|
||||
;;; test-ob-elixir-errors.el --- Error handling tests -*- lexical-binding: t; -*-
|
||||
|
||||
;;; Code:
|
||||
|
||||
(require 'ert)
|
||||
(require 'ob-elixir)
|
||||
|
||||
;;; Error Detection Tests
|
||||
|
||||
(ert-deftest ob-elixir-test-detect-runtime-error ()
|
||||
(let ((output "** (RuntimeError) something went wrong"))
|
||||
(should (ob-elixir--detect-error output))))
|
||||
|
||||
(ert-deftest ob-elixir-test-detect-compile-error ()
|
||||
(let ((output "** (CompileError) test.exs:1: undefined function"))
|
||||
(should (ob-elixir--detect-error output))))
|
||||
|
||||
(ert-deftest ob-elixir-test-detect-no-error ()
|
||||
(should-not (ob-elixir--detect-error "42"))
|
||||
(should-not (ob-elixir--detect-error "[1, 2, 3]"))
|
||||
(should-not (ob-elixir--detect-error ":ok")))
|
||||
|
||||
(ert-deftest ob-elixir-test-error-type-runtime ()
|
||||
(let* ((output "** (RuntimeError) test error")
|
||||
(info (ob-elixir--detect-error output)))
|
||||
(should (eq 'runtime (plist-get info :type)))))
|
||||
|
||||
(ert-deftest ob-elixir-test-error-type-compile ()
|
||||
(let* ((output "** (CompileError) syntax error")
|
||||
(info (ob-elixir--detect-error output)))
|
||||
(should (eq 'compile (plist-get info :type)))))
|
||||
|
||||
;;; Error Execution Tests
|
||||
|
||||
(ert-deftest ob-elixir-test-runtime-error-no-signal ()
|
||||
(skip-unless (executable-find ob-elixir-command))
|
||||
(let ((ob-elixir-signal-errors nil))
|
||||
(let ((result (ob-elixir--execute "raise \"test\"" 'value)))
|
||||
(should (string-match-p "RuntimeError" result)))))
|
||||
|
||||
(ert-deftest ob-elixir-test-runtime-error-signal ()
|
||||
(skip-unless (executable-find ob-elixir-command))
|
||||
(let ((ob-elixir-signal-errors t))
|
||||
(should-error (ob-elixir--execute "raise \"test\"" 'value)
|
||||
:type 'ob-elixir-runtime-error)))
|
||||
|
||||
(ert-deftest ob-elixir-test-compile-error ()
|
||||
(skip-unless (executable-find ob-elixir-command))
|
||||
(let ((ob-elixir-signal-errors nil))
|
||||
(let ((result (ob-elixir--execute "def incomplete(" 'value)))
|
||||
(should (string-match-p "\\(SyntaxError\\|TokenMissingError\\)" result)))))
|
||||
|
||||
(provide 'test-ob-elixir-errors)
|
||||
;;; test-ob-elixir-errors.el ends here
|
||||
```
|
||||
|
||||
### Step 7: Create org integration tests
|
||||
|
||||
`test/test-ob-elixir-org.el`:
|
||||
|
||||
```elisp
|
||||
;;; test-ob-elixir-org.el --- Org integration tests -*- lexical-binding: t; -*-
|
||||
|
||||
;;; Code:
|
||||
|
||||
(require 'ert)
|
||||
(require 'org)
|
||||
(require 'ob)
|
||||
(require 'ob-elixir)
|
||||
|
||||
;;; Helper Functions
|
||||
|
||||
(defun ob-elixir-test--execute-src-block (code &optional header-args)
|
||||
"Execute CODE as an Elixir src block with HEADER-ARGS."
|
||||
(with-temp-buffer
|
||||
(org-mode)
|
||||
(insert (format "#+BEGIN_SRC elixir%s\n%s\n#+END_SRC"
|
||||
(if header-args (concat " " header-args) "")
|
||||
code))
|
||||
(goto-char (point-min))
|
||||
(forward-line 1)
|
||||
(org-babel-execute-src-block)))
|
||||
|
||||
;;; Basic Org Tests
|
||||
|
||||
(ert-deftest ob-elixir-test-org-simple ()
|
||||
(skip-unless (executable-find ob-elixir-command))
|
||||
(should (equal "2" (ob-elixir-test--execute-src-block "1 + 1"))))
|
||||
|
||||
(ert-deftest ob-elixir-test-org-with-var ()
|
||||
(skip-unless (executable-find ob-elixir-command))
|
||||
(should (equal "20" (ob-elixir-test--execute-src-block "x * 2" ":var x=10"))))
|
||||
|
||||
(ert-deftest ob-elixir-test-org-results-output ()
|
||||
(skip-unless (executable-find ob-elixir-command))
|
||||
(should (equal "hello"
|
||||
(ob-elixir-test--execute-src-block
|
||||
"IO.puts(\"hello\")"
|
||||
":results output"))))
|
||||
|
||||
(ert-deftest ob-elixir-test-org-results-value ()
|
||||
(skip-unless (executable-find ob-elixir-command))
|
||||
(should (equal '(1 2 3)
|
||||
(ob-elixir-test--execute-src-block
|
||||
"[1, 2, 3]"
|
||||
":results value"))))
|
||||
|
||||
(ert-deftest ob-elixir-test-org-results-verbatim ()
|
||||
(skip-unless (executable-find ob-elixir-command))
|
||||
(should (stringp (ob-elixir-test--execute-src-block
|
||||
"[1, 2, 3]"
|
||||
":results verbatim"))))
|
||||
|
||||
;;; Table Tests
|
||||
|
||||
(ert-deftest ob-elixir-test-org-table-result ()
|
||||
(skip-unless (executable-find ob-elixir-command))
|
||||
(let ((result (ob-elixir-test--execute-src-block "[[1, 2], [3, 4]]")))
|
||||
(should (equal '((1 2) (3 4)) result))))
|
||||
|
||||
(provide 'test-ob-elixir-org)
|
||||
;;; test-ob-elixir-org.el ends here
|
||||
```
|
||||
|
||||
### Step 8: Update Makefile
|
||||
|
||||
```makefile
|
||||
EMACS ?= emacs
|
||||
BATCH = $(EMACS) -Q -batch -L . -L test
|
||||
|
||||
.PHONY: all compile test test-unit test-integration lint clean
|
||||
|
||||
all: compile test
|
||||
|
||||
compile:
|
||||
$(BATCH) -f batch-byte-compile ob-elixir.el
|
||||
|
||||
test: test-unit
|
||||
|
||||
test-unit:
|
||||
$(BATCH) -l ert \
|
||||
-l test/test-ob-elixir.el \
|
||||
-f ert-run-tests-batch-and-exit
|
||||
|
||||
test-integration:
|
||||
$(BATCH) -l ert \
|
||||
-l org -l ob \
|
||||
-l test/test-ob-elixir.el \
|
||||
-f ert-run-tests-batch-and-exit
|
||||
|
||||
lint:
|
||||
$(BATCH) --eval "(require 'package)" \
|
||||
--eval "(package-initialize)" \
|
||||
--eval "(unless (package-installed-p 'package-lint) \
|
||||
(package-refresh-contents) \
|
||||
(package-install 'package-lint))" \
|
||||
-l package-lint \
|
||||
-f package-lint-batch-and-exit ob-elixir.el
|
||||
|
||||
clean:
|
||||
rm -f *.elc test/*.elc
|
||||
```
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] All test files created and organized
|
||||
- [ ] `make test` runs all tests
|
||||
- [ ] Tests cover core execution, variables, results, and errors
|
||||
- [ ] Org integration tests work
|
||||
- [ ] Tests can run in CI (batch mode)
|
||||
- [ ] Test coverage is comprehensive (major code paths)
|
||||
|
||||
## Test Coverage Goals
|
||||
|
||||
| Component | Tests | Coverage |
|
||||
|-----------|-------|----------|
|
||||
| Type conversion | 12+ tests | All Elisp types |
|
||||
| Execution | 10+ tests | Value/output, types |
|
||||
| Variables | 8+ tests | All var scenarios |
|
||||
| Results | 8+ tests | Parsing, tables |
|
||||
| Errors | 6+ tests | Detection, signaling |
|
||||
| Org integration | 6+ tests | Full workflow |
|
||||
|
||||
## Files Created
|
||||
|
||||
- `test/test-ob-elixir.el` - Main test file
|
||||
- `test/test-ob-elixir-core.el` - Core tests
|
||||
- `test/test-ob-elixir-vars.el` - Variable tests
|
||||
- `test/test-ob-elixir-results.el` - Result tests
|
||||
- `test/test-ob-elixir-errors.el` - Error tests
|
||||
- `test/test-ob-elixir-org.el` - Org integration tests
|
||||
|
||||
## References
|
||||
|
||||
- [docs/02-testing-emacs-elisp.md](../docs/02-testing-emacs-elisp.md)
|
||||
- [ERT Manual](https://www.gnu.org/software/emacs/manual/html_node/ert/)
|
||||
442
tasks/07-session-support.md
Normal file
442
tasks/07-session-support.md
Normal file
@@ -0,0 +1,442 @@
|
||||
# Task 07: IEx Session Support
|
||||
|
||||
**Phase**: 2 - Sessions
|
||||
**Priority**: High
|
||||
**Estimated Time**: 3-4 hours
|
||||
**Dependencies**: Phase 1 Complete
|
||||
|
||||
## Objective
|
||||
|
||||
Implement IEx session support so code blocks can share state when using `:session` header argument.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Phase 1 complete
|
||||
- Understanding of Emacs comint mode
|
||||
- IEx (Elixir REPL) available
|
||||
|
||||
## Background
|
||||
|
||||
Sessions allow multiple code blocks to share state:
|
||||
|
||||
```org
|
||||
#+BEGIN_SRC elixir :session my-session
|
||||
x = 42
|
||||
#+END_SRC
|
||||
|
||||
#+BEGIN_SRC elixir :session my-session
|
||||
x * 2 # Can use x from previous block
|
||||
#+END_SRC
|
||||
```
|
||||
|
||||
This requires:
|
||||
1. Starting an IEx process
|
||||
2. Managing the process via comint
|
||||
3. Sending code and capturing output
|
||||
4. Proper prompt detection
|
||||
5. Session cleanup
|
||||
|
||||
## Steps
|
||||
|
||||
### Step 1: Add session configuration
|
||||
|
||||
Add to `ob-elixir.el`:
|
||||
|
||||
```elisp
|
||||
;;; Session Configuration
|
||||
|
||||
(defcustom ob-elixir-iex-command "iex"
|
||||
"Command to start IEx session."
|
||||
:type 'string
|
||||
:group 'ob-elixir
|
||||
:safe #'stringp)
|
||||
|
||||
(defconst ob-elixir--prompt-regexp
|
||||
"^\\(?:iex\\|\\.\\.\\)([0-9]+)> "
|
||||
"Regexp matching IEx prompt.
|
||||
Matches both regular prompt 'iex(N)> ' and continuation '...(N)> '.")
|
||||
|
||||
(defconst ob-elixir--eoe-marker
|
||||
"__ob_elixir_eoe_marker__"
|
||||
"End-of-evaluation marker for session output.")
|
||||
|
||||
(defvar ob-elixir--sessions (make-hash-table :test 'equal)
|
||||
"Hash table mapping session names to buffer names.")
|
||||
```
|
||||
|
||||
### Step 2: Implement session initialization
|
||||
|
||||
```elisp
|
||||
;;; Session Management
|
||||
|
||||
(require 'ob-comint)
|
||||
|
||||
(defun org-babel-elixir-initiate-session (&optional session params)
|
||||
"Create or return an Elixir session buffer.
|
||||
|
||||
SESSION is the session name (string or nil).
|
||||
PARAMS are the header arguments.
|
||||
|
||||
Returns the session buffer, or nil if SESSION is \"none\"."
|
||||
(unless (or (not session) (string= session "none"))
|
||||
(let* ((session-name (if (stringp session) session "default"))
|
||||
(buffer (ob-elixir--get-or-create-session session-name params)))
|
||||
(when buffer
|
||||
(puthash session-name (buffer-name buffer) ob-elixir--sessions))
|
||||
buffer)))
|
||||
|
||||
(defun ob-elixir--get-or-create-session (name params)
|
||||
"Get or create an IEx session named NAME with PARAMS."
|
||||
(let* ((buffer-name (format "*ob-elixir:%s*" name))
|
||||
(existing (get-buffer buffer-name)))
|
||||
(if (and existing (org-babel-comint-buffer-livep existing))
|
||||
existing
|
||||
(ob-elixir--start-session buffer-name name params))))
|
||||
|
||||
(defun ob-elixir--start-session (buffer-name session-name params)
|
||||
"Start a new IEx session in BUFFER-NAME."
|
||||
(let* ((buffer (get-buffer-create buffer-name))
|
||||
(process-environment (cons "TERM=dumb" process-environment)))
|
||||
(with-current-buffer buffer
|
||||
;; Start the IEx process
|
||||
(make-comint-in-buffer
|
||||
(format "ob-elixir-%s" session-name)
|
||||
buffer
|
||||
ob-elixir-iex-command
|
||||
nil)
|
||||
|
||||
;; Wait for initial prompt
|
||||
(ob-elixir--wait-for-prompt buffer 10)
|
||||
|
||||
;; Configure IEx for programmatic use
|
||||
(ob-elixir--configure-session buffer)
|
||||
|
||||
buffer)))
|
||||
|
||||
(defun ob-elixir--configure-session (buffer)
|
||||
"Configure IEx session in BUFFER for programmatic use."
|
||||
(let ((config-commands
|
||||
'("IEx.configure(colors: [enabled: false])"
|
||||
"IEx.configure(inspect: [limit: :infinity, printable_limit: :infinity])")))
|
||||
(dolist (cmd config-commands)
|
||||
(ob-elixir--send-command buffer cmd)
|
||||
(ob-elixir--wait-for-prompt buffer 5))))
|
||||
```
|
||||
|
||||
### Step 3: Implement prompt detection
|
||||
|
||||
```elisp
|
||||
(defun ob-elixir--wait-for-prompt (buffer timeout)
|
||||
"Wait for IEx prompt in BUFFER with TIMEOUT seconds."
|
||||
(with-current-buffer buffer
|
||||
(let ((end-time (+ (float-time) timeout)))
|
||||
(while (and (< (float-time) end-time)
|
||||
(not (ob-elixir--at-prompt-p)))
|
||||
(accept-process-output (get-buffer-process buffer) 0.1)
|
||||
(goto-char (point-max)))
|
||||
(ob-elixir--at-prompt-p))))
|
||||
|
||||
(defun ob-elixir--at-prompt-p ()
|
||||
"Return t if the last line in buffer looks like an IEx prompt."
|
||||
(save-excursion
|
||||
(goto-char (point-max))
|
||||
(forward-line 0)
|
||||
(looking-at ob-elixir--prompt-regexp)))
|
||||
```
|
||||
|
||||
### Step 4: Implement command sending
|
||||
|
||||
```elisp
|
||||
(defun ob-elixir--send-command (buffer command)
|
||||
"Send COMMAND to IEx process in BUFFER."
|
||||
(with-current-buffer buffer
|
||||
(goto-char (point-max))
|
||||
(insert command)
|
||||
(comint-send-input nil t)))
|
||||
|
||||
(defun ob-elixir--evaluate-in-session (session body result-type)
|
||||
"Evaluate BODY in SESSION, return result.
|
||||
|
||||
RESULT-TYPE is 'value or 'output."
|
||||
(let* ((buffer (org-babel-elixir-initiate-session session nil))
|
||||
(code (if (eq result-type 'value)
|
||||
(ob-elixir--session-wrap-for-value body)
|
||||
body))
|
||||
(start-marker nil)
|
||||
output)
|
||||
(unless buffer
|
||||
(error "Failed to create Elixir session: %s" session))
|
||||
|
||||
(with-current-buffer buffer
|
||||
;; Mark position before output
|
||||
(goto-char (point-max))
|
||||
(setq start-marker (point-marker))
|
||||
|
||||
;; Send the code
|
||||
(ob-elixir--send-command buffer code)
|
||||
(ob-elixir--wait-for-prompt buffer 30)
|
||||
|
||||
;; Send EOE marker
|
||||
(ob-elixir--send-command buffer
|
||||
(format "\"%s\"" ob-elixir--eoe-marker))
|
||||
(ob-elixir--wait-for-prompt buffer 5)
|
||||
|
||||
;; Extract output
|
||||
(setq output (ob-elixir--extract-session-output
|
||||
buffer start-marker)))
|
||||
|
||||
(ob-elixir--clean-session-output output)))
|
||||
|
||||
(defconst ob-elixir--session-value-wrapper
|
||||
"_ob_result_ = (
|
||||
%s
|
||||
)
|
||||
IO.puts(\"__ob_value_start__\")
|
||||
IO.puts(inspect(_ob_result_, limit: :infinity, printable_limit: :infinity))
|
||||
IO.puts(\"__ob_value_end__\")
|
||||
:ok
|
||||
"
|
||||
"Wrapper for capturing value in session mode.")
|
||||
|
||||
(defun ob-elixir--session-wrap-for-value (body)
|
||||
"Wrap BODY to capture its value in session mode."
|
||||
(format ob-elixir--session-value-wrapper body))
|
||||
```
|
||||
|
||||
### Step 5: Implement output extraction
|
||||
|
||||
```elisp
|
||||
(defun ob-elixir--extract-session-output (buffer start-marker)
|
||||
"Extract output from BUFFER since START-MARKER."
|
||||
(with-current-buffer buffer
|
||||
(let ((end-pos (point-max)))
|
||||
(buffer-substring-no-properties start-marker end-pos))))
|
||||
|
||||
(defun ob-elixir--clean-session-output (output)
|
||||
"Clean OUTPUT from IEx session."
|
||||
(let ((result output))
|
||||
;; Remove ANSI escape codes
|
||||
(setq result (ansi-color-filter-apply result))
|
||||
|
||||
;; Remove prompts
|
||||
(setq result (replace-regexp-in-string
|
||||
ob-elixir--prompt-regexp "" result))
|
||||
|
||||
;; Remove the input echo
|
||||
(setq result (replace-regexp-in-string
|
||||
"^.*\n" "" result nil nil nil 1))
|
||||
|
||||
;; Remove EOE marker
|
||||
(setq result (replace-regexp-in-string
|
||||
(format "\"%s\"" ob-elixir--eoe-marker) "" result))
|
||||
|
||||
;; Extract value if using value wrapper
|
||||
(when (string-match "__ob_value_start__\n\\(.*\\)\n__ob_value_end__" result)
|
||||
(setq result (match-string 1 result)))
|
||||
|
||||
;; Remove :ok from wrapper
|
||||
(setq result (replace-regexp-in-string ":ok\n*$" "" result))
|
||||
|
||||
(string-trim result)))
|
||||
```
|
||||
|
||||
### Step 6: Update execute function
|
||||
|
||||
Modify `org-babel-execute:elixir`:
|
||||
|
||||
```elisp
|
||||
(defun org-babel-execute:elixir (body params)
|
||||
"Execute a block of Elixir code with org-babel."
|
||||
(let* ((session (cdr (assq :session params)))
|
||||
(result-type (cdr (assq :result-type params)))
|
||||
(result-params (cdr (assq :result-params params)))
|
||||
(full-body (org-babel-expand-body:generic
|
||||
body params
|
||||
(org-babel-variable-assignments:elixir params)))
|
||||
(result (if (and session (not (string= session "none")))
|
||||
;; Session mode
|
||||
(ob-elixir--evaluate-in-session session full-body result-type)
|
||||
;; External process mode
|
||||
(ob-elixir--execute full-body result-type))))
|
||||
(org-babel-reassemble-table
|
||||
(org-babel-result-cond result-params
|
||||
result
|
||||
(ob-elixir--table-or-string result))
|
||||
(org-babel-pick-name (cdr (assq :colname-names params))
|
||||
(cdr (assq :colnames params)))
|
||||
(org-babel-pick-name (cdr (assq :rowname-names params))
|
||||
(cdr (assq :rownames params))))))
|
||||
```
|
||||
|
||||
### Step 7: Implement prep-session
|
||||
|
||||
```elisp
|
||||
(defun org-babel-prep-session:elixir (session params)
|
||||
"Prepare SESSION according to PARAMS.
|
||||
|
||||
Sends variable assignments to the session."
|
||||
(let ((buffer (org-babel-elixir-initiate-session session params))
|
||||
(var-lines (org-babel-variable-assignments:elixir params)))
|
||||
(when (and buffer var-lines)
|
||||
(dolist (var-line var-lines)
|
||||
(ob-elixir--send-command buffer var-line)
|
||||
(ob-elixir--wait-for-prompt buffer 5)))
|
||||
buffer))
|
||||
```
|
||||
|
||||
### Step 8: Implement session cleanup
|
||||
|
||||
```elisp
|
||||
(defun ob-elixir-kill-session (session)
|
||||
"Kill the Elixir session named SESSION."
|
||||
(interactive
|
||||
(list (completing-read "Kill session: "
|
||||
(hash-table-keys ob-elixir--sessions))))
|
||||
(let ((buffer-name (gethash session ob-elixir--sessions)))
|
||||
(when buffer-name
|
||||
(let ((buffer (get-buffer buffer-name)))
|
||||
(when buffer
|
||||
(let ((process (get-buffer-process buffer)))
|
||||
(when process
|
||||
(delete-process process)))
|
||||
(kill-buffer buffer)))
|
||||
(remhash session ob-elixir--sessions))))
|
||||
|
||||
(defun ob-elixir-kill-all-sessions ()
|
||||
"Kill all Elixir sessions."
|
||||
(interactive)
|
||||
(maphash (lambda (name _buffer)
|
||||
(ob-elixir-kill-session name))
|
||||
ob-elixir--sessions))
|
||||
```
|
||||
|
||||
### Step 9: Add tests
|
||||
|
||||
Add to `test/test-ob-elixir-sessions.el`:
|
||||
|
||||
```elisp
|
||||
;;; test-ob-elixir-sessions.el --- Session tests -*- lexical-binding: t; -*-
|
||||
|
||||
(require 'ert)
|
||||
(require 'ob-elixir)
|
||||
|
||||
(ert-deftest ob-elixir-test-session-creation ()
|
||||
"Test session creation."
|
||||
(skip-unless (executable-find ob-elixir-iex-command))
|
||||
(unwind-protect
|
||||
(let ((buffer (org-babel-elixir-initiate-session "test-create" nil)))
|
||||
(should buffer)
|
||||
(should (org-babel-comint-buffer-livep buffer)))
|
||||
(ob-elixir-kill-session "test-create")))
|
||||
|
||||
(ert-deftest ob-elixir-test-session-persistence ()
|
||||
"Test that sessions persist state."
|
||||
(skip-unless (executable-find ob-elixir-iex-command))
|
||||
(unwind-protect
|
||||
(progn
|
||||
;; First evaluation - define variable
|
||||
(ob-elixir--evaluate-in-session "test-persist" "x = 42" 'value)
|
||||
;; Second evaluation - use variable
|
||||
(let ((result (ob-elixir--evaluate-in-session
|
||||
"test-persist" "x * 2" 'value)))
|
||||
(should (equal "84" result))))
|
||||
(ob-elixir-kill-session "test-persist")))
|
||||
|
||||
(ert-deftest ob-elixir-test-session-none ()
|
||||
"Test that :session none uses external process."
|
||||
(skip-unless (executable-find ob-elixir-command))
|
||||
(should (null (org-babel-elixir-initiate-session "none" nil))))
|
||||
|
||||
(ert-deftest ob-elixir-test-session-module-def ()
|
||||
"Test defining module in session."
|
||||
(skip-unless (executable-find ob-elixir-iex-command))
|
||||
(unwind-protect
|
||||
(progn
|
||||
(ob-elixir--evaluate-in-session
|
||||
"test-module"
|
||||
"defmodule TestMod do\n def double(x), do: x * 2\nend"
|
||||
'value)
|
||||
(let ((result (ob-elixir--evaluate-in-session
|
||||
"test-module" "TestMod.double(21)" 'value)))
|
||||
(should (equal "42" result))))
|
||||
(ob-elixir-kill-session "test-module")))
|
||||
|
||||
(provide 'test-ob-elixir-sessions)
|
||||
```
|
||||
|
||||
### Step 10: Test in org buffer
|
||||
|
||||
Create session tests in `test.org`:
|
||||
|
||||
```org
|
||||
* Session Tests
|
||||
|
||||
** Define variable in session
|
||||
|
||||
#+BEGIN_SRC elixir :session my-session
|
||||
x = 42
|
||||
#+END_SRC
|
||||
|
||||
** Use variable from session
|
||||
|
||||
#+BEGIN_SRC elixir :session my-session
|
||||
x * 2
|
||||
#+END_SRC
|
||||
|
||||
#+RESULTS:
|
||||
: 84
|
||||
|
||||
** Define module in session
|
||||
|
||||
#+BEGIN_SRC elixir :session my-session
|
||||
defmodule Helper do
|
||||
def greet(name), do: "Hello, #{name}!"
|
||||
end
|
||||
#+END_SRC
|
||||
|
||||
** Use module from session
|
||||
|
||||
#+BEGIN_SRC elixir :session my-session
|
||||
Helper.greet("World")
|
||||
#+END_SRC
|
||||
|
||||
#+RESULTS:
|
||||
: "Hello, World!"
|
||||
```
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `:session name` creates persistent IEx session
|
||||
- [ ] Variables persist across blocks in same session
|
||||
- [ ] Module definitions persist
|
||||
- [ ] `:session none` uses external process (default)
|
||||
- [ ] Multiple named sessions work independently
|
||||
- [ ] Sessions can be killed with `ob-elixir-kill-session`
|
||||
- [ ] Proper prompt detection
|
||||
- [ ] Output is clean (no prompts, ANSI codes)
|
||||
- [ ] All tests pass
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Session hangs
|
||||
|
||||
Check for proper prompt detection. IEx prompts can vary.
|
||||
|
||||
### ANSI codes in output
|
||||
|
||||
The `ansi-color-filter-apply` should remove them. Check TERM environment variable.
|
||||
|
||||
### Process dies unexpectedly
|
||||
|
||||
Check for Elixir errors. May need to handle compilation errors in session context.
|
||||
|
||||
## Files Modified
|
||||
|
||||
- `ob-elixir.el` - Add session support
|
||||
- `test/test-ob-elixir-sessions.el` - Add session tests
|
||||
|
||||
## References
|
||||
|
||||
- [docs/03-org-babel-implementation-guide.md](../docs/03-org-babel-implementation-guide.md) - Session Management
|
||||
- [docs/04-elixir-integration-strategies.md](../docs/04-elixir-integration-strategies.md) - IEx Session Management
|
||||
- [Emacs Comint Mode](https://www.gnu.org/software/emacs/manual/html_node/emacs/Shell-Mode.html)
|
||||
421
tasks/08-mix-project-support.md
Normal file
421
tasks/08-mix-project-support.md
Normal file
@@ -0,0 +1,421 @@
|
||||
# Task 08: Mix Project Support
|
||||
|
||||
**Phase**: 3 - Mix Integration
|
||||
**Priority**: High
|
||||
**Estimated Time**: 2-3 hours
|
||||
**Dependencies**: Task 07 (Session Support) or Phase 1 complete
|
||||
|
||||
## Objective
|
||||
|
||||
Implement Mix project support so Elixir code can be executed within the context of a Mix project, with access to project dependencies and modules.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Phase 1 complete (or Phase 2 for session+mix)
|
||||
- Understanding of Mix build tool
|
||||
- A test Mix project
|
||||
|
||||
## Background
|
||||
|
||||
Mix projects have:
|
||||
- Dependencies in `mix.exs`
|
||||
- Compiled modules in `_build/`
|
||||
- Configuration in `config/`
|
||||
|
||||
To execute code in project context, we need:
|
||||
1. Run code from the project directory
|
||||
2. Use `mix run` for one-shot execution
|
||||
3. Use `iex -S mix` for sessions
|
||||
|
||||
## Steps
|
||||
|
||||
### Step 1: Add Mix configuration
|
||||
|
||||
Add to `ob-elixir.el`:
|
||||
|
||||
```elisp
|
||||
;;; Mix Configuration
|
||||
|
||||
(defcustom ob-elixir-mix-command "mix"
|
||||
"Command to run Mix."
|
||||
:type 'string
|
||||
:group 'ob-elixir
|
||||
:safe #'stringp)
|
||||
|
||||
(defcustom ob-elixir-auto-detect-mix t
|
||||
"Whether to automatically detect Mix projects.
|
||||
|
||||
When non-nil and no :mix-project is specified, ob-elixir will
|
||||
search upward from the org file for a mix.exs file."
|
||||
:type 'boolean
|
||||
:group 'ob-elixir)
|
||||
|
||||
(defconst org-babel-header-args:elixir
|
||||
'((mix-project . :any) ; Path to Mix project root
|
||||
(mix-env . :any) ; MIX_ENV (dev, test, prod)
|
||||
(mix-target . :any)) ; MIX_TARGET for Nerves, etc.
|
||||
"Elixir-specific header arguments.")
|
||||
```
|
||||
|
||||
### Step 2: Implement Mix project detection
|
||||
|
||||
```elisp
|
||||
;;; Mix Project Detection
|
||||
|
||||
(defun ob-elixir--find-mix-project (&optional start-dir)
|
||||
"Find Mix project root by searching for mix.exs.
|
||||
|
||||
Starts from START-DIR (default: current directory) and searches
|
||||
upward. Returns the directory containing mix.exs, or nil."
|
||||
(let* ((dir (or start-dir default-directory))
|
||||
(found (locate-dominating-file dir "mix.exs")))
|
||||
(when found
|
||||
(file-name-directory found))))
|
||||
|
||||
(defun ob-elixir--resolve-mix-project (params)
|
||||
"Resolve Mix project path from PARAMS or auto-detection.
|
||||
|
||||
Returns project path or nil."
|
||||
(let ((explicit (cdr (assq :mix-project params))))
|
||||
(cond
|
||||
;; Explicit project path
|
||||
((and explicit (not (eq explicit 'no)))
|
||||
(expand-file-name explicit))
|
||||
;; Explicitly disabled
|
||||
((eq explicit 'no)
|
||||
nil)
|
||||
;; Auto-detect if enabled
|
||||
(ob-elixir-auto-detect-mix
|
||||
(ob-elixir--find-mix-project))
|
||||
;; No project
|
||||
(t nil))))
|
||||
|
||||
(defun ob-elixir--in-mix-project-p (params)
|
||||
"Return t if execution should happen in Mix project context."
|
||||
(not (null (ob-elixir--resolve-mix-project params))))
|
||||
```
|
||||
|
||||
### Step 3: Implement Mix execution
|
||||
|
||||
```elisp
|
||||
;;; Mix Execution
|
||||
|
||||
(defun ob-elixir--execute-with-mix (body result-type params)
|
||||
"Execute BODY in Mix project context.
|
||||
|
||||
RESULT-TYPE is 'value or 'output.
|
||||
PARAMS contains header arguments including :mix-project."
|
||||
(let* ((project-dir (ob-elixir--resolve-mix-project params))
|
||||
(mix-env (cdr (assq :mix-env params)))
|
||||
(mix-target (cdr (assq :mix-target params)))
|
||||
(default-directory project-dir)
|
||||
(tmp-file (org-babel-temp-file "ob-elixir-mix-" ".exs"))
|
||||
(code (if (eq result-type 'value)
|
||||
(ob-elixir--wrap-for-value body)
|
||||
body))
|
||||
(env-vars (ob-elixir--build-mix-env mix-env mix-target)))
|
||||
|
||||
;; Write code to temp file
|
||||
(with-temp-file tmp-file
|
||||
(insert code))
|
||||
|
||||
;; Execute with mix run
|
||||
(let ((command (format "%s%s run %s"
|
||||
env-vars
|
||||
ob-elixir-mix-command
|
||||
(org-babel-process-file-name tmp-file))))
|
||||
(ob-elixir--process-result
|
||||
(shell-command-to-string command)))))
|
||||
|
||||
(defun ob-elixir--build-mix-env (mix-env mix-target)
|
||||
"Build environment variable prefix for Mix execution."
|
||||
(let ((vars '()))
|
||||
(when mix-env
|
||||
(push (format "MIX_ENV=%s" mix-env) vars))
|
||||
(when mix-target
|
||||
(push (format "MIX_TARGET=%s" mix-target) vars))
|
||||
(if vars
|
||||
(concat (mapconcat #'identity vars " ") " ")
|
||||
"")))
|
||||
```
|
||||
|
||||
### Step 4: Update execute function
|
||||
|
||||
Modify `org-babel-execute:elixir`:
|
||||
|
||||
```elisp
|
||||
(defun org-babel-execute:elixir (body params)
|
||||
"Execute a block of Elixir code with org-babel."
|
||||
(let* ((session (cdr (assq :session params)))
|
||||
(result-type (cdr (assq :result-type params)))
|
||||
(result-params (cdr (assq :result-params params)))
|
||||
(mix-project (ob-elixir--resolve-mix-project params))
|
||||
(full-body (org-babel-expand-body:generic
|
||||
body params
|
||||
(org-babel-variable-assignments:elixir params)))
|
||||
(result (cond
|
||||
;; Session mode
|
||||
((and session (not (string= session "none")))
|
||||
(ob-elixir--evaluate-in-session
|
||||
session full-body result-type params))
|
||||
;; Mix project mode
|
||||
(mix-project
|
||||
(ob-elixir--execute-with-mix
|
||||
full-body result-type params))
|
||||
;; Plain execution
|
||||
(t
|
||||
(ob-elixir--execute full-body result-type)))))
|
||||
(org-babel-reassemble-table
|
||||
(org-babel-result-cond result-params
|
||||
result
|
||||
(ob-elixir--table-or-string result))
|
||||
(org-babel-pick-name (cdr (assq :colname-names params))
|
||||
(cdr (assq :colnames params)))
|
||||
(org-babel-pick-name (cdr (assq :rowname-names params))
|
||||
(cdr (assq :rownames params))))))
|
||||
```
|
||||
|
||||
### Step 5: Update session for Mix projects
|
||||
|
||||
Modify session creation to support Mix:
|
||||
|
||||
```elisp
|
||||
(defun ob-elixir--start-session (buffer-name session-name params)
|
||||
"Start a new IEx session in BUFFER-NAME."
|
||||
(let* ((mix-project (ob-elixir--resolve-mix-project params))
|
||||
(mix-env (cdr (assq :mix-env params)))
|
||||
(buffer (get-buffer-create buffer-name))
|
||||
(default-directory (or mix-project default-directory))
|
||||
(process-environment
|
||||
(append
|
||||
(list "TERM=dumb")
|
||||
(when mix-env (list (format "MIX_ENV=%s" mix-env)))
|
||||
process-environment)))
|
||||
|
||||
(with-current-buffer buffer
|
||||
(if mix-project
|
||||
;; Start with mix
|
||||
(make-comint-in-buffer
|
||||
(format "ob-elixir-%s" session-name)
|
||||
buffer
|
||||
ob-elixir-iex-command
|
||||
nil
|
||||
"-S" "mix")
|
||||
;; Start plain IEx
|
||||
(make-comint-in-buffer
|
||||
(format "ob-elixir-%s" session-name)
|
||||
buffer
|
||||
ob-elixir-iex-command
|
||||
nil))
|
||||
|
||||
;; Wait for prompt
|
||||
(ob-elixir--wait-for-prompt buffer 30)
|
||||
|
||||
;; Configure IEx
|
||||
(ob-elixir--configure-session buffer)
|
||||
|
||||
buffer)))
|
||||
```
|
||||
|
||||
### Step 6: Add compilation support
|
||||
|
||||
```elisp
|
||||
(defcustom ob-elixir-compile-before-run nil
|
||||
"Whether to run mix compile before execution.
|
||||
|
||||
When non-nil, ensures project is compiled before running code.
|
||||
This adds overhead but catches compilation errors early."
|
||||
:type 'boolean
|
||||
:group 'ob-elixir)
|
||||
|
||||
(defun ob-elixir--ensure-compiled (project-dir)
|
||||
"Ensure Mix project at PROJECT-DIR is compiled."
|
||||
(let ((default-directory project-dir))
|
||||
(shell-command-to-string
|
||||
(format "%s compile --force-check" ob-elixir-mix-command))))
|
||||
|
||||
(defun ob-elixir--execute-with-mix (body result-type params)
|
||||
"Execute BODY in Mix project context."
|
||||
(let* ((project-dir (ob-elixir--resolve-mix-project params))
|
||||
(default-directory project-dir))
|
||||
|
||||
;; Optionally compile first
|
||||
(when ob-elixir-compile-before-run
|
||||
(ob-elixir--ensure-compiled project-dir))
|
||||
|
||||
;; ... rest of execution
|
||||
))
|
||||
```
|
||||
|
||||
### Step 7: Add tests
|
||||
|
||||
Create `test/test-ob-elixir-mix.el`:
|
||||
|
||||
```elisp
|
||||
;;; test-ob-elixir-mix.el --- Mix project tests -*- lexical-binding: t; -*-
|
||||
|
||||
(require 'ert)
|
||||
(require 'ob-elixir)
|
||||
|
||||
(defvar ob-elixir-test--mix-project-dir nil
|
||||
"Temporary Mix project directory for testing.")
|
||||
|
||||
(defun ob-elixir-test--setup-mix-project ()
|
||||
"Create a temporary Mix project for testing."
|
||||
(let ((dir (make-temp-file "ob-elixir-test-" t)))
|
||||
(setq ob-elixir-test--mix-project-dir dir)
|
||||
(let ((default-directory dir))
|
||||
;; Create mix.exs
|
||||
(with-temp-file (expand-file-name "mix.exs" dir)
|
||||
(insert "defmodule TestProject.MixProject do
|
||||
use Mix.Project
|
||||
|
||||
def project do
|
||||
[app: :test_project, version: \"0.1.0\", elixir: \"~> 1.14\"]
|
||||
end
|
||||
end"))
|
||||
;; Create lib directory and module
|
||||
(make-directory (expand-file-name "lib" dir))
|
||||
(with-temp-file (expand-file-name "lib/test_project.ex" dir)
|
||||
(insert "defmodule TestProject do
|
||||
def hello, do: \"Hello from TestProject!\"
|
||||
def add(a, b), do: a + b
|
||||
end")))
|
||||
dir))
|
||||
|
||||
(defun ob-elixir-test--cleanup-mix-project ()
|
||||
"Clean up temporary Mix project."
|
||||
(when ob-elixir-test--mix-project-dir
|
||||
(delete-directory ob-elixir-test--mix-project-dir t)
|
||||
(setq ob-elixir-test--mix-project-dir nil)))
|
||||
|
||||
;;; Tests
|
||||
|
||||
(ert-deftest ob-elixir-test-find-mix-project ()
|
||||
"Test Mix project detection."
|
||||
(skip-unless (executable-find ob-elixir-mix-command))
|
||||
(unwind-protect
|
||||
(let* ((project-dir (ob-elixir-test--setup-mix-project))
|
||||
(sub-dir (expand-file-name "lib" project-dir))
|
||||
(default-directory sub-dir))
|
||||
(should (equal project-dir
|
||||
(ob-elixir--find-mix-project))))
|
||||
(ob-elixir-test--cleanup-mix-project)))
|
||||
|
||||
(ert-deftest ob-elixir-test-mix-project-execution ()
|
||||
"Test code execution in Mix project context."
|
||||
(skip-unless (and (executable-find ob-elixir-mix-command)
|
||||
(executable-find ob-elixir-command)))
|
||||
(unwind-protect
|
||||
(let* ((project-dir (ob-elixir-test--setup-mix-project))
|
||||
(params `((:mix-project . ,project-dir))))
|
||||
;; Compile first
|
||||
(let ((default-directory project-dir))
|
||||
(shell-command-to-string "mix compile"))
|
||||
;; Test execution
|
||||
(let ((result (ob-elixir--execute-with-mix
|
||||
"TestProject.hello()" 'value params)))
|
||||
(should (string-match-p "Hello from TestProject" result))))
|
||||
(ob-elixir-test--cleanup-mix-project)))
|
||||
|
||||
(ert-deftest ob-elixir-test-mix-env ()
|
||||
"Test MIX_ENV handling."
|
||||
(skip-unless (executable-find ob-elixir-mix-command))
|
||||
(let ((env-str (ob-elixir--build-mix-env "test" nil)))
|
||||
(should (string-match-p "MIX_ENV=test" env-str))))
|
||||
|
||||
(ert-deftest ob-elixir-test-explicit-no-mix ()
|
||||
"Test disabling Mix with :mix-project no."
|
||||
(let ((params '((:mix-project . no))))
|
||||
(should (null (ob-elixir--resolve-mix-project params)))))
|
||||
|
||||
(provide 'test-ob-elixir-mix)
|
||||
```
|
||||
|
||||
### Step 8: Test in org buffer
|
||||
|
||||
Create Mix tests in `test.org`:
|
||||
|
||||
```org
|
||||
* Mix Project Tests
|
||||
|
||||
** Using project module (explicit path)
|
||||
|
||||
#+BEGIN_SRC elixir :mix-project ~/my_elixir_project
|
||||
MyApp.hello()
|
||||
#+END_SRC
|
||||
|
||||
** Using project module (auto-detect)
|
||||
|
||||
When this org file is inside a Mix project:
|
||||
|
||||
#+BEGIN_SRC elixir
|
||||
MyApp.some_function()
|
||||
#+END_SRC
|
||||
|
||||
** With specific MIX_ENV
|
||||
|
||||
#+BEGIN_SRC elixir :mix-project ~/my_elixir_project :mix-env test
|
||||
Application.get_env(:my_app, :some_config)
|
||||
#+END_SRC
|
||||
|
||||
** Session with Mix
|
||||
|
||||
#+BEGIN_SRC elixir :session mix-session :mix-project ~/my_elixir_project
|
||||
# Has access to project modules
|
||||
alias MyApp.SomeModule
|
||||
#+END_SRC
|
||||
|
||||
** Disable auto-detect
|
||||
|
||||
#+BEGIN_SRC elixir :mix-project no
|
||||
# Plain Elixir, no project context
|
||||
1 + 1
|
||||
#+END_SRC
|
||||
```
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `:mix-project path` executes in specified project
|
||||
- [ ] Auto-detection finds `mix.exs` in parent directories
|
||||
- [ ] `:mix-project no` disables auto-detection
|
||||
- [ ] `:mix-env` sets MIX_ENV correctly
|
||||
- [ ] Project modules are accessible
|
||||
- [ ] Sessions with `:mix-project` use `iex -S mix`
|
||||
- [ ] Compilation errors are reported properly
|
||||
- [ ] All tests pass
|
||||
|
||||
## Header Arguments Reference
|
||||
|
||||
| Argument | Values | Description |
|
||||
|----------|--------|-------------|
|
||||
| `:mix-project` | path, `no` | Project path or disable |
|
||||
| `:mix-env` | dev, test, prod | MIX_ENV value |
|
||||
| `:mix-target` | host, target | MIX_TARGET for Nerves |
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Module not found
|
||||
|
||||
Ensure project is compiled:
|
||||
```bash
|
||||
cd /path/to/project && mix compile
|
||||
```
|
||||
|
||||
### Dependencies not available
|
||||
|
||||
Check that `mix deps.get` has been run.
|
||||
|
||||
### Wrong MIX_ENV
|
||||
|
||||
Explicitly set `:mix-env` header argument.
|
||||
|
||||
## Files Modified
|
||||
|
||||
- `ob-elixir.el` - Add Mix support
|
||||
- `test/test-ob-elixir-mix.el` - Add Mix tests
|
||||
|
||||
## References
|
||||
|
||||
- [docs/04-elixir-integration-strategies.md](../docs/04-elixir-integration-strategies.md) - Mix Project Context
|
||||
- [Mix Documentation](https://hexdocs.pm/mix/Mix.html)
|
||||
401
tasks/09-remote-shell.md
Normal file
401
tasks/09-remote-shell.md
Normal file
@@ -0,0 +1,401 @@
|
||||
# Task 09: Remote Shell (remsh) Support
|
||||
|
||||
**Phase**: 4 - Advanced Features
|
||||
**Priority**: Medium
|
||||
**Estimated Time**: 2-3 hours
|
||||
**Dependencies**: Task 07 (Session Support)
|
||||
|
||||
## Objective
|
||||
|
||||
Implement remote shell support to connect to running Elixir/Erlang nodes and execute code against them.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Session support implemented (Task 07)
|
||||
- Understanding of Erlang distribution
|
||||
- Running Elixir node for testing
|
||||
|
||||
## Background
|
||||
|
||||
Elixir/Erlang nodes can connect to each other for distributed computing. The `--remsh` flag allows IEx to connect to a running node:
|
||||
|
||||
```bash
|
||||
iex --sname console --remsh my_app@hostname --cookie secret
|
||||
```
|
||||
|
||||
This is useful for:
|
||||
- Inspecting production systems
|
||||
- Running code against a live application
|
||||
- Debugging distributed systems
|
||||
|
||||
## Steps
|
||||
|
||||
### Step 1: Add remote shell configuration
|
||||
|
||||
Add to `ob-elixir.el`:
|
||||
|
||||
```elisp
|
||||
;;; Remote Shell Configuration
|
||||
|
||||
(defconst org-babel-header-args:elixir
|
||||
'((mix-project . :any)
|
||||
(mix-env . :any)
|
||||
(remsh . :any) ; Remote node to connect to
|
||||
(node-name . :any) ; --name for local node
|
||||
(node-sname . :any) ; --sname for local node
|
||||
(cookie . :any)) ; Erlang cookie
|
||||
"Elixir-specific header arguments.")
|
||||
|
||||
(defcustom ob-elixir-default-cookie nil
|
||||
"Default Erlang cookie for remote connections.
|
||||
|
||||
Set this if all your nodes use the same cookie.
|
||||
Can be overridden with :cookie header argument."
|
||||
:type '(choice (const nil) string)
|
||||
:group 'ob-elixir)
|
||||
```
|
||||
|
||||
### Step 2: Implement remote session creation
|
||||
|
||||
```elisp
|
||||
;;; Remote Shell Sessions
|
||||
|
||||
(defun ob-elixir--start-remote-session (buffer-name session-name params)
|
||||
"Start a remote shell session in BUFFER-NAME.
|
||||
|
||||
Connects to the node specified in PARAMS."
|
||||
(let* ((remsh (cdr (assq :remsh params)))
|
||||
(node-name (cdr (assq :node-name params)))
|
||||
(node-sname (cdr (assq :node-sname params)))
|
||||
(cookie (or (cdr (assq :cookie params))
|
||||
ob-elixir-default-cookie))
|
||||
(buffer (get-buffer-create buffer-name))
|
||||
(local-name (or node-sname
|
||||
node-name
|
||||
(format "ob_elixir_%d" (random 99999))))
|
||||
(process-environment (cons "TERM=dumb" process-environment)))
|
||||
|
||||
(unless remsh
|
||||
(error "No remote node specified. Use :remsh header argument"))
|
||||
|
||||
(with-current-buffer buffer
|
||||
;; Build command arguments
|
||||
(let ((args (append
|
||||
;; Local node name
|
||||
(if node-name
|
||||
(list "--name" node-name)
|
||||
(list "--sname" local-name))
|
||||
;; Cookie
|
||||
(when cookie
|
||||
(list "--cookie" cookie))
|
||||
;; Remote shell
|
||||
(list "--remsh" remsh))))
|
||||
|
||||
(apply #'make-comint-in-buffer
|
||||
(format "ob-elixir-remsh-%s" session-name)
|
||||
buffer
|
||||
ob-elixir-iex-command
|
||||
nil
|
||||
args))
|
||||
|
||||
;; Wait for connection
|
||||
(unless (ob-elixir--wait-for-prompt buffer 30)
|
||||
(error "Failed to connect to remote node: %s" remsh))
|
||||
|
||||
;; Configure session
|
||||
(ob-elixir--configure-session buffer)
|
||||
|
||||
buffer)))
|
||||
|
||||
(defun ob-elixir--is-remote-session-p (params)
|
||||
"Return t if PARAMS specify a remote shell connection."
|
||||
(not (null (cdr (assq :remsh params)))))
|
||||
```
|
||||
|
||||
### Step 3: Update session creation dispatcher
|
||||
|
||||
Modify `ob-elixir--start-session`:
|
||||
|
||||
```elisp
|
||||
(defun ob-elixir--start-session (buffer-name session-name params)
|
||||
"Start a new IEx session in BUFFER-NAME."
|
||||
(cond
|
||||
;; Remote shell
|
||||
((ob-elixir--is-remote-session-p params)
|
||||
(ob-elixir--start-remote-session buffer-name session-name params))
|
||||
|
||||
;; Mix project
|
||||
((ob-elixir--resolve-mix-project params)
|
||||
(ob-elixir--start-mix-session buffer-name session-name params))
|
||||
|
||||
;; Plain IEx
|
||||
(t
|
||||
(ob-elixir--start-plain-session buffer-name session-name params))))
|
||||
|
||||
(defun ob-elixir--start-plain-session (buffer-name session-name params)
|
||||
"Start a plain IEx session in BUFFER-NAME."
|
||||
(let* ((buffer (get-buffer-create buffer-name))
|
||||
(process-environment (cons "TERM=dumb" process-environment)))
|
||||
(with-current-buffer buffer
|
||||
(make-comint-in-buffer
|
||||
(format "ob-elixir-%s" session-name)
|
||||
buffer
|
||||
ob-elixir-iex-command
|
||||
nil)
|
||||
|
||||
(ob-elixir--wait-for-prompt buffer 10)
|
||||
(ob-elixir--configure-session buffer)
|
||||
buffer)))
|
||||
|
||||
(defun ob-elixir--start-mix-session (buffer-name session-name params)
|
||||
"Start an IEx session with Mix in BUFFER-NAME."
|
||||
(let* ((mix-project (ob-elixir--resolve-mix-project params))
|
||||
(mix-env (cdr (assq :mix-env params)))
|
||||
(buffer (get-buffer-create buffer-name))
|
||||
(default-directory mix-project)
|
||||
(process-environment
|
||||
(append
|
||||
(list "TERM=dumb")
|
||||
(when mix-env (list (format "MIX_ENV=%s" mix-env)))
|
||||
process-environment)))
|
||||
(with-current-buffer buffer
|
||||
(make-comint-in-buffer
|
||||
(format "ob-elixir-%s" session-name)
|
||||
buffer
|
||||
ob-elixir-iex-command
|
||||
nil
|
||||
"-S" "mix")
|
||||
|
||||
(ob-elixir--wait-for-prompt buffer 30)
|
||||
(ob-elixir--configure-session buffer)
|
||||
buffer)))
|
||||
```
|
||||
|
||||
### Step 4: Add connection verification
|
||||
|
||||
```elisp
|
||||
(defun ob-elixir--verify-remote-connection (buffer remsh)
|
||||
"Verify that BUFFER is connected to remote node REMSH."
|
||||
(with-current-buffer buffer
|
||||
(let ((result (ob-elixir--send-and-receive buffer "Node.self()")))
|
||||
(when (string-match-p (regexp-quote remsh) result)
|
||||
t))))
|
||||
|
||||
(defun ob-elixir--send-and-receive (buffer command)
|
||||
"Send COMMAND to BUFFER and return the response."
|
||||
(let ((start-pos nil)
|
||||
(result nil))
|
||||
(with-current-buffer buffer
|
||||
(goto-char (point-max))
|
||||
(setq start-pos (point))
|
||||
(ob-elixir--send-command buffer command)
|
||||
(ob-elixir--wait-for-prompt buffer 10)
|
||||
(setq result (buffer-substring-no-properties start-pos (point))))
|
||||
(ob-elixir--clean-session-output result)))
|
||||
```
|
||||
|
||||
### Step 5: Add safety checks
|
||||
|
||||
```elisp
|
||||
(defcustom ob-elixir-remsh-confirm t
|
||||
"Whether to confirm before connecting to remote nodes.
|
||||
|
||||
When non-nil, ask for confirmation before connecting.
|
||||
This is a safety measure for production systems."
|
||||
:type 'boolean
|
||||
:group 'ob-elixir)
|
||||
|
||||
(defun ob-elixir--confirm-remsh (node)
|
||||
"Confirm remote shell connection to NODE."
|
||||
(or (not ob-elixir-remsh-confirm)
|
||||
(yes-or-no-p
|
||||
(format "Connect to remote Elixir node '%s'? " node))))
|
||||
|
||||
(defun ob-elixir--start-remote-session (buffer-name session-name params)
|
||||
"Start a remote shell session in BUFFER-NAME."
|
||||
(let ((remsh (cdr (assq :remsh params))))
|
||||
(unless (ob-elixir--confirm-remsh remsh)
|
||||
(user-error "Remote shell connection cancelled"))
|
||||
;; ... rest of implementation
|
||||
))
|
||||
```
|
||||
|
||||
### Step 6: Add helper commands
|
||||
|
||||
```elisp
|
||||
(defun ob-elixir-connect-to-node (node &optional cookie)
|
||||
"Interactively connect to a remote Elixir NODE.
|
||||
|
||||
Optional COOKIE specifies the Erlang cookie."
|
||||
(interactive
|
||||
(list (read-string "Remote node: ")
|
||||
(when current-prefix-arg
|
||||
(read-string "Cookie: "))))
|
||||
(let ((params `((:remsh . ,node)
|
||||
,@(when cookie `((:cookie . ,cookie))))))
|
||||
(org-babel-elixir-initiate-session "remote" params)))
|
||||
|
||||
(defun ob-elixir-list-sessions ()
|
||||
"List all active ob-elixir sessions."
|
||||
(interactive)
|
||||
(if (= 0 (hash-table-count ob-elixir--sessions))
|
||||
(message "No active sessions")
|
||||
(with-output-to-temp-buffer "*ob-elixir sessions*"
|
||||
(princ "Active ob-elixir sessions:\n\n")
|
||||
(maphash (lambda (name buffer-name)
|
||||
(let ((buffer (get-buffer buffer-name)))
|
||||
(princ (format " %s -> %s (%s)\n"
|
||||
name
|
||||
buffer-name
|
||||
(if (and buffer
|
||||
(get-buffer-process buffer))
|
||||
"running"
|
||||
"dead")))))
|
||||
ob-elixir--sessions))))
|
||||
```
|
||||
|
||||
### Step 7: Add tests
|
||||
|
||||
Create `test/test-ob-elixir-remsh.el`:
|
||||
|
||||
```elisp
|
||||
;;; test-ob-elixir-remsh.el --- Remote shell tests -*- lexical-binding: t; -*-
|
||||
|
||||
(require 'ert)
|
||||
(require 'ob-elixir)
|
||||
|
||||
;; Note: These tests require a running Elixir node
|
||||
;; Start one with: iex --sname testnode --cookie testcookie
|
||||
|
||||
(defvar ob-elixir-test-remote-node "testnode@localhost"
|
||||
"Remote node for testing. Set to your test node.")
|
||||
|
||||
(defvar ob-elixir-test-remote-cookie "testcookie"
|
||||
"Cookie for test node.")
|
||||
|
||||
(ert-deftest ob-elixir-test-is-remote-session ()
|
||||
"Test remote session detection."
|
||||
(should (ob-elixir--is-remote-session-p
|
||||
'((:remsh . "node@host"))))
|
||||
(should-not (ob-elixir--is-remote-session-p
|
||||
'((:session . "test")))))
|
||||
|
||||
(ert-deftest ob-elixir-test-remote-session-creation ()
|
||||
"Test remote session creation."
|
||||
(skip-unless (executable-find ob-elixir-iex-command))
|
||||
(skip-unless (getenv "OB_ELIXIR_TEST_REMSH")) ; Only run if explicitly enabled
|
||||
(let ((ob-elixir-remsh-confirm nil))
|
||||
(unwind-protect
|
||||
(let* ((params `((:remsh . ,ob-elixir-test-remote-node)
|
||||
(:cookie . ,ob-elixir-test-remote-cookie)))
|
||||
(buffer (org-babel-elixir-initiate-session "test-remote" params)))
|
||||
(should buffer)
|
||||
(should (org-babel-comint-buffer-livep buffer)))
|
||||
(ob-elixir-kill-session "test-remote"))))
|
||||
|
||||
(ert-deftest ob-elixir-test-remote-execution ()
|
||||
"Test code execution on remote node."
|
||||
(skip-unless (executable-find ob-elixir-iex-command))
|
||||
(skip-unless (getenv "OB_ELIXIR_TEST_REMSH"))
|
||||
(let ((ob-elixir-remsh-confirm nil))
|
||||
(unwind-protect
|
||||
(let* ((params `((:remsh . ,ob-elixir-test-remote-node)
|
||||
(:cookie . ,ob-elixir-test-remote-cookie)))
|
||||
(result (ob-elixir--evaluate-in-session
|
||||
"test-remote-exec"
|
||||
"Node.self() |> to_string()"
|
||||
'value
|
||||
params)))
|
||||
(should (string-match-p ob-elixir-test-remote-node result)))
|
||||
(ob-elixir-kill-session "test-remote-exec"))))
|
||||
|
||||
(provide 'test-ob-elixir-remsh)
|
||||
```
|
||||
|
||||
### Step 8: Document usage
|
||||
|
||||
Add to documentation:
|
||||
|
||||
```org
|
||||
* Remote Shell Usage
|
||||
|
||||
** Connecting to a running node
|
||||
|
||||
#+BEGIN_SRC elixir :session remote :remsh myapp@localhost :cookie secret
|
||||
Node.self()
|
||||
#+END_SRC
|
||||
|
||||
** With explicit node name
|
||||
|
||||
#+BEGIN_SRC elixir :session remote :remsh myapp@localhost :node-sname console :cookie secret
|
||||
# Local node will be named 'console'
|
||||
Node.list()
|
||||
#+END_SRC
|
||||
|
||||
** With distributed name
|
||||
|
||||
#+BEGIN_SRC elixir :session remote :remsh myapp@myhost.example.com :node-name console@myhost.example.com :cookie secret
|
||||
# Use full node names for cross-machine connections
|
||||
Application.get_env(:my_app, :some_setting)
|
||||
#+END_SRC
|
||||
```
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `:remsh node@host` connects to remote node
|
||||
- [ ] `:cookie` sets the Erlang cookie
|
||||
- [ ] `:node-sname` and `:node-name` set local node name
|
||||
- [ ] Connection failures produce clear error messages
|
||||
- [ ] Safety confirmation before connecting (configurable)
|
||||
- [ ] `ob-elixir-connect-to-node` interactive command works
|
||||
- [ ] Remote session appears in `ob-elixir-list-sessions`
|
||||
- [ ] All tests pass
|
||||
|
||||
## Header Arguments Reference
|
||||
|
||||
| Argument | Values | Description |
|
||||
|----------|--------|-------------|
|
||||
| `:remsh` | node@host | Remote node to connect to |
|
||||
| `:cookie` | string | Erlang cookie for authentication |
|
||||
| `:node-sname` | name | Short name for local node |
|
||||
| `:node-name` | name@host | Full name for local node |
|
||||
|
||||
## Security Considerations
|
||||
|
||||
1. **Cookies**: Never commit cookies to version control
|
||||
2. **Confirmation**: Enable `ob-elixir-remsh-confirm` for production
|
||||
3. **Network**: Ensure proper firewall rules for Erlang distribution port (4369 + dynamic)
|
||||
4. **Scope**: Consider what code could be executed on production systems
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Cannot connect to node
|
||||
|
||||
1. Verify node is running: `epmd -names`
|
||||
2. Check cookie matches
|
||||
3. Verify network connectivity on port 4369
|
||||
4. Check that node allows remote connections
|
||||
|
||||
### Connection times out
|
||||
|
||||
Increase timeout or check for network issues:
|
||||
```elisp
|
||||
(setq ob-elixir-session-timeout 60)
|
||||
```
|
||||
|
||||
### Node not found
|
||||
|
||||
Ensure using correct node name format:
|
||||
- Short names: `node@hostname` (same machine or subnet)
|
||||
- Long names: `node@hostname.domain.com` (cross-network)
|
||||
|
||||
## Files Modified
|
||||
|
||||
- `ob-elixir.el` - Add remote shell support
|
||||
- `test/test-ob-elixir-remsh.el` - Add remote shell tests
|
||||
|
||||
## References
|
||||
|
||||
- [docs/04-elixir-integration-strategies.md](../docs/04-elixir-integration-strategies.md) - Remote Shell section
|
||||
- [Erlang Distribution](https://www.erlang.org/doc/reference_manual/distributed.html)
|
||||
- [IEx Remote Shell](https://hexdocs.pm/iex/IEx.html#module-remote-shells)
|
||||
358
tasks/10-async-execution.md
Normal file
358
tasks/10-async-execution.md
Normal file
@@ -0,0 +1,358 @@
|
||||
# Task 10: Async Execution Support
|
||||
|
||||
**Phase**: 4 - Advanced Features
|
||||
**Priority**: Low
|
||||
**Estimated Time**: 3-4 hours
|
||||
**Dependencies**: Phase 1 Complete
|
||||
|
||||
## Objective
|
||||
|
||||
Implement asynchronous execution so long-running Elixir code blocks don't freeze Emacs.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Phase 1 complete
|
||||
- Understanding of Emacs async processes
|
||||
|
||||
## Background
|
||||
|
||||
Some Elixir operations can take a long time:
|
||||
- Database migrations
|
||||
- Large data processing
|
||||
- Network operations
|
||||
- Build tasks
|
||||
|
||||
Async execution allows:
|
||||
- Continue editing while code runs
|
||||
- Visual indicator of running blocks
|
||||
- Cancel long-running operations
|
||||
|
||||
## Steps
|
||||
|
||||
### Step 1: Add async configuration
|
||||
|
||||
Add to `ob-elixir.el`:
|
||||
|
||||
```elisp
|
||||
;;; Async Configuration
|
||||
|
||||
(defcustom ob-elixir-async-timeout 300
|
||||
"Timeout in seconds for async execution.
|
||||
|
||||
After this time, async execution will be cancelled."
|
||||
:type 'integer
|
||||
:group 'ob-elixir)
|
||||
|
||||
(defvar ob-elixir--async-processes (make-hash-table :test 'equal)
|
||||
"Hash table mapping buffer positions to async processes.")
|
||||
|
||||
(defconst org-babel-header-args:elixir
|
||||
'((mix-project . :any)
|
||||
(mix-env . :any)
|
||||
(remsh . :any)
|
||||
(node-name . :any)
|
||||
(node-sname . :any)
|
||||
(cookie . :any)
|
||||
(async . ((yes no)))) ; NEW: async execution
|
||||
"Elixir-specific header arguments.")
|
||||
```
|
||||
|
||||
### Step 2: Implement async execution
|
||||
|
||||
```elisp
|
||||
;;; Async Execution
|
||||
|
||||
(defun ob-elixir--execute-async (body result-type callback)
|
||||
"Execute BODY asynchronously.
|
||||
|
||||
RESULT-TYPE is 'value or 'output.
|
||||
CALLBACK is called with the result when complete."
|
||||
(let* ((tmp-file (org-babel-temp-file "ob-elixir-async-" ".exs"))
|
||||
(code (if (eq result-type 'value)
|
||||
(ob-elixir--wrap-for-value body)
|
||||
body))
|
||||
(output-buffer (generate-new-buffer " *ob-elixir-async*"))
|
||||
process)
|
||||
|
||||
;; Write code to temp file
|
||||
(with-temp-file tmp-file
|
||||
(insert code))
|
||||
|
||||
;; Start async process
|
||||
(setq process
|
||||
(start-process
|
||||
"ob-elixir-async"
|
||||
output-buffer
|
||||
ob-elixir-command
|
||||
tmp-file))
|
||||
|
||||
;; Set up process sentinel
|
||||
(set-process-sentinel
|
||||
process
|
||||
(lambda (proc event)
|
||||
(when (memq (process-status proc) '(exit signal))
|
||||
(let ((result (with-current-buffer (process-buffer proc)
|
||||
(buffer-string))))
|
||||
;; Clean up
|
||||
(kill-buffer (process-buffer proc))
|
||||
(delete-file tmp-file)
|
||||
;; Call callback with result
|
||||
(funcall callback (ob-elixir--process-result
|
||||
(string-trim result)))))))
|
||||
|
||||
;; Set up timeout
|
||||
(run-at-time ob-elixir-async-timeout nil
|
||||
(lambda ()
|
||||
(when (process-live-p process)
|
||||
(kill-process process)
|
||||
(funcall callback "Error: Async execution timed out"))))
|
||||
|
||||
process))
|
||||
```
|
||||
|
||||
### Step 3: Integrate with org-babel
|
||||
|
||||
```elisp
|
||||
(defun ob-elixir--async-p (params)
|
||||
"Return t if PARAMS specify async execution."
|
||||
(string= "yes" (cdr (assq :async params))))
|
||||
|
||||
(defun org-babel-execute:elixir (body params)
|
||||
"Execute a block of Elixir code with org-babel."
|
||||
(let* ((session (cdr (assq :session params)))
|
||||
(result-type (cdr (assq :result-type params)))
|
||||
(result-params (cdr (assq :result-params params)))
|
||||
(async (ob-elixir--async-p params))
|
||||
(full-body (org-babel-expand-body:generic
|
||||
body params
|
||||
(org-babel-variable-assignments:elixir params))))
|
||||
|
||||
(if async
|
||||
;; Async execution
|
||||
(ob-elixir--execute-async-block full-body result-type params)
|
||||
;; Sync execution (existing code)
|
||||
(let ((result (ob-elixir--execute-sync full-body result-type params)))
|
||||
(org-babel-reassemble-table
|
||||
(org-babel-result-cond result-params
|
||||
result
|
||||
(ob-elixir--table-or-string result))
|
||||
(org-babel-pick-name (cdr (assq :colname-names params))
|
||||
(cdr (assq :colnames params)))
|
||||
(org-babel-pick-name (cdr (assq :rowname-names params))
|
||||
(cdr (assq :rownames params))))))))
|
||||
|
||||
(defun ob-elixir--execute-sync (body result-type params)
|
||||
"Execute BODY synchronously."
|
||||
(cond
|
||||
((and (cdr (assq :session params))
|
||||
(not (string= (cdr (assq :session params)) "none")))
|
||||
(ob-elixir--evaluate-in-session
|
||||
(cdr (assq :session params)) body result-type params))
|
||||
((ob-elixir--resolve-mix-project params)
|
||||
(ob-elixir--execute-with-mix body result-type params))
|
||||
(t
|
||||
(ob-elixir--execute body result-type))))
|
||||
```
|
||||
|
||||
### Step 4: Implement async block handling
|
||||
|
||||
```elisp
|
||||
(defun ob-elixir--execute-async-block (body result-type params)
|
||||
"Execute BODY asynchronously and insert results when done."
|
||||
(let ((buffer (current-buffer))
|
||||
(point (point))
|
||||
(result-params (cdr (assq :result-params params)))
|
||||
(marker (copy-marker (point))))
|
||||
|
||||
;; Show placeholder
|
||||
(ob-elixir--insert-async-placeholder marker)
|
||||
|
||||
;; Execute async
|
||||
(ob-elixir--execute-async
|
||||
body
|
||||
result-type
|
||||
(lambda (result)
|
||||
(ob-elixir--insert-async-result
|
||||
buffer marker result result-params params)))
|
||||
|
||||
;; Return placeholder message
|
||||
"Executing asynchronously..."))
|
||||
|
||||
(defun ob-elixir--insert-async-placeholder (marker)
|
||||
"Insert a placeholder at MARKER indicating async execution."
|
||||
(save-excursion
|
||||
(goto-char marker)
|
||||
(end-of-line)
|
||||
(insert "\n")
|
||||
(insert "#+RESULTS:\n")
|
||||
(insert ": [Executing...]\n")))
|
||||
|
||||
(defun ob-elixir--insert-async-result (buffer marker result result-params params)
|
||||
"Insert RESULT at MARKER in BUFFER."
|
||||
(when (buffer-live-p buffer)
|
||||
(with-current-buffer buffer
|
||||
(save-excursion
|
||||
(goto-char marker)
|
||||
;; Find and remove placeholder
|
||||
(when (search-forward ": [Executing...]" nil t)
|
||||
(beginning-of-line)
|
||||
(let ((start (point)))
|
||||
(forward-line 1)
|
||||
(delete-region start (point))))
|
||||
;; Insert real result
|
||||
(let ((formatted (org-babel-result-cond result-params
|
||||
result
|
||||
(ob-elixir--table-or-string result))))
|
||||
(org-babel-insert-result formatted result-params))))))
|
||||
```
|
||||
|
||||
### Step 5: Add cancellation support
|
||||
|
||||
```elisp
|
||||
(defun ob-elixir-cancel-async ()
|
||||
"Cancel the async execution at point."
|
||||
(interactive)
|
||||
(let* ((pos (point))
|
||||
(process (gethash pos ob-elixir--async-processes)))
|
||||
(if (and process (process-live-p process))
|
||||
(progn
|
||||
(kill-process process)
|
||||
(remhash pos ob-elixir--async-processes)
|
||||
(message "Async execution cancelled"))
|
||||
(message "No async execution at point"))))
|
||||
|
||||
(defun ob-elixir-cancel-all-async ()
|
||||
"Cancel all running async executions."
|
||||
(interactive)
|
||||
(maphash (lambda (_pos process)
|
||||
(when (process-live-p process)
|
||||
(kill-process process)))
|
||||
ob-elixir--async-processes)
|
||||
(clrhash ob-elixir--async-processes)
|
||||
(message "All async executions cancelled"))
|
||||
```
|
||||
|
||||
### Step 6: Add visual indicators
|
||||
|
||||
```elisp
|
||||
(defface ob-elixir-async-running
|
||||
'((t :background "yellow" :foreground "black"))
|
||||
"Face for source blocks with running async execution."
|
||||
:group 'ob-elixir)
|
||||
|
||||
(defun ob-elixir--highlight-async-block (start end)
|
||||
"Highlight the region from START to END as running."
|
||||
(let ((overlay (make-overlay start end)))
|
||||
(overlay-put overlay 'face 'ob-elixir-async-running)
|
||||
(overlay-put overlay 'ob-elixir-async t)
|
||||
overlay))
|
||||
|
||||
(defun ob-elixir--remove-async-highlight ()
|
||||
"Remove async highlighting from current block."
|
||||
(dolist (ov (overlays-in (point-min) (point-max)))
|
||||
(when (overlay-get ov 'ob-elixir-async)
|
||||
(delete-overlay ov))))
|
||||
```
|
||||
|
||||
### Step 7: Add tests
|
||||
|
||||
Create `test/test-ob-elixir-async.el`:
|
||||
|
||||
```elisp
|
||||
;;; test-ob-elixir-async.el --- Async execution tests -*- lexical-binding: t; -*-
|
||||
|
||||
(require 'ert)
|
||||
(require 'ob-elixir)
|
||||
|
||||
(ert-deftest ob-elixir-test-async-detection ()
|
||||
"Test async header argument detection."
|
||||
(should (ob-elixir--async-p '((:async . "yes"))))
|
||||
(should-not (ob-elixir--async-p '((:async . "no"))))
|
||||
(should-not (ob-elixir--async-p '())))
|
||||
|
||||
(ert-deftest ob-elixir-test-async-execution ()
|
||||
"Test async execution completion."
|
||||
(skip-unless (executable-find ob-elixir-command))
|
||||
(let ((result nil)
|
||||
(done nil))
|
||||
(ob-elixir--execute-async
|
||||
"1 + 1"
|
||||
'value
|
||||
(lambda (r)
|
||||
(setq result r)
|
||||
(setq done t)))
|
||||
;; Wait for completion
|
||||
(with-timeout (10 (error "Async test timed out"))
|
||||
(while (not done)
|
||||
(accept-process-output nil 0.1)))
|
||||
(should (equal "2" result))))
|
||||
|
||||
(ert-deftest ob-elixir-test-async-timeout ()
|
||||
"Test async timeout handling."
|
||||
(skip-unless (executable-find ob-elixir-command))
|
||||
(let ((ob-elixir-async-timeout 1)
|
||||
(result nil)
|
||||
(done nil))
|
||||
(ob-elixir--execute-async
|
||||
":timer.sleep(5000)" ; Sleep for 5 seconds
|
||||
'value
|
||||
(lambda (r)
|
||||
(setq result r)
|
||||
(setq done t)))
|
||||
;; Wait for timeout
|
||||
(with-timeout (3 (error "Test timed out"))
|
||||
(while (not done)
|
||||
(accept-process-output nil 0.1)))
|
||||
(should (string-match-p "timed out" result))))
|
||||
|
||||
(provide 'test-ob-elixir-async)
|
||||
```
|
||||
|
||||
### Step 8: Document usage
|
||||
|
||||
Add to documentation:
|
||||
|
||||
```org
|
||||
* Async Execution
|
||||
|
||||
** Long-running computation
|
||||
|
||||
#+BEGIN_SRC elixir :async yes
|
||||
# This won't block Emacs
|
||||
Enum.reduce(1..1000000, 0, &+/2)
|
||||
#+END_SRC
|
||||
|
||||
** Async with Mix project
|
||||
|
||||
#+BEGIN_SRC elixir :async yes :mix-project ~/my_app
|
||||
MyApp.expensive_operation()
|
||||
#+END_SRC
|
||||
|
||||
** Cancel with M-x ob-elixir-cancel-async
|
||||
```
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `:async yes` executes code asynchronously
|
||||
- [ ] Placeholder shown while executing
|
||||
- [ ] Results inserted when complete
|
||||
- [ ] Timeout handled gracefully
|
||||
- [ ] `ob-elixir-cancel-async` cancels execution
|
||||
- [ ] Visual indicator for running blocks
|
||||
- [ ] All tests pass
|
||||
|
||||
## Limitations
|
||||
|
||||
- Sessions cannot be async (they're inherently stateful)
|
||||
- Multiple async blocks may have ordering issues
|
||||
- Async results may not integrate perfectly with noweb
|
||||
|
||||
## Files Modified
|
||||
|
||||
- `ob-elixir.el` - Add async support
|
||||
- `test/test-ob-elixir-async.el` - Add async tests
|
||||
|
||||
## References
|
||||
|
||||
- [Emacs Async Processes](https://www.gnu.org/software/emacs/manual/html_node/elisp/Asynchronous-Processes.html)
|
||||
- [Process Sentinels](https://www.gnu.org/software/emacs/manual/html_node/elisp/Sentinels.html)
|
||||
Reference in New Issue
Block a user