docs and tasks
This commit is contained in:
445
docs/01-emacs-elisp-best-practices.md
Normal file
445
docs/01-emacs-elisp-best-practices.md
Normal file
@@ -0,0 +1,445 @@
|
||||
# Emacs Lisp Best Practices for Package Development
|
||||
|
||||
This document covers best practices for writing Emacs Lisp packages, specifically targeting org-babel language implementations.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [File Structure and Headers](#file-structure-and-headers)
|
||||
- [Naming Conventions](#naming-conventions)
|
||||
- [Variables and Customization](#variables-and-customization)
|
||||
- [Function Definitions](#function-definitions)
|
||||
- [Error Handling](#error-handling)
|
||||
- [Documentation](#documentation)
|
||||
- [Dependencies and Loading](#dependencies-and-loading)
|
||||
- [References](#references)
|
||||
|
||||
---
|
||||
|
||||
## File Structure and Headers
|
||||
|
||||
Every Emacs Lisp package file should follow a standard structure:
|
||||
|
||||
```elisp
|
||||
;;; ob-elixir.el --- Org Babel functions for Elixir evaluation -*- 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: 1.0.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.
|
||||
|
||||
;; This program is distributed in the hope that it will be useful,
|
||||
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
;; GNU General Public License for more details.
|
||||
|
||||
;; You should have received a copy of the GNU General Public License
|
||||
;; along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
;;; Commentary:
|
||||
|
||||
;; This package provides Org Babel support for evaluating Elixir code blocks.
|
||||
;;
|
||||
;; Features:
|
||||
;; - Execute Elixir code in org-mode source blocks
|
||||
;; - Support for Mix project context
|
||||
;; - Session support via IEx
|
||||
;; - Variable passing between code blocks
|
||||
;;
|
||||
;; 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-ref)
|
||||
(require 'ob-comint)
|
||||
(require 'ob-eval)
|
||||
|
||||
;; ... your code here ...
|
||||
|
||||
(provide 'ob-elixir)
|
||||
;;; ob-elixir.el ends here
|
||||
```
|
||||
|
||||
### Key Header Elements
|
||||
|
||||
| Element | Purpose | Example |
|
||||
|----------------------|--------------------------------------------|-----------------------|
|
||||
| `lexical-binding: t` | Enable lexical scoping (always use this) | First line comment |
|
||||
| `Package-Requires` | Declare dependencies with minimum versions | `((emacs "27.1"))` |
|
||||
| `Keywords` | Help with package discovery | Standard keyword list |
|
||||
| `Version` | Semantic versioning | `1.0.0` |
|
||||
|
||||
---
|
||||
|
||||
## Naming Conventions
|
||||
|
||||
### Package Prefix
|
||||
|
||||
All symbols (functions, variables, constants) must be prefixed with the package name to avoid namespace collisions:
|
||||
|
||||
```elisp
|
||||
;; GOOD - properly prefixed
|
||||
(defvar ob-elixir-command "elixir")
|
||||
(defun ob-elixir-evaluate (body params) ...)
|
||||
(defconst ob-elixir-eoe-indicator "---ob-elixir-eoe---")
|
||||
|
||||
;; BAD - no prefix, will collide
|
||||
(defvar elixir-command "elixir")
|
||||
(defun evaluate-elixir (body) ...)
|
||||
```
|
||||
|
||||
### Naming Patterns for org-babel
|
||||
|
||||
Org-babel uses specific naming conventions that must be followed:
|
||||
|
||||
```elisp
|
||||
;; Required function - note the colon syntax
|
||||
(defun org-babel-execute:elixir (body params)
|
||||
"Execute a block of Elixir code with Babel.")
|
||||
|
||||
;; Optional session function
|
||||
(defun org-babel-elixir-initiate-session (&optional session params)
|
||||
"Create or return an Elixir session buffer.")
|
||||
|
||||
;; Variable assignments function
|
||||
(defun org-babel-variable-assignments:elixir (params)
|
||||
"Return Elixir code to assign variables.")
|
||||
|
||||
;; Default header arguments variable
|
||||
(defvar org-babel-default-header-args:elixir '()
|
||||
"Default header arguments for Elixir code blocks.")
|
||||
|
||||
;; Prep session function
|
||||
(defun org-babel-prep-session:elixir (session params)
|
||||
"Prepare SESSION with PARAMS.")
|
||||
```
|
||||
|
||||
### Private Functions
|
||||
|
||||
Use double hyphens for internal/private functions:
|
||||
|
||||
```elisp
|
||||
;; Public API
|
||||
(defun ob-elixir-evaluate (body params)
|
||||
"Evaluate BODY with PARAMS.")
|
||||
|
||||
;; Internal helper - note the double hyphen
|
||||
(defun ob-elixir--format-value (value)
|
||||
"Internal: Format VALUE for Elixir.")
|
||||
|
||||
(defun ob-elixir--clean-output (output)
|
||||
"Internal: Remove ANSI codes from OUTPUT.")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Variables and Customization
|
||||
|
||||
### User-Customizable Variables
|
||||
|
||||
Use `defcustom` for variables users should configure:
|
||||
|
||||
```elisp
|
||||
(defgroup ob-elixir nil
|
||||
"Org Babel support for Elixir."
|
||||
:group 'org-babel
|
||||
:prefix "ob-elixir-")
|
||||
|
||||
(defcustom ob-elixir-command "elixir"
|
||||
"Command to invoke Elixir.
|
||||
Can be a full path or just the command name if in PATH."
|
||||
:type 'string
|
||||
:group 'ob-elixir)
|
||||
|
||||
(defcustom ob-elixir-iex-command "iex"
|
||||
"Command to invoke IEx for session mode."
|
||||
:type 'string
|
||||
:group 'ob-elixir)
|
||||
|
||||
(defcustom ob-elixir-mix-command "mix"
|
||||
"Command to invoke Mix."
|
||||
:type 'string
|
||||
:group 'ob-elixir)
|
||||
|
||||
(defcustom ob-elixir-default-session-timeout 30
|
||||
"Default timeout in seconds for session operations."
|
||||
:type 'integer
|
||||
:group 'ob-elixir)
|
||||
|
||||
;; For options with specific choices
|
||||
(defcustom ob-elixir-output-format 'value
|
||||
"Default output format for results."
|
||||
:type '(choice (const :tag "Return value" value)
|
||||
(const :tag "Standard output" output))
|
||||
:group 'ob-elixir)
|
||||
```
|
||||
|
||||
### Internal Variables
|
||||
|
||||
Use `defvar` for internal state:
|
||||
|
||||
```elisp
|
||||
(defvar ob-elixir--session-buffer nil
|
||||
"Current active session buffer.
|
||||
This is internal state and should not be modified directly.")
|
||||
|
||||
(defvar ob-elixir--pending-output ""
|
||||
"Accumulated output waiting to be processed.")
|
||||
```
|
||||
|
||||
### Constants
|
||||
|
||||
Use `defconst` for values that never change:
|
||||
|
||||
```elisp
|
||||
(defconst ob-elixir-eoe-indicator "__ob_elixir_eoe__"
|
||||
"String used to indicate end of evaluation output.")
|
||||
|
||||
(defconst ob-elixir-error-regexp "\\*\\* (\\w+Error)"
|
||||
"Regexp to match Elixir error messages.")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Function Definitions
|
||||
|
||||
### Function Documentation
|
||||
|
||||
Always include comprehensive docstrings:
|
||||
|
||||
```elisp
|
||||
(defun ob-elixir-evaluate (body params)
|
||||
"Evaluate BODY as Elixir code according to PARAMS.
|
||||
|
||||
BODY is the source code string to execute.
|
||||
PARAMS is an alist of header arguments.
|
||||
|
||||
Supported header arguments:
|
||||
:session - Name of IEx session to use, or \"none\"
|
||||
:results - How to handle results (value, output)
|
||||
:var - Variables to inject into the code
|
||||
|
||||
Returns the result as a string, or nil if execution failed.
|
||||
|
||||
Example:
|
||||
(ob-elixir-evaluate \"1 + 1\" \\='((:results . \"value\")))"
|
||||
...)
|
||||
```
|
||||
|
||||
### Interactive Commands
|
||||
|
||||
Mark user-facing commands as interactive:
|
||||
|
||||
```elisp
|
||||
(defun ob-elixir-check-installation ()
|
||||
"Check if Elixir is properly installed and accessible.
|
||||
Display version information in the minibuffer."
|
||||
(interactive)
|
||||
(let ((version (shell-command-to-string
|
||||
(format "%s --version" ob-elixir-command))))
|
||||
(message "Elixir: %s" (string-trim version))))
|
||||
```
|
||||
|
||||
### Using `cl-lib` Properly
|
||||
|
||||
Prefer `cl-lib` functions over deprecated `cl` package:
|
||||
|
||||
```elisp
|
||||
(require 'cl-lib)
|
||||
|
||||
;; Use cl- prefixed versions
|
||||
(cl-defun ob-elixir-parse-result (output &key (format 'value))
|
||||
"Parse OUTPUT according to FORMAT."
|
||||
...)
|
||||
|
||||
(cl-loop for pair in params
|
||||
when (eq (car pair) :var)
|
||||
collect (cdr pair))
|
||||
|
||||
(cl-destructuring-bind (name . value) variable
|
||||
...)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Signaling Errors
|
||||
|
||||
Use appropriate error functions:
|
||||
|
||||
```elisp
|
||||
;; For user errors (incorrect usage)
|
||||
(user-error "No Elixir session found for %s" session-name)
|
||||
|
||||
;; For programming errors
|
||||
(error "Invalid parameter: %S" param)
|
||||
|
||||
;; Custom error types
|
||||
(define-error 'ob-elixir-error "Ob-elixir error")
|
||||
(define-error 'ob-elixir-execution-error "Elixir execution error" 'ob-elixir-error)
|
||||
(define-error 'ob-elixir-session-error "Elixir session error" 'ob-elixir-error)
|
||||
|
||||
;; Signaling custom errors
|
||||
(signal 'ob-elixir-execution-error (list "Compilation failed" output))
|
||||
```
|
||||
|
||||
### Handling Errors Gracefully
|
||||
|
||||
```elisp
|
||||
(defun ob-elixir-safe-evaluate (body params)
|
||||
"Safely evaluate BODY, handling errors gracefully."
|
||||
(condition-case err
|
||||
(ob-elixir-evaluate body params)
|
||||
(ob-elixir-execution-error
|
||||
(message "Elixir error: %s" (cadr err))
|
||||
nil)
|
||||
(file-error
|
||||
(user-error "Cannot access Elixir command: %s" (error-message-string err)))
|
||||
(error
|
||||
(message "Unexpected error: %s" (error-message-string err))
|
||||
nil)))
|
||||
```
|
||||
|
||||
### Input Validation
|
||||
|
||||
```elisp
|
||||
(defun ob-elixir-evaluate (body params)
|
||||
"Evaluate BODY with PARAMS."
|
||||
;; Validate inputs early
|
||||
(unless (stringp body)
|
||||
(error "BODY must be a string, got %S" (type-of body)))
|
||||
(unless (listp params)
|
||||
(error "PARAMS must be an alist"))
|
||||
(when (string-empty-p body)
|
||||
(user-error "Cannot evaluate empty code block"))
|
||||
...)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Documentation
|
||||
|
||||
### Commentary Section
|
||||
|
||||
Write helpful commentary:
|
||||
|
||||
```elisp
|
||||
;;; Commentary:
|
||||
|
||||
;; ob-elixir provides Org Babel support for the Elixir programming language.
|
||||
;;
|
||||
;; Installation:
|
||||
;;
|
||||
;; 1. Ensure Elixir is installed and in your PATH
|
||||
;; 2. Add to your init.el:
|
||||
;;
|
||||
;; (require 'ob-elixir)
|
||||
;; (org-babel-do-load-languages
|
||||
;; 'org-babel-load-languages
|
||||
;; '((elixir . t)))
|
||||
;;
|
||||
;; Usage:
|
||||
;;
|
||||
;; In an org file, create a source block:
|
||||
;;
|
||||
;; #+BEGIN_SRC elixir
|
||||
;; IO.puts("Hello from Elixir!")
|
||||
;; 1 + 1
|
||||
;; #+END_SRC
|
||||
;;
|
||||
;; Press C-c C-c to evaluate.
|
||||
;;
|
||||
;; Header Arguments:
|
||||
;;
|
||||
;; - :session NAME Use a persistent IEx session
|
||||
;; - :results value Return the last expression's value
|
||||
;; - :results output Capture stdout
|
||||
;; - :mix PROJECT Execute in Mix project context
|
||||
;;
|
||||
;; See the README for complete documentation.
|
||||
```
|
||||
|
||||
### Info Manual Integration (Optional)
|
||||
|
||||
For larger packages, consider an info manual:
|
||||
|
||||
```elisp
|
||||
(defun ob-elixir-info ()
|
||||
"Open the ob-elixir info manual."
|
||||
(interactive)
|
||||
(info "(ob-elixir)"))
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Dependencies and Loading
|
||||
|
||||
### Requiring Dependencies
|
||||
|
||||
```elisp
|
||||
;;; Code:
|
||||
|
||||
;; Required dependencies
|
||||
(require 'ob)
|
||||
(require 'ob-ref)
|
||||
(require 'ob-comint)
|
||||
(require 'ob-eval)
|
||||
|
||||
;; Soft dependencies (optional features)
|
||||
(require 'elixir-mode nil t) ; t = noerror
|
||||
|
||||
;; Check for optional dependency
|
||||
(defvar ob-elixir-has-elixir-mode (featurep 'elixir-mode)
|
||||
"Non-nil if elixir-mode is available.")
|
||||
```
|
||||
|
||||
### Autoloads
|
||||
|
||||
For functions that should be available before the package loads:
|
||||
|
||||
```elisp
|
||||
;;;###autoload
|
||||
(defun org-babel-execute:elixir (body params)
|
||||
"Execute a block of Elixir code with Babel."
|
||||
...)
|
||||
|
||||
;;;###autoload
|
||||
(eval-after-load 'org
|
||||
'(add-to-list 'org-src-lang-modes '("elixir" . elixir)))
|
||||
```
|
||||
|
||||
### Provide Statement
|
||||
|
||||
Always end with provide:
|
||||
|
||||
```elisp
|
||||
(provide 'ob-elixir)
|
||||
;;; ob-elixir.el ends here
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
- [GNU Emacs Lisp Reference Manual](https://www.gnu.org/software/emacs/manual/elisp.html)
|
||||
- [Emacs Lisp Style Guide](https://github.com/bbatsov/emacs-lisp-style-guide)
|
||||
- [Packaging Guidelines (MELPA)](https://github.com/melpa/melpa/blob/master/CONTRIBUTING.org)
|
||||
- [Writing GNU Emacs Extensions](https://www.gnu.org/software/emacs/manual/html_node/eintr/)
|
||||
- [Org Mode Source Code](https://git.savannah.gnu.org/cgit/emacs/org-mode.git/)
|
||||
- [Elisp Conventions (Emacs Wiki)](https://www.emacswiki.org/emacs/ElispConventions)
|
||||
626
docs/02-testing-emacs-elisp.md
Normal file
626
docs/02-testing-emacs-elisp.md
Normal file
@@ -0,0 +1,626 @@
|
||||
# Testing Emacs Lisp Packages
|
||||
|
||||
This document covers testing strategies and tools for Emacs Lisp packages, with focus on org-babel language implementations.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Testing Frameworks Overview](#testing-frameworks-overview)
|
||||
- [ERT (Emacs Lisp Regression Testing)](#ert-emacs-lisp-regression-testing)
|
||||
- [Buttercup](#buttercup)
|
||||
- [Test Organization](#test-organization)
|
||||
- [Testing org-babel Specifics](#testing-org-babel-specifics)
|
||||
- [Mocking and Stubbing](#mocking-and-stubbing)
|
||||
- [CI/CD Integration](#cicd-integration)
|
||||
- [Build Tools](#build-tools)
|
||||
- [References](#references)
|
||||
|
||||
---
|
||||
|
||||
## Testing Frameworks Overview
|
||||
|
||||
| Framework | Style | Best For | Included |
|
||||
|---------------|------------------|-------------------------|------------------------|
|
||||
| **ERT** | xUnit | Unit tests, built-in | Yes, in Emacs |
|
||||
| **Buttercup** | BDD/RSpec | Readable specs, mocking | No, install from MELPA |
|
||||
| **Ecukes** | Cucumber/Gherkin | Integration/acceptance | No, install from MELPA |
|
||||
|
||||
For a package like ob-elixir, **ERT** is recommended as it:
|
||||
- Comes built into Emacs
|
||||
- Is well-documented
|
||||
- Works well with org-mode's own test patterns
|
||||
- Integrates easily with CI
|
||||
|
||||
---
|
||||
|
||||
## ERT (Emacs Lisp Regression Testing)
|
||||
|
||||
### Basic Test Structure
|
||||
|
||||
```elisp
|
||||
;;; test-ob-elixir.el --- Tests for ob-elixir -*- lexical-binding: t; -*-
|
||||
|
||||
(require 'ert)
|
||||
(require 'ob-elixir)
|
||||
|
||||
;; Simple test
|
||||
(ert-deftest ob-elixir-test-basic-evaluation ()
|
||||
"Test basic Elixir code evaluation."
|
||||
(should (equal "2" (ob-elixir-evaluate "1 + 1" '()))))
|
||||
|
||||
;; Test with setup
|
||||
(ert-deftest ob-elixir-test-variable-injection ()
|
||||
"Test that variables are properly injected."
|
||||
(let ((params '((:var . ("x" . 5)))))
|
||||
(should (equal "10"
|
||||
(ob-elixir-evaluate "x * 2" params)))))
|
||||
|
||||
;; Test for expected errors
|
||||
(ert-deftest ob-elixir-test-syntax-error ()
|
||||
"Test that syntax errors are handled."
|
||||
(should-error (ob-elixir-evaluate "def foo(" '())
|
||||
:type 'ob-elixir-execution-error))
|
||||
|
||||
;; Test with negation
|
||||
(ert-deftest ob-elixir-test-not-nil ()
|
||||
"Test result is not nil."
|
||||
(should-not (null (ob-elixir-evaluate ":ok" '()))))
|
||||
```
|
||||
|
||||
### ERT Assertions
|
||||
|
||||
```elisp
|
||||
;; Basic assertions
|
||||
(should FORM) ; FORM should be non-nil
|
||||
(should-not FORM) ; FORM should be nil
|
||||
(should-error FORM) ; FORM should signal an error
|
||||
(should-error FORM :type 'error-type) ; Specific error type
|
||||
|
||||
;; Equality checks
|
||||
(should (equal expected actual))
|
||||
(should (string= "expected" actual))
|
||||
(should (= 42 actual)) ; Numeric equality
|
||||
(should (eq 'symbol actual)) ; Symbol identity
|
||||
(should (eql 1.0 actual)) ; Numeric with type
|
||||
|
||||
;; Pattern matching
|
||||
(should (string-match-p "pattern" actual))
|
||||
(should (cl-every #'integerp list))
|
||||
|
||||
;; With custom messages (ERT 28+)
|
||||
(ert-deftest example-with-message ()
|
||||
(let ((result (some-function)))
|
||||
(should (equal expected result)
|
||||
(format "Expected %s but got %s" expected result))))
|
||||
```
|
||||
|
||||
### Test Fixtures
|
||||
|
||||
```elisp
|
||||
;; Using let for setup
|
||||
(ert-deftest ob-elixir-test-with-fixture ()
|
||||
(let ((org-babel-default-header-args:elixir '((:results . "value")))
|
||||
(test-code "Enum.sum([1, 2, 3])"))
|
||||
(should (equal "6" (ob-elixir-evaluate test-code '())))))
|
||||
|
||||
;; Shared setup with macros
|
||||
(defmacro ob-elixir-test-with-temp-buffer (&rest body)
|
||||
"Execute BODY in a temporary org buffer."
|
||||
`(with-temp-buffer
|
||||
(org-mode)
|
||||
,@body))
|
||||
|
||||
(ert-deftest ob-elixir-test-in-org-buffer ()
|
||||
(ob-elixir-test-with-temp-buffer
|
||||
(insert "#+BEGIN_SRC elixir\n1 + 1\n#+END_SRC")
|
||||
(goto-char (point-min))
|
||||
(forward-line 1)
|
||||
(should (equal "2" (org-babel-execute-src-block)))))
|
||||
|
||||
;; Setup/teardown pattern
|
||||
(defvar ob-elixir-test--saved-command nil)
|
||||
|
||||
(defun ob-elixir-test-setup ()
|
||||
"Setup for ob-elixir tests."
|
||||
(setq ob-elixir-test--saved-command ob-elixir-command))
|
||||
|
||||
(defun ob-elixir-test-teardown ()
|
||||
"Teardown for ob-elixir tests."
|
||||
(setq ob-elixir-command ob-elixir-test--saved-command))
|
||||
|
||||
(ert-deftest ob-elixir-test-with-setup-teardown ()
|
||||
(ob-elixir-test-setup)
|
||||
(unwind-protect
|
||||
(progn
|
||||
(setq ob-elixir-command "/custom/path/elixir")
|
||||
(should (string= "/custom/path/elixir" ob-elixir-command)))
|
||||
(ob-elixir-test-teardown)))
|
||||
```
|
||||
|
||||
### Running ERT Tests
|
||||
|
||||
```elisp
|
||||
;; Interactive (in Emacs)
|
||||
M-x ert RET t RET ; Run all tests
|
||||
M-x ert RET ob-elixir RET ; Run tests matching "ob-elixir"
|
||||
M-x ert-run-tests-interactively
|
||||
|
||||
;; From command line
|
||||
emacs -batch -l ert -l ob-elixir.el -l test-ob-elixir.el \
|
||||
-f ert-run-tests-batch-and-exit
|
||||
|
||||
;; With specific selector
|
||||
emacs -batch -l ert -l test-ob-elixir.el \
|
||||
--eval "(ert-run-tests-batch-and-exit 'ob-elixir-test-basic)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Buttercup
|
||||
|
||||
Buttercup provides RSpec-style BDD testing with better mocking support.
|
||||
|
||||
### Installation
|
||||
|
||||
```elisp
|
||||
;; In Cask file or package installation
|
||||
(depends-on "buttercup")
|
||||
|
||||
;; Or via use-package
|
||||
(use-package buttercup :ensure t)
|
||||
```
|
||||
|
||||
### Basic Structure
|
||||
|
||||
```elisp
|
||||
;;; test-ob-elixir-buttercup.el --- Buttercup tests -*- lexical-binding: t; -*-
|
||||
|
||||
(require 'buttercup)
|
||||
(require 'ob-elixir)
|
||||
|
||||
(describe "ob-elixir"
|
||||
(describe "basic evaluation"
|
||||
(it "evaluates simple arithmetic"
|
||||
(expect (ob-elixir-evaluate "1 + 1" '())
|
||||
:to-equal "2"))
|
||||
|
||||
(it "returns nil for empty input"
|
||||
(expect (ob-elixir-evaluate "" '())
|
||||
:to-be nil)))
|
||||
|
||||
(describe "variable injection"
|
||||
(before-each
|
||||
(setq test-params '((:var . ("x" . 10)))))
|
||||
|
||||
(it "injects variables into code"
|
||||
(expect (ob-elixir-evaluate "x * 2" test-params)
|
||||
:to-equal "20"))
|
||||
|
||||
(it "handles multiple variables"
|
||||
(setq test-params '((:var . ("x" . 10))
|
||||
(:var . ("y" . 5))))
|
||||
(expect (ob-elixir-evaluate "x + y" test-params)
|
||||
:to-equal "15")))
|
||||
|
||||
(describe "error handling"
|
||||
(it "signals error on syntax error"
|
||||
(expect (ob-elixir-evaluate "def foo(" '())
|
||||
:to-throw 'ob-elixir-execution-error))))
|
||||
```
|
||||
|
||||
### Buttercup Matchers
|
||||
|
||||
```elisp
|
||||
;; Equality
|
||||
(expect value :to-equal expected)
|
||||
(expect value :to-be expected) ; eq comparison
|
||||
(expect value :to-be-truthy)
|
||||
(expect value :to-be nil)
|
||||
|
||||
;; Comparison
|
||||
(expect value :to-be-greater-than 5)
|
||||
(expect value :to-be-less-than 10)
|
||||
|
||||
;; Strings
|
||||
(expect string :to-match "regexp")
|
||||
(expect string :to-contain-string "substring")
|
||||
|
||||
;; Errors
|
||||
(expect (lambda () (error-func)) :to-throw)
|
||||
(expect (lambda () (error-func)) :to-throw 'error-type)
|
||||
|
||||
;; Spies (see Mocking section)
|
||||
(expect 'function :to-have-been-called)
|
||||
(expect 'function :to-have-been-called-with args)
|
||||
(expect 'function :to-have-been-called-times n)
|
||||
```
|
||||
|
||||
### Running Buttercup
|
||||
|
||||
```bash
|
||||
# With Cask
|
||||
cask exec buttercup -L .
|
||||
|
||||
# With Eldev
|
||||
eldev test buttercup
|
||||
|
||||
# From command line directly
|
||||
emacs -batch -L . -l buttercup \
|
||||
-f buttercup-run-discover
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Test Organization
|
||||
|
||||
### Directory Structure
|
||||
|
||||
```
|
||||
ob-elixir/
|
||||
├── ob-elixir.el
|
||||
├── test/
|
||||
│ ├── test-ob-elixir.el # Main test file
|
||||
│ ├── test-ob-elixir-session.el # Session-specific tests
|
||||
│ ├── test-ob-elixir-vars.el # Variable handling tests
|
||||
│ └── resources/
|
||||
│ ├── sample.org # Test org files
|
||||
│ └── test-project/ # Test Mix project
|
||||
├── Cask # Or Eldev file
|
||||
└── Makefile
|
||||
```
|
||||
|
||||
### Test File Template
|
||||
|
||||
```elisp
|
||||
;;; test-ob-elixir.el --- Tests for ob-elixir -*- lexical-binding: t; -*-
|
||||
|
||||
;;; Commentary:
|
||||
|
||||
;; Tests for ob-elixir package.
|
||||
;; Run with: make test
|
||||
;; Or: emacs -batch -l ert -l 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)))
|
||||
|
||||
(require 'ob-elixir)
|
||||
|
||||
;;; Test Helpers
|
||||
|
||||
(defun ob-elixir-test-org-block (code &optional header-args)
|
||||
"Create org src block with CODE and evaluate it."
|
||||
(with-temp-buffer
|
||||
(org-mode)
|
||||
(insert (format "#+BEGIN_SRC elixir %s\n%s\n#+END_SRC"
|
||||
(or header-args "")
|
||||
code))
|
||||
(goto-char (point-min))
|
||||
(forward-line 1)
|
||||
(org-babel-execute-src-block)))
|
||||
|
||||
;;; Unit Tests
|
||||
|
||||
(ert-deftest ob-elixir-test-command-exists ()
|
||||
"Test that Elixir command is accessible."
|
||||
(should (executable-find ob-elixir-command)))
|
||||
|
||||
(ert-deftest ob-elixir-test-simple-evaluation ()
|
||||
"Test simple code evaluation."
|
||||
(should (equal "2" (ob-elixir-test-org-block "1 + 1"))))
|
||||
|
||||
;;; Integration Tests
|
||||
|
||||
(ert-deftest ob-elixir-integration-full-block ()
|
||||
"Test full org-babel workflow."
|
||||
(skip-unless (executable-find "elixir"))
|
||||
(should (equal "6" (ob-elixir-test-org-block "Enum.sum([1, 2, 3])"))))
|
||||
|
||||
(provide 'test-ob-elixir)
|
||||
;;; test-ob-elixir.el ends here
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing org-babel Specifics
|
||||
|
||||
### Testing with Real Org Buffers
|
||||
|
||||
```elisp
|
||||
(ert-deftest ob-elixir-test-full-org-workflow ()
|
||||
"Test the complete org-babel evaluation workflow."
|
||||
(skip-unless (executable-find "elixir"))
|
||||
(with-temp-buffer
|
||||
(org-mode)
|
||||
(insert "#+NAME: test-block
|
||||
#+BEGIN_SRC elixir :results value
|
||||
Enum.map([1, 2, 3], &(&1 * 2))
|
||||
#+END_SRC")
|
||||
(goto-char (point-min))
|
||||
(org-babel-goto-named-src-block "test-block")
|
||||
(let ((result (org-babel-execute-src-block)))
|
||||
(should (equal '(2 4 6) result)))))
|
||||
```
|
||||
|
||||
### Testing Variable Passing
|
||||
|
||||
```elisp
|
||||
(ert-deftest ob-elixir-test-var-passing ()
|
||||
"Test :var header argument handling."
|
||||
(with-temp-buffer
|
||||
(org-mode)
|
||||
(insert "#+BEGIN_SRC elixir :var x=5 :var y=10
|
||||
x + y
|
||||
#+END_SRC")
|
||||
(goto-char (point-min))
|
||||
(forward-line 1)
|
||||
(should (equal "15" (org-babel-execute-src-block)))))
|
||||
```
|
||||
|
||||
### Testing Table Input/Output
|
||||
|
||||
```elisp
|
||||
(ert-deftest ob-elixir-test-table-input ()
|
||||
"Test that tables are passed correctly."
|
||||
(with-temp-buffer
|
||||
(org-mode)
|
||||
(insert "#+NAME: test-data
|
||||
| a | 1 |
|
||||
| b | 2 |
|
||||
|
||||
#+BEGIN_SRC elixir :var data=test-data
|
||||
Enum.map(data, fn [k, v] -> {k, v * 2} end)
|
||||
#+END_SRC")
|
||||
(goto-char (point-min))
|
||||
(search-forward "BEGIN_SRC")
|
||||
(let ((result (org-babel-execute-src-block)))
|
||||
(should (equal '(("a" 2) ("b" 4)) result)))))
|
||||
```
|
||||
|
||||
### Testing Session Mode
|
||||
|
||||
```elisp
|
||||
(ert-deftest ob-elixir-test-session-persistence ()
|
||||
"Test that session maintains state between evaluations."
|
||||
(skip-unless (executable-find "iex"))
|
||||
(unwind-protect
|
||||
(progn
|
||||
;; First block: define variable
|
||||
(with-temp-buffer
|
||||
(org-mode)
|
||||
(insert "#+BEGIN_SRC elixir :session test-session
|
||||
x = 42
|
||||
#+END_SRC")
|
||||
(goto-char (point-min))
|
||||
(forward-line 1)
|
||||
(org-babel-execute-src-block))
|
||||
|
||||
;; Second block: use variable
|
||||
(with-temp-buffer
|
||||
(org-mode)
|
||||
(insert "#+BEGIN_SRC elixir :session test-session
|
||||
x * 2
|
||||
#+END_SRC")
|
||||
(goto-char (point-min))
|
||||
(forward-line 1)
|
||||
(should (equal "84" (org-babel-execute-src-block)))))
|
||||
;; Cleanup
|
||||
(when-let ((buf (get-buffer "*elixir-test-session*")))
|
||||
(kill-buffer buf))))
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Mocking and Stubbing
|
||||
|
||||
### ERT with cl-letf
|
||||
|
||||
```elisp
|
||||
(require 'cl-lib)
|
||||
|
||||
(ert-deftest ob-elixir-test-mock-shell-command ()
|
||||
"Test with mocked shell command."
|
||||
(cl-letf (((symbol-function 'shell-command-to-string)
|
||||
(lambda (cmd) "42")))
|
||||
(should (equal "42" (ob-elixir-evaluate "anything" '())))))
|
||||
|
||||
(ert-deftest ob-elixir-test-mock-process ()
|
||||
"Test with mocked external process."
|
||||
(cl-letf (((symbol-function 'call-process)
|
||||
(lambda (&rest _args) 0))
|
||||
((symbol-function 'org-babel-eval)
|
||||
(lambda (_cmd body) (format "result: %s" body))))
|
||||
(should (string-match "result:" (ob-elixir-evaluate "code" '())))))
|
||||
```
|
||||
|
||||
### Buttercup Spies
|
||||
|
||||
```elisp
|
||||
(describe "ob-elixir with mocks"
|
||||
(before-each
|
||||
(spy-on 'shell-command-to-string :and-return-value "mocked-result")
|
||||
(spy-on 'message))
|
||||
|
||||
(it "uses shell command"
|
||||
(ob-elixir-evaluate "code" '())
|
||||
(expect 'shell-command-to-string :to-have-been-called))
|
||||
|
||||
(it "passes correct arguments"
|
||||
(ob-elixir-evaluate "1 + 1" '())
|
||||
(expect 'shell-command-to-string
|
||||
:to-have-been-called-with
|
||||
(string-match-p "elixir" (spy-calls-args-for
|
||||
'shell-command-to-string 0))))
|
||||
|
||||
(it "can call through to original"
|
||||
(spy-on 'some-func :and-call-through)
|
||||
;; This will call the real function but track calls
|
||||
))
|
||||
```
|
||||
|
||||
### Testing Without External Dependencies
|
||||
|
||||
```elisp
|
||||
(ert-deftest ob-elixir-test-without-elixir ()
|
||||
"Test behavior when Elixir is not installed."
|
||||
(cl-letf (((symbol-function 'executable-find)
|
||||
(lambda (_) nil)))
|
||||
(should-error (ob-elixir-check-installation)
|
||||
:type 'user-error)))
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## CI/CD Integration
|
||||
|
||||
### GitHub Actions Workflow
|
||||
|
||||
```yaml
|
||||
# .github/workflows/test.yml
|
||||
name: Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, master]
|
||||
pull_request:
|
||||
branches: [main, master]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
emacs-version: ['27.2', '28.2', '29.1']
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Emacs
|
||||
uses: purcell/setup-emacs@master
|
||||
with:
|
||||
version: ${{ matrix.emacs-version }}
|
||||
|
||||
- name: Set up Elixir
|
||||
uses: erlef/setup-beam@v1
|
||||
with:
|
||||
elixir-version: '1.15'
|
||||
otp-version: '26'
|
||||
|
||||
- name: Install Eldev
|
||||
run: curl -fsSL https://raw.github.com/doublep/eldev/master/webinstall/github-eldev | sh
|
||||
|
||||
- name: Run tests
|
||||
run: eldev test
|
||||
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Emacs
|
||||
uses: purcell/setup-emacs@master
|
||||
with:
|
||||
version: '29.1'
|
||||
|
||||
- name: Install Eldev
|
||||
run: curl -fsSL https://raw.github.com/doublep/eldev/master/webinstall/github-eldev | sh
|
||||
|
||||
- name: Lint
|
||||
run: eldev lint
|
||||
```
|
||||
|
||||
### Makefile
|
||||
|
||||
```makefile
|
||||
EMACS ?= emacs
|
||||
BATCH = $(EMACS) -Q -batch -L .
|
||||
|
||||
.PHONY: test test-ert compile lint clean
|
||||
|
||||
all: compile test
|
||||
|
||||
compile:
|
||||
$(BATCH) -f batch-byte-compile ob-elixir.el
|
||||
|
||||
test: test-ert
|
||||
|
||||
test-ert:
|
||||
$(BATCH) -l ert -l test/test-ob-elixir.el \
|
||||
-f ert-run-tests-batch-and-exit
|
||||
|
||||
lint:
|
||||
$(BATCH) -l package-lint \
|
||||
--eval "(setq package-lint-main-file \"ob-elixir.el\")" \
|
||||
-f package-lint-batch-and-exit ob-elixir.el
|
||||
|
||||
clean:
|
||||
rm -f *.elc test/*.elc
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Build Tools
|
||||
|
||||
### Eldev (Recommended)
|
||||
|
||||
```elisp
|
||||
; Eldev file
|
||||
(eldev-use-package-archive 'gnu)
|
||||
(eldev-use-package-archive 'melpa)
|
||||
|
||||
; Dependencies
|
||||
(eldev-add-extra-dependencies 'test 'buttercup)
|
||||
|
||||
; Test configuration
|
||||
(setf eldev-test-framework 'ert) ; or 'buttercup
|
||||
|
||||
; Lint configuration
|
||||
(setf eldev-lint-default '(elisp package))
|
||||
```
|
||||
|
||||
```bash
|
||||
# Common Eldev commands
|
||||
eldev test # Run tests
|
||||
eldev lint # Run linters
|
||||
eldev compile # Byte-compile
|
||||
eldev clean # Clean build artifacts
|
||||
eldev doctor # Check project health
|
||||
```
|
||||
|
||||
### Cask
|
||||
|
||||
```elisp
|
||||
; Cask file
|
||||
(source gnu)
|
||||
(source melpa)
|
||||
|
||||
(package-file "ob-elixir.el")
|
||||
|
||||
(development
|
||||
(depends-on "ert")
|
||||
(depends-on "buttercup")
|
||||
(depends-on "package-lint"))
|
||||
```
|
||||
|
||||
```bash
|
||||
# Common Cask commands
|
||||
cask install # Install dependencies
|
||||
cask exec ert-runner # Run ERT tests
|
||||
cask exec buttercup -L . # Run Buttercup tests
|
||||
cask build # Byte-compile
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
- [ERT Manual](https://www.gnu.org/software/emacs/manual/html_node/ert/)
|
||||
- [Buttercup Documentation](https://github.com/jorgenschaefer/emacs-buttercup)
|
||||
- [Eldev Documentation](https://github.com/doublep/eldev)
|
||||
- [Cask Documentation](https://github.com/cask/cask)
|
||||
- [GitHub Actions for Emacs](https://github.com/purcell/setup-emacs)
|
||||
- [Org Mode Test Suite](https://git.savannah.gnu.org/cgit/emacs/org-mode.git/tree/testing)
|
||||
- [Package-lint](https://github.com/purcell/package-lint)
|
||||
747
docs/03-org-babel-implementation-guide.md
Normal file
747
docs/03-org-babel-implementation-guide.md
Normal file
@@ -0,0 +1,747 @@
|
||||
# Org Babel Language Implementation Guide
|
||||
|
||||
This document provides a comprehensive guide to implementing org-babel support for a new programming language, specifically targeting Elixir.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Architecture Overview](#architecture-overview)
|
||||
- [Required Components](#required-components)
|
||||
- [Optional Components](#optional-components)
|
||||
- [Header Arguments](#header-arguments)
|
||||
- [Result Handling](#result-handling)
|
||||
- [Session Management](#session-management)
|
||||
- [Variable Handling](#variable-handling)
|
||||
- [Utility Functions](#utility-functions)
|
||||
- [Complete Implementation Template](#complete-implementation-template)
|
||||
- [References](#references)
|
||||
|
||||
---
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
### How Org Babel Executes Code
|
||||
|
||||
```
|
||||
User presses C-c C-c on source block
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────┐
|
||||
│ org-babel-execute-src-block │
|
||||
└─────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────┐
|
||||
│ Parse header arguments │
|
||||
│ Get block body │
|
||||
└─────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────┐
|
||||
│ org-babel-execute:LANG │ ◄── Your implementation
|
||||
│ (language-specific) │
|
||||
└─────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────┐
|
||||
│ Process and format results │
|
||||
└─────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────┐
|
||||
│ Insert results in buffer │
|
||||
└─────────────────────────────┘
|
||||
```
|
||||
|
||||
### Source Block Structure
|
||||
|
||||
```org
|
||||
#+NAME: block-name
|
||||
#+HEADER: :var x=5
|
||||
#+BEGIN_SRC elixir :results value :session my-session
|
||||
# This is the body
|
||||
x * 2
|
||||
#+END_SRC
|
||||
|
||||
#+RESULTS: block-name
|
||||
: 10
|
||||
```
|
||||
|
||||
### Key Data Structures
|
||||
|
||||
**params alist** - Association list passed to execute function:
|
||||
|
||||
```elisp
|
||||
((:results . "value")
|
||||
(:session . "my-session")
|
||||
(:var . ("x" . 5))
|
||||
(:var . ("y" . 10))
|
||||
(:colnames . no)
|
||||
(:rownames . no)
|
||||
(:result-params . ("value" "replace"))
|
||||
(:result-type . value)
|
||||
...)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Required Components
|
||||
|
||||
### 1. The Execute Function
|
||||
|
||||
This is the **only strictly required** function. It must be named exactly `org-babel-execute:LANG`:
|
||||
|
||||
```elisp
|
||||
(defun org-babel-execute:elixir (body params)
|
||||
"Execute a block of Elixir code with org-babel.
|
||||
|
||||
BODY is the code to execute.
|
||||
PARAMS is an alist of header arguments.
|
||||
|
||||
This function is called by `org-babel-execute-src-block'."
|
||||
(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))))
|
||||
;; Execute and return results
|
||||
(org-babel-reassemble-table
|
||||
(org-babel-result-cond result-params
|
||||
;; For :results output - return raw output
|
||||
(ob-elixir--execute full-body session)
|
||||
;; For :results value - parse as elisp data
|
||||
(ob-elixir--table-or-string
|
||||
(ob-elixir--execute full-body session)))
|
||||
(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))))))
|
||||
```
|
||||
|
||||
### 2. Default Header Arguments
|
||||
|
||||
Define language-specific defaults:
|
||||
|
||||
```elisp
|
||||
(defvar org-babel-default-header-args:elixir
|
||||
'((:results . "value")
|
||||
(:session . "none"))
|
||||
"Default header arguments for Elixir code blocks.")
|
||||
```
|
||||
|
||||
### 3. Language Registration
|
||||
|
||||
Enable the language for evaluation:
|
||||
|
||||
```elisp
|
||||
;; Add to tangle extensions (for org-babel-tangle)
|
||||
(add-to-list 'org-babel-tangle-lang-exts '("elixir" . "ex"))
|
||||
|
||||
;; Add to language mode mapping (for syntax highlighting)
|
||||
(add-to-list 'org-src-lang-modes '("elixir" . elixir))
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Optional Components
|
||||
|
||||
### Language-Specific Header Arguments
|
||||
|
||||
Define what header arguments your language supports:
|
||||
|
||||
```elisp
|
||||
(defconst org-babel-header-args:elixir
|
||||
'((mix-project . :any) ; Path to mix project
|
||||
(mix-env . :any) ; MIX_ENV value
|
||||
(iex-args . :any) ; Extra args for iex
|
||||
(timeout . :any) ; Execution timeout
|
||||
(async . ((yes no)))) ; Async execution
|
||||
"Elixir-specific header arguments.")
|
||||
```
|
||||
|
||||
### Initiate Session Function
|
||||
|
||||
For persistent REPL sessions:
|
||||
|
||||
```elisp
|
||||
(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 (string= session "none")
|
||||
(let ((session-name (or session "default")))
|
||||
(ob-elixir--get-or-create-session session-name params))))
|
||||
```
|
||||
|
||||
### Prep Session Function
|
||||
|
||||
Prepare a session with variables before execution:
|
||||
|
||||
```elisp
|
||||
(defun org-babel-prep-session:elixir (session params)
|
||||
"Prepare SESSION according to PARAMS.
|
||||
|
||||
Injects variables into the session before code execution."
|
||||
(let ((session-buffer (org-babel-elixir-initiate-session session params))
|
||||
(var-lines (org-babel-variable-assignments:elixir params)))
|
||||
(when session-buffer
|
||||
(org-babel-comint-in-buffer session-buffer
|
||||
(dolist (var-line var-lines)
|
||||
(insert var-line)
|
||||
(comint-send-input nil t)
|
||||
(org-babel-comint-wait-for-output session-buffer))))
|
||||
session-buffer))
|
||||
```
|
||||
|
||||
### Load Session Function
|
||||
|
||||
Load code into session without executing:
|
||||
|
||||
```elisp
|
||||
(defun org-babel-load-session:elixir (session body params)
|
||||
"Load BODY into SESSION without executing.
|
||||
|
||||
Useful for setting up definitions to be used later."
|
||||
(save-window-excursion
|
||||
(let ((buffer (org-babel-prep-session:elixir session params)))
|
||||
(with-current-buffer buffer
|
||||
(goto-char (process-mark (get-buffer-process buffer)))
|
||||
(insert (org-babel-chomp body)))
|
||||
buffer)))
|
||||
```
|
||||
|
||||
### Variable Assignments Function
|
||||
|
||||
Convert Elisp values to language-specific variable assignments:
|
||||
|
||||
```elisp
|
||||
(defun org-babel-variable-assignments:elixir (params)
|
||||
"Return list of Elixir statements to assign variables from PARAMS."
|
||||
(mapcar
|
||||
(lambda (pair)
|
||||
(format "%s = %s"
|
||||
(car pair)
|
||||
(ob-elixir--elisp-to-elixir (cdr pair))))
|
||||
(org-babel--get-vars params)))
|
||||
```
|
||||
|
||||
### Expand Body Function
|
||||
|
||||
Customize how the code body is expanded (optional, generic works for most):
|
||||
|
||||
```elisp
|
||||
(defun org-babel-expand-body:elixir (body params)
|
||||
"Expand BODY according to PARAMS.
|
||||
|
||||
Add variable bindings, prologue, epilogue."
|
||||
(let ((vars (org-babel-variable-assignments:elixir params))
|
||||
(prologue (cdr (assq :prologue params)))
|
||||
(epilogue (cdr (assq :epilogue params))))
|
||||
(concat
|
||||
(when prologue (concat prologue "\n"))
|
||||
(mapconcat #'identity vars "\n")
|
||||
(when vars "\n")
|
||||
body
|
||||
(when epilogue (concat "\n" epilogue)))))
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Header Arguments
|
||||
|
||||
### Standard Header Arguments (Always Available)
|
||||
|
||||
| Argument | Values | Description |
|
||||
|-------------|---------------------------|----------------------|
|
||||
| `:results` | value, output | What to capture |
|
||||
| `:session` | name, "none" | Session to use |
|
||||
| `:var` | name=value | Variable binding |
|
||||
| `:exports` | code, results, both, none | Export behavior |
|
||||
| `:file` | path | Save results to file |
|
||||
| `:dir` | path | Working directory |
|
||||
| `:prologue` | code | Code to run before |
|
||||
| `:epilogue` | code | Code to run after |
|
||||
| `:noweb` | yes, no, tangle | Noweb expansion |
|
||||
| `:cache` | yes, no | Cache results |
|
||||
| `:tangle` | yes, no, filename | Tangle behavior |
|
||||
|
||||
### Result Types
|
||||
|
||||
```elisp
|
||||
;; Accessing result configuration
|
||||
(let* ((result-params (cdr (assq :result-params params)))
|
||||
(result-type (cdr (assq :result-type params))))
|
||||
|
||||
;; result-type is 'value or 'output
|
||||
;; result-params is a list like ("value" "replace" "scalar")
|
||||
|
||||
(cond
|
||||
((eq result-type 'output)
|
||||
;; Capture stdout
|
||||
(ob-elixir--execute-output body))
|
||||
((eq result-type 'value)
|
||||
;; Return last expression value
|
||||
(ob-elixir--execute-value body))))
|
||||
```
|
||||
|
||||
### Parsing Header Arguments
|
||||
|
||||
```elisp
|
||||
(defun ob-elixir--parse-params (params)
|
||||
"Parse PARAMS into a structured format."
|
||||
(let ((session (cdr (assq :session params)))
|
||||
(result-type (cdr (assq :result-type params)))
|
||||
(result-params (cdr (assq :result-params params)))
|
||||
(dir (cdr (assq :dir params)))
|
||||
(vars (org-babel--get-vars params))
|
||||
;; Custom header args
|
||||
(mix-project (cdr (assq :mix-project params)))
|
||||
(mix-env (cdr (assq :mix-env params)))
|
||||
(timeout (cdr (assq :timeout params))))
|
||||
(list :session session
|
||||
:result-type result-type
|
||||
:result-params result-params
|
||||
:dir dir
|
||||
:vars vars
|
||||
:mix-project mix-project
|
||||
:mix-env mix-env
|
||||
:timeout (or timeout ob-elixir-default-timeout))))
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Result Handling
|
||||
|
||||
### The `org-babel-result-cond` Macro
|
||||
|
||||
This handles result formatting based on `:results` header:
|
||||
|
||||
```elisp
|
||||
(org-babel-result-cond result-params
|
||||
;; This form is used for :results output/scalar/verbatim
|
||||
(ob-elixir--raw-output body)
|
||||
|
||||
;; This form is used for :results value (default)
|
||||
;; Should return elisp data for possible table conversion
|
||||
(ob-elixir--parsed-value body))
|
||||
```
|
||||
|
||||
### Converting Results to Tables
|
||||
|
||||
```elisp
|
||||
(defun ob-elixir--table-or-string (result)
|
||||
"Convert RESULT to an Emacs table or string.
|
||||
|
||||
If RESULT looks like a list/table, parse it.
|
||||
Otherwise return as string."
|
||||
(let ((res (org-babel-script-escape result)))
|
||||
(if (listp res)
|
||||
(mapcar (lambda (el)
|
||||
(if (eq el 'nil)
|
||||
org-babel-elixir-nil-to
|
||||
el))
|
||||
res)
|
||||
res)))
|
||||
|
||||
(defvar org-babel-elixir-nil-to 'hline
|
||||
"Value to use for nil in Elixir results.
|
||||
When nil values appear in lists/tables, convert to this.")
|
||||
```
|
||||
|
||||
### Using `org-babel-reassemble-table`
|
||||
|
||||
Restore column/row names to tables:
|
||||
|
||||
```elisp
|
||||
(org-babel-reassemble-table
|
||||
result ; The result data
|
||||
(org-babel-pick-name ; Column names
|
||||
(cdr (assq :colname-names params))
|
||||
(cdr (assq :colnames params)))
|
||||
(org-babel-pick-name ; Row names
|
||||
(cdr (assq :rowname-names params))
|
||||
(cdr (assq :rownames params))))
|
||||
```
|
||||
|
||||
### File Results
|
||||
|
||||
For `:results file`:
|
||||
|
||||
```elisp
|
||||
(defun ob-elixir--handle-file-result (result params)
|
||||
"Handle file output for RESULT based on PARAMS."
|
||||
(let ((file (cdr (assq :file params))))
|
||||
(when file
|
||||
(with-temp-file file
|
||||
(insert result))
|
||||
file))) ; Return filename for link creation
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Session Management
|
||||
|
||||
### Using Comint for Sessions
|
||||
|
||||
Org-babel provides utilities for managing comint-based REPLs:
|
||||
|
||||
```elisp
|
||||
(require 'ob-comint)
|
||||
|
||||
(defvar ob-elixir-prompt-regexp "^iex([0-9]+)> "
|
||||
"Regexp matching the IEx prompt.")
|
||||
|
||||
(defun ob-elixir--get-or-create-session (name params)
|
||||
"Get or create an IEx session named NAME."
|
||||
(let ((buffer-name (format "*ob-elixir-%s*" name)))
|
||||
(if (org-babel-comint-buffer-livep (get-buffer buffer-name))
|
||||
(get-buffer buffer-name)
|
||||
(ob-elixir--start-session buffer-name params))))
|
||||
|
||||
(defun ob-elixir--start-session (buffer-name params)
|
||||
"Start a new IEx session in BUFFER-NAME."
|
||||
(let* ((iex-cmd (or (cdr (assq :iex-command params)) "iex"))
|
||||
(mix-project (cdr (assq :mix-project params)))
|
||||
(cmd (if mix-project
|
||||
(format "cd %s && iex -S mix"
|
||||
(shell-quote-argument mix-project))
|
||||
iex-cmd)))
|
||||
(with-current-buffer (get-buffer-create buffer-name)
|
||||
(make-comint-in-buffer "ob-elixir" (current-buffer)
|
||||
shell-file-name nil "-c" cmd)
|
||||
;; Wait for prompt
|
||||
(sit-for 1)
|
||||
;; Configure IEx for non-interactive use
|
||||
(ob-elixir--configure-session (current-buffer))
|
||||
(current-buffer))))
|
||||
|
||||
(defun ob-elixir--configure-session (buffer)
|
||||
"Configure IEx session in BUFFER for programmatic use."
|
||||
(org-babel-comint-in-buffer buffer
|
||||
(insert "IEx.configure(colors: [enabled: false])")
|
||||
(comint-send-input nil t)
|
||||
(org-babel-comint-wait-for-output buffer)))
|
||||
```
|
||||
|
||||
### Evaluating in Session
|
||||
|
||||
```elisp
|
||||
(defun ob-elixir--evaluate-in-session (session body)
|
||||
"Evaluate BODY in SESSION buffer, return output."
|
||||
(let ((session-buffer (ob-elixir--get-or-create-session session nil))
|
||||
(eoe-indicator ob-elixir-eoe-indicator))
|
||||
(org-babel-comint-with-output
|
||||
(session-buffer ob-elixir-prompt-regexp t eoe-indicator)
|
||||
(insert body)
|
||||
(comint-send-input nil t)
|
||||
;; Send EOE marker
|
||||
(insert (format "\"%s\"" eoe-indicator))
|
||||
(comint-send-input nil t))))
|
||||
```
|
||||
|
||||
### Key Comint Utilities
|
||||
|
||||
```elisp
|
||||
;; Check if buffer has live process
|
||||
(org-babel-comint-buffer-livep buffer)
|
||||
|
||||
;; Execute forms in comint buffer context
|
||||
(org-babel-comint-in-buffer buffer
|
||||
(insert "code")
|
||||
(comint-send-input))
|
||||
|
||||
;; Wait for output with timeout
|
||||
(org-babel-comint-wait-for-output buffer)
|
||||
|
||||
;; Capture output until EOE marker
|
||||
(org-babel-comint-with-output (buffer prompt t eoe-marker)
|
||||
body...)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Variable Handling
|
||||
|
||||
### Getting Variables from Params
|
||||
|
||||
```elisp
|
||||
;; org-babel--get-vars returns list of (name . value) pairs
|
||||
(let ((vars (org-babel--get-vars params)))
|
||||
;; vars = (("x" . 5) ("y" . "hello") ("data" . ((1 2) (3 4))))
|
||||
(dolist (var vars)
|
||||
(let ((name (car var))
|
||||
(value (cdr var)))
|
||||
;; Process each variable
|
||||
)))
|
||||
```
|
||||
|
||||
### Converting Elisp to Elixir
|
||||
|
||||
```elisp
|
||||
(defun ob-elixir--elisp-to-elixir (value)
|
||||
"Convert Elisp VALUE to Elixir literal syntax."
|
||||
(cond
|
||||
;; nil -> nil
|
||||
((null value) "nil")
|
||||
|
||||
;; t -> true
|
||||
((eq value t) "true")
|
||||
|
||||
;; Numbers stay as-is
|
||||
((numberp value) (number-to-string value))
|
||||
|
||||
;; Strings get quoted
|
||||
((stringp value) (format "\"%s\"" (ob-elixir--escape-string value)))
|
||||
|
||||
;; Symbols become atoms
|
||||
((symbolp value) (format ":%s" (symbol-name value)))
|
||||
|
||||
;; Lists become Elixir lists
|
||||
((listp value)
|
||||
(if (ob-elixir--alist-p value)
|
||||
;; Association list -> keyword list or map
|
||||
(ob-elixir--alist-to-elixir value)
|
||||
;; Regular list
|
||||
(format "[%s]"
|
||||
(mapconcat #'ob-elixir--elisp-to-elixir value ", "))))
|
||||
|
||||
;; Fallback: convert to string
|
||||
(t (format "%S" value))))
|
||||
|
||||
(defun ob-elixir--escape-string (str)
|
||||
"Escape special characters in STR for Elixir."
|
||||
(replace-regexp-in-string
|
||||
"\\\\" "\\\\\\\\"
|
||||
(replace-regexp-in-string
|
||||
"\"" "\\\\\""
|
||||
(replace-regexp-in-string
|
||||
"\n" "\\\\n" str))))
|
||||
|
||||
(defun ob-elixir--alist-p (list)
|
||||
"Return t if LIST is an association list."
|
||||
(and (listp list)
|
||||
(cl-every (lambda (el)
|
||||
(and (consp el) (atom (car el))))
|
||||
list)))
|
||||
|
||||
(defun ob-elixir--alist-to-elixir (alist)
|
||||
"Convert ALIST to Elixir keyword list or map."
|
||||
(format "[%s]"
|
||||
(mapconcat
|
||||
(lambda (pair)
|
||||
(format "%s: %s"
|
||||
(car pair)
|
||||
(ob-elixir--elisp-to-elixir (cdr pair))))
|
||||
alist
|
||||
", ")))
|
||||
```
|
||||
|
||||
### Handling Tables (hlines)
|
||||
|
||||
```elisp
|
||||
(defvar org-babel-elixir-hline-to ":hline"
|
||||
"Elixir representation for org table hlines.")
|
||||
|
||||
(defun ob-elixir--table-to-elixir (table)
|
||||
"Convert org TABLE to Elixir list of lists."
|
||||
(format "[%s]"
|
||||
(mapconcat
|
||||
(lambda (row)
|
||||
(if (eq row 'hline)
|
||||
org-babel-elixir-hline-to
|
||||
(format "[%s]"
|
||||
(mapconcat #'ob-elixir--elisp-to-elixir row ", "))))
|
||||
table
|
||||
", ")))
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Utility Functions
|
||||
|
||||
### From ob.el / ob-core.el
|
||||
|
||||
```elisp
|
||||
;; Execute external command with body as stdin
|
||||
(org-babel-eval command body)
|
||||
;; Returns stdout as string
|
||||
|
||||
;; Create temporary file
|
||||
(org-babel-temp-file "prefix-")
|
||||
;; Returns temp file path
|
||||
|
||||
;; Read file contents
|
||||
(org-babel-eval-read-file filename)
|
||||
|
||||
;; Process file path for use in scripts
|
||||
(org-babel-process-file-name filename)
|
||||
|
||||
;; Parse script output as Elisp data
|
||||
(org-babel-script-escape output)
|
||||
;; Parses "[1, 2, 3]" -> (1 2 3)
|
||||
|
||||
;; Remove trailing newlines
|
||||
(org-babel-chomp string)
|
||||
|
||||
;; Expand generic body with vars
|
||||
(org-babel-expand-body:generic body params var-lines)
|
||||
```
|
||||
|
||||
### Wrapper Method Pattern
|
||||
|
||||
For `:results value`, wrap code to capture return value:
|
||||
|
||||
```elisp
|
||||
(defconst ob-elixir-wrapper-method
|
||||
"
|
||||
result = (fn ->
|
||||
%s
|
||||
end).()
|
||||
|
||||
result
|
||||
|> inspect(limit: :infinity, printable_limit: :infinity)
|
||||
|> IO.puts()
|
||||
"
|
||||
"Template for wrapping Elixir code to capture value.
|
||||
%s is replaced with the user's code.")
|
||||
|
||||
(defun ob-elixir--wrap-for-value (body)
|
||||
"Wrap BODY to capture its return value."
|
||||
(format ob-elixir-wrapper-method body))
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Complete Implementation Template
|
||||
|
||||
Here's a minimal but functional template:
|
||||
|
||||
```elisp
|
||||
;;; ob-elixir.el --- Org Babel functions for Elixir -*- lexical-binding: t; -*-
|
||||
|
||||
;; Copyright (C) 2024 Your Name
|
||||
;; Author: Your Name <email@example.com>
|
||||
;; Version: 1.0.0
|
||||
;; Package-Requires: ((emacs "27.1") (org "9.4"))
|
||||
;; Keywords: literate programming, elixir
|
||||
;; URL: https://github.com/user/ob-elixir
|
||||
|
||||
;;; Commentary:
|
||||
|
||||
;; Org Babel support for evaluating Elixir code blocks.
|
||||
|
||||
;;; Code:
|
||||
|
||||
(require 'ob)
|
||||
(require 'ob-eval)
|
||||
|
||||
;;; 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."
|
||||
:type 'string
|
||||
:group 'ob-elixir)
|
||||
|
||||
;;; Header Arguments
|
||||
|
||||
(defvar org-babel-default-header-args:elixir
|
||||
'((:results . "value")
|
||||
(:session . "none"))
|
||||
"Default header arguments for Elixir code blocks.")
|
||||
|
||||
(defconst org-babel-header-args:elixir
|
||||
'((mix-project . :any))
|
||||
"Elixir-specific header arguments.")
|
||||
|
||||
;;; File Extension
|
||||
|
||||
(add-to-list 'org-babel-tangle-lang-exts '("elixir" . "ex"))
|
||||
|
||||
;;; Execution
|
||||
|
||||
(defun org-babel-execute:elixir (body params)
|
||||
"Execute Elixir BODY according to PARAMS."
|
||||
(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
|
||||
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))))))
|
||||
|
||||
(defun ob-elixir--execute (body result-type)
|
||||
"Execute BODY and return result based on RESULT-TYPE."
|
||||
(let* ((tmp-file (org-babel-temp-file "elixir-" ".exs"))
|
||||
(wrapper (if (eq result-type 'value)
|
||||
(ob-elixir--wrap-for-value body)
|
||||
body)))
|
||||
(with-temp-file tmp-file
|
||||
(insert wrapper))
|
||||
(org-babel-eval
|
||||
(format "%s %s" ob-elixir-command
|
||||
(org-babel-process-file-name tmp-file))
|
||||
"")))
|
||||
|
||||
(defconst ob-elixir--value-wrapper
|
||||
"result = (\n%s\n)\nIO.puts(inspect(result, limit: :infinity))"
|
||||
"Wrapper to capture return value.")
|
||||
|
||||
(defun ob-elixir--wrap-for-value (body)
|
||||
"Wrap BODY to capture its return value."
|
||||
(format ob-elixir--value-wrapper body))
|
||||
|
||||
;;; Variables
|
||||
|
||||
(defun org-babel-variable-assignments:elixir (params)
|
||||
"Return Elixir code to assign variables from PARAMS."
|
||||
(mapcar
|
||||
(lambda (pair)
|
||||
(format "%s = %s"
|
||||
(car pair)
|
||||
(ob-elixir--elisp-to-elixir (cdr pair))))
|
||||
(org-babel--get-vars params)))
|
||||
|
||||
(defun ob-elixir--elisp-to-elixir (value)
|
||||
"Convert Elisp VALUE to Elixir syntax."
|
||||
(cond
|
||||
((null value) "nil")
|
||||
((eq value t) "true")
|
||||
((numberp value) (number-to-string value))
|
||||
((stringp value) (format "\"%s\"" value))
|
||||
((symbolp value) (format ":%s" (symbol-name value)))
|
||||
((listp value)
|
||||
(format "[%s]" (mapconcat #'ob-elixir--elisp-to-elixir value ", ")))
|
||||
(t (format "%S" value))))
|
||||
|
||||
(provide 'ob-elixir)
|
||||
;;; ob-elixir.el ends here
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
- [Org Mode Manual - Working with Source Code](https://orgmode.org/manual/Working-with-Source-Code.html)
|
||||
- [Org Mode Manual - Languages](https://orgmode.org/manual/Languages.html)
|
||||
- [Org Mode Manual - Results of Evaluation](https://orgmode.org/manual/Results-of-Evaluation.html)
|
||||
- [Worg - Babel Language Template](https://orgmode.org/worg/org-contrib/babel/languages/ob-doc-template.html)
|
||||
- [Worg - Python Documentation](https://orgmode.org/worg/org-contrib/babel/languages/ob-doc-python.html)
|
||||
- [Org Source - ob.el](https://git.savannah.gnu.org/cgit/emacs/org-mode.git/tree/lisp/ob.el)
|
||||
- [Org Source - ob-python.el](https://git.savannah.gnu.org/cgit/emacs/org-mode.git/tree/lisp/ob-python.el)
|
||||
- [Org Source - ob-ruby.el](https://git.savannah.gnu.org/cgit/emacs/org-mode.git/tree/lisp/ob-ruby.el)
|
||||
- [Org Element API](https://orgmode.org/worg/dev/org-element-api.html)
|
||||
644
docs/04-elixir-integration-strategies.md
Normal file
644
docs/04-elixir-integration-strategies.md
Normal file
@@ -0,0 +1,644 @@
|
||||
# Elixir Integration Strategies
|
||||
|
||||
This document covers strategies for integrating Elixir with Emacs and org-babel, including execution modes, data conversion, and Mix project support.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Execution Modes](#execution-modes)
|
||||
- [One-Shot Execution](#one-shot-execution)
|
||||
- [IEx Session Management](#iex-session-management)
|
||||
- [Mix Project Context](#mix-project-context)
|
||||
- [Remote Shell (remsh)](#remote-shell-remsh)
|
||||
- [Data Type Conversion](#data-type-conversion)
|
||||
- [Error Handling](#error-handling)
|
||||
- [elixir-mode Integration](#elixir-mode-integration)
|
||||
- [Performance Considerations](#performance-considerations)
|
||||
- [References](#references)
|
||||
|
||||
---
|
||||
|
||||
## Execution Modes
|
||||
|
||||
### Overview of Execution Strategies
|
||||
|
||||
| Mode | Command | Use Case | Startup Time | State |
|
||||
|--------------|-------------------|----------------|--------------|-------------|
|
||||
| One-shot | `elixir -e` | Simple scripts | ~500ms | None |
|
||||
| Script file | `elixir file.exs` | Larger code | ~500ms | None |
|
||||
| IEx session | `iex` | Interactive | Once | Persistent |
|
||||
| Mix context | `iex -S mix` | Projects | Slower | Project |
|
||||
| Remote shell | `iex --remsh` | Production | Fast | Remote node |
|
||||
|
||||
### Recommended Approach
|
||||
|
||||
For org-babel, we recommend:
|
||||
|
||||
1. **Default**: Script file execution (reliable, predictable)
|
||||
2. **With `:session`**: IEx via comint (persistent state)
|
||||
3. **With `:mix-project`**: Mix context execution
|
||||
|
||||
---
|
||||
|
||||
## One-Shot Execution
|
||||
|
||||
### Using `elixir -e`
|
||||
|
||||
For simple, single expressions:
|
||||
|
||||
```elisp
|
||||
(defun ob-elixir--eval-simple (code)
|
||||
"Evaluate CODE using elixir -e."
|
||||
(shell-command-to-string
|
||||
(format "elixir -e %s"
|
||||
(shell-quote-argument code))))
|
||||
```
|
||||
|
||||
**Pros**: Simple, no temp files
|
||||
**Cons**: Limited code size, quoting issues
|
||||
|
||||
### Using Script Files (Recommended)
|
||||
|
||||
More robust for complex code:
|
||||
|
||||
```elisp
|
||||
(defun ob-elixir--eval-script (code)
|
||||
"Evaluate CODE using a temporary script file."
|
||||
(let ((script-file (org-babel-temp-file "elixir-" ".exs")))
|
||||
(with-temp-file script-file
|
||||
(insert code))
|
||||
(org-babel-eval
|
||||
(format "elixir %s" (org-babel-process-file-name script-file))
|
||||
"")))
|
||||
```
|
||||
|
||||
### Capturing Return Values vs Output
|
||||
|
||||
```elisp
|
||||
;; For :results output - capture stdout
|
||||
(defun ob-elixir--eval-output (code)
|
||||
"Execute CODE and capture stdout."
|
||||
(ob-elixir--eval-script code))
|
||||
|
||||
;; For :results value - wrap to capture return value
|
||||
(defconst ob-elixir--value-wrapper
|
||||
"
|
||||
_result_ = (
|
||||
%s
|
||||
)
|
||||
IO.puts(inspect(_result_, limit: :infinity, printable_limit: :infinity, charlists: :as_lists))
|
||||
"
|
||||
"Wrapper to capture the return value of code.")
|
||||
|
||||
(defun ob-elixir--eval-value (code)
|
||||
"Execute CODE and return its value."
|
||||
(let ((wrapped (format ob-elixir--value-wrapper code)))
|
||||
(string-trim (ob-elixir--eval-script wrapped))))
|
||||
```
|
||||
|
||||
### Handling Multiline Output
|
||||
|
||||
```elisp
|
||||
(defconst ob-elixir--value-wrapper-with-marker
|
||||
"
|
||||
_result_ = (
|
||||
%s
|
||||
)
|
||||
IO.puts(\"__OB_ELIXIR_RESULT_START__\")
|
||||
IO.puts(inspect(_result_, limit: :infinity, printable_limit: :infinity))
|
||||
IO.puts(\"__OB_ELIXIR_RESULT_END__\")
|
||||
"
|
||||
"Wrapper with markers for reliable output parsing.")
|
||||
|
||||
(defun ob-elixir--extract-result (output)
|
||||
"Extract result from OUTPUT between markers."
|
||||
(when (string-match "__OB_ELIXIR_RESULT_START__\n\\(.*\\)\n__OB_ELIXIR_RESULT_END__"
|
||||
output)
|
||||
(match-string 1 output)))
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## IEx Session Management
|
||||
|
||||
### Starting an IEx Session
|
||||
|
||||
```elisp
|
||||
(defvar ob-elixir-iex-buffer-name "*ob-elixir-iex*"
|
||||
"Buffer name for the IEx process.")
|
||||
|
||||
(defvar ob-elixir-prompt-regexp "^iex\\([0-9]+\\)> "
|
||||
"Regexp matching the IEx prompt.")
|
||||
|
||||
(defvar ob-elixir-continued-prompt-regexp "^\\.\\.\\.\\([0-9]+\\)> "
|
||||
"Regexp matching the IEx continuation prompt.")
|
||||
|
||||
(defun ob-elixir--start-iex-session (session-name &optional params)
|
||||
"Start an IEx session named SESSION-NAME."
|
||||
(let* ((buffer-name (format "*ob-elixir-%s*" session-name))
|
||||
(buffer (get-buffer-create buffer-name))
|
||||
(mix-project (when params (cdr (assq :mix-project params))))
|
||||
(iex-args (when params (cdr (assq :iex-args params)))))
|
||||
|
||||
(unless (comint-check-proc buffer)
|
||||
(with-current-buffer buffer
|
||||
;; Set up environment
|
||||
(setenv "TERM" "dumb")
|
||||
(setenv "IEX_WITH_WERL" nil)
|
||||
|
||||
;; Start the process
|
||||
(apply #'make-comint-in-buffer
|
||||
(format "ob-elixir-%s" session-name)
|
||||
buffer
|
||||
"iex"
|
||||
nil
|
||||
(append
|
||||
(when mix-project
|
||||
(list "-S" "mix"))
|
||||
(when iex-args
|
||||
(split-string iex-args))))
|
||||
|
||||
;; Wait for initial prompt
|
||||
(ob-elixir--wait-for-prompt buffer 10)
|
||||
|
||||
;; Configure IEx for programmatic use
|
||||
(ob-elixir--configure-iex-session buffer)))
|
||||
buffer))
|
||||
```
|
||||
|
||||
### Configuring IEx for Non-Interactive Use
|
||||
|
||||
```elisp
|
||||
(defun ob-elixir--configure-iex-session (buffer)
|
||||
"Configure IEx in BUFFER for non-interactive use."
|
||||
(with-current-buffer buffer
|
||||
(let ((config-commands
|
||||
'("IEx.configure(colors: [enabled: false])"
|
||||
"IEx.configure(inspect: [limit: :infinity, printable_limit: :infinity])"
|
||||
"Application.put_env(:elixir, :ansi_enabled, false)")))
|
||||
(dolist (cmd config-commands)
|
||||
(ob-elixir--send-to-iex buffer cmd)
|
||||
(ob-elixir--wait-for-prompt buffer 5)))))
|
||||
```
|
||||
|
||||
### Sending Code to IEx
|
||||
|
||||
```elisp
|
||||
(defvar ob-elixir-eoe-marker "__OB_ELIXIR_EOE__"
|
||||
"End-of-evaluation marker.")
|
||||
|
||||
(defun ob-elixir--send-to-iex (buffer code)
|
||||
"Send CODE to IEx process in BUFFER."
|
||||
(with-current-buffer buffer
|
||||
(goto-char (point-max))
|
||||
(insert code)
|
||||
(comint-send-input nil t)))
|
||||
|
||||
(defun ob-elixir--eval-in-session (session code)
|
||||
"Evaluate CODE in SESSION, return output."
|
||||
(let* ((buffer (ob-elixir--get-or-create-session session))
|
||||
(start-marker nil))
|
||||
(with-current-buffer buffer
|
||||
(goto-char (point-max))
|
||||
(setq start-marker (point-marker))
|
||||
|
||||
;; Send the code
|
||||
(ob-elixir--send-to-iex buffer code)
|
||||
(ob-elixir--wait-for-prompt buffer 30)
|
||||
|
||||
;; Send EOE marker to clearly delineate output
|
||||
(ob-elixir--send-to-iex buffer (format "\"%s\"" ob-elixir-eoe-marker))
|
||||
(ob-elixir--wait-for-prompt buffer 5)
|
||||
|
||||
;; Extract output
|
||||
(ob-elixir--extract-session-output buffer start-marker))))
|
||||
|
||||
(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))))))
|
||||
|
||||
(defun ob-elixir--at-prompt-p ()
|
||||
"Return t if point is at an IEx prompt."
|
||||
(save-excursion
|
||||
(beginning-of-line)
|
||||
(looking-at ob-elixir-prompt-regexp)))
|
||||
```
|
||||
|
||||
### Cleaning Session Output
|
||||
|
||||
```elisp
|
||||
(defun ob-elixir--clean-iex-output (output)
|
||||
"Clean OUTPUT from IEx session."
|
||||
(let ((cleaned output))
|
||||
;; Remove ANSI escape codes
|
||||
(setq cleaned (ansi-color-filter-apply cleaned))
|
||||
;; Remove prompts
|
||||
(setq cleaned (replace-regexp-in-string
|
||||
"^iex([0-9]+)> " "" cleaned))
|
||||
(setq cleaned (replace-regexp-in-string
|
||||
"^\\.\\.\\.([0-9]+)> " "" cleaned))
|
||||
;; Remove EOE marker
|
||||
(setq cleaned (replace-regexp-in-string
|
||||
(regexp-quote (format "\"%s\"" ob-elixir-eoe-marker))
|
||||
"" cleaned))
|
||||
;; Remove trailing whitespace
|
||||
(string-trim cleaned)))
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Mix Project Context
|
||||
|
||||
### Detecting Mix Projects
|
||||
|
||||
```elisp
|
||||
(defun ob-elixir--find-mix-project (dir)
|
||||
"Find mix.exs file starting from DIR, searching up."
|
||||
(let ((mix-file (locate-dominating-file dir "mix.exs")))
|
||||
(when mix-file
|
||||
(file-name-directory mix-file))))
|
||||
|
||||
(defun ob-elixir--in-mix-project-p ()
|
||||
"Return t if current buffer is in a Mix project."
|
||||
(ob-elixir--find-mix-project default-directory))
|
||||
```
|
||||
|
||||
### Executing in Mix Context
|
||||
|
||||
```elisp
|
||||
(defun ob-elixir--eval-with-mix (code project-dir &optional mix-env)
|
||||
"Evaluate CODE in the context of Mix project at PROJECT-DIR."
|
||||
(let* ((default-directory project-dir)
|
||||
(script-file (org-babel-temp-file "elixir-" ".exs"))
|
||||
(env-vars (when mix-env
|
||||
(format "MIX_ENV=%s " mix-env))))
|
||||
(with-temp-file script-file
|
||||
(insert code))
|
||||
(shell-command-to-string
|
||||
(format "%smix run %s"
|
||||
(or env-vars "")
|
||||
(org-babel-process-file-name script-file)))))
|
||||
```
|
||||
|
||||
### Compiling Before Execution
|
||||
|
||||
```elisp
|
||||
(defun ob-elixir--ensure-compiled (project-dir)
|
||||
"Ensure Mix project at PROJECT-DIR is compiled."
|
||||
(let ((default-directory project-dir))
|
||||
(shell-command-to-string "mix compile --force-check")))
|
||||
|
||||
(defun ob-elixir--eval-with-compilation (code project-dir)
|
||||
"Compile and evaluate CODE in PROJECT-DIR."
|
||||
(ob-elixir--ensure-compiled project-dir)
|
||||
(ob-elixir--eval-with-mix code project-dir))
|
||||
```
|
||||
|
||||
### Using Mix Aliases
|
||||
|
||||
```elisp
|
||||
(defun ob-elixir--run-mix-task (task project-dir &optional args)
|
||||
"Run Mix TASK in PROJECT-DIR with ARGS."
|
||||
(let ((default-directory project-dir))
|
||||
(shell-command-to-string
|
||||
(format "mix %s %s" task (or args "")))))
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Remote Shell (remsh)
|
||||
|
||||
### Connecting to Running Nodes
|
||||
|
||||
```elisp
|
||||
(defun ob-elixir--start-remsh-session (node-name &optional cookie sname)
|
||||
"Connect to remote Elixir node NODE-NAME.
|
||||
|
||||
COOKIE is the Erlang cookie (optional).
|
||||
SNAME is the short name for the local node."
|
||||
(let* ((local-name (or sname (format "ob_elixir_%d" (random 10000))))
|
||||
(buffer-name (format "*ob-elixir-remsh-%s*" node-name))
|
||||
(buffer (get-buffer-create buffer-name))
|
||||
(args (append
|
||||
(list "--sname" local-name)
|
||||
(when cookie (list "--cookie" cookie))
|
||||
(list "--remsh" node-name))))
|
||||
|
||||
(unless (comint-check-proc buffer)
|
||||
(with-current-buffer buffer
|
||||
(apply #'make-comint-in-buffer
|
||||
(format "ob-elixir-remsh-%s" node-name)
|
||||
buffer
|
||||
"iex"
|
||||
nil
|
||||
args)
|
||||
(ob-elixir--wait-for-prompt buffer 30)
|
||||
(ob-elixir--configure-iex-session buffer)))
|
||||
buffer))
|
||||
```
|
||||
|
||||
### Remote Evaluation
|
||||
|
||||
```elisp
|
||||
(defun ob-elixir--eval-remote (code node-name &optional cookie)
|
||||
"Evaluate CODE on remote NODE-NAME."
|
||||
(let ((session (ob-elixir--start-remsh-session node-name cookie)))
|
||||
(ob-elixir--eval-in-session session code)))
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Data Type Conversion
|
||||
|
||||
### Elisp to Elixir
|
||||
|
||||
```elisp
|
||||
(defun ob-elixir--elisp-to-elixir (value)
|
||||
"Convert Elisp VALUE to Elixir literal syntax."
|
||||
(pcase value
|
||||
('nil "nil")
|
||||
('t "true")
|
||||
((pred numberp) (number-to-string value))
|
||||
((pred stringp) (ob-elixir--format-string value))
|
||||
((pred symbolp) (ob-elixir--format-atom value))
|
||||
((pred vectorp) (ob-elixir--format-tuple value))
|
||||
((pred listp)
|
||||
(cond
|
||||
((ob-elixir--alist-p value) (ob-elixir--format-keyword-list value))
|
||||
((ob-elixir--plist-p value) (ob-elixir--format-map value))
|
||||
(t (ob-elixir--format-list value))))
|
||||
(_ (format "%S" value))))
|
||||
|
||||
(defun ob-elixir--format-string (str)
|
||||
"Format STR as Elixir string."
|
||||
(format "\"%s\""
|
||||
(replace-regexp-in-string
|
||||
"\\\\" "\\\\\\\\"
|
||||
(replace-regexp-in-string
|
||||
"\"" "\\\\\""
|
||||
(replace-regexp-in-string
|
||||
"\n" "\\\\n"
|
||||
str)))))
|
||||
|
||||
(defun ob-elixir--format-atom (sym)
|
||||
"Format symbol SYM as Elixir atom."
|
||||
(let ((name (symbol-name sym)))
|
||||
(if (string-match-p "^[a-z_][a-zA-Z0-9_]*[?!]?$" name)
|
||||
(format ":%s" name)
|
||||
(format ":\"%s\"" name))))
|
||||
|
||||
(defun ob-elixir--format-list (lst)
|
||||
"Format LST as Elixir list."
|
||||
(format "[%s]"
|
||||
(mapconcat #'ob-elixir--elisp-to-elixir lst ", ")))
|
||||
|
||||
(defun ob-elixir--format-tuple (vec)
|
||||
"Format vector VEC as Elixir tuple."
|
||||
(format "{%s}"
|
||||
(mapconcat #'ob-elixir--elisp-to-elixir (append vec nil) ", ")))
|
||||
|
||||
(defun ob-elixir--format-keyword-list (alist)
|
||||
"Format ALIST as Elixir keyword list."
|
||||
(format "[%s]"
|
||||
(mapconcat
|
||||
(lambda (pair)
|
||||
(format "%s: %s"
|
||||
(car pair)
|
||||
(ob-elixir--elisp-to-elixir (cdr pair))))
|
||||
alist ", ")))
|
||||
|
||||
(defun ob-elixir--format-map (plist)
|
||||
"Format PLIST as Elixir map."
|
||||
(let ((pairs '()))
|
||||
(while plist
|
||||
(push (format "%s => %s"
|
||||
(ob-elixir--elisp-to-elixir (car plist))
|
||||
(ob-elixir--elisp-to-elixir (cadr plist)))
|
||||
pairs)
|
||||
(setq plist (cddr plist)))
|
||||
(format "%%{%s}" (mapconcat #'identity (nreverse pairs) ", "))))
|
||||
```
|
||||
|
||||
### Elixir to Elisp
|
||||
|
||||
```elisp
|
||||
(defun ob-elixir--parse-result (output)
|
||||
"Parse Elixir OUTPUT into Elisp value."
|
||||
(let ((trimmed (string-trim output)))
|
||||
(cond
|
||||
;; nil
|
||||
((string= trimmed "nil") nil)
|
||||
|
||||
;; Booleans
|
||||
((string= trimmed "true") t)
|
||||
((string= trimmed "false") nil)
|
||||
|
||||
;; Numbers
|
||||
((string-match-p "^-?[0-9]+$" trimmed)
|
||||
(string-to-number trimmed))
|
||||
((string-match-p "^-?[0-9]+\\.[0-9]+\\(e[+-]?[0-9]+\\)?$" trimmed)
|
||||
(string-to-number trimmed))
|
||||
|
||||
;; Atoms (convert to symbols)
|
||||
((string-match "^:\\([a-zA-Z_][a-zA-Z0-9_]*\\)$" trimmed)
|
||||
(intern (match-string 1 trimmed)))
|
||||
|
||||
;; Strings
|
||||
((string-match "^\"\\(.*\\)\"$" trimmed)
|
||||
(match-string 1 trimmed))
|
||||
|
||||
;; Lists/tuples - use org-babel-script-escape
|
||||
((string-match-p "^\\[.*\\]$" trimmed)
|
||||
(org-babel-script-escape trimmed))
|
||||
((string-match-p "^{.*}$" trimmed)
|
||||
(org-babel-script-escape
|
||||
(replace-regexp-in-string "^{\\(.*\\)}$" "[\\1]" trimmed)))
|
||||
|
||||
;; Maps - convert to alist
|
||||
((string-match-p "^%{.*}$" trimmed)
|
||||
(ob-elixir--parse-map trimmed))
|
||||
|
||||
;; Default: return as string
|
||||
(t trimmed))))
|
||||
|
||||
(defun ob-elixir--parse-map (map-string)
|
||||
"Parse Elixir MAP-STRING to alist."
|
||||
;; Simplified - for complex maps, use JSON encoding
|
||||
(let ((content (substring map-string 2 -1))) ; Remove %{ and }
|
||||
(mapcar
|
||||
(lambda (pair)
|
||||
(when (string-match "\\(.+?\\) => \\(.+\\)" pair)
|
||||
(cons (ob-elixir--parse-result (match-string 1 pair))
|
||||
(ob-elixir--parse-result (match-string 2 pair)))))
|
||||
(split-string content ", "))))
|
||||
```
|
||||
|
||||
### Using JSON for Complex Data
|
||||
|
||||
For complex nested structures, JSON is more reliable:
|
||||
|
||||
```elisp
|
||||
(defconst ob-elixir--json-wrapper
|
||||
"
|
||||
_result_ = (
|
||||
%s
|
||||
)
|
||||
IO.puts(Jason.encode!(_result_))
|
||||
"
|
||||
"Wrapper that outputs result as JSON.")
|
||||
|
||||
(defun ob-elixir--eval-as-json (code)
|
||||
"Evaluate CODE and parse result as JSON."
|
||||
(let* ((wrapped (format ob-elixir--json-wrapper code))
|
||||
(output (ob-elixir--eval-script wrapped)))
|
||||
(json-read-from-string (string-trim output))))
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Detecting Elixir Errors
|
||||
|
||||
```elisp
|
||||
(defconst ob-elixir-error-patterns
|
||||
'("^\\*\\* (\\(\\w+Error\\))" ; ** (RuntimeError) ...
|
||||
"^\\*\\* (\\(\\w+\\)) \\(.+\\)" ; ** (exit) ...
|
||||
"^\\(CompileError\\)" ; CompileError ...
|
||||
"^\\(SyntaxError\\)") ; SyntaxError ...
|
||||
"Patterns matching Elixir error output.")
|
||||
|
||||
(defun ob-elixir--error-p (output)
|
||||
"Return error info if OUTPUT contains an error, nil otherwise."
|
||||
(catch 'found
|
||||
(dolist (pattern ob-elixir-error-patterns)
|
||||
(when (string-match pattern output)
|
||||
(throw 'found (list :type (match-string 1 output)
|
||||
:message output))))))
|
||||
```
|
||||
|
||||
### Signaling Errors to Org
|
||||
|
||||
```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)
|
||||
|
||||
(defun ob-elixir--handle-error (output)
|
||||
"Handle error in OUTPUT, signaling appropriate condition."
|
||||
(when-let ((error-info (ob-elixir--error-p output)))
|
||||
(let ((type (plist-get error-info :type))
|
||||
(message (plist-get error-info :message)))
|
||||
(cond
|
||||
((member type '("CompileError" "SyntaxError" "TokenMissingError"))
|
||||
(signal 'ob-elixir-compile-error (list message)))
|
||||
(t
|
||||
(signal 'ob-elixir-runtime-error (list message)))))))
|
||||
```
|
||||
|
||||
### Timeout Handling
|
||||
|
||||
```elisp
|
||||
(defcustom ob-elixir-timeout 30
|
||||
"Default timeout in seconds for Elixir evaluation."
|
||||
:type 'integer
|
||||
:group 'ob-elixir)
|
||||
|
||||
(defun ob-elixir--eval-with-timeout (code timeout)
|
||||
"Evaluate CODE with TIMEOUT seconds limit."
|
||||
(with-timeout (timeout
|
||||
(error "Elixir evaluation timed out after %d seconds" timeout))
|
||||
(ob-elixir--eval-script code)))
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## elixir-mode Integration
|
||||
|
||||
### Syntax Highlighting
|
||||
|
||||
```elisp
|
||||
;; Register with org-src for editing
|
||||
(add-to-list 'org-src-lang-modes '("elixir" . elixir))
|
||||
|
||||
;; If elixir-mode isn't available, use a fallback
|
||||
(unless (fboundp 'elixir-mode)
|
||||
(add-to-list 'org-src-lang-modes '("elixir" . prog)))
|
||||
```
|
||||
|
||||
### Using elixir-mode Functions
|
||||
|
||||
```elisp
|
||||
(defun ob-elixir--format-code (code)
|
||||
"Format CODE using mix format if available."
|
||||
(when (and (executable-find "mix")
|
||||
(> (length code) 0))
|
||||
(with-temp-buffer
|
||||
(insert code)
|
||||
(let ((temp-file (make-temp-file "elixir-format" nil ".ex")))
|
||||
(unwind-protect
|
||||
(progn
|
||||
(write-region (point-min) (point-max) temp-file)
|
||||
(when (= 0 (call-process "mix" nil nil nil
|
||||
"format" temp-file))
|
||||
(erase-buffer)
|
||||
(insert-file-contents temp-file)
|
||||
(buffer-string)))
|
||||
(delete-file temp-file))))))
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
### Caching Compilation
|
||||
|
||||
```elisp
|
||||
(defvar ob-elixir--module-cache (make-hash-table :test 'equal)
|
||||
"Cache of compiled Elixir modules.")
|
||||
|
||||
(defun ob-elixir--get-cached-module (code)
|
||||
"Get cached module for CODE, or compile and cache."
|
||||
(let ((hash (md5 code)))
|
||||
(or (gethash hash ob-elixir--module-cache)
|
||||
(let ((module-name (format "ObElixir_%s" (substring hash 0 8))))
|
||||
(ob-elixir--compile-module module-name code)
|
||||
(puthash hash module-name ob-elixir--module-cache)
|
||||
module-name))))
|
||||
```
|
||||
|
||||
### Reusing Sessions
|
||||
|
||||
```elisp
|
||||
(defcustom ob-elixir-reuse-sessions t
|
||||
"Whether to reuse sessions between evaluations.
|
||||
When non-nil, sessions persist until explicitly killed."
|
||||
:type 'boolean
|
||||
:group 'ob-elixir)
|
||||
```
|
||||
|
||||
### Startup Time Optimization
|
||||
|
||||
```elisp
|
||||
;; Pre-start a session on package load (optional)
|
||||
(defun ob-elixir-warm-up ()
|
||||
"Pre-start an IEx session for faster first evaluation."
|
||||
(interactive)
|
||||
(ob-elixir--start-iex-session "warmup"))
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
- [Elixir Documentation](https://elixir-lang.org/docs.html)
|
||||
- [IEx Documentation](https://hexdocs.pm/iex/IEx.html)
|
||||
- [Mix Documentation](https://hexdocs.pm/mix/Mix.html)
|
||||
- [Erlang Distribution Protocol](https://www.erlang.org/doc/reference_manual/distributed.html)
|
||||
- [elixir-mode GitHub](https://github.com/elixir-editors/emacs-elixir)
|
||||
- [inf-elixir for REPL](https://github.com/J3RN/inf-elixir)
|
||||
- [Elixir LS (Language Server)](https://github.com/elixir-lsp/elixir-ls)
|
||||
435
docs/05-existing-implementations-analysis.md
Normal file
435
docs/05-existing-implementations-analysis.md
Normal file
@@ -0,0 +1,435 @@
|
||||
# Existing Implementations Analysis
|
||||
|
||||
This document analyzes existing org-babel Elixir implementations and related packages, identifying gaps and improvement opportunities.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [zweifisch/ob-elixir Analysis](#zweifischob-elixir-analysis)
|
||||
- [Comparison with Other ob-* Packages](#comparison-with-other-ob--packages)
|
||||
- [Feature Gap Analysis](#feature-gap-analysis)
|
||||
- [Recommendations](#recommendations)
|
||||
- [References](#references)
|
||||
|
||||
---
|
||||
|
||||
## zweifisch/ob-elixir Analysis
|
||||
|
||||
### Repository Information
|
||||
|
||||
- **URL**: https://github.com/zweifisch/ob-elixir
|
||||
- **Stars**: 29 (as of 2024)
|
||||
- **Last Updated**: Limited recent activity
|
||||
- **License**: GPL-3.0
|
||||
|
||||
### Source Code Review
|
||||
|
||||
```elisp
|
||||
;;; Current implementation (simplified)
|
||||
|
||||
(defvar ob-elixir-process-output "")
|
||||
|
||||
(defconst org-babel-header-args:elixir
|
||||
'((cookie . :any)
|
||||
(name . :any)
|
||||
(remsh . :any)
|
||||
(sname . :any))
|
||||
"elixir header arguments")
|
||||
|
||||
(defvar ob-elixir-eoe "\u2029") ; Unicode paragraph separator
|
||||
|
||||
(defun org-babel-execute:elixir (body params)
|
||||
(let ((session (cdr (assoc :session params)))
|
||||
(tmp (org-babel-temp-file "elixir-")))
|
||||
(ob-elixir-ensure-session session params)
|
||||
(with-temp-file tmp (insert body))
|
||||
(ob-elixir-eval session (format "import_file(\"%s\")" tmp))))
|
||||
|
||||
(defun ob-elixir-eval (session body)
|
||||
(let ((result (ob-elixir-eval-in-repl session body)))
|
||||
;; Heavy regex cleanup of IEx output
|
||||
(replace-regexp-in-string
|
||||
"^\\(import_file([^)]+)\\)+\n" ""
|
||||
(replace-regexp-in-string
|
||||
"\r" ""
|
||||
(replace-regexp-in-string
|
||||
"\n\\(\\(iex\\|[.]+\\)\\(([^@]+@[^)]+)[0-9]+\\|([0-9]+)\\)> \\)+" ""
|
||||
(replace-regexp-in-string
|
||||
"\e\\[[0-9;]*[A-Za-z]" ""
|
||||
(replace-regexp-in-string
|
||||
"\"\\\\u2029\"" ""
|
||||
result)))))))
|
||||
```
|
||||
|
||||
### Strengths
|
||||
|
||||
| Feature | Description |
|
||||
|---------------------|--------------------------------------------------------|
|
||||
| **Session support** | Uses IEx sessions for persistent state |
|
||||
| **Remote shell** | Supports `--remsh` for connecting to running nodes |
|
||||
| **Node naming** | Supports `--sname` and `--name` for distributed Erlang |
|
||||
| **Cookie support** | Can specify Erlang cookie for authentication |
|
||||
| **Simple design** | Minimal code, easy to understand |
|
||||
|
||||
### Weaknesses
|
||||
|
||||
| Issue | Description | Impact |
|
||||
|----------------------------|-----------------------------------------------------|----------------------------|
|
||||
| **Always uses sessions** | No one-shot execution mode | Slow for simple scripts |
|
||||
| **No `:results value`** | Only captures output, not return values | Limited functionality |
|
||||
| **No `:var` support** | Cannot pass variables to code blocks | Poor org-babel integration |
|
||||
| **Fragile output parsing** | Multiple regex replacements to clean IEx output | Unreliable |
|
||||
| **Uses `import_file`** | Relies on IEx-specific command | Tight coupling to IEx |
|
||||
| **Global state** | Uses `ob-elixir-process-output` global variable | Not thread-safe |
|
||||
| **No Mix support** | Cannot execute in Mix project context | Limited for real projects |
|
||||
| **Hardcoded `sit-for`** | Uses fixed delays instead of proper synchronization | Timing issues |
|
||||
| **No error handling** | Errors not properly detected or reported | Poor UX |
|
||||
| **No tests** | No test suite | Quality concerns |
|
||||
| **No `defcustom`** | Settings not customizable via Customize | Poor UX |
|
||||
|
||||
### Specific Issues in Code
|
||||
|
||||
1. **Timing-based synchronization**:
|
||||
```elisp
|
||||
(sit-for 0.5) ; Wait 500ms and hope IEx is ready
|
||||
;; This is fragile - slow systems may fail
|
||||
```
|
||||
|
||||
2. **No prompt detection**:
|
||||
```elisp
|
||||
(defun ob-elixir-wait ()
|
||||
(while (not (string-match-p ob-elixir-eoe ob-elixir-process-output))
|
||||
(sit-for 0.2)))
|
||||
;; Busy-waiting instead of using process filter properly
|
||||
```
|
||||
|
||||
3. **Heavy output cleanup**:
|
||||
```elisp
|
||||
;; 5 nested regex replacements - fragile and hard to debug
|
||||
(replace-regexp-in-string "pattern1" ""
|
||||
(replace-regexp-in-string "pattern2" ""
|
||||
(replace-regexp-in-string "pattern3" ""
|
||||
...)))
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Comparison with Other ob-* Packages
|
||||
|
||||
### ob-python (Standard Library)
|
||||
|
||||
**Features we should match:**
|
||||
|
||||
```elisp
|
||||
;; Multiple execution modes
|
||||
(defcustom org-babel-python-command "python3"
|
||||
"Command to execute Python code.")
|
||||
|
||||
;; Proper result handling
|
||||
(defun org-babel-execute:python (body params)
|
||||
(let* ((session (org-babel-python-initiate-session
|
||||
(cdr (assq :session params)) params))
|
||||
(result-params (cdr (assq :result-params params)))
|
||||
(result-type (cdr (assq :result-type params)))
|
||||
(full-body (org-babel-expand-body:generic ...))
|
||||
(result (org-babel-python-evaluate ...)))
|
||||
;; Proper result formatting
|
||||
(org-babel-reassemble-table
|
||||
(org-babel-result-cond result-params
|
||||
result
|
||||
(org-babel-python-table-or-string result))
|
||||
...)))
|
||||
|
||||
;; Variable injection
|
||||
(defun org-babel-variable-assignments:python (params)
|
||||
(mapcar (lambda (pair)
|
||||
(format "%s=%s" (car pair)
|
||||
(org-babel-python-var-to-python (cdr pair))))
|
||||
(org-babel--get-vars params)))
|
||||
|
||||
;; Both session and non-session evaluation
|
||||
(defun org-babel-python-evaluate (session body &optional result-type result-params)
|
||||
(if session
|
||||
(org-babel-python-evaluate-session ...)
|
||||
(org-babel-python-evaluate-external-process ...)))
|
||||
```
|
||||
|
||||
### ob-ruby (Standard Library)
|
||||
|
||||
**Patterns to adopt:**
|
||||
|
||||
```elisp
|
||||
;; Clean wrapper for value capture
|
||||
(defconst org-babel-ruby-wrapper-method
|
||||
"
|
||||
def main
|
||||
%s
|
||||
end
|
||||
puts main.inspect
|
||||
")
|
||||
|
||||
;; Proper session management with comint
|
||||
(defun org-babel-ruby-initiate-session (&optional session params)
|
||||
(unless (string= session "none")
|
||||
(let ((session-buffer (or (get-buffer ...) ...)))
|
||||
(if (org-babel-comint-buffer-livep session-buffer)
|
||||
session-buffer
|
||||
(org-babel-ruby-initiate-session session)))))
|
||||
|
||||
;; Type conversion
|
||||
(defun org-babel-ruby-var-to-ruby (var)
|
||||
(if (listp var)
|
||||
(concat "[" (mapconcat #'org-babel-ruby-var-to-ruby var ", ") "]")
|
||||
(format "%S" var)))
|
||||
```
|
||||
|
||||
### ob-shell (Standard Library)
|
||||
|
||||
**Useful patterns:**
|
||||
|
||||
```elisp
|
||||
;; Multiple shell types
|
||||
(defcustom org-babel-shell-names
|
||||
'("sh" "bash" "zsh" "fish" ...)
|
||||
"Shells to register with org-babel.")
|
||||
|
||||
;; Header arg for shell selection
|
||||
(defconst org-babel-header-args:shell
|
||||
'((shebang . :any)))
|
||||
|
||||
;; Proper prep-session
|
||||
(defun org-babel-prep-session:shell (session params)
|
||||
(let* ((session (org-babel-sh-initiate-session session))
|
||||
(var-lines (org-babel-variable-assignments:shell params)))
|
||||
(org-babel-comint-in-buffer session
|
||||
(mapc (lambda (var)
|
||||
(insert var) (comint-send-input nil t)
|
||||
(org-babel-comint-wait-for-output session))
|
||||
var-lines))
|
||||
session))
|
||||
```
|
||||
|
||||
### Feature Comparison Matrix
|
||||
|
||||
| Feature | ob-python | ob-ruby | ob-shell | ob-elixir (zweifisch) |
|
||||
|--------------------|-----------|---------|----------|-----------------------|
|
||||
| One-shot execution | Yes | Yes | Yes | No |
|
||||
| Session support | Yes | Yes | Yes | Yes |
|
||||
| `:results value` | Yes | Yes | Yes | No |
|
||||
| `:results output` | Yes | Yes | Yes | Yes (only) |
|
||||
| Variable injection | Yes | Yes | Yes | No |
|
||||
| Table output | Yes | Yes | Yes | No |
|
||||
| Error handling | Yes | Yes | Yes | No |
|
||||
| Customization | Yes | Yes | Yes | No |
|
||||
| Tests | Yes | Yes | Yes | No |
|
||||
| Graphics support | Yes | No | No | No |
|
||||
| Async support | Yes | No | No | No |
|
||||
|
||||
---
|
||||
|
||||
## Feature Gap Analysis
|
||||
|
||||
### Critical Gaps (Must Fix)
|
||||
|
||||
1. **No `:results value` support**
|
||||
- Cannot capture return value of expressions
|
||||
- Users must use `IO.inspect` workarounds
|
||||
- **Fix**: Implement wrapper method pattern
|
||||
|
||||
2. **No variable injection**
|
||||
- Cannot use `:var` header argument
|
||||
- Breaks org-babel's data passing paradigm
|
||||
- **Fix**: Implement `org-babel-variable-assignments:elixir`
|
||||
|
||||
3. **No one-shot execution**
|
||||
- Every evaluation starts/uses IEx session
|
||||
- Slow for simple scripts
|
||||
- **Fix**: Add external process execution mode
|
||||
|
||||
4. **Fragile output parsing**
|
||||
- Multiple regex replacements prone to breaking
|
||||
- **Fix**: Use markers or JSON encoding
|
||||
|
||||
### Important Gaps (Should Fix)
|
||||
|
||||
1. **No Mix project support**
|
||||
- Cannot run code in project context
|
||||
- No access to project dependencies
|
||||
- **Fix**: Add `:mix-project` header argument
|
||||
|
||||
2. **No proper error handling**
|
||||
- Errors appear as raw output
|
||||
- No structured error information
|
||||
- **Fix**: Detect and signal Elixir errors
|
||||
|
||||
3. **No customization**
|
||||
- Hardcoded paths and settings
|
||||
- **Fix**: Add `defcustom` for all configurable values
|
||||
|
||||
4. **No tests**
|
||||
- No way to verify correctness
|
||||
- **Fix**: Add comprehensive test suite
|
||||
|
||||
### Nice-to-Have Gaps (Could Fix)
|
||||
|
||||
1. **No async execution**
|
||||
- Long-running code blocks UI
|
||||
- Consider for future versions
|
||||
|
||||
2. **No graphics support**
|
||||
- Elixir has visualization libraries (VegaLite)
|
||||
- Could add in future
|
||||
|
||||
3. **No LSP integration**
|
||||
- Could integrate with ElixirLS for completions
|
||||
- Future enhancement
|
||||
|
||||
---
|
||||
|
||||
## Recommendations
|
||||
|
||||
### Approach: New Implementation
|
||||
|
||||
Given the significant gaps and architectural issues in zweifisch/ob-elixir, we recommend **creating a new implementation** rather than forking. Reasons:
|
||||
|
||||
1. Need to change fundamental architecture (session-only → dual mode)
|
||||
2. Core data flow needs redesign
|
||||
3. Limited active maintenance upstream
|
||||
4. Cleaner API design possible with fresh start
|
||||
|
||||
### Proposed Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ org-babel-execute:elixir │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
┌──────────┴──────────┐
|
||||
│ │
|
||||
▼ ▼
|
||||
┌─────────────────┐ ┌─────────────────┐
|
||||
│ Session Mode │ │ External Mode │
|
||||
│ (IEx/comint) │ │ (elixir cmd) │
|
||||
└─────────────────┘ └─────────────────┘
|
||||
│ │
|
||||
▼ ▼
|
||||
┌─────────────────┐ ┌─────────────────┐
|
||||
│ With Mix │ │ With Mix │
|
||||
│ (iex -S mix) │ │ (mix run) │
|
||||
└─────────────────┘ └─────────────────┘
|
||||
```
|
||||
|
||||
### Implementation Phases
|
||||
|
||||
#### Phase 1: Core (MVP)
|
||||
- [x] One-shot execution with `elixir` command
|
||||
- [x] `:results value` and `:results output` support
|
||||
- [x] Variable injection with `:var`
|
||||
- [x] Basic error detection
|
||||
- [x] Customizable commands
|
||||
- [x] Test suite foundation
|
||||
|
||||
#### Phase 2: Sessions
|
||||
- [ ] IEx session support via comint
|
||||
- [ ] Session persistence
|
||||
- [ ] Proper prompt detection
|
||||
- [ ] Session cleanup
|
||||
|
||||
#### Phase 3: Mix Integration
|
||||
- [ ] `:mix-project` header argument
|
||||
- [ ] Automatic Mix project detection
|
||||
- [ ] `iex -S mix` for sessions
|
||||
- [ ] `mix run` for one-shot
|
||||
|
||||
#### Phase 4: Advanced Features
|
||||
- [ ] Remote shell (remsh) support
|
||||
- [ ] Table input/output
|
||||
- [ ] Async execution
|
||||
- [ ] Better error messages with line numbers
|
||||
|
||||
### Header Arguments to Support
|
||||
|
||||
```elisp
|
||||
(defconst org-babel-header-args:elixir
|
||||
'(;; Standard org-babel args work automatically
|
||||
;; Language-specific:
|
||||
(mix-project . :any) ; Path to mix.exs directory
|
||||
(mix-env . :any) ; MIX_ENV (dev, test, prod)
|
||||
(iex-args . :any) ; Extra IEx arguments
|
||||
(timeout . :any) ; Execution timeout
|
||||
(node-name . :any) ; --name for distributed
|
||||
(node-sname . :any) ; --sname for distributed
|
||||
(cookie . :any) ; Erlang cookie
|
||||
(remsh . :any)) ; Remote shell node
|
||||
"Elixir-specific header arguments.")
|
||||
```
|
||||
|
||||
### Example Usage (Target)
|
||||
|
||||
```org
|
||||
* Basic Evaluation
|
||||
|
||||
#+BEGIN_SRC elixir
|
||||
Enum.map([1, 2, 3], &(&1 * 2))
|
||||
#+END_SRC
|
||||
|
||||
#+RESULTS:
|
||||
| 2 | 4 | 6 |
|
||||
|
||||
* With Variables
|
||||
|
||||
#+NAME: my-data
|
||||
| a | 1 |
|
||||
| b | 2 |
|
||||
|
||||
#+BEGIN_SRC elixir :var data=my-data
|
||||
Enum.map(data, fn [k, v] -> {k, v * 10} end)
|
||||
#+END_SRC
|
||||
|
||||
#+RESULTS:
|
||||
| a | 10 |
|
||||
| b | 20 |
|
||||
|
||||
* In Mix Project
|
||||
|
||||
#+BEGIN_SRC elixir :mix-project ~/my_app :mix-env test
|
||||
MyApp.some_function()
|
||||
#+END_SRC
|
||||
|
||||
* With Session
|
||||
|
||||
#+BEGIN_SRC elixir :session my-session
|
||||
defmodule Helper do
|
||||
def double(x), do: x * 2
|
||||
end
|
||||
#+END_SRC
|
||||
|
||||
#+BEGIN_SRC elixir :session my-session
|
||||
Helper.double(21)
|
||||
#+END_SRC
|
||||
|
||||
#+RESULTS:
|
||||
: 42
|
||||
|
||||
* Capturing Output
|
||||
|
||||
#+BEGIN_SRC elixir :results output
|
||||
IO.puts("Hello")
|
||||
IO.puts("World")
|
||||
#+END_SRC
|
||||
|
||||
#+RESULTS:
|
||||
: Hello
|
||||
: World
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
- [zweifisch/ob-elixir](https://github.com/zweifisch/ob-elixir)
|
||||
- [Org Mode ob-python.el](https://git.savannah.gnu.org/cgit/emacs/org-mode.git/tree/lisp/ob-python.el)
|
||||
- [Org Mode ob-ruby.el](https://git.savannah.gnu.org/cgit/emacs/org-mode.git/tree/lisp/ob-ruby.el)
|
||||
- [Org Mode ob-shell.el](https://git.savannah.gnu.org/cgit/emacs/org-mode.git/tree/lisp/ob-shell.el)
|
||||
- [Org Mode ob-emacs-lisp.el](https://git.savannah.gnu.org/cgit/emacs/org-mode.git/tree/lisp/ob-emacs-lisp.el)
|
||||
- [elixir-editors/emacs-elixir](https://github.com/elixir-editors/emacs-elixir)
|
||||
- [Worg Babel Languages](https://orgmode.org/worg/org-contrib/babel/languages/)
|
||||
144
tasks/00-index.md
Normal file
144
tasks/00-index.md
Normal file
@@ -0,0 +1,144 @@
|
||||
# ob-elixir Implementation Tasks
|
||||
|
||||
This directory contains step-by-step implementation tasks for building the ob-elixir package.
|
||||
|
||||
## Overview
|
||||
|
||||
The implementation is organized into 4 phases:
|
||||
|
||||
| Phase | Description | Tasks | Priority |
|
||||
|-------|-------------|-------|----------|
|
||||
| **Phase 1** | Core MVP | 01-06 | Critical |
|
||||
| **Phase 2** | Sessions | 07 | High |
|
||||
| **Phase 3** | Mix Integration | 08 | High |
|
||||
| **Phase 4** | Advanced Features | 09-10 | Medium/Low |
|
||||
|
||||
## Task List
|
||||
|
||||
### Phase 1: Core (MVP)
|
||||
|
||||
These tasks implement the minimum viable product - basic Elixir code execution in org-mode.
|
||||
|
||||
| Task | Title | Time | Status |
|
||||
|------|-------|------|--------|
|
||||
| [01](01-project-setup.md) | Project Setup | 30 min | Pending |
|
||||
| [02](02-basic-execution.md) | Basic Code Execution | 1-2 hrs | Pending |
|
||||
| [03](03-variable-injection.md) | Variable Injection | 1-2 hrs | Pending |
|
||||
| [04](04-error-handling.md) | Error Handling | 1 hr | Pending |
|
||||
| [05](05-result-formatting.md) | Result Formatting | 1-2 hrs | Pending |
|
||||
| [06](06-test-suite.md) | Comprehensive Test Suite | 2-3 hrs | Pending |
|
||||
|
||||
**Phase 1 Deliverables:**
|
||||
- Execute Elixir code with `C-c C-c`
|
||||
- `:results value` and `:results output` work
|
||||
- `:var` header arguments work
|
||||
- Errors are properly reported
|
||||
- Lists become org tables
|
||||
- Comprehensive test coverage
|
||||
|
||||
### Phase 2: Sessions
|
||||
|
||||
| Task | Title | Time | Status |
|
||||
|------|-------|------|--------|
|
||||
| [07](07-session-support.md) | IEx Session Support | 3-4 hrs | Pending |
|
||||
|
||||
**Phase 2 Deliverables:**
|
||||
- `:session name` creates persistent IEx sessions
|
||||
- Variables and modules persist across blocks
|
||||
- Session cleanup commands
|
||||
|
||||
### Phase 3: Mix Integration
|
||||
|
||||
| Task | Title | Time | Status |
|
||||
|------|-------|------|--------|
|
||||
| [08](08-mix-project-support.md) | Mix Project Support | 2-3 hrs | Pending |
|
||||
|
||||
**Phase 3 Deliverables:**
|
||||
- `:mix-project path` executes in project context
|
||||
- Auto-detection of Mix projects
|
||||
- `:mix-env` sets MIX_ENV
|
||||
- Sessions with Mix (`iex -S mix`)
|
||||
|
||||
### Phase 4: Advanced Features
|
||||
|
||||
| Task | Title | Time | Status |
|
||||
|------|-------|------|--------|
|
||||
| [09](09-remote-shell.md) | Remote Shell (remsh) | 2-3 hrs | Pending |
|
||||
| [10](10-async-execution.md) | Async Execution | 3-4 hrs | Pending |
|
||||
|
||||
**Phase 4 Deliverables:**
|
||||
- `:remsh node@host` connects to running nodes
|
||||
- `:async yes` for non-blocking execution
|
||||
|
||||
## Implementation Order
|
||||
|
||||
```
|
||||
Phase 1 (Must complete in order)
|
||||
├── 01-project-setup
|
||||
├── 02-basic-execution
|
||||
├── 03-variable-injection
|
||||
├── 04-error-handling
|
||||
├── 05-result-formatting
|
||||
└── 06-test-suite
|
||||
|
||||
Phase 2 (After Phase 1)
|
||||
└── 07-session-support
|
||||
|
||||
Phase 3 (After Phase 1, can parallel with Phase 2)
|
||||
└── 08-mix-project-support
|
||||
|
||||
Phase 4 (After relevant dependencies)
|
||||
├── 09-remote-shell (after 07)
|
||||
└── 10-async-execution (after Phase 1)
|
||||
```
|
||||
|
||||
## Time Estimates
|
||||
|
||||
| Phase | Estimated Time |
|
||||
|-------|----------------|
|
||||
| Phase 1 | 8-12 hours |
|
||||
| Phase 2 | 3-4 hours |
|
||||
| Phase 3 | 2-3 hours |
|
||||
| Phase 4 | 5-7 hours |
|
||||
| **Total** | **18-26 hours** |
|
||||
|
||||
## Getting Started
|
||||
|
||||
1. Start with [Task 01: Project Setup](01-project-setup.md)
|
||||
2. Complete Phase 1 tasks in order
|
||||
3. Phases 2-4 can be done based on your priorities
|
||||
|
||||
## Task Template
|
||||
|
||||
Each task file includes:
|
||||
|
||||
- **Objective**: What the task accomplishes
|
||||
- **Prerequisites**: What must be done first
|
||||
- **Steps**: Detailed implementation steps with code
|
||||
- **Tests**: Test cases to verify the implementation
|
||||
- **Acceptance Criteria**: Checklist of requirements
|
||||
- **Troubleshooting**: Common issues and solutions
|
||||
|
||||
## Testing
|
||||
|
||||
Run tests after each task:
|
||||
|
||||
```bash
|
||||
make test
|
||||
```
|
||||
|
||||
For integration tests with org-mode:
|
||||
|
||||
```bash
|
||||
make test-integration
|
||||
```
|
||||
|
||||
## Documentation References
|
||||
|
||||
| Document | Content |
|
||||
|----------|---------|
|
||||
| [01-emacs-elisp-best-practices.md](../docs/01-emacs-elisp-best-practices.md) | Elisp conventions |
|
||||
| [02-testing-emacs-elisp.md](../docs/02-testing-emacs-elisp.md) | Testing strategies |
|
||||
| [03-org-babel-implementation-guide.md](../docs/03-org-babel-implementation-guide.md) | Org-babel internals |
|
||||
| [04-elixir-integration-strategies.md](../docs/04-elixir-integration-strategies.md) | Elixir execution |
|
||||
| [05-existing-implementations-analysis.md](../docs/05-existing-implementations-analysis.md) | Prior art analysis |
|
||||
216
tasks/01-project-setup.md
Normal file
216
tasks/01-project-setup.md
Normal file
@@ -0,0 +1,216 @@
|
||||
# Task 01: Project Setup
|
||||
|
||||
**Phase**: 1 - Core (MVP)
|
||||
**Priority**: Critical
|
||||
**Estimated Time**: 30 minutes
|
||||
**Dependencies**: None
|
||||
|
||||
## Objective
|
||||
|
||||
Set up the project structure with proper Emacs Lisp package conventions, including file headers, licensing, and build tooling.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Emacs 27.1+ installed
|
||||
- Elixir installed and in PATH
|
||||
- Git repository initialized
|
||||
|
||||
## Steps
|
||||
|
||||
### Step 1: Create the main package file
|
||||
|
||||
Create `ob-elixir.el` with proper headers:
|
||||
|
||||
```elisp
|
||||
;;; ob-elixir.el --- Org Babel functions for Elixir -*- lexical-binding: t; -*-
|
||||
|
||||
;; Copyright (C) 2024 Your Name
|
||||
|
||||
;; Author: Your Name <your.email@example.com>
|
||||
;; URL: https://github.com/username/ob-elixir
|
||||
;; Keywords: literate programming, reproducible research, elixir
|
||||
;; Version: 0.1.0
|
||||
;; Package-Requires: ((emacs "27.1") (org "9.4"))
|
||||
|
||||
;; This file is not part of GNU Emacs.
|
||||
|
||||
;; This program is free software; you can redistribute it and/or modify
|
||||
;; it under the terms of the GNU General Public License as published by
|
||||
;; the Free Software Foundation, either version 3 of the License, or
|
||||
;; (at your option) any later version.
|
||||
|
||||
;;; Commentary:
|
||||
|
||||
;; Org Babel support for evaluating Elixir code blocks.
|
||||
;;
|
||||
;; Features:
|
||||
;; - Execute Elixir code in org-mode source blocks
|
||||
;; - Support for :results value and :results output
|
||||
;; - Variable passing with :var header argument
|
||||
;; - Mix project context support
|
||||
;;
|
||||
;; Usage:
|
||||
;; Add (elixir . t) to `org-babel-load-languages':
|
||||
;;
|
||||
;; (org-babel-do-load-languages
|
||||
;; 'org-babel-load-languages
|
||||
;; '((elixir . t)))
|
||||
|
||||
;;; Code:
|
||||
|
||||
(require 'ob)
|
||||
(require 'ob-eval)
|
||||
|
||||
(provide 'ob-elixir)
|
||||
;;; ob-elixir.el ends here
|
||||
```
|
||||
|
||||
### Step 2: Create the Eldev file for build tooling
|
||||
|
||||
Create `Eldev` file:
|
||||
|
||||
```elisp
|
||||
; -*- mode: emacs-lisp; lexical-binding: t; -*-
|
||||
|
||||
(eldev-use-package-archive 'gnu)
|
||||
(eldev-use-package-archive 'melpa)
|
||||
|
||||
;; Test dependencies
|
||||
(eldev-add-extra-dependencies 'test 'ert)
|
||||
|
||||
;; Use ERT for testing
|
||||
(setf eldev-test-framework 'ert)
|
||||
|
||||
;; Lint configuration
|
||||
(setf eldev-lint-default '(elisp package))
|
||||
```
|
||||
|
||||
### Step 3: Create test directory structure
|
||||
|
||||
```bash
|
||||
mkdir -p test
|
||||
```
|
||||
|
||||
Create `test/test-ob-elixir.el`:
|
||||
|
||||
```elisp
|
||||
;;; test-ob-elixir.el --- Tests for ob-elixir -*- lexical-binding: t; -*-
|
||||
|
||||
;;; Commentary:
|
||||
|
||||
;; Test suite for ob-elixir package.
|
||||
|
||||
;;; Code:
|
||||
|
||||
(require 'ert)
|
||||
|
||||
;; Add parent directory to load path
|
||||
(let ((dir (file-name-directory (or load-file-name buffer-file-name))))
|
||||
(add-to-list 'load-path (expand-file-name ".." dir)))
|
||||
|
||||
(require 'ob-elixir)
|
||||
|
||||
(ert-deftest ob-elixir-test-package-loads ()
|
||||
"Test that the package loads successfully."
|
||||
(should (featurep 'ob-elixir)))
|
||||
|
||||
(provide 'test-ob-elixir)
|
||||
;;; test-ob-elixir.el ends here
|
||||
```
|
||||
|
||||
### Step 4: Create Makefile
|
||||
|
||||
Create `Makefile`:
|
||||
|
||||
```makefile
|
||||
EMACS ?= emacs
|
||||
BATCH = $(EMACS) -Q -batch -L .
|
||||
|
||||
.PHONY: all compile test lint clean
|
||||
|
||||
all: compile test
|
||||
|
||||
compile:
|
||||
$(BATCH) -f batch-byte-compile ob-elixir.el
|
||||
|
||||
test:
|
||||
$(BATCH) -l ert -l test/test-ob-elixir.el \
|
||||
-f ert-run-tests-batch-and-exit
|
||||
|
||||
lint:
|
||||
$(BATCH) --eval "(require 'package)" \
|
||||
--eval "(package-initialize)" \
|
||||
--eval "(package-refresh-contents)" \
|
||||
--eval "(package-install 'package-lint)" \
|
||||
-l package-lint \
|
||||
-f package-lint-batch-and-exit ob-elixir.el
|
||||
|
||||
clean:
|
||||
rm -f *.elc test/*.elc
|
||||
```
|
||||
|
||||
### Step 5: Create .gitignore
|
||||
|
||||
Create `.gitignore`:
|
||||
|
||||
```
|
||||
# Byte-compiled files
|
||||
*.elc
|
||||
|
||||
# Eldev
|
||||
.eldev/
|
||||
Eldev-local
|
||||
|
||||
# Package archives
|
||||
/packages/
|
||||
|
||||
# Test artifacts
|
||||
/test/tmp/
|
||||
|
||||
# Editor
|
||||
*~
|
||||
\#*\#
|
||||
.#*
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
```
|
||||
|
||||
### Step 6: Verify setup
|
||||
|
||||
Run the following commands to verify:
|
||||
|
||||
```bash
|
||||
# Check Emacs version
|
||||
emacs --version
|
||||
|
||||
# Check Elixir version
|
||||
elixir --version
|
||||
|
||||
# Run tests
|
||||
make test
|
||||
|
||||
# Compile
|
||||
make compile
|
||||
```
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `ob-elixir.el` exists with proper headers
|
||||
- [ ] Package loads without errors: `(require 'ob-elixir)`
|
||||
- [ ] `make test` runs successfully
|
||||
- [ ] `make compile` produces no warnings
|
||||
- [ ] All files follow Emacs Lisp conventions
|
||||
|
||||
## Files Created
|
||||
|
||||
- `ob-elixir.el` - Main package file
|
||||
- `Eldev` - Build tool configuration
|
||||
- `Makefile` - Make targets
|
||||
- `test/test-ob-elixir.el` - Test file
|
||||
- `.gitignore` - Git ignore rules
|
||||
|
||||
## References
|
||||
|
||||
- [docs/01-emacs-elisp-best-practices.md](../docs/01-emacs-elisp-best-practices.md)
|
||||
- [docs/02-testing-emacs-elisp.md](../docs/02-testing-emacs-elisp.md)
|
||||
242
tasks/02-basic-execution.md
Normal file
242
tasks/02-basic-execution.md
Normal file
@@ -0,0 +1,242 @@
|
||||
# Task 02: Basic Code Execution
|
||||
|
||||
**Phase**: 1 - Core (MVP)
|
||||
**Priority**: Critical
|
||||
**Estimated Time**: 1-2 hours
|
||||
**Dependencies**: Task 01 (Project Setup)
|
||||
|
||||
## Objective
|
||||
|
||||
Implement the core `org-babel-execute:elixir` function that can execute Elixir code blocks using external process (one-shot execution).
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Task 01 completed
|
||||
- Elixir installed and accessible via `elixir` command
|
||||
|
||||
## Steps
|
||||
|
||||
### Step 1: Add customization group and variables
|
||||
|
||||
Add to `ob-elixir.el`:
|
||||
|
||||
```elisp
|
||||
;;; Customization
|
||||
|
||||
(defgroup ob-elixir nil
|
||||
"Org Babel support for Elixir."
|
||||
:group 'org-babel
|
||||
:prefix "ob-elixir-")
|
||||
|
||||
(defcustom ob-elixir-command "elixir"
|
||||
"Command to execute Elixir code.
|
||||
Can be a full path or command name if in PATH."
|
||||
:type 'string
|
||||
:group 'ob-elixir
|
||||
:safe #'stringp)
|
||||
```
|
||||
|
||||
### Step 2: Add default header arguments
|
||||
|
||||
```elisp
|
||||
;;; Header Arguments
|
||||
|
||||
(defvar org-babel-default-header-args:elixir
|
||||
'((:results . "value")
|
||||
(:session . "none"))
|
||||
"Default header arguments for Elixir code blocks.")
|
||||
```
|
||||
|
||||
### Step 3: Register the language
|
||||
|
||||
```elisp
|
||||
;;; Language Registration
|
||||
|
||||
;; File extension for tangling
|
||||
(add-to-list 'org-babel-tangle-lang-exts '("elixir" . "ex"))
|
||||
|
||||
;; Associate with elixir-mode for syntax highlighting (if available)
|
||||
(with-eval-after-load 'org-src
|
||||
(add-to-list 'org-src-lang-modes '("elixir" . elixir)))
|
||||
```
|
||||
|
||||
### Step 4: Implement the execute function
|
||||
|
||||
```elisp
|
||||
;;; Execution
|
||||
|
||||
(defun org-babel-execute:elixir (body params)
|
||||
"Execute a block of Elixir code with org-babel.
|
||||
|
||||
BODY is the Elixir code to execute.
|
||||
PARAMS is an alist of header arguments.
|
||||
|
||||
This function is called by `org-babel-execute-src-block'."
|
||||
(let* ((result-type (cdr (assq :result-type params)))
|
||||
(result-params (cdr (assq :result-params params)))
|
||||
(result (ob-elixir--execute body result-type)))
|
||||
(org-babel-reassemble-table
|
||||
(org-babel-result-cond result-params
|
||||
result
|
||||
(org-babel-script-escape result))
|
||||
(org-babel-pick-name (cdr (assq :colname-names params))
|
||||
(cdr (assq :colnames params)))
|
||||
(org-babel-pick-name (cdr (assq :rowname-names params))
|
||||
(cdr (assq :rownames params))))))
|
||||
```
|
||||
|
||||
### Step 5: Implement the internal execute function
|
||||
|
||||
```elisp
|
||||
(defun ob-elixir--execute (body result-type)
|
||||
"Execute BODY as Elixir code.
|
||||
|
||||
RESULT-TYPE is either `value' or `output'.
|
||||
For `value', wraps code to capture return value.
|
||||
For `output', captures stdout directly.
|
||||
|
||||
Returns the result as a string."
|
||||
(let* ((tmp-file (org-babel-temp-file "ob-elixir-" ".exs"))
|
||||
(code (if (eq result-type 'value)
|
||||
(ob-elixir--wrap-for-value body)
|
||||
body)))
|
||||
(with-temp-file tmp-file
|
||||
(insert code))
|
||||
(let ((result (org-babel-eval
|
||||
(format "%s %s"
|
||||
ob-elixir-command
|
||||
(org-babel-process-file-name tmp-file))
|
||||
"")))
|
||||
(string-trim result))))
|
||||
```
|
||||
|
||||
### Step 6: Implement the value wrapper
|
||||
|
||||
```elisp
|
||||
(defconst ob-elixir--value-wrapper
|
||||
"result = (
|
||||
%s
|
||||
)
|
||||
IO.puts(inspect(result, limit: :infinity, printable_limit: :infinity, charlists: :as_lists))
|
||||
"
|
||||
"Wrapper template for capturing Elixir expression value.
|
||||
%s is replaced with the user's code.")
|
||||
|
||||
(defun ob-elixir--wrap-for-value (body)
|
||||
"Wrap BODY to capture its return value.
|
||||
|
||||
The wrapper evaluates BODY, then prints the result using
|
||||
`inspect/2` with infinite limits to avoid truncation."
|
||||
(format ob-elixir--value-wrapper body))
|
||||
```
|
||||
|
||||
### Step 7: Add tests
|
||||
|
||||
Add to `test/test-ob-elixir.el`:
|
||||
|
||||
```elisp
|
||||
(ert-deftest ob-elixir-test-elixir-available ()
|
||||
"Test that Elixir is available."
|
||||
(should (executable-find ob-elixir-command)))
|
||||
|
||||
(ert-deftest ob-elixir-test-simple-value ()
|
||||
"Test simple value evaluation."
|
||||
(skip-unless (executable-find ob-elixir-command))
|
||||
(let ((result (ob-elixir--execute "1 + 1" 'value)))
|
||||
(should (equal "2" result))))
|
||||
|
||||
(ert-deftest ob-elixir-test-simple-output ()
|
||||
"Test simple output evaluation."
|
||||
(skip-unless (executable-find ob-elixir-command))
|
||||
(let ((result (ob-elixir--execute "IO.puts(\"hello\")" 'output)))
|
||||
(should (equal "hello" result))))
|
||||
|
||||
(ert-deftest ob-elixir-test-multiline-value ()
|
||||
"Test multiline code value evaluation."
|
||||
(skip-unless (executable-find ob-elixir-command))
|
||||
(let ((result (ob-elixir--execute "x = 10\ny = 20\nx + y" 'value)))
|
||||
(should (equal "30" result))))
|
||||
|
||||
(ert-deftest ob-elixir-test-list-result ()
|
||||
"Test list result."
|
||||
(skip-unless (executable-find ob-elixir-command))
|
||||
(let ((result (ob-elixir--execute "[1, 2, 3]" 'value)))
|
||||
(should (equal "[1, 2, 3]" result))))
|
||||
|
||||
(ert-deftest ob-elixir-test-map-result ()
|
||||
"Test map result."
|
||||
(skip-unless (executable-find ob-elixir-command))
|
||||
(let ((result (ob-elixir--execute "%{a: 1, b: 2}" 'value)))
|
||||
(should (string-match-p "%{a: 1, b: 2}" result))))
|
||||
```
|
||||
|
||||
### Step 8: Test in an org buffer
|
||||
|
||||
Create a test org file `test.org`:
|
||||
|
||||
```org
|
||||
* Test ob-elixir
|
||||
|
||||
** Basic arithmetic (value)
|
||||
|
||||
#+BEGIN_SRC elixir
|
||||
1 + 1
|
||||
#+END_SRC
|
||||
|
||||
** Output test
|
||||
|
||||
#+BEGIN_SRC elixir :results output
|
||||
IO.puts("Hello, World!")
|
||||
#+END_SRC
|
||||
|
||||
** List manipulation
|
||||
|
||||
#+BEGIN_SRC elixir
|
||||
Enum.map([1, 2, 3], fn x -> x * 2 end)
|
||||
#+END_SRC
|
||||
```
|
||||
|
||||
Press `C-c C-c` on each block to test.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `org-babel-execute:elixir` function exists
|
||||
- [ ] Simple expressions evaluate correctly: `1 + 1` returns `2`
|
||||
- [ ] `:results value` captures return value (default)
|
||||
- [ ] `:results output` captures stdout
|
||||
- [ ] Multiline code executes correctly
|
||||
- [ ] Lists and maps are returned in Elixir format
|
||||
- [ ] All tests pass: `make test`
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "Cannot find elixir"
|
||||
|
||||
Ensure Elixir is in PATH:
|
||||
```bash
|
||||
which elixir
|
||||
elixir --version
|
||||
```
|
||||
|
||||
Or set the full path:
|
||||
```elisp
|
||||
(setq ob-elixir-command "/usr/local/bin/elixir")
|
||||
```
|
||||
|
||||
### Results are truncated
|
||||
|
||||
The wrapper uses `limit: :infinity` to prevent truncation. If still truncated, check for very large outputs.
|
||||
|
||||
### ANSI codes in output
|
||||
|
||||
We'll handle this in a later task. For now, output should be clean with the current approach.
|
||||
|
||||
## Files Modified
|
||||
|
||||
- `ob-elixir.el` - Add execution functions
|
||||
- `test/test-ob-elixir.el` - Add execution tests
|
||||
|
||||
## References
|
||||
|
||||
- [docs/03-org-babel-implementation-guide.md](../docs/03-org-babel-implementation-guide.md)
|
||||
- [docs/04-elixir-integration-strategies.md](../docs/04-elixir-integration-strategies.md)
|
||||
334
tasks/03-variable-injection.md
Normal file
334
tasks/03-variable-injection.md
Normal file
@@ -0,0 +1,334 @@
|
||||
# Task 03: Variable Injection
|
||||
|
||||
**Phase**: 1 - Core (MVP)
|
||||
**Priority**: High
|
||||
**Estimated Time**: 1-2 hours
|
||||
**Dependencies**: Task 02 (Basic Execution)
|
||||
|
||||
## Objective
|
||||
|
||||
Implement variable injection so that `:var` header arguments work correctly, allowing data to be passed from org-mode into Elixir code blocks.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Task 02 completed
|
||||
- Basic execution working
|
||||
|
||||
## Background
|
||||
|
||||
Org-babel allows passing variables to code blocks:
|
||||
|
||||
```org
|
||||
#+BEGIN_SRC elixir :var x=5 :var name="Alice"
|
||||
"Hello, #{name}! x = #{x}"
|
||||
#+END_SRC
|
||||
```
|
||||
|
||||
We need to:
|
||||
1. Convert Elisp values to Elixir syntax
|
||||
2. Generate Elixir variable assignment statements
|
||||
3. Prepend these to the code before execution
|
||||
|
||||
## Steps
|
||||
|
||||
### Step 1: Implement Elisp to Elixir conversion
|
||||
|
||||
Add to `ob-elixir.el`:
|
||||
|
||||
```elisp
|
||||
;;; Type Conversion
|
||||
|
||||
(defun ob-elixir--elisp-to-elixir (value)
|
||||
"Convert Elisp VALUE to Elixir literal syntax.
|
||||
|
||||
Handles:
|
||||
- nil -> nil
|
||||
- t -> true
|
||||
- numbers -> numbers
|
||||
- strings -> quoted strings
|
||||
- symbols -> atoms
|
||||
- lists -> Elixir lists
|
||||
- vectors -> tuples"
|
||||
(cond
|
||||
;; nil
|
||||
((null value) "nil")
|
||||
|
||||
;; Boolean true
|
||||
((eq value t) "true")
|
||||
|
||||
;; Numbers
|
||||
((numberp value)
|
||||
(number-to-string value))
|
||||
|
||||
;; Strings
|
||||
((stringp value)
|
||||
(format "\"%s\"" (ob-elixir--escape-string value)))
|
||||
|
||||
;; Symbols become atoms (except special ones)
|
||||
((symbolp value)
|
||||
(let ((name (symbol-name value)))
|
||||
(cond
|
||||
((string= name "hline") ":hline")
|
||||
((string-match-p "^[a-z_][a-zA-Z0-9_]*[?!]?$" name)
|
||||
(format ":%s" name))
|
||||
(t (format ":\"%s\"" name)))))
|
||||
|
||||
;; Vectors become tuples
|
||||
((vectorp value)
|
||||
(format "{%s}"
|
||||
(mapconcat #'ob-elixir--elisp-to-elixir
|
||||
(append value nil) ", ")))
|
||||
|
||||
;; Lists
|
||||
((listp value)
|
||||
(format "[%s]"
|
||||
(mapconcat #'ob-elixir--elisp-to-elixir value ", ")))
|
||||
|
||||
;; Fallback
|
||||
(t (format "%S" value))))
|
||||
```
|
||||
|
||||
### Step 2: Implement string escaping
|
||||
|
||||
```elisp
|
||||
(defun ob-elixir--escape-string (str)
|
||||
"Escape special characters in STR for Elixir string literal."
|
||||
(let ((result str))
|
||||
;; Escape backslashes first
|
||||
(setq result (replace-regexp-in-string "\\\\" "\\\\\\\\" result))
|
||||
;; Escape double quotes
|
||||
(setq result (replace-regexp-in-string "\"" "\\\\\"" result))
|
||||
;; Escape newlines
|
||||
(setq result (replace-regexp-in-string "\n" "\\\\n" result))
|
||||
;; Escape tabs
|
||||
(setq result (replace-regexp-in-string "\t" "\\\\t" result))
|
||||
result))
|
||||
```
|
||||
|
||||
### Step 3: Implement variable assignments function
|
||||
|
||||
```elisp
|
||||
;;; Variable Handling
|
||||
|
||||
(defun org-babel-variable-assignments:elixir (params)
|
||||
"Return list of Elixir statements assigning variables from PARAMS.
|
||||
|
||||
Each statement has the form: var_name = value"
|
||||
(mapcar
|
||||
(lambda (pair)
|
||||
(let ((name (car pair))
|
||||
(value (cdr pair)))
|
||||
(format "%s = %s"
|
||||
(ob-elixir--var-name name)
|
||||
(ob-elixir--elisp-to-elixir value))))
|
||||
(org-babel--get-vars params)))
|
||||
|
||||
(defun ob-elixir--var-name (name)
|
||||
"Convert NAME to a valid Elixir variable name.
|
||||
|
||||
Elixir variables must start with lowercase or underscore."
|
||||
(let ((str (if (symbolp name) (symbol-name name) name)))
|
||||
;; Ensure starts with lowercase or underscore
|
||||
(if (string-match-p "^[a-z_]" str)
|
||||
str
|
||||
(concat "_" str))))
|
||||
```
|
||||
|
||||
### Step 4: Update the execute function
|
||||
|
||||
Modify `org-babel-execute:elixir` to use variable assignments:
|
||||
|
||||
```elisp
|
||||
(defun org-babel-execute:elixir (body params)
|
||||
"Execute a block of Elixir code with org-babel.
|
||||
|
||||
BODY is the Elixir code to execute.
|
||||
PARAMS is an alist of header arguments.
|
||||
|
||||
This function is called by `org-babel-execute-src-block'."
|
||||
(let* ((result-type (cdr (assq :result-type params)))
|
||||
(result-params (cdr (assq :result-params params)))
|
||||
;; Expand body with variable assignments
|
||||
(full-body (org-babel-expand-body:generic
|
||||
body params
|
||||
(org-babel-variable-assignments:elixir params)))
|
||||
(result (ob-elixir--execute full-body result-type)))
|
||||
(org-babel-reassemble-table
|
||||
(org-babel-result-cond result-params
|
||||
result
|
||||
(org-babel-script-escape result))
|
||||
(org-babel-pick-name (cdr (assq :colname-names params))
|
||||
(cdr (assq :colnames params)))
|
||||
(org-babel-pick-name (cdr (assq :rowname-names params))
|
||||
(cdr (assq :rownames params))))))
|
||||
```
|
||||
|
||||
### Step 5: Add tests for type conversion
|
||||
|
||||
Add to `test/test-ob-elixir.el`:
|
||||
|
||||
```elisp
|
||||
;;; Type Conversion Tests
|
||||
|
||||
(ert-deftest ob-elixir-test-convert-nil ()
|
||||
"Test nil conversion."
|
||||
(should (equal "nil" (ob-elixir--elisp-to-elixir nil))))
|
||||
|
||||
(ert-deftest ob-elixir-test-convert-true ()
|
||||
"Test t conversion."
|
||||
(should (equal "true" (ob-elixir--elisp-to-elixir t))))
|
||||
|
||||
(ert-deftest ob-elixir-test-convert-integer ()
|
||||
"Test integer conversion."
|
||||
(should (equal "42" (ob-elixir--elisp-to-elixir 42)))
|
||||
(should (equal "-10" (ob-elixir--elisp-to-elixir -10))))
|
||||
|
||||
(ert-deftest ob-elixir-test-convert-float ()
|
||||
"Test float conversion."
|
||||
(should (equal "3.14" (ob-elixir--elisp-to-elixir 3.14))))
|
||||
|
||||
(ert-deftest ob-elixir-test-convert-string ()
|
||||
"Test string conversion."
|
||||
(should (equal "\"hello\"" (ob-elixir--elisp-to-elixir "hello"))))
|
||||
|
||||
(ert-deftest ob-elixir-test-convert-string-escaping ()
|
||||
"Test string escaping."
|
||||
(should (equal "\"say \\\"hi\\\"\""
|
||||
(ob-elixir--elisp-to-elixir "say \"hi\"")))
|
||||
(should (equal "\"line1\\nline2\""
|
||||
(ob-elixir--elisp-to-elixir "line1\nline2"))))
|
||||
|
||||
(ert-deftest ob-elixir-test-convert-symbol ()
|
||||
"Test symbol conversion to atom."
|
||||
(should (equal ":foo" (ob-elixir--elisp-to-elixir 'foo)))
|
||||
(should (equal ":ok" (ob-elixir--elisp-to-elixir 'ok))))
|
||||
|
||||
(ert-deftest ob-elixir-test-convert-list ()
|
||||
"Test list conversion."
|
||||
(should (equal "[1, 2, 3]" (ob-elixir--elisp-to-elixir '(1 2 3))))
|
||||
(should (equal "[\"a\", \"b\"]" (ob-elixir--elisp-to-elixir '("a" "b")))))
|
||||
|
||||
(ert-deftest ob-elixir-test-convert-nested-list ()
|
||||
"Test nested list conversion."
|
||||
(should (equal "[[1, 2], [3, 4]]"
|
||||
(ob-elixir--elisp-to-elixir '((1 2) (3 4))))))
|
||||
|
||||
(ert-deftest ob-elixir-test-convert-vector ()
|
||||
"Test vector to tuple conversion."
|
||||
(should (equal "{1, 2, 3}" (ob-elixir--elisp-to-elixir [1 2 3]))))
|
||||
```
|
||||
|
||||
### Step 6: Add tests for variable injection
|
||||
|
||||
```elisp
|
||||
;;; Variable Injection Tests
|
||||
|
||||
(ert-deftest ob-elixir-test-variable-assignments ()
|
||||
"Test variable assignment generation."
|
||||
(let ((params '((:var . ("x" . 5))
|
||||
(:var . ("name" . "Alice")))))
|
||||
(let ((assignments (org-babel-variable-assignments:elixir params)))
|
||||
(should (member "x = 5" assignments))
|
||||
(should (member "name = \"Alice\"" assignments)))))
|
||||
|
||||
(ert-deftest ob-elixir-test-var-execution ()
|
||||
"Test code execution with variables."
|
||||
(skip-unless (executable-find ob-elixir-command))
|
||||
(let* ((params '((:var . ("x" . 10))))
|
||||
(var-lines (org-babel-variable-assignments:elixir params))
|
||||
(full-body (concat (mapconcat #'identity var-lines "\n")
|
||||
"\nx * 2")))
|
||||
(should (equal "20" (ob-elixir--execute full-body 'value)))))
|
||||
|
||||
(ert-deftest ob-elixir-test-var-list ()
|
||||
"Test passing list as variable."
|
||||
(skip-unless (executable-find ob-elixir-command))
|
||||
(let* ((params '((:var . ("data" . (1 2 3)))))
|
||||
(var-lines (org-babel-variable-assignments:elixir params))
|
||||
(full-body (concat (mapconcat #'identity var-lines "\n")
|
||||
"\nEnum.sum(data)")))
|
||||
(should (equal "6" (ob-elixir--execute full-body 'value)))))
|
||||
```
|
||||
|
||||
### Step 7: Test in an org buffer
|
||||
|
||||
Update `test.org`:
|
||||
|
||||
```org
|
||||
* Variable Injection Tests
|
||||
|
||||
** Simple variable
|
||||
|
||||
#+BEGIN_SRC elixir :var x=42
|
||||
x * 2
|
||||
#+END_SRC
|
||||
|
||||
#+RESULTS:
|
||||
: 84
|
||||
|
||||
** String variable
|
||||
|
||||
#+BEGIN_SRC elixir :var name="World"
|
||||
"Hello, #{name}!"
|
||||
#+END_SRC
|
||||
|
||||
#+RESULTS:
|
||||
: Hello, World!
|
||||
|
||||
** Multiple variables
|
||||
|
||||
#+BEGIN_SRC elixir :var x=10 :var y=20
|
||||
x + y
|
||||
#+END_SRC
|
||||
|
||||
#+RESULTS:
|
||||
: 30
|
||||
|
||||
** List variable
|
||||
|
||||
#+BEGIN_SRC elixir :var numbers='(1 2 3 4 5)
|
||||
Enum.sum(numbers)
|
||||
#+END_SRC
|
||||
|
||||
#+RESULTS:
|
||||
: 15
|
||||
|
||||
** Table as variable
|
||||
|
||||
#+NAME: my-data
|
||||
| a | 1 |
|
||||
| b | 2 |
|
||||
| c | 3 |
|
||||
|
||||
#+BEGIN_SRC elixir :var data=my-data
|
||||
Enum.map(data, fn [k, v] -> "#{k}=#{v}" end)
|
||||
#+END_SRC
|
||||
```
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `ob-elixir--elisp-to-elixir` correctly converts all Elisp types
|
||||
- [ ] `org-babel-variable-assignments:elixir` generates valid Elixir code
|
||||
- [ ] `:var x=5` works in org blocks
|
||||
- [ ] `:var name="string"` works with string values
|
||||
- [ ] Multiple `:var` arguments work
|
||||
- [ ] Lists and tables can be passed as variables
|
||||
- [ ] All tests pass: `make test`
|
||||
|
||||
## Edge Cases to Consider
|
||||
|
||||
1. **Variable name conflicts**: Elixir variables must start with lowercase
|
||||
2. **Special characters in strings**: Quotes, newlines, backslashes
|
||||
3. **Empty lists**: Should produce `[]`
|
||||
4. **Mixed type lists**: `[1, "two", :three]`
|
||||
5. **hline in tables**: Special symbol for table separators
|
||||
|
||||
## Files Modified
|
||||
|
||||
- `ob-elixir.el` - Add variable handling functions
|
||||
- `test/test-ob-elixir.el` - Add variable tests
|
||||
|
||||
## References
|
||||
|
||||
- [docs/03-org-babel-implementation-guide.md](../docs/03-org-babel-implementation-guide.md) - Variable Handling section
|
||||
- [docs/04-elixir-integration-strategies.md](../docs/04-elixir-integration-strategies.md) - Data Type Conversion section
|
||||
296
tasks/04-error-handling.md
Normal file
296
tasks/04-error-handling.md
Normal file
@@ -0,0 +1,296 @@
|
||||
# Task 04: Error Handling
|
||||
|
||||
**Phase**: 1 - Core (MVP)
|
||||
**Priority**: High
|
||||
**Estimated Time**: 1 hour
|
||||
**Dependencies**: Task 02 (Basic Execution)
|
||||
|
||||
## Objective
|
||||
|
||||
Implement proper error detection and reporting for Elixir code execution, so users get meaningful feedback when their code fails.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Task 02 completed
|
||||
- Basic execution working
|
||||
|
||||
## Background
|
||||
|
||||
Currently, Elixir errors are returned as raw output. We need to:
|
||||
1. Detect when Elixir reports an error
|
||||
2. Extract useful error information
|
||||
3. Present errors clearly to the user
|
||||
4. Optionally signal Emacs error conditions
|
||||
|
||||
## Steps
|
||||
|
||||
### Step 1: Define error patterns
|
||||
|
||||
Add to `ob-elixir.el`:
|
||||
|
||||
```elisp
|
||||
;;; Error Handling
|
||||
|
||||
(defconst ob-elixir--error-regexp
|
||||
"^\\*\\* (\\([A-Za-z.]+Error\\))\\(.*\\)"
|
||||
"Regexp matching Elixir runtime errors.
|
||||
Group 1 is the error type, group 2 is the message.")
|
||||
|
||||
(defconst ob-elixir--compile-error-regexp
|
||||
"^\\*\\* (\\(CompileError\\|TokenMissingError\\|SyntaxError\\))\\(.*\\)"
|
||||
"Regexp matching Elixir compile-time errors.")
|
||||
|
||||
(defconst ob-elixir--warning-regexp
|
||||
"^warning: \\(.*\\)"
|
||||
"Regexp matching Elixir warnings.")
|
||||
```
|
||||
|
||||
### Step 2: Define custom error types
|
||||
|
||||
```elisp
|
||||
(define-error 'ob-elixir-error
|
||||
"Elixir evaluation error")
|
||||
|
||||
(define-error 'ob-elixir-compile-error
|
||||
"Elixir compilation error"
|
||||
'ob-elixir-error)
|
||||
|
||||
(define-error 'ob-elixir-runtime-error
|
||||
"Elixir runtime error"
|
||||
'ob-elixir-error)
|
||||
```
|
||||
|
||||
### Step 3: Implement error detection
|
||||
|
||||
```elisp
|
||||
(defun ob-elixir--detect-error (output)
|
||||
"Check OUTPUT for Elixir errors.
|
||||
|
||||
Returns a plist with :type, :message, and :line if an error is found.
|
||||
Returns nil if no error detected."
|
||||
(cond
|
||||
;; Compile-time error
|
||||
((string-match ob-elixir--compile-error-regexp output)
|
||||
(list :type 'compile
|
||||
:error-type (match-string 1 output)
|
||||
:message (string-trim (match-string 2 output))
|
||||
:full-output output))
|
||||
|
||||
;; Runtime error
|
||||
((string-match ob-elixir--error-regexp output)
|
||||
(list :type 'runtime
|
||||
:error-type (match-string 1 output)
|
||||
:message (string-trim (match-string 2 output))
|
||||
:full-output output))
|
||||
|
||||
;; No error
|
||||
(t nil)))
|
||||
```
|
||||
|
||||
### Step 4: Implement error formatting
|
||||
|
||||
```elisp
|
||||
(defun ob-elixir--format-error (error-info)
|
||||
"Format ERROR-INFO into a user-friendly message."
|
||||
(let ((type (plist-get error-info :type))
|
||||
(error-type (plist-get error-info :error-type))
|
||||
(message (plist-get error-info :message)))
|
||||
(format "Elixir %s: (%s) %s"
|
||||
(if (eq type 'compile) "Compile Error" "Runtime Error")
|
||||
error-type
|
||||
message)))
|
||||
|
||||
(defcustom ob-elixir-signal-errors t
|
||||
"Whether to signal Emacs errors on Elixir execution failure.
|
||||
|
||||
When non-nil, Elixir errors will be signaled as Emacs errors.
|
||||
When nil, errors are returned as the result string."
|
||||
:type 'boolean
|
||||
:group 'ob-elixir)
|
||||
```
|
||||
|
||||
### Step 5: Update the execute function
|
||||
|
||||
Modify `ob-elixir--execute`:
|
||||
|
||||
```elisp
|
||||
(defun ob-elixir--execute (body result-type)
|
||||
"Execute BODY as Elixir code.
|
||||
|
||||
RESULT-TYPE is either `value' or `output'.
|
||||
For `value', wraps code to capture return value.
|
||||
For `output', captures stdout directly.
|
||||
|
||||
Returns the result as a string.
|
||||
May signal `ob-elixir-error' if execution fails and
|
||||
`ob-elixir-signal-errors' is non-nil."
|
||||
(let* ((tmp-file (org-babel-temp-file "ob-elixir-" ".exs"))
|
||||
(code (if (eq result-type 'value)
|
||||
(ob-elixir--wrap-for-value body)
|
||||
body)))
|
||||
(with-temp-file tmp-file
|
||||
(insert code))
|
||||
(let ((result (org-babel-eval
|
||||
(format "%s %s"
|
||||
ob-elixir-command
|
||||
(org-babel-process-file-name tmp-file))
|
||||
"")))
|
||||
(ob-elixir--process-result result))))
|
||||
|
||||
(defun ob-elixir--process-result (result)
|
||||
"Process RESULT from Elixir execution.
|
||||
|
||||
Checks for errors and handles them according to `ob-elixir-signal-errors'.
|
||||
Returns the cleaned result string."
|
||||
(let ((trimmed (string-trim result))
|
||||
(error-info (ob-elixir--detect-error result)))
|
||||
(if error-info
|
||||
(if ob-elixir-signal-errors
|
||||
(signal (if (eq (plist-get error-info :type) 'compile)
|
||||
'ob-elixir-compile-error
|
||||
'ob-elixir-runtime-error)
|
||||
(list (ob-elixir--format-error error-info)))
|
||||
;; Return error as result
|
||||
(plist-get error-info :full-output))
|
||||
;; No error, return trimmed result
|
||||
trimmed)))
|
||||
```
|
||||
|
||||
### Step 6: Handle warnings
|
||||
|
||||
```elisp
|
||||
(defcustom ob-elixir-show-warnings t
|
||||
"Whether to include warnings in output.
|
||||
|
||||
When non-nil, Elixir warnings are included in the result.
|
||||
When nil, warnings are stripped from the output."
|
||||
:type 'boolean
|
||||
:group 'ob-elixir)
|
||||
|
||||
(defun ob-elixir--strip-warnings (output)
|
||||
"Remove warning lines from OUTPUT if configured."
|
||||
(if ob-elixir-show-warnings
|
||||
output
|
||||
(let ((lines (split-string output "\n")))
|
||||
(mapconcat #'identity
|
||||
(cl-remove-if (lambda (line)
|
||||
(string-match-p ob-elixir--warning-regexp line))
|
||||
lines)
|
||||
"\n"))))
|
||||
```
|
||||
|
||||
### Step 7: Add tests
|
||||
|
||||
Add to `test/test-ob-elixir.el`:
|
||||
|
||||
```elisp
|
||||
;;; Error Handling Tests
|
||||
|
||||
(ert-deftest ob-elixir-test-detect-runtime-error ()
|
||||
"Test runtime error detection."
|
||||
(let ((output "** (RuntimeError) something went wrong"))
|
||||
(let ((error-info (ob-elixir--detect-error output)))
|
||||
(should error-info)
|
||||
(should (eq 'runtime (plist-get error-info :type)))
|
||||
(should (equal "RuntimeError" (plist-get error-info :error-type))))))
|
||||
|
||||
(ert-deftest ob-elixir-test-detect-compile-error ()
|
||||
"Test compile error detection."
|
||||
(let ((output "** (CompileError) test.exs:1: undefined function foo/0"))
|
||||
(let ((error-info (ob-elixir--detect-error output)))
|
||||
(should error-info)
|
||||
(should (eq 'compile (plist-get error-info :type)))
|
||||
(should (equal "CompileError" (plist-get error-info :error-type))))))
|
||||
|
||||
(ert-deftest ob-elixir-test-no-error ()
|
||||
"Test that valid output is not detected as error."
|
||||
(should-not (ob-elixir--detect-error "42"))
|
||||
(should-not (ob-elixir--detect-error "[1, 2, 3]"))
|
||||
(should-not (ob-elixir--detect-error "\"hello\"")))
|
||||
|
||||
(ert-deftest ob-elixir-test-error-execution ()
|
||||
"Test that errors are properly handled during execution."
|
||||
(skip-unless (executable-find ob-elixir-command))
|
||||
(let ((ob-elixir-signal-errors nil))
|
||||
(let ((result (ob-elixir--execute "raise \"test error\"" 'value)))
|
||||
(should (string-match-p "RuntimeError" result)))))
|
||||
|
||||
(ert-deftest ob-elixir-test-error-signaling ()
|
||||
"Test that errors are signaled when configured."
|
||||
(skip-unless (executable-find ob-elixir-command))
|
||||
(let ((ob-elixir-signal-errors t))
|
||||
(should-error (ob-elixir--execute "raise \"test error\"" 'value)
|
||||
:type 'ob-elixir-runtime-error)))
|
||||
|
||||
(ert-deftest ob-elixir-test-undefined-function ()
|
||||
"Test handling of undefined function error."
|
||||
(skip-unless (executable-find ob-elixir-command))
|
||||
(let ((ob-elixir-signal-errors nil))
|
||||
(let ((result (ob-elixir--execute "undefined_function()" 'value)))
|
||||
(should (string-match-p "\\(UndefinedFunctionError\\|CompileError\\)" result)))))
|
||||
```
|
||||
|
||||
### Step 8: Test in an org buffer
|
||||
|
||||
Add to `test.org`:
|
||||
|
||||
```org
|
||||
* Error Handling Tests
|
||||
|
||||
** Runtime Error
|
||||
|
||||
#+BEGIN_SRC elixir
|
||||
raise "This is a test error"
|
||||
#+END_SRC
|
||||
|
||||
** Compile Error
|
||||
|
||||
#+BEGIN_SRC elixir
|
||||
def incomplete_function(
|
||||
#+END_SRC
|
||||
|
||||
** Undefined Function
|
||||
|
||||
#+BEGIN_SRC elixir
|
||||
this_function_does_not_exist()
|
||||
#+END_SRC
|
||||
|
||||
** Warning (should still execute)
|
||||
|
||||
#+BEGIN_SRC elixir
|
||||
x = 1
|
||||
y = 2
|
||||
x # y is unused, may generate warning
|
||||
#+END_SRC
|
||||
```
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] Runtime errors are detected (e.g., `raise "error"`)
|
||||
- [ ] Compile errors are detected (e.g., syntax errors)
|
||||
- [ ] Errors are formatted with type and message
|
||||
- [ ] `ob-elixir-signal-errors` controls error behavior
|
||||
- [ ] Warnings are handled according to `ob-elixir-show-warnings`
|
||||
- [ ] Valid output is not mistakenly detected as errors
|
||||
- [ ] All tests pass: `make test`
|
||||
|
||||
## Error Types to Handle
|
||||
|
||||
| Error Type | Example | Detection |
|
||||
|------------|---------|-----------|
|
||||
| RuntimeError | `raise "msg"` | `** (RuntimeError)` |
|
||||
| ArgumentError | Bad function arg | `** (ArgumentError)` |
|
||||
| ArithmeticError | Division by zero | `** (ArithmeticError)` |
|
||||
| CompileError | Syntax error | `** (CompileError)` |
|
||||
| SyntaxError | Invalid syntax | `** (SyntaxError)` |
|
||||
| TokenMissingError | Missing end | `** (TokenMissingError)` |
|
||||
| UndefinedFunctionError | Unknown function | `** (UndefinedFunctionError)` |
|
||||
|
||||
## Files Modified
|
||||
|
||||
- `ob-elixir.el` - Add error handling functions
|
||||
- `test/test-ob-elixir.el` - Add error handling tests
|
||||
|
||||
## References
|
||||
|
||||
- [docs/04-elixir-integration-strategies.md](../docs/04-elixir-integration-strategies.md) - Error Handling section
|
||||
330
tasks/05-result-formatting.md
Normal file
330
tasks/05-result-formatting.md
Normal file
@@ -0,0 +1,330 @@
|
||||
# Task 05: Result Formatting and Table Support
|
||||
|
||||
**Phase**: 1 - Core (MVP)
|
||||
**Priority**: Medium
|
||||
**Estimated Time**: 1-2 hours
|
||||
**Dependencies**: Task 02 (Basic Execution), Task 03 (Variable Injection)
|
||||
|
||||
## Objective
|
||||
|
||||
Implement proper result formatting so Elixir lists become org tables and results are properly parsed back into Elisp data structures.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Task 02 and 03 completed
|
||||
- Basic execution and variables working
|
||||
|
||||
## Background
|
||||
|
||||
Org-babel can display results as:
|
||||
- Scalar values (`:results scalar`)
|
||||
- Tables (`:results table`)
|
||||
- Raw org markup (`:results raw`)
|
||||
- Verbatim (`:results verbatim`)
|
||||
|
||||
When Elixir returns a list like `[[1, 2], [3, 4]]`, org should display it as a table:
|
||||
|
||||
```
|
||||
| 1 | 2 |
|
||||
| 3 | 4 |
|
||||
```
|
||||
|
||||
## Steps
|
||||
|
||||
### Step 1: Implement result parsing
|
||||
|
||||
Add to `ob-elixir.el`:
|
||||
|
||||
```elisp
|
||||
;;; Result Formatting
|
||||
|
||||
(defun ob-elixir--table-or-string (result)
|
||||
"Convert RESULT to Emacs table or string.
|
||||
|
||||
If RESULT looks like a list, parse it into an Elisp list.
|
||||
Otherwise return as string.
|
||||
|
||||
Uses `org-babel-script-escape' for parsing."
|
||||
(let ((trimmed (string-trim result)))
|
||||
(cond
|
||||
;; Empty result
|
||||
((string-empty-p trimmed) nil)
|
||||
|
||||
;; Looks like a list - try to parse
|
||||
((string-match-p "^\\[.*\\]$" trimmed)
|
||||
(condition-case nil
|
||||
(let ((parsed (org-babel-script-escape trimmed)))
|
||||
(ob-elixir--sanitize-table parsed))
|
||||
(error trimmed)))
|
||||
|
||||
;; Looks like a tuple - convert to list first
|
||||
((string-match-p "^{.*}$" trimmed)
|
||||
(condition-case nil
|
||||
(let* ((as-list (replace-regexp-in-string
|
||||
"^{\\(.*\\)}$" "[\\1]" trimmed))
|
||||
(parsed (org-babel-script-escape as-list)))
|
||||
(ob-elixir--sanitize-table parsed))
|
||||
(error trimmed)))
|
||||
|
||||
;; Scalar value
|
||||
(t trimmed))))
|
||||
```
|
||||
|
||||
### Step 2: Implement table sanitization
|
||||
|
||||
```elisp
|
||||
(defvar ob-elixir-nil-to 'hline
|
||||
"Elisp value to use for Elixir nil in table cells.
|
||||
|
||||
When nil appears in an Elixir list that becomes a table,
|
||||
it is replaced with this value. Use `hline' for org table
|
||||
horizontal lines, or nil for empty cells.")
|
||||
|
||||
(defun ob-elixir--sanitize-table (data)
|
||||
"Sanitize DATA for use as an org table.
|
||||
|
||||
Replaces nil values according to `ob-elixir-nil-to'.
|
||||
Ensures consistent structure for table rendering."
|
||||
(cond
|
||||
;; Not a list - return as-is
|
||||
((not (listp data)) data)
|
||||
|
||||
;; Empty list
|
||||
((null data) nil)
|
||||
|
||||
;; List of lists - could be table
|
||||
((and (listp (car data)) (not (null (car data))))
|
||||
(mapcar #'ob-elixir--sanitize-row data))
|
||||
|
||||
;; Simple list - single row
|
||||
(t (ob-elixir--sanitize-row data))))
|
||||
|
||||
(defun ob-elixir--sanitize-row (row)
|
||||
"Sanitize a single ROW for table display."
|
||||
(if (listp row)
|
||||
(mapcar (lambda (cell)
|
||||
(cond
|
||||
((null cell) ob-elixir-nil-to)
|
||||
((eq cell 'nil) ob-elixir-nil-to)
|
||||
(t cell)))
|
||||
row)
|
||||
row))
|
||||
```
|
||||
|
||||
### Step 3: Handle keyword lists and maps
|
||||
|
||||
```elisp
|
||||
(defun ob-elixir--parse-keyword-list (str)
|
||||
"Parse STR as Elixir keyword list into alist.
|
||||
|
||||
Handles format like: [a: 1, b: 2]"
|
||||
(when (string-match "^\\[\\(.*\\)\\]$" str)
|
||||
(let ((content (match-string 1 str)))
|
||||
(when (string-match-p "^[a-z_]+:" content)
|
||||
(let ((pairs '()))
|
||||
(dolist (part (split-string content ", "))
|
||||
(when (string-match "^\\([a-z_]+\\):\\s-*\\(.+\\)$" part)
|
||||
(push (cons (intern (match-string 1 part))
|
||||
(ob-elixir--parse-value (match-string 2 part)))
|
||||
pairs)))
|
||||
(nreverse pairs))))))
|
||||
|
||||
(defun ob-elixir--parse-value (str)
|
||||
"Parse STR as a simple Elixir value."
|
||||
(let ((trimmed (string-trim str)))
|
||||
(cond
|
||||
((string= trimmed "nil") nil)
|
||||
((string= trimmed "true") t)
|
||||
((string= trimmed "false") nil)
|
||||
((string-match-p "^[0-9]+$" trimmed)
|
||||
(string-to-number trimmed))
|
||||
((string-match-p "^[0-9]+\\.[0-9]+$" trimmed)
|
||||
(string-to-number trimmed))
|
||||
((string-match-p "^\".*\"$" trimmed)
|
||||
(substring trimmed 1 -1))
|
||||
((string-match-p "^:.*$" trimmed)
|
||||
(intern (substring trimmed 1)))
|
||||
(t trimmed))))
|
||||
```
|
||||
|
||||
### Step 4: Update the execute function
|
||||
|
||||
Ensure `org-babel-execute:elixir` uses the parsing:
|
||||
|
||||
```elisp
|
||||
(defun org-babel-execute:elixir (body params)
|
||||
"Execute a block of Elixir code with org-babel.
|
||||
|
||||
BODY is the Elixir code to execute.
|
||||
PARAMS is an alist of header arguments."
|
||||
(let* ((result-type (cdr (assq :result-type params)))
|
||||
(result-params (cdr (assq :result-params params)))
|
||||
(full-body (org-babel-expand-body:generic
|
||||
body params
|
||||
(org-babel-variable-assignments:elixir params)))
|
||||
(result (ob-elixir--execute full-body result-type)))
|
||||
(org-babel-reassemble-table
|
||||
(org-babel-result-cond result-params
|
||||
;; For output/scalar/verbatim - return as-is
|
||||
result
|
||||
;; For value - parse into Elisp data
|
||||
(ob-elixir--table-or-string result))
|
||||
(org-babel-pick-name (cdr (assq :colname-names params))
|
||||
(cdr (assq :colnames params)))
|
||||
(org-babel-pick-name (cdr (assq :rowname-names params))
|
||||
(cdr (assq :rownames params))))))
|
||||
```
|
||||
|
||||
### Step 5: Support column names
|
||||
|
||||
```elisp
|
||||
(defun ob-elixir--maybe-add-colnames (result params)
|
||||
"Add column names to RESULT if specified in PARAMS."
|
||||
(let ((colnames (cdr (assq :colnames params))))
|
||||
(if (and colnames (listp result) (listp (car result)))
|
||||
(cons (if (listp colnames) colnames (car result))
|
||||
(if (listp colnames) result (cdr result)))
|
||||
result)))
|
||||
```
|
||||
|
||||
### Step 6: Add tests
|
||||
|
||||
Add to `test/test-ob-elixir.el`:
|
||||
|
||||
```elisp
|
||||
;;; Result Formatting Tests
|
||||
|
||||
(ert-deftest ob-elixir-test-parse-simple-list ()
|
||||
"Test parsing simple list result."
|
||||
(should (equal '(1 2 3) (ob-elixir--table-or-string "[1, 2, 3]"))))
|
||||
|
||||
(ert-deftest ob-elixir-test-parse-nested-list ()
|
||||
"Test parsing nested list (table) result."
|
||||
(should (equal '((1 2) (3 4))
|
||||
(ob-elixir--table-or-string "[[1, 2], [3, 4]]"))))
|
||||
|
||||
(ert-deftest ob-elixir-test-parse-tuple ()
|
||||
"Test parsing tuple result."
|
||||
(should (equal '(1 2 3) (ob-elixir--table-or-string "{1, 2, 3}"))))
|
||||
|
||||
(ert-deftest ob-elixir-test-parse-scalar ()
|
||||
"Test that scalars are returned as strings."
|
||||
(should (equal "42" (ob-elixir--table-or-string "42")))
|
||||
(should (equal ":ok" (ob-elixir--table-or-string ":ok"))))
|
||||
|
||||
(ert-deftest ob-elixir-test-parse-string ()
|
||||
"Test parsing string result."
|
||||
(should (equal "\"hello\"" (ob-elixir--table-or-string "\"hello\""))))
|
||||
|
||||
(ert-deftest ob-elixir-test-sanitize-table-nil ()
|
||||
"Test that nil values are sanitized in tables."
|
||||
(let ((ob-elixir-nil-to 'hline))
|
||||
(should (equal '((1 hline) (hline 2))
|
||||
(ob-elixir--sanitize-table '((1 nil) (nil 2)))))))
|
||||
|
||||
(ert-deftest ob-elixir-test-execution-returns-table ()
|
||||
"Test that list results become tables."
|
||||
(skip-unless (executable-find ob-elixir-command))
|
||||
(let ((result (ob-elixir--table-or-string
|
||||
(ob-elixir--execute "[[1, 2], [3, 4]]" 'value))))
|
||||
(should (equal '((1 2) (3 4)) result))))
|
||||
|
||||
(ert-deftest ob-elixir-test-mixed-list ()
|
||||
"Test parsing mixed-type list."
|
||||
(skip-unless (executable-find ob-elixir-command))
|
||||
(let ((result (ob-elixir--table-or-string
|
||||
(ob-elixir--execute "[1, \"two\", :three]" 'value))))
|
||||
(should (listp result))
|
||||
(should (= 3 (length result)))))
|
||||
```
|
||||
|
||||
### Step 7: Test in an org buffer
|
||||
|
||||
Add to `test.org`:
|
||||
|
||||
```org
|
||||
* Result Formatting Tests
|
||||
|
||||
** Simple list as table row
|
||||
|
||||
#+BEGIN_SRC elixir
|
||||
[1, 2, 3, 4, 5]
|
||||
#+END_SRC
|
||||
|
||||
#+RESULTS:
|
||||
| 1 | 2 | 3 | 4 | 5 |
|
||||
|
||||
** Nested list as table
|
||||
|
||||
#+BEGIN_SRC elixir
|
||||
[
|
||||
["Alice", 30],
|
||||
["Bob", 25],
|
||||
["Charlie", 35]
|
||||
]
|
||||
#+END_SRC
|
||||
|
||||
#+RESULTS:
|
||||
| Alice | 30 |
|
||||
| Bob | 25 |
|
||||
| Charlie | 35 |
|
||||
|
||||
** Map result (verbatim)
|
||||
|
||||
#+BEGIN_SRC elixir :results verbatim
|
||||
%{name: "Alice", age: 30}
|
||||
#+END_SRC
|
||||
|
||||
#+RESULTS:
|
||||
: %{age: 30, name: "Alice"}
|
||||
|
||||
** Enum operations returning lists
|
||||
|
||||
#+BEGIN_SRC elixir
|
||||
Enum.map(1..5, fn x -> [x, x * x] end)
|
||||
#+END_SRC
|
||||
|
||||
#+RESULTS:
|
||||
| 1 | 1 |
|
||||
| 2 | 4 |
|
||||
| 3 | 9 |
|
||||
| 4 | 16 |
|
||||
| 5 | 25 |
|
||||
|
||||
** Tuple result
|
||||
|
||||
#+BEGIN_SRC elixir
|
||||
{:ok, "success", 123}
|
||||
#+END_SRC
|
||||
```
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] Simple lists `[1, 2, 3]` become table rows
|
||||
- [ ] Nested lists `[[1, 2], [3, 4]]` become tables
|
||||
- [ ] Tuples are handled (converted to lists)
|
||||
- [ ] Scalar values remain as strings
|
||||
- [ ] `:results verbatim` bypasses table conversion
|
||||
- [ ] nil values in tables are handled according to config
|
||||
- [ ] All tests pass: `make test`
|
||||
|
||||
## Result Format Reference
|
||||
|
||||
| Elixir Value | Org Display |
|
||||
|--------------|-------------|
|
||||
| `[1, 2, 3]` | `\| 1 \| 2 \| 3 \|` |
|
||||
| `[[1], [2]]` | Multi-row table |
|
||||
| `{:ok, 1}` | `\| ok \| 1 \|` |
|
||||
| `42` | `: 42` |
|
||||
| `"hello"` | `: "hello"` |
|
||||
| `%{a: 1}` | `: %{a: 1}` |
|
||||
|
||||
## Files Modified
|
||||
|
||||
- `ob-elixir.el` - Add result formatting functions
|
||||
- `test/test-ob-elixir.el` - Add formatting tests
|
||||
|
||||
## References
|
||||
|
||||
- [docs/03-org-babel-implementation-guide.md](../docs/03-org-babel-implementation-guide.md) - Result Handling section
|
||||
- [Org Manual - Results of Evaluation](https://orgmode.org/manual/Results-of-Evaluation.html)
|
||||
623
tasks/06-test-suite.md
Normal file
623
tasks/06-test-suite.md
Normal file
@@ -0,0 +1,623 @@
|
||||
# Task 06: Comprehensive Test Suite
|
||||
|
||||
**Phase**: 1 - Core (MVP)
|
||||
**Priority**: High
|
||||
**Estimated Time**: 2-3 hours
|
||||
**Dependencies**: Tasks 01-05 (All Core Tasks)
|
||||
|
||||
## Objective
|
||||
|
||||
Create a comprehensive test suite that covers all implemented functionality, including unit tests, integration tests, and org-buffer tests.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- All Phase 1 tasks completed
|
||||
- Basic functionality working
|
||||
|
||||
## Background
|
||||
|
||||
A good test suite should:
|
||||
1. Test each function in isolation (unit tests)
|
||||
2. Test the integration with org-mode (integration tests)
|
||||
3. Be runnable in CI/CD (batch mode tests)
|
||||
4. Provide good coverage of edge cases
|
||||
|
||||
## Steps
|
||||
|
||||
### Step 1: Organize test file structure
|
||||
|
||||
Create the test directory structure:
|
||||
|
||||
```
|
||||
test/
|
||||
├── test-ob-elixir.el # Main test file (loads all)
|
||||
├── test-ob-elixir-core.el # Core execution tests
|
||||
├── test-ob-elixir-vars.el # Variable handling tests
|
||||
├── test-ob-elixir-results.el # Result formatting tests
|
||||
├── test-ob-elixir-errors.el # Error handling tests
|
||||
└── test-ob-elixir-org.el # Org integration tests
|
||||
```
|
||||
|
||||
### Step 2: Create main test file
|
||||
|
||||
`test/test-ob-elixir.el`:
|
||||
|
||||
```elisp
|
||||
;;; test-ob-elixir.el --- Tests for ob-elixir -*- lexical-binding: t; -*-
|
||||
|
||||
;;; Commentary:
|
||||
|
||||
;; Main test file that loads all test modules.
|
||||
;; Run with: make test
|
||||
;; Or: emacs -batch -l ert -l test/test-ob-elixir.el -f ert-run-tests-batch-and-exit
|
||||
|
||||
;;; Code:
|
||||
|
||||
(require 'ert)
|
||||
|
||||
;; Add source directory to load path
|
||||
(let ((dir (file-name-directory (or load-file-name buffer-file-name))))
|
||||
(add-to-list 'load-path (expand-file-name ".." dir))
|
||||
(add-to-list 'load-path dir))
|
||||
|
||||
;; Load the package
|
||||
(require 'ob-elixir)
|
||||
|
||||
;; Load test modules
|
||||
(require 'test-ob-elixir-core)
|
||||
(require 'test-ob-elixir-vars)
|
||||
(require 'test-ob-elixir-results)
|
||||
(require 'test-ob-elixir-errors)
|
||||
(require 'test-ob-elixir-org)
|
||||
|
||||
;;; Test Helpers
|
||||
|
||||
(defvar ob-elixir-test--elixir-available
|
||||
(executable-find ob-elixir-command)
|
||||
"Non-nil if Elixir is available for testing.")
|
||||
|
||||
(defmacro ob-elixir-test-with-elixir (&rest body)
|
||||
"Execute BODY only if Elixir is available."
|
||||
`(if ob-elixir-test--elixir-available
|
||||
(progn ,@body)
|
||||
(ert-skip "Elixir not available")))
|
||||
|
||||
(defmacro ob-elixir-test-with-temp-org-buffer (&rest body)
|
||||
"Execute BODY in a temporary org-mode buffer."
|
||||
`(with-temp-buffer
|
||||
(org-mode)
|
||||
(ob-elixir--ensure-org-babel-loaded)
|
||||
,@body))
|
||||
|
||||
(defun ob-elixir--ensure-org-babel-loaded ()
|
||||
"Ensure org-babel is loaded with Elixir support."
|
||||
(require 'org)
|
||||
(require 'ob)
|
||||
(org-babel-do-load-languages
|
||||
'org-babel-load-languages
|
||||
'((elixir . t))))
|
||||
|
||||
;;; Smoke Test
|
||||
|
||||
(ert-deftest ob-elixir-test-smoke ()
|
||||
"Basic smoke test - package loads and Elixir is available."
|
||||
(should (featurep 'ob-elixir))
|
||||
(should (fboundp 'org-babel-execute:elixir))
|
||||
(should (boundp 'org-babel-default-header-args:elixir)))
|
||||
|
||||
(provide 'test-ob-elixir)
|
||||
;;; test-ob-elixir.el ends here
|
||||
```
|
||||
|
||||
### Step 3: Create core execution tests
|
||||
|
||||
`test/test-ob-elixir-core.el`:
|
||||
|
||||
```elisp
|
||||
;;; test-ob-elixir-core.el --- Core execution tests -*- lexical-binding: t; -*-
|
||||
|
||||
;;; Code:
|
||||
|
||||
(require 'ert)
|
||||
(require 'ob-elixir)
|
||||
|
||||
;;; Command Tests
|
||||
|
||||
(ert-deftest ob-elixir-test-command-exists ()
|
||||
"Test that the Elixir command is configured."
|
||||
(should (stringp ob-elixir-command))
|
||||
(should (not (string-empty-p ob-elixir-command))))
|
||||
|
||||
(ert-deftest ob-elixir-test-command-executable ()
|
||||
"Test that the Elixir command is executable."
|
||||
(skip-unless (executable-find ob-elixir-command))
|
||||
(should (executable-find ob-elixir-command)))
|
||||
|
||||
;;; Basic Execution Tests
|
||||
|
||||
(ert-deftest ob-elixir-test-execute-simple-value ()
|
||||
"Test simple value evaluation."
|
||||
(skip-unless (executable-find ob-elixir-command))
|
||||
(should (equal "2" (ob-elixir--execute "1 + 1" 'value))))
|
||||
|
||||
(ert-deftest ob-elixir-test-execute-simple-output ()
|
||||
"Test simple output evaluation."
|
||||
(skip-unless (executable-find ob-elixir-command))
|
||||
(should (equal "hello" (ob-elixir--execute "IO.puts(\"hello\")" 'output))))
|
||||
|
||||
(ert-deftest ob-elixir-test-execute-multiline ()
|
||||
"Test multiline code execution."
|
||||
(skip-unless (executable-find ob-elixir-command))
|
||||
(let ((code "x = 10\ny = 20\nx + y"))
|
||||
(should (equal "30" (ob-elixir--execute code 'value)))))
|
||||
|
||||
(ert-deftest ob-elixir-test-execute-function-def ()
|
||||
"Test function definition and call."
|
||||
(skip-unless (executable-find ob-elixir-command))
|
||||
(let ((code "
|
||||
defmodule Test do
|
||||
def double(x), do: x * 2
|
||||
end
|
||||
Test.double(21)"))
|
||||
(should (equal "42" (ob-elixir--execute code 'value)))))
|
||||
|
||||
(ert-deftest ob-elixir-test-execute-enum ()
|
||||
"Test Enum module usage."
|
||||
(skip-unless (executable-find ob-elixir-command))
|
||||
(should (equal "15"
|
||||
(ob-elixir--execute "Enum.sum([1, 2, 3, 4, 5])" 'value))))
|
||||
|
||||
(ert-deftest ob-elixir-test-execute-pipe ()
|
||||
"Test pipe operator."
|
||||
(skip-unless (executable-find ob-elixir-command))
|
||||
(let ((code "[1, 2, 3] |> Enum.map(&(&1 * 2)) |> Enum.sum()"))
|
||||
(should (equal "12" (ob-elixir--execute code 'value)))))
|
||||
|
||||
;;; Data Type Tests
|
||||
|
||||
(ert-deftest ob-elixir-test-execute-list ()
|
||||
"Test list result."
|
||||
(skip-unless (executable-find ob-elixir-command))
|
||||
(should (equal "[1, 2, 3]" (ob-elixir--execute "[1, 2, 3]" 'value))))
|
||||
|
||||
(ert-deftest ob-elixir-test-execute-tuple ()
|
||||
"Test tuple result."
|
||||
(skip-unless (executable-find ob-elixir-command))
|
||||
(should (equal "{:ok, 42}" (ob-elixir--execute "{:ok, 42}" 'value))))
|
||||
|
||||
(ert-deftest ob-elixir-test-execute-map ()
|
||||
"Test map result."
|
||||
(skip-unless (executable-find ob-elixir-command))
|
||||
(let ((result (ob-elixir--execute "%{a: 1, b: 2}" 'value)))
|
||||
(should (string-match-p "%{" result))
|
||||
(should (string-match-p "a:" result))
|
||||
(should (string-match-p "b:" result))))
|
||||
|
||||
(ert-deftest ob-elixir-test-execute-string ()
|
||||
"Test string result."
|
||||
(skip-unless (executable-find ob-elixir-command))
|
||||
(should (equal "\"hello world\""
|
||||
(ob-elixir--execute "\"hello world\"" 'value))))
|
||||
|
||||
(ert-deftest ob-elixir-test-execute-atom ()
|
||||
"Test atom result."
|
||||
(skip-unless (executable-find ob-elixir-command))
|
||||
(should (equal ":ok" (ob-elixir--execute ":ok" 'value))))
|
||||
|
||||
(ert-deftest ob-elixir-test-execute-boolean ()
|
||||
"Test boolean result."
|
||||
(skip-unless (executable-find ob-elixir-command))
|
||||
(should (equal "true" (ob-elixir--execute "true" 'value)))
|
||||
(should (equal "false" (ob-elixir--execute "false" 'value))))
|
||||
|
||||
(ert-deftest ob-elixir-test-execute-nil ()
|
||||
"Test nil result."
|
||||
(skip-unless (executable-find ob-elixir-command))
|
||||
(should (equal "nil" (ob-elixir--execute "nil" 'value))))
|
||||
|
||||
;;; Wrapper Tests
|
||||
|
||||
(ert-deftest ob-elixir-test-wrap-for-value ()
|
||||
"Test value wrapper generation."
|
||||
(let ((wrapped (ob-elixir--wrap-for-value "1 + 1")))
|
||||
(should (string-match-p "result = " wrapped))
|
||||
(should (string-match-p "IO\\.puts" wrapped))
|
||||
(should (string-match-p "inspect" wrapped))))
|
||||
|
||||
(provide 'test-ob-elixir-core)
|
||||
;;; test-ob-elixir-core.el ends here
|
||||
```
|
||||
|
||||
### Step 4: Create variable tests
|
||||
|
||||
`test/test-ob-elixir-vars.el`:
|
||||
|
||||
```elisp
|
||||
;;; test-ob-elixir-vars.el --- Variable handling tests -*- lexical-binding: t; -*-
|
||||
|
||||
;;; Code:
|
||||
|
||||
(require 'ert)
|
||||
(require 'ob-elixir)
|
||||
|
||||
;;; Type Conversion Tests
|
||||
|
||||
(ert-deftest ob-elixir-test-convert-nil ()
|
||||
(should (equal "nil" (ob-elixir--elisp-to-elixir nil))))
|
||||
|
||||
(ert-deftest ob-elixir-test-convert-true ()
|
||||
(should (equal "true" (ob-elixir--elisp-to-elixir t))))
|
||||
|
||||
(ert-deftest ob-elixir-test-convert-integer ()
|
||||
(should (equal "42" (ob-elixir--elisp-to-elixir 42)))
|
||||
(should (equal "-10" (ob-elixir--elisp-to-elixir -10)))
|
||||
(should (equal "0" (ob-elixir--elisp-to-elixir 0))))
|
||||
|
||||
(ert-deftest ob-elixir-test-convert-float ()
|
||||
(should (equal "3.14" (ob-elixir--elisp-to-elixir 3.14)))
|
||||
(should (equal "-2.5" (ob-elixir--elisp-to-elixir -2.5))))
|
||||
|
||||
(ert-deftest ob-elixir-test-convert-string ()
|
||||
(should (equal "\"hello\"" (ob-elixir--elisp-to-elixir "hello")))
|
||||
(should (equal "\"\"" (ob-elixir--elisp-to-elixir ""))))
|
||||
|
||||
(ert-deftest ob-elixir-test-convert-string-escaping ()
|
||||
(should (equal "\"say \\\"hi\\\"\""
|
||||
(ob-elixir--elisp-to-elixir "say \"hi\"")))
|
||||
(should (equal "\"line1\\nline2\""
|
||||
(ob-elixir--elisp-to-elixir "line1\nline2")))
|
||||
(should (equal "\"tab\\there\""
|
||||
(ob-elixir--elisp-to-elixir "tab\there"))))
|
||||
|
||||
(ert-deftest ob-elixir-test-convert-symbol ()
|
||||
(should (equal ":foo" (ob-elixir--elisp-to-elixir 'foo)))
|
||||
(should (equal ":ok" (ob-elixir--elisp-to-elixir 'ok)))
|
||||
(should (equal ":error" (ob-elixir--elisp-to-elixir 'error))))
|
||||
|
||||
(ert-deftest ob-elixir-test-convert-list ()
|
||||
(should (equal "[1, 2, 3]" (ob-elixir--elisp-to-elixir '(1 2 3))))
|
||||
(should (equal "[]" (ob-elixir--elisp-to-elixir '())))
|
||||
(should (equal "[\"a\", \"b\"]" (ob-elixir--elisp-to-elixir '("a" "b")))))
|
||||
|
||||
(ert-deftest ob-elixir-test-convert-nested-list ()
|
||||
(should (equal "[[1, 2], [3, 4]]"
|
||||
(ob-elixir--elisp-to-elixir '((1 2) (3 4))))))
|
||||
|
||||
(ert-deftest ob-elixir-test-convert-vector ()
|
||||
(should (equal "{1, 2, 3}" (ob-elixir--elisp-to-elixir [1 2 3]))))
|
||||
|
||||
(ert-deftest ob-elixir-test-convert-mixed ()
|
||||
(should (equal "[1, \"two\", :three]"
|
||||
(ob-elixir--elisp-to-elixir '(1 "two" three)))))
|
||||
|
||||
;;; Variable Assignment Tests
|
||||
|
||||
(ert-deftest ob-elixir-test-var-assignments-single ()
|
||||
(let ((params '((:var . ("x" . 5)))))
|
||||
(should (equal '("x = 5")
|
||||
(org-babel-variable-assignments:elixir params)))))
|
||||
|
||||
(ert-deftest ob-elixir-test-var-assignments-multiple ()
|
||||
(let ((params '((:var . ("x" . 5))
|
||||
(:var . ("y" . 10)))))
|
||||
(let ((assignments (org-babel-variable-assignments:elixir params)))
|
||||
(should (= 2 (length assignments)))
|
||||
(should (member "x = 5" assignments))
|
||||
(should (member "y = 10" assignments)))))
|
||||
|
||||
(ert-deftest ob-elixir-test-var-assignments-string ()
|
||||
(let ((params '((:var . ("name" . "Alice")))))
|
||||
(should (equal '("name = \"Alice\"")
|
||||
(org-babel-variable-assignments:elixir params)))))
|
||||
|
||||
(ert-deftest ob-elixir-test-var-assignments-list ()
|
||||
(let ((params '((:var . ("data" . (1 2 3))))))
|
||||
(should (equal '("data = [1, 2, 3]")
|
||||
(org-babel-variable-assignments:elixir params)))))
|
||||
|
||||
;;; Execution with Variables
|
||||
|
||||
(ert-deftest ob-elixir-test-execute-with-var ()
|
||||
(skip-unless (executable-find ob-elixir-command))
|
||||
(let* ((params '((:var . ("x" . 10))))
|
||||
(var-lines (org-babel-variable-assignments:elixir params))
|
||||
(full-body (concat (mapconcat #'identity var-lines "\n")
|
||||
"\nx * 2")))
|
||||
(should (equal "20" (ob-elixir--execute full-body 'value)))))
|
||||
|
||||
(ert-deftest ob-elixir-test-execute-with-list-var ()
|
||||
(skip-unless (executable-find ob-elixir-command))
|
||||
(let* ((params '((:var . ("nums" . (1 2 3 4 5)))))
|
||||
(var-lines (org-babel-variable-assignments:elixir params))
|
||||
(full-body (concat (mapconcat #'identity var-lines "\n")
|
||||
"\nEnum.sum(nums)")))
|
||||
(should (equal "15" (ob-elixir--execute full-body 'value)))))
|
||||
|
||||
(ert-deftest ob-elixir-test-execute-with-string-var ()
|
||||
(skip-unless (executable-find ob-elixir-command))
|
||||
(let* ((params '((:var . ("name" . "World"))))
|
||||
(var-lines (org-babel-variable-assignments:elixir params))
|
||||
(full-body (concat (mapconcat #'identity var-lines "\n")
|
||||
"\n\"Hello, #{name}!\"")))
|
||||
(should (equal "\"Hello, World!\""
|
||||
(ob-elixir--execute full-body 'value)))))
|
||||
|
||||
(provide 'test-ob-elixir-vars)
|
||||
;;; test-ob-elixir-vars.el ends here
|
||||
```
|
||||
|
||||
### Step 5: Create result formatting tests
|
||||
|
||||
`test/test-ob-elixir-results.el`:
|
||||
|
||||
```elisp
|
||||
;;; test-ob-elixir-results.el --- Result formatting tests -*- lexical-binding: t; -*-
|
||||
|
||||
;;; Code:
|
||||
|
||||
(require 'ert)
|
||||
(require 'ob-elixir)
|
||||
|
||||
;;; Parsing Tests
|
||||
|
||||
(ert-deftest ob-elixir-test-parse-simple-list ()
|
||||
(should (equal '(1 2 3) (ob-elixir--table-or-string "[1, 2, 3]"))))
|
||||
|
||||
(ert-deftest ob-elixir-test-parse-nested-list ()
|
||||
(should (equal '((1 2) (3 4))
|
||||
(ob-elixir--table-or-string "[[1, 2], [3, 4]]"))))
|
||||
|
||||
(ert-deftest ob-elixir-test-parse-empty-list ()
|
||||
(should (equal '() (ob-elixir--table-or-string "[]"))))
|
||||
|
||||
(ert-deftest ob-elixir-test-parse-tuple ()
|
||||
(should (equal '(1 2 3) (ob-elixir--table-or-string "{1, 2, 3}"))))
|
||||
|
||||
(ert-deftest ob-elixir-test-parse-scalar-number ()
|
||||
(should (equal "42" (ob-elixir--table-or-string "42"))))
|
||||
|
||||
(ert-deftest ob-elixir-test-parse-scalar-atom ()
|
||||
(should (equal ":ok" (ob-elixir--table-or-string ":ok"))))
|
||||
|
||||
(ert-deftest ob-elixir-test-parse-scalar-string ()
|
||||
(should (equal "\"hello\"" (ob-elixir--table-or-string "\"hello\""))))
|
||||
|
||||
(ert-deftest ob-elixir-test-parse-empty ()
|
||||
(should (null (ob-elixir--table-or-string "")))
|
||||
(should (null (ob-elixir--table-or-string " "))))
|
||||
|
||||
;;; Table Sanitization Tests
|
||||
|
||||
(ert-deftest ob-elixir-test-sanitize-nil-values ()
|
||||
(let ((ob-elixir-nil-to 'hline))
|
||||
(should (equal '((1 hline) (hline 2))
|
||||
(ob-elixir--sanitize-table '((1 nil) (nil 2)))))))
|
||||
|
||||
(ert-deftest ob-elixir-test-sanitize-nested ()
|
||||
(let ((ob-elixir-nil-to 'hline))
|
||||
(should (equal '((1 2) (3 4))
|
||||
(ob-elixir--sanitize-table '((1 2) (3 4)))))))
|
||||
|
||||
(ert-deftest ob-elixir-test-sanitize-simple ()
|
||||
(should (equal '(1 2 3)
|
||||
(ob-elixir--sanitize-table '(1 2 3)))))
|
||||
|
||||
;;; Integration Tests
|
||||
|
||||
(ert-deftest ob-elixir-test-full-result-list ()
|
||||
(skip-unless (executable-find ob-elixir-command))
|
||||
(let ((result (ob-elixir--table-or-string
|
||||
(ob-elixir--execute "[1, 2, 3]" 'value))))
|
||||
(should (equal '(1 2 3) result))))
|
||||
|
||||
(ert-deftest ob-elixir-test-full-result-table ()
|
||||
(skip-unless (executable-find ob-elixir-command))
|
||||
(let ((result (ob-elixir--table-or-string
|
||||
(ob-elixir--execute "[[1, 2], [3, 4]]" 'value))))
|
||||
(should (equal '((1 2) (3 4)) result))))
|
||||
|
||||
(provide 'test-ob-elixir-results)
|
||||
;;; test-ob-elixir-results.el ends here
|
||||
```
|
||||
|
||||
### Step 6: Create error handling tests
|
||||
|
||||
`test/test-ob-elixir-errors.el`:
|
||||
|
||||
```elisp
|
||||
;;; test-ob-elixir-errors.el --- Error handling tests -*- lexical-binding: t; -*-
|
||||
|
||||
;;; Code:
|
||||
|
||||
(require 'ert)
|
||||
(require 'ob-elixir)
|
||||
|
||||
;;; Error Detection Tests
|
||||
|
||||
(ert-deftest ob-elixir-test-detect-runtime-error ()
|
||||
(let ((output "** (RuntimeError) something went wrong"))
|
||||
(should (ob-elixir--detect-error output))))
|
||||
|
||||
(ert-deftest ob-elixir-test-detect-compile-error ()
|
||||
(let ((output "** (CompileError) test.exs:1: undefined function"))
|
||||
(should (ob-elixir--detect-error output))))
|
||||
|
||||
(ert-deftest ob-elixir-test-detect-no-error ()
|
||||
(should-not (ob-elixir--detect-error "42"))
|
||||
(should-not (ob-elixir--detect-error "[1, 2, 3]"))
|
||||
(should-not (ob-elixir--detect-error ":ok")))
|
||||
|
||||
(ert-deftest ob-elixir-test-error-type-runtime ()
|
||||
(let* ((output "** (RuntimeError) test error")
|
||||
(info (ob-elixir--detect-error output)))
|
||||
(should (eq 'runtime (plist-get info :type)))))
|
||||
|
||||
(ert-deftest ob-elixir-test-error-type-compile ()
|
||||
(let* ((output "** (CompileError) syntax error")
|
||||
(info (ob-elixir--detect-error output)))
|
||||
(should (eq 'compile (plist-get info :type)))))
|
||||
|
||||
;;; Error Execution Tests
|
||||
|
||||
(ert-deftest ob-elixir-test-runtime-error-no-signal ()
|
||||
(skip-unless (executable-find ob-elixir-command))
|
||||
(let ((ob-elixir-signal-errors nil))
|
||||
(let ((result (ob-elixir--execute "raise \"test\"" 'value)))
|
||||
(should (string-match-p "RuntimeError" result)))))
|
||||
|
||||
(ert-deftest ob-elixir-test-runtime-error-signal ()
|
||||
(skip-unless (executable-find ob-elixir-command))
|
||||
(let ((ob-elixir-signal-errors t))
|
||||
(should-error (ob-elixir--execute "raise \"test\"" 'value)
|
||||
:type 'ob-elixir-runtime-error)))
|
||||
|
||||
(ert-deftest ob-elixir-test-compile-error ()
|
||||
(skip-unless (executable-find ob-elixir-command))
|
||||
(let ((ob-elixir-signal-errors nil))
|
||||
(let ((result (ob-elixir--execute "def incomplete(" 'value)))
|
||||
(should (string-match-p "\\(SyntaxError\\|TokenMissingError\\)" result)))))
|
||||
|
||||
(provide 'test-ob-elixir-errors)
|
||||
;;; test-ob-elixir-errors.el ends here
|
||||
```
|
||||
|
||||
### Step 7: Create org integration tests
|
||||
|
||||
`test/test-ob-elixir-org.el`:
|
||||
|
||||
```elisp
|
||||
;;; test-ob-elixir-org.el --- Org integration tests -*- lexical-binding: t; -*-
|
||||
|
||||
;;; Code:
|
||||
|
||||
(require 'ert)
|
||||
(require 'org)
|
||||
(require 'ob)
|
||||
(require 'ob-elixir)
|
||||
|
||||
;;; Helper Functions
|
||||
|
||||
(defun ob-elixir-test--execute-src-block (code &optional header-args)
|
||||
"Execute CODE as an Elixir src block with HEADER-ARGS."
|
||||
(with-temp-buffer
|
||||
(org-mode)
|
||||
(insert (format "#+BEGIN_SRC elixir%s\n%s\n#+END_SRC"
|
||||
(if header-args (concat " " header-args) "")
|
||||
code))
|
||||
(goto-char (point-min))
|
||||
(forward-line 1)
|
||||
(org-babel-execute-src-block)))
|
||||
|
||||
;;; Basic Org Tests
|
||||
|
||||
(ert-deftest ob-elixir-test-org-simple ()
|
||||
(skip-unless (executable-find ob-elixir-command))
|
||||
(should (equal "2" (ob-elixir-test--execute-src-block "1 + 1"))))
|
||||
|
||||
(ert-deftest ob-elixir-test-org-with-var ()
|
||||
(skip-unless (executable-find ob-elixir-command))
|
||||
(should (equal "20" (ob-elixir-test--execute-src-block "x * 2" ":var x=10"))))
|
||||
|
||||
(ert-deftest ob-elixir-test-org-results-output ()
|
||||
(skip-unless (executable-find ob-elixir-command))
|
||||
(should (equal "hello"
|
||||
(ob-elixir-test--execute-src-block
|
||||
"IO.puts(\"hello\")"
|
||||
":results output"))))
|
||||
|
||||
(ert-deftest ob-elixir-test-org-results-value ()
|
||||
(skip-unless (executable-find ob-elixir-command))
|
||||
(should (equal '(1 2 3)
|
||||
(ob-elixir-test--execute-src-block
|
||||
"[1, 2, 3]"
|
||||
":results value"))))
|
||||
|
||||
(ert-deftest ob-elixir-test-org-results-verbatim ()
|
||||
(skip-unless (executable-find ob-elixir-command))
|
||||
(should (stringp (ob-elixir-test--execute-src-block
|
||||
"[1, 2, 3]"
|
||||
":results verbatim"))))
|
||||
|
||||
;;; Table Tests
|
||||
|
||||
(ert-deftest ob-elixir-test-org-table-result ()
|
||||
(skip-unless (executable-find ob-elixir-command))
|
||||
(let ((result (ob-elixir-test--execute-src-block "[[1, 2], [3, 4]]")))
|
||||
(should (equal '((1 2) (3 4)) result))))
|
||||
|
||||
(provide 'test-ob-elixir-org)
|
||||
;;; test-ob-elixir-org.el ends here
|
||||
```
|
||||
|
||||
### Step 8: Update Makefile
|
||||
|
||||
```makefile
|
||||
EMACS ?= emacs
|
||||
BATCH = $(EMACS) -Q -batch -L . -L test
|
||||
|
||||
.PHONY: all compile test test-unit test-integration lint clean
|
||||
|
||||
all: compile test
|
||||
|
||||
compile:
|
||||
$(BATCH) -f batch-byte-compile ob-elixir.el
|
||||
|
||||
test: test-unit
|
||||
|
||||
test-unit:
|
||||
$(BATCH) -l ert \
|
||||
-l test/test-ob-elixir.el \
|
||||
-f ert-run-tests-batch-and-exit
|
||||
|
||||
test-integration:
|
||||
$(BATCH) -l ert \
|
||||
-l org -l ob \
|
||||
-l test/test-ob-elixir.el \
|
||||
-f ert-run-tests-batch-and-exit
|
||||
|
||||
lint:
|
||||
$(BATCH) --eval "(require 'package)" \
|
||||
--eval "(package-initialize)" \
|
||||
--eval "(unless (package-installed-p 'package-lint) \
|
||||
(package-refresh-contents) \
|
||||
(package-install 'package-lint))" \
|
||||
-l package-lint \
|
||||
-f package-lint-batch-and-exit ob-elixir.el
|
||||
|
||||
clean:
|
||||
rm -f *.elc test/*.elc
|
||||
```
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] All test files created and organized
|
||||
- [ ] `make test` runs all tests
|
||||
- [ ] Tests cover core execution, variables, results, and errors
|
||||
- [ ] Org integration tests work
|
||||
- [ ] Tests can run in CI (batch mode)
|
||||
- [ ] Test coverage is comprehensive (major code paths)
|
||||
|
||||
## Test Coverage Goals
|
||||
|
||||
| Component | Tests | Coverage |
|
||||
|-----------|-------|----------|
|
||||
| Type conversion | 12+ tests | All Elisp types |
|
||||
| Execution | 10+ tests | Value/output, types |
|
||||
| Variables | 8+ tests | All var scenarios |
|
||||
| Results | 8+ tests | Parsing, tables |
|
||||
| Errors | 6+ tests | Detection, signaling |
|
||||
| Org integration | 6+ tests | Full workflow |
|
||||
|
||||
## Files Created
|
||||
|
||||
- `test/test-ob-elixir.el` - Main test file
|
||||
- `test/test-ob-elixir-core.el` - Core tests
|
||||
- `test/test-ob-elixir-vars.el` - Variable tests
|
||||
- `test/test-ob-elixir-results.el` - Result tests
|
||||
- `test/test-ob-elixir-errors.el` - Error tests
|
||||
- `test/test-ob-elixir-org.el` - Org integration tests
|
||||
|
||||
## References
|
||||
|
||||
- [docs/02-testing-emacs-elisp.md](../docs/02-testing-emacs-elisp.md)
|
||||
- [ERT Manual](https://www.gnu.org/software/emacs/manual/html_node/ert/)
|
||||
442
tasks/07-session-support.md
Normal file
442
tasks/07-session-support.md
Normal file
@@ -0,0 +1,442 @@
|
||||
# Task 07: IEx Session Support
|
||||
|
||||
**Phase**: 2 - Sessions
|
||||
**Priority**: High
|
||||
**Estimated Time**: 3-4 hours
|
||||
**Dependencies**: Phase 1 Complete
|
||||
|
||||
## Objective
|
||||
|
||||
Implement IEx session support so code blocks can share state when using `:session` header argument.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Phase 1 complete
|
||||
- Understanding of Emacs comint mode
|
||||
- IEx (Elixir REPL) available
|
||||
|
||||
## Background
|
||||
|
||||
Sessions allow multiple code blocks to share state:
|
||||
|
||||
```org
|
||||
#+BEGIN_SRC elixir :session my-session
|
||||
x = 42
|
||||
#+END_SRC
|
||||
|
||||
#+BEGIN_SRC elixir :session my-session
|
||||
x * 2 # Can use x from previous block
|
||||
#+END_SRC
|
||||
```
|
||||
|
||||
This requires:
|
||||
1. Starting an IEx process
|
||||
2. Managing the process via comint
|
||||
3. Sending code and capturing output
|
||||
4. Proper prompt detection
|
||||
5. Session cleanup
|
||||
|
||||
## Steps
|
||||
|
||||
### Step 1: Add session configuration
|
||||
|
||||
Add to `ob-elixir.el`:
|
||||
|
||||
```elisp
|
||||
;;; Session Configuration
|
||||
|
||||
(defcustom ob-elixir-iex-command "iex"
|
||||
"Command to start IEx session."
|
||||
:type 'string
|
||||
:group 'ob-elixir
|
||||
:safe #'stringp)
|
||||
|
||||
(defconst ob-elixir--prompt-regexp
|
||||
"^\\(?:iex\\|\\.\\.\\)([0-9]+)> "
|
||||
"Regexp matching IEx prompt.
|
||||
Matches both regular prompt 'iex(N)> ' and continuation '...(N)> '.")
|
||||
|
||||
(defconst ob-elixir--eoe-marker
|
||||
"__ob_elixir_eoe_marker__"
|
||||
"End-of-evaluation marker for session output.")
|
||||
|
||||
(defvar ob-elixir--sessions (make-hash-table :test 'equal)
|
||||
"Hash table mapping session names to buffer names.")
|
||||
```
|
||||
|
||||
### Step 2: Implement session initialization
|
||||
|
||||
```elisp
|
||||
;;; Session Management
|
||||
|
||||
(require 'ob-comint)
|
||||
|
||||
(defun org-babel-elixir-initiate-session (&optional session params)
|
||||
"Create or return an Elixir session buffer.
|
||||
|
||||
SESSION is the session name (string or nil).
|
||||
PARAMS are the header arguments.
|
||||
|
||||
Returns the session buffer, or nil if SESSION is \"none\"."
|
||||
(unless (or (not session) (string= session "none"))
|
||||
(let* ((session-name (if (stringp session) session "default"))
|
||||
(buffer (ob-elixir--get-or-create-session session-name params)))
|
||||
(when buffer
|
||||
(puthash session-name (buffer-name buffer) ob-elixir--sessions))
|
||||
buffer)))
|
||||
|
||||
(defun ob-elixir--get-or-create-session (name params)
|
||||
"Get or create an IEx session named NAME with PARAMS."
|
||||
(let* ((buffer-name (format "*ob-elixir:%s*" name))
|
||||
(existing (get-buffer buffer-name)))
|
||||
(if (and existing (org-babel-comint-buffer-livep existing))
|
||||
existing
|
||||
(ob-elixir--start-session buffer-name name params))))
|
||||
|
||||
(defun ob-elixir--start-session (buffer-name session-name params)
|
||||
"Start a new IEx session in BUFFER-NAME."
|
||||
(let* ((buffer (get-buffer-create buffer-name))
|
||||
(process-environment (cons "TERM=dumb" process-environment)))
|
||||
(with-current-buffer buffer
|
||||
;; Start the IEx process
|
||||
(make-comint-in-buffer
|
||||
(format "ob-elixir-%s" session-name)
|
||||
buffer
|
||||
ob-elixir-iex-command
|
||||
nil)
|
||||
|
||||
;; Wait for initial prompt
|
||||
(ob-elixir--wait-for-prompt buffer 10)
|
||||
|
||||
;; Configure IEx for programmatic use
|
||||
(ob-elixir--configure-session buffer)
|
||||
|
||||
buffer)))
|
||||
|
||||
(defun ob-elixir--configure-session (buffer)
|
||||
"Configure IEx session in BUFFER for programmatic use."
|
||||
(let ((config-commands
|
||||
'("IEx.configure(colors: [enabled: false])"
|
||||
"IEx.configure(inspect: [limit: :infinity, printable_limit: :infinity])")))
|
||||
(dolist (cmd config-commands)
|
||||
(ob-elixir--send-command buffer cmd)
|
||||
(ob-elixir--wait-for-prompt buffer 5))))
|
||||
```
|
||||
|
||||
### Step 3: Implement prompt detection
|
||||
|
||||
```elisp
|
||||
(defun ob-elixir--wait-for-prompt (buffer timeout)
|
||||
"Wait for IEx prompt in BUFFER with TIMEOUT seconds."
|
||||
(with-current-buffer buffer
|
||||
(let ((end-time (+ (float-time) timeout)))
|
||||
(while (and (< (float-time) end-time)
|
||||
(not (ob-elixir--at-prompt-p)))
|
||||
(accept-process-output (get-buffer-process buffer) 0.1)
|
||||
(goto-char (point-max)))
|
||||
(ob-elixir--at-prompt-p))))
|
||||
|
||||
(defun ob-elixir--at-prompt-p ()
|
||||
"Return t if the last line in buffer looks like an IEx prompt."
|
||||
(save-excursion
|
||||
(goto-char (point-max))
|
||||
(forward-line 0)
|
||||
(looking-at ob-elixir--prompt-regexp)))
|
||||
```
|
||||
|
||||
### Step 4: Implement command sending
|
||||
|
||||
```elisp
|
||||
(defun ob-elixir--send-command (buffer command)
|
||||
"Send COMMAND to IEx process in BUFFER."
|
||||
(with-current-buffer buffer
|
||||
(goto-char (point-max))
|
||||
(insert command)
|
||||
(comint-send-input nil t)))
|
||||
|
||||
(defun ob-elixir--evaluate-in-session (session body result-type)
|
||||
"Evaluate BODY in SESSION, return result.
|
||||
|
||||
RESULT-TYPE is 'value or 'output."
|
||||
(let* ((buffer (org-babel-elixir-initiate-session session nil))
|
||||
(code (if (eq result-type 'value)
|
||||
(ob-elixir--session-wrap-for-value body)
|
||||
body))
|
||||
(start-marker nil)
|
||||
output)
|
||||
(unless buffer
|
||||
(error "Failed to create Elixir session: %s" session))
|
||||
|
||||
(with-current-buffer buffer
|
||||
;; Mark position before output
|
||||
(goto-char (point-max))
|
||||
(setq start-marker (point-marker))
|
||||
|
||||
;; Send the code
|
||||
(ob-elixir--send-command buffer code)
|
||||
(ob-elixir--wait-for-prompt buffer 30)
|
||||
|
||||
;; Send EOE marker
|
||||
(ob-elixir--send-command buffer
|
||||
(format "\"%s\"" ob-elixir--eoe-marker))
|
||||
(ob-elixir--wait-for-prompt buffer 5)
|
||||
|
||||
;; Extract output
|
||||
(setq output (ob-elixir--extract-session-output
|
||||
buffer start-marker)))
|
||||
|
||||
(ob-elixir--clean-session-output output)))
|
||||
|
||||
(defconst ob-elixir--session-value-wrapper
|
||||
"_ob_result_ = (
|
||||
%s
|
||||
)
|
||||
IO.puts(\"__ob_value_start__\")
|
||||
IO.puts(inspect(_ob_result_, limit: :infinity, printable_limit: :infinity))
|
||||
IO.puts(\"__ob_value_end__\")
|
||||
:ok
|
||||
"
|
||||
"Wrapper for capturing value in session mode.")
|
||||
|
||||
(defun ob-elixir--session-wrap-for-value (body)
|
||||
"Wrap BODY to capture its value in session mode."
|
||||
(format ob-elixir--session-value-wrapper body))
|
||||
```
|
||||
|
||||
### Step 5: Implement output extraction
|
||||
|
||||
```elisp
|
||||
(defun ob-elixir--extract-session-output (buffer start-marker)
|
||||
"Extract output from BUFFER since START-MARKER."
|
||||
(with-current-buffer buffer
|
||||
(let ((end-pos (point-max)))
|
||||
(buffer-substring-no-properties start-marker end-pos))))
|
||||
|
||||
(defun ob-elixir--clean-session-output (output)
|
||||
"Clean OUTPUT from IEx session."
|
||||
(let ((result output))
|
||||
;; Remove ANSI escape codes
|
||||
(setq result (ansi-color-filter-apply result))
|
||||
|
||||
;; Remove prompts
|
||||
(setq result (replace-regexp-in-string
|
||||
ob-elixir--prompt-regexp "" result))
|
||||
|
||||
;; Remove the input echo
|
||||
(setq result (replace-regexp-in-string
|
||||
"^.*\n" "" result nil nil nil 1))
|
||||
|
||||
;; Remove EOE marker
|
||||
(setq result (replace-regexp-in-string
|
||||
(format "\"%s\"" ob-elixir--eoe-marker) "" result))
|
||||
|
||||
;; Extract value if using value wrapper
|
||||
(when (string-match "__ob_value_start__\n\\(.*\\)\n__ob_value_end__" result)
|
||||
(setq result (match-string 1 result)))
|
||||
|
||||
;; Remove :ok from wrapper
|
||||
(setq result (replace-regexp-in-string ":ok\n*$" "" result))
|
||||
|
||||
(string-trim result)))
|
||||
```
|
||||
|
||||
### Step 6: Update execute function
|
||||
|
||||
Modify `org-babel-execute:elixir`:
|
||||
|
||||
```elisp
|
||||
(defun org-babel-execute:elixir (body params)
|
||||
"Execute a block of Elixir code with org-babel."
|
||||
(let* ((session (cdr (assq :session params)))
|
||||
(result-type (cdr (assq :result-type params)))
|
||||
(result-params (cdr (assq :result-params params)))
|
||||
(full-body (org-babel-expand-body:generic
|
||||
body params
|
||||
(org-babel-variable-assignments:elixir params)))
|
||||
(result (if (and session (not (string= session "none")))
|
||||
;; Session mode
|
||||
(ob-elixir--evaluate-in-session session full-body result-type)
|
||||
;; External process mode
|
||||
(ob-elixir--execute full-body result-type))))
|
||||
(org-babel-reassemble-table
|
||||
(org-babel-result-cond result-params
|
||||
result
|
||||
(ob-elixir--table-or-string result))
|
||||
(org-babel-pick-name (cdr (assq :colname-names params))
|
||||
(cdr (assq :colnames params)))
|
||||
(org-babel-pick-name (cdr (assq :rowname-names params))
|
||||
(cdr (assq :rownames params))))))
|
||||
```
|
||||
|
||||
### Step 7: Implement prep-session
|
||||
|
||||
```elisp
|
||||
(defun org-babel-prep-session:elixir (session params)
|
||||
"Prepare SESSION according to PARAMS.
|
||||
|
||||
Sends variable assignments to the session."
|
||||
(let ((buffer (org-babel-elixir-initiate-session session params))
|
||||
(var-lines (org-babel-variable-assignments:elixir params)))
|
||||
(when (and buffer var-lines)
|
||||
(dolist (var-line var-lines)
|
||||
(ob-elixir--send-command buffer var-line)
|
||||
(ob-elixir--wait-for-prompt buffer 5)))
|
||||
buffer))
|
||||
```
|
||||
|
||||
### Step 8: Implement session cleanup
|
||||
|
||||
```elisp
|
||||
(defun ob-elixir-kill-session (session)
|
||||
"Kill the Elixir session named SESSION."
|
||||
(interactive
|
||||
(list (completing-read "Kill session: "
|
||||
(hash-table-keys ob-elixir--sessions))))
|
||||
(let ((buffer-name (gethash session ob-elixir--sessions)))
|
||||
(when buffer-name
|
||||
(let ((buffer (get-buffer buffer-name)))
|
||||
(when buffer
|
||||
(let ((process (get-buffer-process buffer)))
|
||||
(when process
|
||||
(delete-process process)))
|
||||
(kill-buffer buffer)))
|
||||
(remhash session ob-elixir--sessions))))
|
||||
|
||||
(defun ob-elixir-kill-all-sessions ()
|
||||
"Kill all Elixir sessions."
|
||||
(interactive)
|
||||
(maphash (lambda (name _buffer)
|
||||
(ob-elixir-kill-session name))
|
||||
ob-elixir--sessions))
|
||||
```
|
||||
|
||||
### Step 9: Add tests
|
||||
|
||||
Add to `test/test-ob-elixir-sessions.el`:
|
||||
|
||||
```elisp
|
||||
;;; test-ob-elixir-sessions.el --- Session tests -*- lexical-binding: t; -*-
|
||||
|
||||
(require 'ert)
|
||||
(require 'ob-elixir)
|
||||
|
||||
(ert-deftest ob-elixir-test-session-creation ()
|
||||
"Test session creation."
|
||||
(skip-unless (executable-find ob-elixir-iex-command))
|
||||
(unwind-protect
|
||||
(let ((buffer (org-babel-elixir-initiate-session "test-create" nil)))
|
||||
(should buffer)
|
||||
(should (org-babel-comint-buffer-livep buffer)))
|
||||
(ob-elixir-kill-session "test-create")))
|
||||
|
||||
(ert-deftest ob-elixir-test-session-persistence ()
|
||||
"Test that sessions persist state."
|
||||
(skip-unless (executable-find ob-elixir-iex-command))
|
||||
(unwind-protect
|
||||
(progn
|
||||
;; First evaluation - define variable
|
||||
(ob-elixir--evaluate-in-session "test-persist" "x = 42" 'value)
|
||||
;; Second evaluation - use variable
|
||||
(let ((result (ob-elixir--evaluate-in-session
|
||||
"test-persist" "x * 2" 'value)))
|
||||
(should (equal "84" result))))
|
||||
(ob-elixir-kill-session "test-persist")))
|
||||
|
||||
(ert-deftest ob-elixir-test-session-none ()
|
||||
"Test that :session none uses external process."
|
||||
(skip-unless (executable-find ob-elixir-command))
|
||||
(should (null (org-babel-elixir-initiate-session "none" nil))))
|
||||
|
||||
(ert-deftest ob-elixir-test-session-module-def ()
|
||||
"Test defining module in session."
|
||||
(skip-unless (executable-find ob-elixir-iex-command))
|
||||
(unwind-protect
|
||||
(progn
|
||||
(ob-elixir--evaluate-in-session
|
||||
"test-module"
|
||||
"defmodule TestMod do\n def double(x), do: x * 2\nend"
|
||||
'value)
|
||||
(let ((result (ob-elixir--evaluate-in-session
|
||||
"test-module" "TestMod.double(21)" 'value)))
|
||||
(should (equal "42" result))))
|
||||
(ob-elixir-kill-session "test-module")))
|
||||
|
||||
(provide 'test-ob-elixir-sessions)
|
||||
```
|
||||
|
||||
### Step 10: Test in org buffer
|
||||
|
||||
Create session tests in `test.org`:
|
||||
|
||||
```org
|
||||
* Session Tests
|
||||
|
||||
** Define variable in session
|
||||
|
||||
#+BEGIN_SRC elixir :session my-session
|
||||
x = 42
|
||||
#+END_SRC
|
||||
|
||||
** Use variable from session
|
||||
|
||||
#+BEGIN_SRC elixir :session my-session
|
||||
x * 2
|
||||
#+END_SRC
|
||||
|
||||
#+RESULTS:
|
||||
: 84
|
||||
|
||||
** Define module in session
|
||||
|
||||
#+BEGIN_SRC elixir :session my-session
|
||||
defmodule Helper do
|
||||
def greet(name), do: "Hello, #{name}!"
|
||||
end
|
||||
#+END_SRC
|
||||
|
||||
** Use module from session
|
||||
|
||||
#+BEGIN_SRC elixir :session my-session
|
||||
Helper.greet("World")
|
||||
#+END_SRC
|
||||
|
||||
#+RESULTS:
|
||||
: "Hello, World!"
|
||||
```
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `:session name` creates persistent IEx session
|
||||
- [ ] Variables persist across blocks in same session
|
||||
- [ ] Module definitions persist
|
||||
- [ ] `:session none` uses external process (default)
|
||||
- [ ] Multiple named sessions work independently
|
||||
- [ ] Sessions can be killed with `ob-elixir-kill-session`
|
||||
- [ ] Proper prompt detection
|
||||
- [ ] Output is clean (no prompts, ANSI codes)
|
||||
- [ ] All tests pass
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Session hangs
|
||||
|
||||
Check for proper prompt detection. IEx prompts can vary.
|
||||
|
||||
### ANSI codes in output
|
||||
|
||||
The `ansi-color-filter-apply` should remove them. Check TERM environment variable.
|
||||
|
||||
### Process dies unexpectedly
|
||||
|
||||
Check for Elixir errors. May need to handle compilation errors in session context.
|
||||
|
||||
## Files Modified
|
||||
|
||||
- `ob-elixir.el` - Add session support
|
||||
- `test/test-ob-elixir-sessions.el` - Add session tests
|
||||
|
||||
## References
|
||||
|
||||
- [docs/03-org-babel-implementation-guide.md](../docs/03-org-babel-implementation-guide.md) - Session Management
|
||||
- [docs/04-elixir-integration-strategies.md](../docs/04-elixir-integration-strategies.md) - IEx Session Management
|
||||
- [Emacs Comint Mode](https://www.gnu.org/software/emacs/manual/html_node/emacs/Shell-Mode.html)
|
||||
421
tasks/08-mix-project-support.md
Normal file
421
tasks/08-mix-project-support.md
Normal file
@@ -0,0 +1,421 @@
|
||||
# Task 08: Mix Project Support
|
||||
|
||||
**Phase**: 3 - Mix Integration
|
||||
**Priority**: High
|
||||
**Estimated Time**: 2-3 hours
|
||||
**Dependencies**: Task 07 (Session Support) or Phase 1 complete
|
||||
|
||||
## Objective
|
||||
|
||||
Implement Mix project support so Elixir code can be executed within the context of a Mix project, with access to project dependencies and modules.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Phase 1 complete (or Phase 2 for session+mix)
|
||||
- Understanding of Mix build tool
|
||||
- A test Mix project
|
||||
|
||||
## Background
|
||||
|
||||
Mix projects have:
|
||||
- Dependencies in `mix.exs`
|
||||
- Compiled modules in `_build/`
|
||||
- Configuration in `config/`
|
||||
|
||||
To execute code in project context, we need:
|
||||
1. Run code from the project directory
|
||||
2. Use `mix run` for one-shot execution
|
||||
3. Use `iex -S mix` for sessions
|
||||
|
||||
## Steps
|
||||
|
||||
### Step 1: Add Mix configuration
|
||||
|
||||
Add to `ob-elixir.el`:
|
||||
|
||||
```elisp
|
||||
;;; Mix Configuration
|
||||
|
||||
(defcustom ob-elixir-mix-command "mix"
|
||||
"Command to run Mix."
|
||||
:type 'string
|
||||
:group 'ob-elixir
|
||||
:safe #'stringp)
|
||||
|
||||
(defcustom ob-elixir-auto-detect-mix t
|
||||
"Whether to automatically detect Mix projects.
|
||||
|
||||
When non-nil and no :mix-project is specified, ob-elixir will
|
||||
search upward from the org file for a mix.exs file."
|
||||
:type 'boolean
|
||||
:group 'ob-elixir)
|
||||
|
||||
(defconst org-babel-header-args:elixir
|
||||
'((mix-project . :any) ; Path to Mix project root
|
||||
(mix-env . :any) ; MIX_ENV (dev, test, prod)
|
||||
(mix-target . :any)) ; MIX_TARGET for Nerves, etc.
|
||||
"Elixir-specific header arguments.")
|
||||
```
|
||||
|
||||
### Step 2: Implement Mix project detection
|
||||
|
||||
```elisp
|
||||
;;; Mix Project Detection
|
||||
|
||||
(defun ob-elixir--find-mix-project (&optional start-dir)
|
||||
"Find Mix project root by searching for mix.exs.
|
||||
|
||||
Starts from START-DIR (default: current directory) and searches
|
||||
upward. Returns the directory containing mix.exs, or nil."
|
||||
(let* ((dir (or start-dir default-directory))
|
||||
(found (locate-dominating-file dir "mix.exs")))
|
||||
(when found
|
||||
(file-name-directory found))))
|
||||
|
||||
(defun ob-elixir--resolve-mix-project (params)
|
||||
"Resolve Mix project path from PARAMS or auto-detection.
|
||||
|
||||
Returns project path or nil."
|
||||
(let ((explicit (cdr (assq :mix-project params))))
|
||||
(cond
|
||||
;; Explicit project path
|
||||
((and explicit (not (eq explicit 'no)))
|
||||
(expand-file-name explicit))
|
||||
;; Explicitly disabled
|
||||
((eq explicit 'no)
|
||||
nil)
|
||||
;; Auto-detect if enabled
|
||||
(ob-elixir-auto-detect-mix
|
||||
(ob-elixir--find-mix-project))
|
||||
;; No project
|
||||
(t nil))))
|
||||
|
||||
(defun ob-elixir--in-mix-project-p (params)
|
||||
"Return t if execution should happen in Mix project context."
|
||||
(not (null (ob-elixir--resolve-mix-project params))))
|
||||
```
|
||||
|
||||
### Step 3: Implement Mix execution
|
||||
|
||||
```elisp
|
||||
;;; Mix Execution
|
||||
|
||||
(defun ob-elixir--execute-with-mix (body result-type params)
|
||||
"Execute BODY in Mix project context.
|
||||
|
||||
RESULT-TYPE is 'value or 'output.
|
||||
PARAMS contains header arguments including :mix-project."
|
||||
(let* ((project-dir (ob-elixir--resolve-mix-project params))
|
||||
(mix-env (cdr (assq :mix-env params)))
|
||||
(mix-target (cdr (assq :mix-target params)))
|
||||
(default-directory project-dir)
|
||||
(tmp-file (org-babel-temp-file "ob-elixir-mix-" ".exs"))
|
||||
(code (if (eq result-type 'value)
|
||||
(ob-elixir--wrap-for-value body)
|
||||
body))
|
||||
(env-vars (ob-elixir--build-mix-env mix-env mix-target)))
|
||||
|
||||
;; Write code to temp file
|
||||
(with-temp-file tmp-file
|
||||
(insert code))
|
||||
|
||||
;; Execute with mix run
|
||||
(let ((command (format "%s%s run %s"
|
||||
env-vars
|
||||
ob-elixir-mix-command
|
||||
(org-babel-process-file-name tmp-file))))
|
||||
(ob-elixir--process-result
|
||||
(shell-command-to-string command)))))
|
||||
|
||||
(defun ob-elixir--build-mix-env (mix-env mix-target)
|
||||
"Build environment variable prefix for Mix execution."
|
||||
(let ((vars '()))
|
||||
(when mix-env
|
||||
(push (format "MIX_ENV=%s" mix-env) vars))
|
||||
(when mix-target
|
||||
(push (format "MIX_TARGET=%s" mix-target) vars))
|
||||
(if vars
|
||||
(concat (mapconcat #'identity vars " ") " ")
|
||||
"")))
|
||||
```
|
||||
|
||||
### Step 4: Update execute function
|
||||
|
||||
Modify `org-babel-execute:elixir`:
|
||||
|
||||
```elisp
|
||||
(defun org-babel-execute:elixir (body params)
|
||||
"Execute a block of Elixir code with org-babel."
|
||||
(let* ((session (cdr (assq :session params)))
|
||||
(result-type (cdr (assq :result-type params)))
|
||||
(result-params (cdr (assq :result-params params)))
|
||||
(mix-project (ob-elixir--resolve-mix-project params))
|
||||
(full-body (org-babel-expand-body:generic
|
||||
body params
|
||||
(org-babel-variable-assignments:elixir params)))
|
||||
(result (cond
|
||||
;; Session mode
|
||||
((and session (not (string= session "none")))
|
||||
(ob-elixir--evaluate-in-session
|
||||
session full-body result-type params))
|
||||
;; Mix project mode
|
||||
(mix-project
|
||||
(ob-elixir--execute-with-mix
|
||||
full-body result-type params))
|
||||
;; Plain execution
|
||||
(t
|
||||
(ob-elixir--execute full-body result-type)))))
|
||||
(org-babel-reassemble-table
|
||||
(org-babel-result-cond result-params
|
||||
result
|
||||
(ob-elixir--table-or-string result))
|
||||
(org-babel-pick-name (cdr (assq :colname-names params))
|
||||
(cdr (assq :colnames params)))
|
||||
(org-babel-pick-name (cdr (assq :rowname-names params))
|
||||
(cdr (assq :rownames params))))))
|
||||
```
|
||||
|
||||
### Step 5: Update session for Mix projects
|
||||
|
||||
Modify session creation to support Mix:
|
||||
|
||||
```elisp
|
||||
(defun ob-elixir--start-session (buffer-name session-name params)
|
||||
"Start a new IEx session in BUFFER-NAME."
|
||||
(let* ((mix-project (ob-elixir--resolve-mix-project params))
|
||||
(mix-env (cdr (assq :mix-env params)))
|
||||
(buffer (get-buffer-create buffer-name))
|
||||
(default-directory (or mix-project default-directory))
|
||||
(process-environment
|
||||
(append
|
||||
(list "TERM=dumb")
|
||||
(when mix-env (list (format "MIX_ENV=%s" mix-env)))
|
||||
process-environment)))
|
||||
|
||||
(with-current-buffer buffer
|
||||
(if mix-project
|
||||
;; Start with mix
|
||||
(make-comint-in-buffer
|
||||
(format "ob-elixir-%s" session-name)
|
||||
buffer
|
||||
ob-elixir-iex-command
|
||||
nil
|
||||
"-S" "mix")
|
||||
;; Start plain IEx
|
||||
(make-comint-in-buffer
|
||||
(format "ob-elixir-%s" session-name)
|
||||
buffer
|
||||
ob-elixir-iex-command
|
||||
nil))
|
||||
|
||||
;; Wait for prompt
|
||||
(ob-elixir--wait-for-prompt buffer 30)
|
||||
|
||||
;; Configure IEx
|
||||
(ob-elixir--configure-session buffer)
|
||||
|
||||
buffer)))
|
||||
```
|
||||
|
||||
### Step 6: Add compilation support
|
||||
|
||||
```elisp
|
||||
(defcustom ob-elixir-compile-before-run nil
|
||||
"Whether to run mix compile before execution.
|
||||
|
||||
When non-nil, ensures project is compiled before running code.
|
||||
This adds overhead but catches compilation errors early."
|
||||
:type 'boolean
|
||||
:group 'ob-elixir)
|
||||
|
||||
(defun ob-elixir--ensure-compiled (project-dir)
|
||||
"Ensure Mix project at PROJECT-DIR is compiled."
|
||||
(let ((default-directory project-dir))
|
||||
(shell-command-to-string
|
||||
(format "%s compile --force-check" ob-elixir-mix-command))))
|
||||
|
||||
(defun ob-elixir--execute-with-mix (body result-type params)
|
||||
"Execute BODY in Mix project context."
|
||||
(let* ((project-dir (ob-elixir--resolve-mix-project params))
|
||||
(default-directory project-dir))
|
||||
|
||||
;; Optionally compile first
|
||||
(when ob-elixir-compile-before-run
|
||||
(ob-elixir--ensure-compiled project-dir))
|
||||
|
||||
;; ... rest of execution
|
||||
))
|
||||
```
|
||||
|
||||
### Step 7: Add tests
|
||||
|
||||
Create `test/test-ob-elixir-mix.el`:
|
||||
|
||||
```elisp
|
||||
;;; test-ob-elixir-mix.el --- Mix project tests -*- lexical-binding: t; -*-
|
||||
|
||||
(require 'ert)
|
||||
(require 'ob-elixir)
|
||||
|
||||
(defvar ob-elixir-test--mix-project-dir nil
|
||||
"Temporary Mix project directory for testing.")
|
||||
|
||||
(defun ob-elixir-test--setup-mix-project ()
|
||||
"Create a temporary Mix project for testing."
|
||||
(let ((dir (make-temp-file "ob-elixir-test-" t)))
|
||||
(setq ob-elixir-test--mix-project-dir dir)
|
||||
(let ((default-directory dir))
|
||||
;; Create mix.exs
|
||||
(with-temp-file (expand-file-name "mix.exs" dir)
|
||||
(insert "defmodule TestProject.MixProject do
|
||||
use Mix.Project
|
||||
|
||||
def project do
|
||||
[app: :test_project, version: \"0.1.0\", elixir: \"~> 1.14\"]
|
||||
end
|
||||
end"))
|
||||
;; Create lib directory and module
|
||||
(make-directory (expand-file-name "lib" dir))
|
||||
(with-temp-file (expand-file-name "lib/test_project.ex" dir)
|
||||
(insert "defmodule TestProject do
|
||||
def hello, do: \"Hello from TestProject!\"
|
||||
def add(a, b), do: a + b
|
||||
end")))
|
||||
dir))
|
||||
|
||||
(defun ob-elixir-test--cleanup-mix-project ()
|
||||
"Clean up temporary Mix project."
|
||||
(when ob-elixir-test--mix-project-dir
|
||||
(delete-directory ob-elixir-test--mix-project-dir t)
|
||||
(setq ob-elixir-test--mix-project-dir nil)))
|
||||
|
||||
;;; Tests
|
||||
|
||||
(ert-deftest ob-elixir-test-find-mix-project ()
|
||||
"Test Mix project detection."
|
||||
(skip-unless (executable-find ob-elixir-mix-command))
|
||||
(unwind-protect
|
||||
(let* ((project-dir (ob-elixir-test--setup-mix-project))
|
||||
(sub-dir (expand-file-name "lib" project-dir))
|
||||
(default-directory sub-dir))
|
||||
(should (equal project-dir
|
||||
(ob-elixir--find-mix-project))))
|
||||
(ob-elixir-test--cleanup-mix-project)))
|
||||
|
||||
(ert-deftest ob-elixir-test-mix-project-execution ()
|
||||
"Test code execution in Mix project context."
|
||||
(skip-unless (and (executable-find ob-elixir-mix-command)
|
||||
(executable-find ob-elixir-command)))
|
||||
(unwind-protect
|
||||
(let* ((project-dir (ob-elixir-test--setup-mix-project))
|
||||
(params `((:mix-project . ,project-dir))))
|
||||
;; Compile first
|
||||
(let ((default-directory project-dir))
|
||||
(shell-command-to-string "mix compile"))
|
||||
;; Test execution
|
||||
(let ((result (ob-elixir--execute-with-mix
|
||||
"TestProject.hello()" 'value params)))
|
||||
(should (string-match-p "Hello from TestProject" result))))
|
||||
(ob-elixir-test--cleanup-mix-project)))
|
||||
|
||||
(ert-deftest ob-elixir-test-mix-env ()
|
||||
"Test MIX_ENV handling."
|
||||
(skip-unless (executable-find ob-elixir-mix-command))
|
||||
(let ((env-str (ob-elixir--build-mix-env "test" nil)))
|
||||
(should (string-match-p "MIX_ENV=test" env-str))))
|
||||
|
||||
(ert-deftest ob-elixir-test-explicit-no-mix ()
|
||||
"Test disabling Mix with :mix-project no."
|
||||
(let ((params '((:mix-project . no))))
|
||||
(should (null (ob-elixir--resolve-mix-project params)))))
|
||||
|
||||
(provide 'test-ob-elixir-mix)
|
||||
```
|
||||
|
||||
### Step 8: Test in org buffer
|
||||
|
||||
Create Mix tests in `test.org`:
|
||||
|
||||
```org
|
||||
* Mix Project Tests
|
||||
|
||||
** Using project module (explicit path)
|
||||
|
||||
#+BEGIN_SRC elixir :mix-project ~/my_elixir_project
|
||||
MyApp.hello()
|
||||
#+END_SRC
|
||||
|
||||
** Using project module (auto-detect)
|
||||
|
||||
When this org file is inside a Mix project:
|
||||
|
||||
#+BEGIN_SRC elixir
|
||||
MyApp.some_function()
|
||||
#+END_SRC
|
||||
|
||||
** With specific MIX_ENV
|
||||
|
||||
#+BEGIN_SRC elixir :mix-project ~/my_elixir_project :mix-env test
|
||||
Application.get_env(:my_app, :some_config)
|
||||
#+END_SRC
|
||||
|
||||
** Session with Mix
|
||||
|
||||
#+BEGIN_SRC elixir :session mix-session :mix-project ~/my_elixir_project
|
||||
# Has access to project modules
|
||||
alias MyApp.SomeModule
|
||||
#+END_SRC
|
||||
|
||||
** Disable auto-detect
|
||||
|
||||
#+BEGIN_SRC elixir :mix-project no
|
||||
# Plain Elixir, no project context
|
||||
1 + 1
|
||||
#+END_SRC
|
||||
```
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `:mix-project path` executes in specified project
|
||||
- [ ] Auto-detection finds `mix.exs` in parent directories
|
||||
- [ ] `:mix-project no` disables auto-detection
|
||||
- [ ] `:mix-env` sets MIX_ENV correctly
|
||||
- [ ] Project modules are accessible
|
||||
- [ ] Sessions with `:mix-project` use `iex -S mix`
|
||||
- [ ] Compilation errors are reported properly
|
||||
- [ ] All tests pass
|
||||
|
||||
## Header Arguments Reference
|
||||
|
||||
| Argument | Values | Description |
|
||||
|----------|--------|-------------|
|
||||
| `:mix-project` | path, `no` | Project path or disable |
|
||||
| `:mix-env` | dev, test, prod | MIX_ENV value |
|
||||
| `:mix-target` | host, target | MIX_TARGET for Nerves |
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Module not found
|
||||
|
||||
Ensure project is compiled:
|
||||
```bash
|
||||
cd /path/to/project && mix compile
|
||||
```
|
||||
|
||||
### Dependencies not available
|
||||
|
||||
Check that `mix deps.get` has been run.
|
||||
|
||||
### Wrong MIX_ENV
|
||||
|
||||
Explicitly set `:mix-env` header argument.
|
||||
|
||||
## Files Modified
|
||||
|
||||
- `ob-elixir.el` - Add Mix support
|
||||
- `test/test-ob-elixir-mix.el` - Add Mix tests
|
||||
|
||||
## References
|
||||
|
||||
- [docs/04-elixir-integration-strategies.md](../docs/04-elixir-integration-strategies.md) - Mix Project Context
|
||||
- [Mix Documentation](https://hexdocs.pm/mix/Mix.html)
|
||||
401
tasks/09-remote-shell.md
Normal file
401
tasks/09-remote-shell.md
Normal file
@@ -0,0 +1,401 @@
|
||||
# Task 09: Remote Shell (remsh) Support
|
||||
|
||||
**Phase**: 4 - Advanced Features
|
||||
**Priority**: Medium
|
||||
**Estimated Time**: 2-3 hours
|
||||
**Dependencies**: Task 07 (Session Support)
|
||||
|
||||
## Objective
|
||||
|
||||
Implement remote shell support to connect to running Elixir/Erlang nodes and execute code against them.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Session support implemented (Task 07)
|
||||
- Understanding of Erlang distribution
|
||||
- Running Elixir node for testing
|
||||
|
||||
## Background
|
||||
|
||||
Elixir/Erlang nodes can connect to each other for distributed computing. The `--remsh` flag allows IEx to connect to a running node:
|
||||
|
||||
```bash
|
||||
iex --sname console --remsh my_app@hostname --cookie secret
|
||||
```
|
||||
|
||||
This is useful for:
|
||||
- Inspecting production systems
|
||||
- Running code against a live application
|
||||
- Debugging distributed systems
|
||||
|
||||
## Steps
|
||||
|
||||
### Step 1: Add remote shell configuration
|
||||
|
||||
Add to `ob-elixir.el`:
|
||||
|
||||
```elisp
|
||||
;;; Remote Shell Configuration
|
||||
|
||||
(defconst org-babel-header-args:elixir
|
||||
'((mix-project . :any)
|
||||
(mix-env . :any)
|
||||
(remsh . :any) ; Remote node to connect to
|
||||
(node-name . :any) ; --name for local node
|
||||
(node-sname . :any) ; --sname for local node
|
||||
(cookie . :any)) ; Erlang cookie
|
||||
"Elixir-specific header arguments.")
|
||||
|
||||
(defcustom ob-elixir-default-cookie nil
|
||||
"Default Erlang cookie for remote connections.
|
||||
|
||||
Set this if all your nodes use the same cookie.
|
||||
Can be overridden with :cookie header argument."
|
||||
:type '(choice (const nil) string)
|
||||
:group 'ob-elixir)
|
||||
```
|
||||
|
||||
### Step 2: Implement remote session creation
|
||||
|
||||
```elisp
|
||||
;;; Remote Shell Sessions
|
||||
|
||||
(defun ob-elixir--start-remote-session (buffer-name session-name params)
|
||||
"Start a remote shell session in BUFFER-NAME.
|
||||
|
||||
Connects to the node specified in PARAMS."
|
||||
(let* ((remsh (cdr (assq :remsh params)))
|
||||
(node-name (cdr (assq :node-name params)))
|
||||
(node-sname (cdr (assq :node-sname params)))
|
||||
(cookie (or (cdr (assq :cookie params))
|
||||
ob-elixir-default-cookie))
|
||||
(buffer (get-buffer-create buffer-name))
|
||||
(local-name (or node-sname
|
||||
node-name
|
||||
(format "ob_elixir_%d" (random 99999))))
|
||||
(process-environment (cons "TERM=dumb" process-environment)))
|
||||
|
||||
(unless remsh
|
||||
(error "No remote node specified. Use :remsh header argument"))
|
||||
|
||||
(with-current-buffer buffer
|
||||
;; Build command arguments
|
||||
(let ((args (append
|
||||
;; Local node name
|
||||
(if node-name
|
||||
(list "--name" node-name)
|
||||
(list "--sname" local-name))
|
||||
;; Cookie
|
||||
(when cookie
|
||||
(list "--cookie" cookie))
|
||||
;; Remote shell
|
||||
(list "--remsh" remsh))))
|
||||
|
||||
(apply #'make-comint-in-buffer
|
||||
(format "ob-elixir-remsh-%s" session-name)
|
||||
buffer
|
||||
ob-elixir-iex-command
|
||||
nil
|
||||
args))
|
||||
|
||||
;; Wait for connection
|
||||
(unless (ob-elixir--wait-for-prompt buffer 30)
|
||||
(error "Failed to connect to remote node: %s" remsh))
|
||||
|
||||
;; Configure session
|
||||
(ob-elixir--configure-session buffer)
|
||||
|
||||
buffer)))
|
||||
|
||||
(defun ob-elixir--is-remote-session-p (params)
|
||||
"Return t if PARAMS specify a remote shell connection."
|
||||
(not (null (cdr (assq :remsh params)))))
|
||||
```
|
||||
|
||||
### Step 3: Update session creation dispatcher
|
||||
|
||||
Modify `ob-elixir--start-session`:
|
||||
|
||||
```elisp
|
||||
(defun ob-elixir--start-session (buffer-name session-name params)
|
||||
"Start a new IEx session in BUFFER-NAME."
|
||||
(cond
|
||||
;; Remote shell
|
||||
((ob-elixir--is-remote-session-p params)
|
||||
(ob-elixir--start-remote-session buffer-name session-name params))
|
||||
|
||||
;; Mix project
|
||||
((ob-elixir--resolve-mix-project params)
|
||||
(ob-elixir--start-mix-session buffer-name session-name params))
|
||||
|
||||
;; Plain IEx
|
||||
(t
|
||||
(ob-elixir--start-plain-session buffer-name session-name params))))
|
||||
|
||||
(defun ob-elixir--start-plain-session (buffer-name session-name params)
|
||||
"Start a plain IEx session in BUFFER-NAME."
|
||||
(let* ((buffer (get-buffer-create buffer-name))
|
||||
(process-environment (cons "TERM=dumb" process-environment)))
|
||||
(with-current-buffer buffer
|
||||
(make-comint-in-buffer
|
||||
(format "ob-elixir-%s" session-name)
|
||||
buffer
|
||||
ob-elixir-iex-command
|
||||
nil)
|
||||
|
||||
(ob-elixir--wait-for-prompt buffer 10)
|
||||
(ob-elixir--configure-session buffer)
|
||||
buffer)))
|
||||
|
||||
(defun ob-elixir--start-mix-session (buffer-name session-name params)
|
||||
"Start an IEx session with Mix in BUFFER-NAME."
|
||||
(let* ((mix-project (ob-elixir--resolve-mix-project params))
|
||||
(mix-env (cdr (assq :mix-env params)))
|
||||
(buffer (get-buffer-create buffer-name))
|
||||
(default-directory mix-project)
|
||||
(process-environment
|
||||
(append
|
||||
(list "TERM=dumb")
|
||||
(when mix-env (list (format "MIX_ENV=%s" mix-env)))
|
||||
process-environment)))
|
||||
(with-current-buffer buffer
|
||||
(make-comint-in-buffer
|
||||
(format "ob-elixir-%s" session-name)
|
||||
buffer
|
||||
ob-elixir-iex-command
|
||||
nil
|
||||
"-S" "mix")
|
||||
|
||||
(ob-elixir--wait-for-prompt buffer 30)
|
||||
(ob-elixir--configure-session buffer)
|
||||
buffer)))
|
||||
```
|
||||
|
||||
### Step 4: Add connection verification
|
||||
|
||||
```elisp
|
||||
(defun ob-elixir--verify-remote-connection (buffer remsh)
|
||||
"Verify that BUFFER is connected to remote node REMSH."
|
||||
(with-current-buffer buffer
|
||||
(let ((result (ob-elixir--send-and-receive buffer "Node.self()")))
|
||||
(when (string-match-p (regexp-quote remsh) result)
|
||||
t))))
|
||||
|
||||
(defun ob-elixir--send-and-receive (buffer command)
|
||||
"Send COMMAND to BUFFER and return the response."
|
||||
(let ((start-pos nil)
|
||||
(result nil))
|
||||
(with-current-buffer buffer
|
||||
(goto-char (point-max))
|
||||
(setq start-pos (point))
|
||||
(ob-elixir--send-command buffer command)
|
||||
(ob-elixir--wait-for-prompt buffer 10)
|
||||
(setq result (buffer-substring-no-properties start-pos (point))))
|
||||
(ob-elixir--clean-session-output result)))
|
||||
```
|
||||
|
||||
### Step 5: Add safety checks
|
||||
|
||||
```elisp
|
||||
(defcustom ob-elixir-remsh-confirm t
|
||||
"Whether to confirm before connecting to remote nodes.
|
||||
|
||||
When non-nil, ask for confirmation before connecting.
|
||||
This is a safety measure for production systems."
|
||||
:type 'boolean
|
||||
:group 'ob-elixir)
|
||||
|
||||
(defun ob-elixir--confirm-remsh (node)
|
||||
"Confirm remote shell connection to NODE."
|
||||
(or (not ob-elixir-remsh-confirm)
|
||||
(yes-or-no-p
|
||||
(format "Connect to remote Elixir node '%s'? " node))))
|
||||
|
||||
(defun ob-elixir--start-remote-session (buffer-name session-name params)
|
||||
"Start a remote shell session in BUFFER-NAME."
|
||||
(let ((remsh (cdr (assq :remsh params))))
|
||||
(unless (ob-elixir--confirm-remsh remsh)
|
||||
(user-error "Remote shell connection cancelled"))
|
||||
;; ... rest of implementation
|
||||
))
|
||||
```
|
||||
|
||||
### Step 6: Add helper commands
|
||||
|
||||
```elisp
|
||||
(defun ob-elixir-connect-to-node (node &optional cookie)
|
||||
"Interactively connect to a remote Elixir NODE.
|
||||
|
||||
Optional COOKIE specifies the Erlang cookie."
|
||||
(interactive
|
||||
(list (read-string "Remote node: ")
|
||||
(when current-prefix-arg
|
||||
(read-string "Cookie: "))))
|
||||
(let ((params `((:remsh . ,node)
|
||||
,@(when cookie `((:cookie . ,cookie))))))
|
||||
(org-babel-elixir-initiate-session "remote" params)))
|
||||
|
||||
(defun ob-elixir-list-sessions ()
|
||||
"List all active ob-elixir sessions."
|
||||
(interactive)
|
||||
(if (= 0 (hash-table-count ob-elixir--sessions))
|
||||
(message "No active sessions")
|
||||
(with-output-to-temp-buffer "*ob-elixir sessions*"
|
||||
(princ "Active ob-elixir sessions:\n\n")
|
||||
(maphash (lambda (name buffer-name)
|
||||
(let ((buffer (get-buffer buffer-name)))
|
||||
(princ (format " %s -> %s (%s)\n"
|
||||
name
|
||||
buffer-name
|
||||
(if (and buffer
|
||||
(get-buffer-process buffer))
|
||||
"running"
|
||||
"dead")))))
|
||||
ob-elixir--sessions))))
|
||||
```
|
||||
|
||||
### Step 7: Add tests
|
||||
|
||||
Create `test/test-ob-elixir-remsh.el`:
|
||||
|
||||
```elisp
|
||||
;;; test-ob-elixir-remsh.el --- Remote shell tests -*- lexical-binding: t; -*-
|
||||
|
||||
(require 'ert)
|
||||
(require 'ob-elixir)
|
||||
|
||||
;; Note: These tests require a running Elixir node
|
||||
;; Start one with: iex --sname testnode --cookie testcookie
|
||||
|
||||
(defvar ob-elixir-test-remote-node "testnode@localhost"
|
||||
"Remote node for testing. Set to your test node.")
|
||||
|
||||
(defvar ob-elixir-test-remote-cookie "testcookie"
|
||||
"Cookie for test node.")
|
||||
|
||||
(ert-deftest ob-elixir-test-is-remote-session ()
|
||||
"Test remote session detection."
|
||||
(should (ob-elixir--is-remote-session-p
|
||||
'((:remsh . "node@host"))))
|
||||
(should-not (ob-elixir--is-remote-session-p
|
||||
'((:session . "test")))))
|
||||
|
||||
(ert-deftest ob-elixir-test-remote-session-creation ()
|
||||
"Test remote session creation."
|
||||
(skip-unless (executable-find ob-elixir-iex-command))
|
||||
(skip-unless (getenv "OB_ELIXIR_TEST_REMSH")) ; Only run if explicitly enabled
|
||||
(let ((ob-elixir-remsh-confirm nil))
|
||||
(unwind-protect
|
||||
(let* ((params `((:remsh . ,ob-elixir-test-remote-node)
|
||||
(:cookie . ,ob-elixir-test-remote-cookie)))
|
||||
(buffer (org-babel-elixir-initiate-session "test-remote" params)))
|
||||
(should buffer)
|
||||
(should (org-babel-comint-buffer-livep buffer)))
|
||||
(ob-elixir-kill-session "test-remote"))))
|
||||
|
||||
(ert-deftest ob-elixir-test-remote-execution ()
|
||||
"Test code execution on remote node."
|
||||
(skip-unless (executable-find ob-elixir-iex-command))
|
||||
(skip-unless (getenv "OB_ELIXIR_TEST_REMSH"))
|
||||
(let ((ob-elixir-remsh-confirm nil))
|
||||
(unwind-protect
|
||||
(let* ((params `((:remsh . ,ob-elixir-test-remote-node)
|
||||
(:cookie . ,ob-elixir-test-remote-cookie)))
|
||||
(result (ob-elixir--evaluate-in-session
|
||||
"test-remote-exec"
|
||||
"Node.self() |> to_string()"
|
||||
'value
|
||||
params)))
|
||||
(should (string-match-p ob-elixir-test-remote-node result)))
|
||||
(ob-elixir-kill-session "test-remote-exec"))))
|
||||
|
||||
(provide 'test-ob-elixir-remsh)
|
||||
```
|
||||
|
||||
### Step 8: Document usage
|
||||
|
||||
Add to documentation:
|
||||
|
||||
```org
|
||||
* Remote Shell Usage
|
||||
|
||||
** Connecting to a running node
|
||||
|
||||
#+BEGIN_SRC elixir :session remote :remsh myapp@localhost :cookie secret
|
||||
Node.self()
|
||||
#+END_SRC
|
||||
|
||||
** With explicit node name
|
||||
|
||||
#+BEGIN_SRC elixir :session remote :remsh myapp@localhost :node-sname console :cookie secret
|
||||
# Local node will be named 'console'
|
||||
Node.list()
|
||||
#+END_SRC
|
||||
|
||||
** With distributed name
|
||||
|
||||
#+BEGIN_SRC elixir :session remote :remsh myapp@myhost.example.com :node-name console@myhost.example.com :cookie secret
|
||||
# Use full node names for cross-machine connections
|
||||
Application.get_env(:my_app, :some_setting)
|
||||
#+END_SRC
|
||||
```
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `:remsh node@host` connects to remote node
|
||||
- [ ] `:cookie` sets the Erlang cookie
|
||||
- [ ] `:node-sname` and `:node-name` set local node name
|
||||
- [ ] Connection failures produce clear error messages
|
||||
- [ ] Safety confirmation before connecting (configurable)
|
||||
- [ ] `ob-elixir-connect-to-node` interactive command works
|
||||
- [ ] Remote session appears in `ob-elixir-list-sessions`
|
||||
- [ ] All tests pass
|
||||
|
||||
## Header Arguments Reference
|
||||
|
||||
| Argument | Values | Description |
|
||||
|----------|--------|-------------|
|
||||
| `:remsh` | node@host | Remote node to connect to |
|
||||
| `:cookie` | string | Erlang cookie for authentication |
|
||||
| `:node-sname` | name | Short name for local node |
|
||||
| `:node-name` | name@host | Full name for local node |
|
||||
|
||||
## Security Considerations
|
||||
|
||||
1. **Cookies**: Never commit cookies to version control
|
||||
2. **Confirmation**: Enable `ob-elixir-remsh-confirm` for production
|
||||
3. **Network**: Ensure proper firewall rules for Erlang distribution port (4369 + dynamic)
|
||||
4. **Scope**: Consider what code could be executed on production systems
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Cannot connect to node
|
||||
|
||||
1. Verify node is running: `epmd -names`
|
||||
2. Check cookie matches
|
||||
3. Verify network connectivity on port 4369
|
||||
4. Check that node allows remote connections
|
||||
|
||||
### Connection times out
|
||||
|
||||
Increase timeout or check for network issues:
|
||||
```elisp
|
||||
(setq ob-elixir-session-timeout 60)
|
||||
```
|
||||
|
||||
### Node not found
|
||||
|
||||
Ensure using correct node name format:
|
||||
- Short names: `node@hostname` (same machine or subnet)
|
||||
- Long names: `node@hostname.domain.com` (cross-network)
|
||||
|
||||
## Files Modified
|
||||
|
||||
- `ob-elixir.el` - Add remote shell support
|
||||
- `test/test-ob-elixir-remsh.el` - Add remote shell tests
|
||||
|
||||
## References
|
||||
|
||||
- [docs/04-elixir-integration-strategies.md](../docs/04-elixir-integration-strategies.md) - Remote Shell section
|
||||
- [Erlang Distribution](https://www.erlang.org/doc/reference_manual/distributed.html)
|
||||
- [IEx Remote Shell](https://hexdocs.pm/iex/IEx.html#module-remote-shells)
|
||||
358
tasks/10-async-execution.md
Normal file
358
tasks/10-async-execution.md
Normal file
@@ -0,0 +1,358 @@
|
||||
# Task 10: Async Execution Support
|
||||
|
||||
**Phase**: 4 - Advanced Features
|
||||
**Priority**: Low
|
||||
**Estimated Time**: 3-4 hours
|
||||
**Dependencies**: Phase 1 Complete
|
||||
|
||||
## Objective
|
||||
|
||||
Implement asynchronous execution so long-running Elixir code blocks don't freeze Emacs.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Phase 1 complete
|
||||
- Understanding of Emacs async processes
|
||||
|
||||
## Background
|
||||
|
||||
Some Elixir operations can take a long time:
|
||||
- Database migrations
|
||||
- Large data processing
|
||||
- Network operations
|
||||
- Build tasks
|
||||
|
||||
Async execution allows:
|
||||
- Continue editing while code runs
|
||||
- Visual indicator of running blocks
|
||||
- Cancel long-running operations
|
||||
|
||||
## Steps
|
||||
|
||||
### Step 1: Add async configuration
|
||||
|
||||
Add to `ob-elixir.el`:
|
||||
|
||||
```elisp
|
||||
;;; Async Configuration
|
||||
|
||||
(defcustom ob-elixir-async-timeout 300
|
||||
"Timeout in seconds for async execution.
|
||||
|
||||
After this time, async execution will be cancelled."
|
||||
:type 'integer
|
||||
:group 'ob-elixir)
|
||||
|
||||
(defvar ob-elixir--async-processes (make-hash-table :test 'equal)
|
||||
"Hash table mapping buffer positions to async processes.")
|
||||
|
||||
(defconst org-babel-header-args:elixir
|
||||
'((mix-project . :any)
|
||||
(mix-env . :any)
|
||||
(remsh . :any)
|
||||
(node-name . :any)
|
||||
(node-sname . :any)
|
||||
(cookie . :any)
|
||||
(async . ((yes no)))) ; NEW: async execution
|
||||
"Elixir-specific header arguments.")
|
||||
```
|
||||
|
||||
### Step 2: Implement async execution
|
||||
|
||||
```elisp
|
||||
;;; Async Execution
|
||||
|
||||
(defun ob-elixir--execute-async (body result-type callback)
|
||||
"Execute BODY asynchronously.
|
||||
|
||||
RESULT-TYPE is 'value or 'output.
|
||||
CALLBACK is called with the result when complete."
|
||||
(let* ((tmp-file (org-babel-temp-file "ob-elixir-async-" ".exs"))
|
||||
(code (if (eq result-type 'value)
|
||||
(ob-elixir--wrap-for-value body)
|
||||
body))
|
||||
(output-buffer (generate-new-buffer " *ob-elixir-async*"))
|
||||
process)
|
||||
|
||||
;; Write code to temp file
|
||||
(with-temp-file tmp-file
|
||||
(insert code))
|
||||
|
||||
;; Start async process
|
||||
(setq process
|
||||
(start-process
|
||||
"ob-elixir-async"
|
||||
output-buffer
|
||||
ob-elixir-command
|
||||
tmp-file))
|
||||
|
||||
;; Set up process sentinel
|
||||
(set-process-sentinel
|
||||
process
|
||||
(lambda (proc event)
|
||||
(when (memq (process-status proc) '(exit signal))
|
||||
(let ((result (with-current-buffer (process-buffer proc)
|
||||
(buffer-string))))
|
||||
;; Clean up
|
||||
(kill-buffer (process-buffer proc))
|
||||
(delete-file tmp-file)
|
||||
;; Call callback with result
|
||||
(funcall callback (ob-elixir--process-result
|
||||
(string-trim result)))))))
|
||||
|
||||
;; Set up timeout
|
||||
(run-at-time ob-elixir-async-timeout nil
|
||||
(lambda ()
|
||||
(when (process-live-p process)
|
||||
(kill-process process)
|
||||
(funcall callback "Error: Async execution timed out"))))
|
||||
|
||||
process))
|
||||
```
|
||||
|
||||
### Step 3: Integrate with org-babel
|
||||
|
||||
```elisp
|
||||
(defun ob-elixir--async-p (params)
|
||||
"Return t if PARAMS specify async execution."
|
||||
(string= "yes" (cdr (assq :async params))))
|
||||
|
||||
(defun org-babel-execute:elixir (body params)
|
||||
"Execute a block of Elixir code with org-babel."
|
||||
(let* ((session (cdr (assq :session params)))
|
||||
(result-type (cdr (assq :result-type params)))
|
||||
(result-params (cdr (assq :result-params params)))
|
||||
(async (ob-elixir--async-p params))
|
||||
(full-body (org-babel-expand-body:generic
|
||||
body params
|
||||
(org-babel-variable-assignments:elixir params))))
|
||||
|
||||
(if async
|
||||
;; Async execution
|
||||
(ob-elixir--execute-async-block full-body result-type params)
|
||||
;; Sync execution (existing code)
|
||||
(let ((result (ob-elixir--execute-sync full-body result-type params)))
|
||||
(org-babel-reassemble-table
|
||||
(org-babel-result-cond result-params
|
||||
result
|
||||
(ob-elixir--table-or-string result))
|
||||
(org-babel-pick-name (cdr (assq :colname-names params))
|
||||
(cdr (assq :colnames params)))
|
||||
(org-babel-pick-name (cdr (assq :rowname-names params))
|
||||
(cdr (assq :rownames params))))))))
|
||||
|
||||
(defun ob-elixir--execute-sync (body result-type params)
|
||||
"Execute BODY synchronously."
|
||||
(cond
|
||||
((and (cdr (assq :session params))
|
||||
(not (string= (cdr (assq :session params)) "none")))
|
||||
(ob-elixir--evaluate-in-session
|
||||
(cdr (assq :session params)) body result-type params))
|
||||
((ob-elixir--resolve-mix-project params)
|
||||
(ob-elixir--execute-with-mix body result-type params))
|
||||
(t
|
||||
(ob-elixir--execute body result-type))))
|
||||
```
|
||||
|
||||
### Step 4: Implement async block handling
|
||||
|
||||
```elisp
|
||||
(defun ob-elixir--execute-async-block (body result-type params)
|
||||
"Execute BODY asynchronously and insert results when done."
|
||||
(let ((buffer (current-buffer))
|
||||
(point (point))
|
||||
(result-params (cdr (assq :result-params params)))
|
||||
(marker (copy-marker (point))))
|
||||
|
||||
;; Show placeholder
|
||||
(ob-elixir--insert-async-placeholder marker)
|
||||
|
||||
;; Execute async
|
||||
(ob-elixir--execute-async
|
||||
body
|
||||
result-type
|
||||
(lambda (result)
|
||||
(ob-elixir--insert-async-result
|
||||
buffer marker result result-params params)))
|
||||
|
||||
;; Return placeholder message
|
||||
"Executing asynchronously..."))
|
||||
|
||||
(defun ob-elixir--insert-async-placeholder (marker)
|
||||
"Insert a placeholder at MARKER indicating async execution."
|
||||
(save-excursion
|
||||
(goto-char marker)
|
||||
(end-of-line)
|
||||
(insert "\n")
|
||||
(insert "#+RESULTS:\n")
|
||||
(insert ": [Executing...]\n")))
|
||||
|
||||
(defun ob-elixir--insert-async-result (buffer marker result result-params params)
|
||||
"Insert RESULT at MARKER in BUFFER."
|
||||
(when (buffer-live-p buffer)
|
||||
(with-current-buffer buffer
|
||||
(save-excursion
|
||||
(goto-char marker)
|
||||
;; Find and remove placeholder
|
||||
(when (search-forward ": [Executing...]" nil t)
|
||||
(beginning-of-line)
|
||||
(let ((start (point)))
|
||||
(forward-line 1)
|
||||
(delete-region start (point))))
|
||||
;; Insert real result
|
||||
(let ((formatted (org-babel-result-cond result-params
|
||||
result
|
||||
(ob-elixir--table-or-string result))))
|
||||
(org-babel-insert-result formatted result-params))))))
|
||||
```
|
||||
|
||||
### Step 5: Add cancellation support
|
||||
|
||||
```elisp
|
||||
(defun ob-elixir-cancel-async ()
|
||||
"Cancel the async execution at point."
|
||||
(interactive)
|
||||
(let* ((pos (point))
|
||||
(process (gethash pos ob-elixir--async-processes)))
|
||||
(if (and process (process-live-p process))
|
||||
(progn
|
||||
(kill-process process)
|
||||
(remhash pos ob-elixir--async-processes)
|
||||
(message "Async execution cancelled"))
|
||||
(message "No async execution at point"))))
|
||||
|
||||
(defun ob-elixir-cancel-all-async ()
|
||||
"Cancel all running async executions."
|
||||
(interactive)
|
||||
(maphash (lambda (_pos process)
|
||||
(when (process-live-p process)
|
||||
(kill-process process)))
|
||||
ob-elixir--async-processes)
|
||||
(clrhash ob-elixir--async-processes)
|
||||
(message "All async executions cancelled"))
|
||||
```
|
||||
|
||||
### Step 6: Add visual indicators
|
||||
|
||||
```elisp
|
||||
(defface ob-elixir-async-running
|
||||
'((t :background "yellow" :foreground "black"))
|
||||
"Face for source blocks with running async execution."
|
||||
:group 'ob-elixir)
|
||||
|
||||
(defun ob-elixir--highlight-async-block (start end)
|
||||
"Highlight the region from START to END as running."
|
||||
(let ((overlay (make-overlay start end)))
|
||||
(overlay-put overlay 'face 'ob-elixir-async-running)
|
||||
(overlay-put overlay 'ob-elixir-async t)
|
||||
overlay))
|
||||
|
||||
(defun ob-elixir--remove-async-highlight ()
|
||||
"Remove async highlighting from current block."
|
||||
(dolist (ov (overlays-in (point-min) (point-max)))
|
||||
(when (overlay-get ov 'ob-elixir-async)
|
||||
(delete-overlay ov))))
|
||||
```
|
||||
|
||||
### Step 7: Add tests
|
||||
|
||||
Create `test/test-ob-elixir-async.el`:
|
||||
|
||||
```elisp
|
||||
;;; test-ob-elixir-async.el --- Async execution tests -*- lexical-binding: t; -*-
|
||||
|
||||
(require 'ert)
|
||||
(require 'ob-elixir)
|
||||
|
||||
(ert-deftest ob-elixir-test-async-detection ()
|
||||
"Test async header argument detection."
|
||||
(should (ob-elixir--async-p '((:async . "yes"))))
|
||||
(should-not (ob-elixir--async-p '((:async . "no"))))
|
||||
(should-not (ob-elixir--async-p '())))
|
||||
|
||||
(ert-deftest ob-elixir-test-async-execution ()
|
||||
"Test async execution completion."
|
||||
(skip-unless (executable-find ob-elixir-command))
|
||||
(let ((result nil)
|
||||
(done nil))
|
||||
(ob-elixir--execute-async
|
||||
"1 + 1"
|
||||
'value
|
||||
(lambda (r)
|
||||
(setq result r)
|
||||
(setq done t)))
|
||||
;; Wait for completion
|
||||
(with-timeout (10 (error "Async test timed out"))
|
||||
(while (not done)
|
||||
(accept-process-output nil 0.1)))
|
||||
(should (equal "2" result))))
|
||||
|
||||
(ert-deftest ob-elixir-test-async-timeout ()
|
||||
"Test async timeout handling."
|
||||
(skip-unless (executable-find ob-elixir-command))
|
||||
(let ((ob-elixir-async-timeout 1)
|
||||
(result nil)
|
||||
(done nil))
|
||||
(ob-elixir--execute-async
|
||||
":timer.sleep(5000)" ; Sleep for 5 seconds
|
||||
'value
|
||||
(lambda (r)
|
||||
(setq result r)
|
||||
(setq done t)))
|
||||
;; Wait for timeout
|
||||
(with-timeout (3 (error "Test timed out"))
|
||||
(while (not done)
|
||||
(accept-process-output nil 0.1)))
|
||||
(should (string-match-p "timed out" result))))
|
||||
|
||||
(provide 'test-ob-elixir-async)
|
||||
```
|
||||
|
||||
### Step 8: Document usage
|
||||
|
||||
Add to documentation:
|
||||
|
||||
```org
|
||||
* Async Execution
|
||||
|
||||
** Long-running computation
|
||||
|
||||
#+BEGIN_SRC elixir :async yes
|
||||
# This won't block Emacs
|
||||
Enum.reduce(1..1000000, 0, &+/2)
|
||||
#+END_SRC
|
||||
|
||||
** Async with Mix project
|
||||
|
||||
#+BEGIN_SRC elixir :async yes :mix-project ~/my_app
|
||||
MyApp.expensive_operation()
|
||||
#+END_SRC
|
||||
|
||||
** Cancel with M-x ob-elixir-cancel-async
|
||||
```
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `:async yes` executes code asynchronously
|
||||
- [ ] Placeholder shown while executing
|
||||
- [ ] Results inserted when complete
|
||||
- [ ] Timeout handled gracefully
|
||||
- [ ] `ob-elixir-cancel-async` cancels execution
|
||||
- [ ] Visual indicator for running blocks
|
||||
- [ ] All tests pass
|
||||
|
||||
## Limitations
|
||||
|
||||
- Sessions cannot be async (they're inherently stateful)
|
||||
- Multiple async blocks may have ordering issues
|
||||
- Async results may not integrate perfectly with noweb
|
||||
|
||||
## Files Modified
|
||||
|
||||
- `ob-elixir.el` - Add async support
|
||||
- `test/test-ob-elixir-async.el` - Add async tests
|
||||
|
||||
## References
|
||||
|
||||
- [Emacs Async Processes](https://www.gnu.org/software/emacs/manual/html_node/elisp/Asynchronous-Processes.html)
|
||||
- [Process Sentinels](https://www.gnu.org/software/emacs/manual/html_node/elisp/Sentinels.html)
|
||||
Reference in New Issue
Block a user