docs and tasks

This commit is contained in:
2026-01-24 13:44:20 +01:00
commit 3ca9c10678
16 changed files with 6704 additions and 0 deletions

144
tasks/00-index.md Normal file
View 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
View 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
View 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)

View 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
View 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

View 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
View 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
View 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)

View 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
View 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
View 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)