20 KiB
20 KiB
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:
- Test each function in isolation (unit tests)
- Test the integration with org-mode (integration tests)
- Be runnable in CI/CD (batch mode tests)
- 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:
;;; 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:
;;; 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:
;;; 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:
;;; 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:
;;; 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:
;;; 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
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 testruns 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 filetest/test-ob-elixir-core.el- Core teststest/test-ob-elixir-vars.el- Variable teststest/test-ob-elixir-results.el- Result teststest/test-ob-elixir-errors.el- Error teststest/test-ob-elixir-org.el- Org integration tests