mix deps support
This commit is contained in:
306
ob-elixir.el
306
ob-elixir.el
@@ -90,6 +90,33 @@ Matches both regular prompt `iex(N)> ' and continuation `...(N)> '.")
|
|||||||
(defvar ob-elixir--sessions (make-hash-table :test 'equal)
|
(defvar ob-elixir--sessions (make-hash-table :test 'equal)
|
||||||
"Hash table mapping session names to buffer names.")
|
"Hash table mapping session names to buffer names.")
|
||||||
|
|
||||||
|
;;; Mix Dependencies Configuration
|
||||||
|
|
||||||
|
(defcustom ob-elixir-mix-command "mix"
|
||||||
|
"Command to run Mix."
|
||||||
|
:type 'string
|
||||||
|
:group 'ob-elixir
|
||||||
|
:safe #'stringp)
|
||||||
|
|
||||||
|
(defcustom ob-elixir-deps-cache-dir
|
||||||
|
(expand-file-name "ob-elixir" (or (getenv "XDG_CACHE_HOME") "~/.cache"))
|
||||||
|
"Directory for caching temporary Mix projects.
|
||||||
|
|
||||||
|
Each unique set of dependencies gets its own subdirectory,
|
||||||
|
named by the hash of the dependencies."
|
||||||
|
:type 'directory
|
||||||
|
:group 'ob-elixir)
|
||||||
|
|
||||||
|
(defvar ob-elixir--deps-projects (make-hash-table :test 'equal)
|
||||||
|
"Hash table mapping deps-hash to project directory path.")
|
||||||
|
|
||||||
|
(defvar ob-elixir--session-deps (make-hash-table :test 'equal)
|
||||||
|
"Hash table mapping session names to their deps-hash.
|
||||||
|
Used to ensure session deps consistency.")
|
||||||
|
|
||||||
|
(defvar ob-elixir--cleanup-registered nil
|
||||||
|
"Whether the cleanup hook has been registered.")
|
||||||
|
|
||||||
;;; Header Arguments
|
;;; Header Arguments
|
||||||
|
|
||||||
(defvar org-babel-default-header-args:elixir
|
(defvar org-babel-default-header-args:elixir
|
||||||
@@ -385,6 +412,156 @@ Each statement has the form: var_name = value"
|
|||||||
(ob-elixir--elisp-to-elixir value))))
|
(ob-elixir--elisp-to-elixir value))))
|
||||||
(org-babel--get-vars params)))
|
(org-babel--get-vars params)))
|
||||||
|
|
||||||
|
;;; Deps Block Parsing
|
||||||
|
|
||||||
|
(defconst ob-elixir--deps-block-regexp
|
||||||
|
"^[ \t]*#\\+BEGIN_DEPS[ \t]+elixir[ \t]*\n\\(\\(?:.*\n\\)*?\\)[ \t]*#\\+END_DEPS"
|
||||||
|
"Regexp matching a deps block.
|
||||||
|
Group 1 captures the deps content.")
|
||||||
|
|
||||||
|
(defun ob-elixir--find-deps-for-position (pos)
|
||||||
|
"Find the most recent deps block before POS.
|
||||||
|
|
||||||
|
Returns the deps content as a string, or nil if no deps block found."
|
||||||
|
(save-excursion
|
||||||
|
(goto-char pos)
|
||||||
|
(let ((found nil))
|
||||||
|
;; Search backward for deps blocks
|
||||||
|
(while (and (not found)
|
||||||
|
(re-search-backward ob-elixir--deps-block-regexp nil t))
|
||||||
|
(when (< (match-end 0) pos)
|
||||||
|
(setq found (match-string-no-properties 1))))
|
||||||
|
found)))
|
||||||
|
|
||||||
|
(defun ob-elixir--normalize-deps (deps-string)
|
||||||
|
"Normalize DEPS-STRING for consistent hashing.
|
||||||
|
|
||||||
|
Removes comments, extra whitespace, and normalizes formatting."
|
||||||
|
(let ((normalized deps-string))
|
||||||
|
;; Remove Elixir comments
|
||||||
|
(setq normalized (replace-regexp-in-string "#.*$" "" normalized))
|
||||||
|
;; Normalize whitespace
|
||||||
|
(setq normalized (replace-regexp-in-string "[ \t\n]+" " " normalized))
|
||||||
|
;; Trim
|
||||||
|
(string-trim normalized)))
|
||||||
|
|
||||||
|
(defun ob-elixir--hash-deps (deps-string)
|
||||||
|
"Compute SHA256 hash of DEPS-STRING for caching."
|
||||||
|
(secure-hash 'sha256 (ob-elixir--normalize-deps deps-string)))
|
||||||
|
|
||||||
|
;;; Temporary Mix Project Management
|
||||||
|
|
||||||
|
(defconst ob-elixir--mix-exs-template
|
||||||
|
"defmodule ObElixirTemp.MixProject do
|
||||||
|
use Mix.Project
|
||||||
|
|
||||||
|
def project do
|
||||||
|
[
|
||||||
|
app: :ob_elixir_temp,
|
||||||
|
version: \"0.1.0\",
|
||||||
|
elixir: \"~> 1.14\",
|
||||||
|
start_permanent: false,
|
||||||
|
deps: deps()
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
def application do
|
||||||
|
[extra_applications: [:logger]]
|
||||||
|
end
|
||||||
|
|
||||||
|
defp deps do
|
||||||
|
%s
|
||||||
|
end
|
||||||
|
end
|
||||||
|
"
|
||||||
|
"Template for temporary Mix project mix.exs file.
|
||||||
|
%s is replaced with the deps list.")
|
||||||
|
|
||||||
|
(defun ob-elixir--ensure-cache-dir ()
|
||||||
|
"Ensure the cache directory exists."
|
||||||
|
(unless (file-directory-p ob-elixir-deps-cache-dir)
|
||||||
|
(make-directory ob-elixir-deps-cache-dir t)))
|
||||||
|
|
||||||
|
(defun ob-elixir--get-deps-project (deps-string)
|
||||||
|
"Get or create a Mix project for DEPS-STRING.
|
||||||
|
|
||||||
|
Returns the project directory path, creating and initializing
|
||||||
|
the project if necessary."
|
||||||
|
(let* ((deps-hash (ob-elixir--hash-deps deps-string))
|
||||||
|
(cached (gethash deps-hash ob-elixir--deps-projects)))
|
||||||
|
(if (and cached (file-directory-p cached))
|
||||||
|
cached
|
||||||
|
;; Create new project
|
||||||
|
(ob-elixir--create-deps-project deps-hash deps-string))))
|
||||||
|
|
||||||
|
(defun ob-elixir--create-deps-project (deps-hash deps-string)
|
||||||
|
"Create a new temporary Mix project for DEPS-STRING.
|
||||||
|
|
||||||
|
DEPS-HASH is used as the directory name.
|
||||||
|
Returns the project directory path."
|
||||||
|
(ob-elixir--ensure-cache-dir)
|
||||||
|
(ob-elixir--register-cleanup)
|
||||||
|
|
||||||
|
(let* ((project-dir (expand-file-name deps-hash ob-elixir-deps-cache-dir))
|
||||||
|
(mix-exs-path (expand-file-name "mix.exs" project-dir))
|
||||||
|
(lib-dir (expand-file-name "lib" project-dir)))
|
||||||
|
|
||||||
|
;; Create project structure
|
||||||
|
(make-directory project-dir t)
|
||||||
|
(make-directory lib-dir t)
|
||||||
|
|
||||||
|
;; Write mix.exs
|
||||||
|
(with-temp-file mix-exs-path
|
||||||
|
(insert (format ob-elixir--mix-exs-template deps-string)))
|
||||||
|
|
||||||
|
;; Write a placeholder module
|
||||||
|
(with-temp-file (expand-file-name "ob_elixir_temp.ex" lib-dir)
|
||||||
|
(insert "defmodule ObElixirTemp do\nend\n"))
|
||||||
|
|
||||||
|
;; Fetch dependencies
|
||||||
|
(let ((default-directory project-dir))
|
||||||
|
(message "ob-elixir: Fetching dependencies...")
|
||||||
|
(let ((output (shell-command-to-string
|
||||||
|
(format "%s deps.get 2>&1" ob-elixir-mix-command))))
|
||||||
|
(when (string-match-p "\\*\\*.*error\\|Could not" output)
|
||||||
|
(error "ob-elixir: Failed to fetch dependencies:\n%s" output)))
|
||||||
|
|
||||||
|
;; Compile
|
||||||
|
(message "ob-elixir: Compiling dependencies...")
|
||||||
|
(let ((output (shell-command-to-string
|
||||||
|
(format "%s compile 2>&1" ob-elixir-mix-command))))
|
||||||
|
(when (string-match-p "\\*\\*.*[Ee]rror" output)
|
||||||
|
(error "ob-elixir: Failed to compile dependencies:\n%s" output))))
|
||||||
|
|
||||||
|
;; Cache and return
|
||||||
|
(puthash deps-hash project-dir ob-elixir--deps-projects)
|
||||||
|
(message "ob-elixir: Dependencies ready")
|
||||||
|
project-dir))
|
||||||
|
|
||||||
|
;;; Execution with Dependencies
|
||||||
|
|
||||||
|
(defun ob-elixir--execute-with-deps (body result-type deps-string)
|
||||||
|
"Execute BODY with dependencies from DEPS-STRING.
|
||||||
|
|
||||||
|
RESULT-TYPE is `value' or `output'."
|
||||||
|
(let* ((project-dir (ob-elixir--get-deps-project deps-string))
|
||||||
|
(default-directory project-dir)
|
||||||
|
(tmp-file (org-babel-temp-file "ob-elixir-" ".exs"))
|
||||||
|
(code (if (eq result-type 'value)
|
||||||
|
(ob-elixir--wrap-for-value body)
|
||||||
|
body)))
|
||||||
|
|
||||||
|
;; Write code to temp file
|
||||||
|
(with-temp-file tmp-file
|
||||||
|
(insert code))
|
||||||
|
|
||||||
|
;; Execute with mix run
|
||||||
|
(let ((command (format "%s run --no-compile %s 2>&1"
|
||||||
|
ob-elixir-mix-command
|
||||||
|
(shell-quote-argument tmp-file))))
|
||||||
|
(ob-elixir--process-result
|
||||||
|
(shell-command-to-string command)))))
|
||||||
|
|
||||||
;;; Session Management
|
;;; Session Management
|
||||||
|
|
||||||
(defun org-babel-elixir-initiate-session (&optional session params)
|
(defun org-babel-elixir-initiate-session (&optional session params)
|
||||||
@@ -583,6 +760,114 @@ Sends variable assignments to the session."
|
|||||||
(ob-elixir-kill-session name))
|
(ob-elixir-kill-session name))
|
||||||
ob-elixir--sessions))
|
ob-elixir--sessions))
|
||||||
|
|
||||||
|
;;; Session with Dependencies
|
||||||
|
|
||||||
|
(defun ob-elixir--start-session-with-deps (buffer-name session-name deps-string)
|
||||||
|
"Start a new IEx session with dependencies in BUFFER-NAME.
|
||||||
|
|
||||||
|
SESSION-NAME is used for the process name.
|
||||||
|
DEPS-STRING contains the dependencies specification."
|
||||||
|
(let* ((project-dir (ob-elixir--get-deps-project deps-string))
|
||||||
|
(buffer (get-buffer-create buffer-name))
|
||||||
|
(default-directory project-dir)
|
||||||
|
(process-environment (cons "TERM=dumb" process-environment)))
|
||||||
|
|
||||||
|
(with-current-buffer buffer
|
||||||
|
;; Start IEx with Mix project
|
||||||
|
(make-comint-in-buffer
|
||||||
|
(format "ob-elixir-%s" session-name)
|
||||||
|
buffer
|
||||||
|
ob-elixir-iex-command
|
||||||
|
nil
|
||||||
|
"-S" "mix")
|
||||||
|
|
||||||
|
;; Wait for initial prompt (longer timeout for deps loading)
|
||||||
|
(ob-elixir--wait-for-prompt buffer 30)
|
||||||
|
|
||||||
|
;; Configure IEx for programmatic use
|
||||||
|
(ob-elixir--configure-session buffer)
|
||||||
|
|
||||||
|
buffer)))
|
||||||
|
|
||||||
|
(defun ob-elixir--get-or-create-session-with-deps (name deps-string)
|
||||||
|
"Get or create an IEx session NAME with DEPS-STRING."
|
||||||
|
(let* ((deps-hash (ob-elixir--hash-deps deps-string))
|
||||||
|
(buffer-name (format "*ob-elixir:%s*" name))
|
||||||
|
(existing (get-buffer buffer-name))
|
||||||
|
(existing-deps (gethash name ob-elixir--session-deps)))
|
||||||
|
|
||||||
|
;; Check if existing session has different deps
|
||||||
|
(when (and existing existing-deps (not (string= existing-deps deps-hash)))
|
||||||
|
(message "ob-elixir: Session %s has different deps, recreating..." name)
|
||||||
|
(ob-elixir-kill-session name)
|
||||||
|
(setq existing nil))
|
||||||
|
|
||||||
|
(if (and existing (org-babel-comint-buffer-livep existing))
|
||||||
|
existing
|
||||||
|
;; Create new session
|
||||||
|
(let ((buffer (ob-elixir--start-session-with-deps buffer-name name deps-string)))
|
||||||
|
(puthash name (buffer-name buffer) ob-elixir--sessions)
|
||||||
|
(puthash name deps-hash ob-elixir--session-deps)
|
||||||
|
buffer))))
|
||||||
|
|
||||||
|
(defun ob-elixir--evaluate-in-session-with-deps (session body result-type deps-string)
|
||||||
|
"Evaluate BODY in SESSION with DEPS-STRING context.
|
||||||
|
|
||||||
|
RESULT-TYPE is `value' or `output'."
|
||||||
|
(let* ((buffer (ob-elixir--get-or-create-session-with-deps session deps-string))
|
||||||
|
(code (if (eq result-type 'value)
|
||||||
|
(ob-elixir--session-wrap-for-value body)
|
||||||
|
body))
|
||||||
|
(eoe-indicator ob-elixir--eoe-marker)
|
||||||
|
(full-body (concat code "\nIO.puts(\"" eoe-indicator "\")"))
|
||||||
|
output)
|
||||||
|
|
||||||
|
(unless buffer
|
||||||
|
(error "Failed to create Elixir session with deps: %s" session))
|
||||||
|
|
||||||
|
(setq output
|
||||||
|
(org-babel-comint-with-output
|
||||||
|
(buffer eoe-indicator t full-body)
|
||||||
|
(ob-elixir--send-command buffer full-body)))
|
||||||
|
|
||||||
|
(ob-elixir--clean-session-output output result-type)))
|
||||||
|
|
||||||
|
;;; Deps Project Cleanup
|
||||||
|
|
||||||
|
(defun ob-elixir--register-cleanup ()
|
||||||
|
"Register cleanup hook if not already registered."
|
||||||
|
(unless ob-elixir--cleanup-registered
|
||||||
|
(add-hook 'kill-emacs-hook #'ob-elixir-cleanup-deps-projects)
|
||||||
|
(setq ob-elixir--cleanup-registered t)))
|
||||||
|
|
||||||
|
(defun ob-elixir-cleanup-deps-projects ()
|
||||||
|
"Delete all temporary Mix projects created by ob-elixir.
|
||||||
|
|
||||||
|
Called automatically on Emacs exit."
|
||||||
|
(interactive)
|
||||||
|
(maphash
|
||||||
|
(lambda (_hash project-dir)
|
||||||
|
(when (and project-dir (file-directory-p project-dir))
|
||||||
|
(condition-case err
|
||||||
|
(delete-directory project-dir t)
|
||||||
|
(error
|
||||||
|
(message "ob-elixir: Failed to delete %s: %s" project-dir err)))))
|
||||||
|
ob-elixir--deps-projects)
|
||||||
|
(clrhash ob-elixir--deps-projects)
|
||||||
|
(message "ob-elixir: Cleaned up temporary Mix projects"))
|
||||||
|
|
||||||
|
(defun ob-elixir-list-deps-projects ()
|
||||||
|
"List all cached dependency projects."
|
||||||
|
(interactive)
|
||||||
|
(if (= (hash-table-count ob-elixir--deps-projects) 0)
|
||||||
|
(message "No cached dependency projects")
|
||||||
|
(with-output-to-temp-buffer "*ob-elixir deps projects*"
|
||||||
|
(princ "Cached ob-elixir dependency projects:\n\n")
|
||||||
|
(maphash
|
||||||
|
(lambda (hash dir)
|
||||||
|
(princ (format " %s\n -> %s\n" (substring hash 0 12) dir)))
|
||||||
|
ob-elixir--deps-projects))))
|
||||||
|
|
||||||
;;; Execution
|
;;; Execution
|
||||||
|
|
||||||
(defconst ob-elixir--value-wrapper
|
(defconst ob-elixir--value-wrapper
|
||||||
@@ -634,15 +919,26 @@ This function is called by `org-babel-execute-src-block'."
|
|||||||
(let* ((session (cdr (assq :session params)))
|
(let* ((session (cdr (assq :session params)))
|
||||||
(result-type (cdr (assq :result-type params)))
|
(result-type (cdr (assq :result-type params)))
|
||||||
(result-params (cdr (assq :result-params params)))
|
(result-params (cdr (assq :result-params params)))
|
||||||
|
;; Find deps for this block's position
|
||||||
|
(deps-string (ob-elixir--find-deps-for-position (point)))
|
||||||
;; Expand body with variable assignments
|
;; Expand body with variable assignments
|
||||||
(full-body (org-babel-expand-body:generic
|
(full-body (org-babel-expand-body:generic
|
||||||
body params
|
body params
|
||||||
(org-babel-variable-assignments:elixir params)))
|
(org-babel-variable-assignments:elixir params)))
|
||||||
(result (if (and session (not (string= session "none")))
|
(result (cond
|
||||||
;; Session mode
|
;; Session mode with deps
|
||||||
(ob-elixir--evaluate-in-session session full-body result-type)
|
((and session (not (string= session "none")) deps-string)
|
||||||
;; External process mode
|
(ob-elixir--evaluate-in-session-with-deps
|
||||||
(ob-elixir--execute full-body result-type))))
|
session full-body result-type deps-string))
|
||||||
|
;; Session mode without deps
|
||||||
|
((and session (not (string= session "none")))
|
||||||
|
(ob-elixir--evaluate-in-session session full-body result-type))
|
||||||
|
;; Non-session with deps
|
||||||
|
(deps-string
|
||||||
|
(ob-elixir--execute-with-deps full-body result-type deps-string))
|
||||||
|
;; Plain execution
|
||||||
|
(t
|
||||||
|
(ob-elixir--execute full-body result-type)))))
|
||||||
(org-babel-reassemble-table
|
(org-babel-reassemble-table
|
||||||
(org-babel-result-cond result-params
|
(org-babel-result-cond result-params
|
||||||
;; For output/scalar/verbatim - return as-is
|
;; For output/scalar/verbatim - return as-is
|
||||||
|
|||||||
213
test/test-ob-elixir-deps.el
Normal file
213
test/test-ob-elixir-deps.el
Normal file
@@ -0,0 +1,213 @@
|
|||||||
|
;;; test-ob-elixir-deps.el --- Deps block tests -*- lexical-binding: t; -*-
|
||||||
|
|
||||||
|
;; Copyright (C) 2024 Your Name
|
||||||
|
|
||||||
|
;; Author: Your Name <your.email@example.com>
|
||||||
|
|
||||||
|
;; This file is not part of GNU Emacs.
|
||||||
|
|
||||||
|
;;; Commentary:
|
||||||
|
|
||||||
|
;; Tests for ob-elixir Mix dependencies support.
|
||||||
|
|
||||||
|
;;; Code:
|
||||||
|
|
||||||
|
(require 'ert)
|
||||||
|
(require 'ob-elixir)
|
||||||
|
|
||||||
|
;;; Parsing Tests
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-deps-block-parsing ()
|
||||||
|
"Test finding deps blocks in buffer."
|
||||||
|
(with-temp-buffer
|
||||||
|
(insert "#+BEGIN_DEPS elixir\n[{:jason, \"~> 1.4\"}]\n#+END_DEPS\n\n")
|
||||||
|
(insert "#+BEGIN_SRC elixir\nJason.encode!(%{})\n#+END_SRC\n")
|
||||||
|
(goto-char (point-max))
|
||||||
|
(let ((deps (ob-elixir--find-deps-for-position (point))))
|
||||||
|
(should deps)
|
||||||
|
(should (string-match-p ":jason" deps)))))
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-deps-block-override ()
|
||||||
|
"Test that later deps blocks override earlier ones."
|
||||||
|
(with-temp-buffer
|
||||||
|
(insert "#+BEGIN_DEPS elixir\n[{:jason, \"~> 1.4\"}]\n#+END_DEPS\n\n")
|
||||||
|
(insert "#+BEGIN_SRC elixir\ncode1\n#+END_SRC\n\n")
|
||||||
|
(insert "#+BEGIN_DEPS elixir\n[{:httpoison, \"~> 2.0\"}]\n#+END_DEPS\n\n")
|
||||||
|
(let ((pos (point)))
|
||||||
|
(insert "#+BEGIN_SRC elixir\ncode2\n#+END_SRC\n")
|
||||||
|
(let ((deps (ob-elixir--find-deps-for-position (+ pos 10))))
|
||||||
|
(should deps)
|
||||||
|
(should (string-match-p ":httpoison" deps))
|
||||||
|
(should-not (string-match-p ":jason" deps))))))
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-no-deps-block ()
|
||||||
|
"Test behavior when no deps block exists."
|
||||||
|
(with-temp-buffer
|
||||||
|
(insert "#+BEGIN_SRC elixir\n1 + 1\n#+END_SRC\n")
|
||||||
|
(should (null (ob-elixir--find-deps-for-position (point))))))
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-deps-block-before-position ()
|
||||||
|
"Test that deps block must be before the position."
|
||||||
|
(with-temp-buffer
|
||||||
|
(insert "#+BEGIN_SRC elixir\n1 + 1\n#+END_SRC\n\n")
|
||||||
|
(let ((pos (point)))
|
||||||
|
(insert "#+BEGIN_DEPS elixir\n[{:jason, \"~> 1.4\"}]\n#+END_DEPS\n")
|
||||||
|
;; Position is before deps block, should not find it
|
||||||
|
(should (null (ob-elixir--find-deps-for-position pos))))))
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-deps-hash-consistency ()
|
||||||
|
"Test that same deps produce same hash."
|
||||||
|
(let ((deps1 "[{:jason, \"~> 1.4\"}]")
|
||||||
|
(deps2 "[{:jason, \"~> 1.4\"}]") ; extra space
|
||||||
|
(deps3 "[{:httpoison, \"~> 2.0\"}]"))
|
||||||
|
(should (string= (ob-elixir--hash-deps deps1)
|
||||||
|
(ob-elixir--hash-deps deps2)))
|
||||||
|
(should-not (string= (ob-elixir--hash-deps deps1)
|
||||||
|
(ob-elixir--hash-deps deps3)))))
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-normalize-deps ()
|
||||||
|
"Test deps normalization."
|
||||||
|
;; Extra spaces between tokens get normalized to single space
|
||||||
|
(should (string= (ob-elixir--normalize-deps "[{:a, \"1\"}]")
|
||||||
|
(ob-elixir--normalize-deps "[{:a, \"1\"}]")))
|
||||||
|
;; Comments are stripped
|
||||||
|
(should (string= (ob-elixir--normalize-deps "[{:a, \"1\"}] # comment")
|
||||||
|
(ob-elixir--normalize-deps "[{:a, \"1\"}]"))))
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-normalize-deps-multiline ()
|
||||||
|
"Test normalization of multiline deps."
|
||||||
|
;; Multiline deps with same content should produce the same hash
|
||||||
|
(let ((multiline1 "[\n {:jason, \"~> 1.4\"},\n {:decimal, \"~> 2.0\"}\n]")
|
||||||
|
(multiline2 "[\n{:jason, \"~> 1.4\"},\n{:decimal, \"~> 2.0\"}\n]"))
|
||||||
|
;; Both should hash to the same value since they have equivalent whitespace normalization
|
||||||
|
(should (string= (ob-elixir--hash-deps multiline1)
|
||||||
|
(ob-elixir--hash-deps multiline2)))))
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-deps-block-with-comments ()
|
||||||
|
"Test deps block parsing with Elixir comments."
|
||||||
|
(with-temp-buffer
|
||||||
|
(insert "#+BEGIN_DEPS elixir\n")
|
||||||
|
(insert "[\n")
|
||||||
|
(insert " # JSON library\n")
|
||||||
|
(insert " {:jason, \"~> 1.4\"}\n")
|
||||||
|
(insert "]\n")
|
||||||
|
(insert "#+END_DEPS\n\n")
|
||||||
|
(insert "#+BEGIN_SRC elixir\ncode\n#+END_SRC\n")
|
||||||
|
(goto-char (point-max))
|
||||||
|
(let ((deps (ob-elixir--find-deps-for-position (point))))
|
||||||
|
(should deps)
|
||||||
|
(should (string-match-p ":jason" deps)))))
|
||||||
|
|
||||||
|
;;; Mix Project Template Tests
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-mix-exs-template ()
|
||||||
|
"Test that mix.exs template is valid."
|
||||||
|
(should (stringp ob-elixir--mix-exs-template))
|
||||||
|
(should (string-match-p "defmodule" ob-elixir--mix-exs-template))
|
||||||
|
(should (string-match-p "use Mix.Project" ob-elixir--mix-exs-template))
|
||||||
|
(should (string-match-p "deps()" ob-elixir--mix-exs-template))
|
||||||
|
(should (string-match-p "%s" ob-elixir--mix-exs-template)))
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-mix-exs-generation ()
|
||||||
|
"Test generating mix.exs content."
|
||||||
|
(let ((deps "[{:jason, \"~> 1.4\"}]")
|
||||||
|
(mix-exs (format ob-elixir--mix-exs-template "[{:jason, \"~> 1.4\"}]")))
|
||||||
|
(should (string-match-p ":jason" mix-exs))
|
||||||
|
(should (string-match-p "~> 1.4" mix-exs))))
|
||||||
|
|
||||||
|
;;; Cache Directory Tests
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-cache-dir-default ()
|
||||||
|
"Test default cache directory."
|
||||||
|
(should (stringp ob-elixir-deps-cache-dir))
|
||||||
|
(should (string-match-p "ob-elixir" ob-elixir-deps-cache-dir)))
|
||||||
|
|
||||||
|
;;; Integration Tests (require network and mix)
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-deps-project-creation ()
|
||||||
|
"Test creating a temporary Mix project with deps."
|
||||||
|
:tags '(:integration :network)
|
||||||
|
(skip-unless (and (executable-find "mix")
|
||||||
|
(executable-find "elixir")))
|
||||||
|
(let ((ob-elixir-deps-cache-dir (make-temp-file "ob-elixir-test-" t))
|
||||||
|
(deps "[{:jason, \"~> 1.4\"}]"))
|
||||||
|
(unwind-protect
|
||||||
|
(let ((project-dir (ob-elixir--get-deps-project deps)))
|
||||||
|
(should (file-directory-p project-dir))
|
||||||
|
(should (file-exists-p (expand-file-name "mix.exs" project-dir)))
|
||||||
|
(should (file-directory-p (expand-file-name "deps/jason" project-dir))))
|
||||||
|
;; Cleanup
|
||||||
|
(ob-elixir-cleanup-deps-projects)
|
||||||
|
(when (file-directory-p ob-elixir-deps-cache-dir)
|
||||||
|
(delete-directory ob-elixir-deps-cache-dir t)))))
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-deps-execution ()
|
||||||
|
"Test executing code with deps."
|
||||||
|
:tags '(:integration :network)
|
||||||
|
(skip-unless (and (executable-find "mix")
|
||||||
|
(executable-find "elixir")))
|
||||||
|
(let ((ob-elixir-deps-cache-dir (make-temp-file "ob-elixir-test-" t))
|
||||||
|
(deps "[{:jason, \"~> 1.4\"}]"))
|
||||||
|
(unwind-protect
|
||||||
|
(let ((result (ob-elixir--execute-with-deps
|
||||||
|
"Jason.encode!(%{a: 1})"
|
||||||
|
'value
|
||||||
|
deps)))
|
||||||
|
;; Result should contain JSON output with key "a"
|
||||||
|
;; Jason.encode! returns "{\"a\":1}" which when inspected becomes "\"{\\\"a\\\":1}\""
|
||||||
|
(should (string-match-p "a" result)))
|
||||||
|
;; Cleanup
|
||||||
|
(ob-elixir-cleanup-deps-projects)
|
||||||
|
(when (file-directory-p ob-elixir-deps-cache-dir)
|
||||||
|
(delete-directory ob-elixir-deps-cache-dir t)))))
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-deps-caching ()
|
||||||
|
"Test that deps projects are cached and reused."
|
||||||
|
:tags '(:integration :network)
|
||||||
|
(skip-unless (executable-find "mix"))
|
||||||
|
(let ((ob-elixir-deps-cache-dir (make-temp-file "ob-elixir-test-" t))
|
||||||
|
(deps "[{:jason, \"~> 1.4\"}]"))
|
||||||
|
(unwind-protect
|
||||||
|
(let ((project1 (ob-elixir--get-deps-project deps))
|
||||||
|
(project2 (ob-elixir--get-deps-project deps)))
|
||||||
|
;; Should return same directory
|
||||||
|
(should (string= project1 project2))
|
||||||
|
;; Should only have one entry in hash table
|
||||||
|
(should (= 1 (hash-table-count ob-elixir--deps-projects))))
|
||||||
|
;; Cleanup
|
||||||
|
(ob-elixir-cleanup-deps-projects)
|
||||||
|
(when (file-directory-p ob-elixir-deps-cache-dir)
|
||||||
|
(delete-directory ob-elixir-deps-cache-dir t)))))
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-different-deps-different-projects ()
|
||||||
|
"Test that different deps create different projects."
|
||||||
|
:tags '(:integration :network)
|
||||||
|
(skip-unless (executable-find "mix"))
|
||||||
|
(let ((ob-elixir-deps-cache-dir (make-temp-file "ob-elixir-test-" t))
|
||||||
|
(deps1 "[{:jason, \"~> 1.4\"}]")
|
||||||
|
(deps2 "[{:decimal, \"~> 2.0\"}]"))
|
||||||
|
(unwind-protect
|
||||||
|
(let ((project1 (ob-elixir--get-deps-project deps1))
|
||||||
|
(project2 (ob-elixir--get-deps-project deps2)))
|
||||||
|
;; Should be different directories
|
||||||
|
(should-not (string= project1 project2))
|
||||||
|
;; Should have two entries in hash table
|
||||||
|
(should (= 2 (hash-table-count ob-elixir--deps-projects))))
|
||||||
|
;; Cleanup
|
||||||
|
(ob-elixir-cleanup-deps-projects)
|
||||||
|
(when (file-directory-p ob-elixir-deps-cache-dir)
|
||||||
|
(delete-directory ob-elixir-deps-cache-dir t)))))
|
||||||
|
|
||||||
|
;;; Cleanup Tests
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-cleanup-clears-hash-table ()
|
||||||
|
"Test that cleanup clears the hash table."
|
||||||
|
(let ((ob-elixir--deps-projects (make-hash-table :test 'equal)))
|
||||||
|
(puthash "test-hash" "/tmp/test-dir" ob-elixir--deps-projects)
|
||||||
|
(should (= 1 (hash-table-count ob-elixir--deps-projects)))
|
||||||
|
;; Cleanup should clear the table (dir doesn't exist, so no error)
|
||||||
|
(ob-elixir-cleanup-deps-projects)
|
||||||
|
(should (= 0 (hash-table-count ob-elixir--deps-projects)))))
|
||||||
|
|
||||||
|
(provide 'test-ob-elixir-deps)
|
||||||
|
;;; test-ob-elixir-deps.el ends here
|
||||||
Reference in New Issue
Block a user