mirror of
https://github.com/chrisbarrett/nursery
synced 2025-08-01 12:17:22 -05:00
246 lines
7.3 KiB
EmacsLisp
246 lines
7.3 KiB
EmacsLisp
;;; ert-bbd.el --- BBD-style syntax for ERT -*- lexical-binding: t; -*-
|
||
|
||
;;; Commentary:
|
||
|
||
;; Provides a nicer syntax for defining tests to run via ERT. It uses similar
|
||
;; BDD-style tests to `buttercup', but since it uses ERT it is better-suited to
|
||
;; interactive development.
|
||
;;
|
||
;; The main functions are:
|
||
;;
|
||
;; - `+describe' which encloses a series of tests, and
|
||
;;
|
||
;; - `+it', which describes a test cases--usually a single assertion using
|
||
;; ert's `should', `should-not', `should-error', etc.
|
||
|
||
;;; Code:
|
||
|
||
(require 'cl-lib)
|
||
(require 'ert)
|
||
|
||
;;; HACK: Initially define no-op so tests can be defined inline.
|
||
|
||
(cl-eval-when (compile load eval)
|
||
(unless (macrop 'ert-bdd)
|
||
(defmacro ert-bdd (&rest _))))
|
||
|
||
|
||
;; Since the macro is the primary autoloaded entrypoint, the functions it uses
|
||
;; must be evaluated when byte-compiling.
|
||
|
||
(eval-and-compile
|
||
(defun ert-bdd--render-test-name (desc-stack)
|
||
"Build a test name, as a symbol, from a stack of descriptions.
|
||
|
||
DESC-STACK is a list of descriptions collected from surrounding
|
||
`+describe' and `+it' forms."
|
||
|
||
(when (null desc-stack)
|
||
(error "Input must be non-empty"))
|
||
|
||
(let* ((ordered (seq-reverse (seq-map (lambda (+it) (format "%s" +it)) desc-stack)))
|
||
(transformed (string-replace " " "-"
|
||
(string-join ordered "--"))))
|
||
(intern transformed)))
|
||
|
||
(defun ert-bdd-compile (form &optional inside-it-p)
|
||
(cl-labels ((compile (form desc-stack inside-it-p)
|
||
(pcase form
|
||
|
||
(`(+it ,desc . ,body)
|
||
|
||
;; (when inside-it-p
|
||
;; (error "Cannot write an `+it' inside another `+it'"))
|
||
|
||
(cl-assert (or (stringp desc) (symbolp desc)))
|
||
|
||
`(ert-deftest ,(ert-bdd--render-test-name (cons desc desc-stack)) ()
|
||
,@(seq-map (lambda (it) (compile it nil t))
|
||
body)))
|
||
|
||
|
||
(`(+describe ,desc . ,body)
|
||
|
||
;; (when inside-it-p
|
||
;; (error "Cannot write a `+describe' inside an `+it'"))
|
||
|
||
(cl-assert (or (stringp desc) (symbolp desc)))
|
||
|
||
(let* ((new-stack (cons desc desc-stack))
|
||
(new-body (seq-map (lambda (it) (compile it new-stack nil)) body)))
|
||
(pcase new-body
|
||
(`() nil)
|
||
(`(,x) x)
|
||
(xs `(progn ,@xs)))))
|
||
|
||
((pred listp)
|
||
(seq-map (lambda (it) (compile it desc-stack inside-it-p))
|
||
form))
|
||
|
||
(_
|
||
form))))
|
||
(compile form nil inside-it-p))))
|
||
|
||
|
||
;;;###autoload
|
||
(defmacro +describe (desc &rest forms)
|
||
"Declare a suite of ERT tests using BDD syntax.
|
||
|
||
DESC is a description of the test suite--either a symbol or a
|
||
string.
|
||
|
||
Within FORMS, you may use additional BDD-style `+describe' forms
|
||
to build up a hierarchy of tests. Tests within these blocks are
|
||
declared using `+it'.
|
||
|
||
For example:
|
||
|
||
\(+describe \"arithmetic operations\"
|
||
(let ((input 100))
|
||
(+describe \"addition\"
|
||
(+it \"has an identity (zero)\"
|
||
(should (equal (+ 0 input) input))))
|
||
|
||
;; etc...
|
||
))
|
||
|
||
Tests will be excluded from byte-compiled output."
|
||
(declare (indent 1))
|
||
(unless load-file-name
|
||
(ert-bdd-compile `(+describe ,desc ,@forms))))
|
||
|
||
;;;###autoload
|
||
(defmacro +it (desc &rest forms)
|
||
"An ERT test case using BDD syntax.
|
||
|
||
DESC is a description of the test case--either a symbol or a
|
||
string. It will be concatenated with the descriptions from
|
||
enclosing `+describe' forms.
|
||
|
||
FORMS are the implementation of the test, and should use ert
|
||
macros like `should', `should-not' and `should-error'.
|
||
|
||
Tests will be excluded from byte-compiled output."
|
||
(declare (indent 1))
|
||
(unless load-file-name
|
||
(ert-bdd-compile `(+it ,desc ,@forms) t)))
|
||
|
||
|
||
;;; Tests - nothing like a bit of dogfooding!
|
||
|
||
(+describe ert-bdd-render-test-name
|
||
|
||
(+describe "empty stack"
|
||
(+it "errors"
|
||
(should-error (ert-bdd--render-test-name nil))))
|
||
|
||
(+describe "one string element in stack"
|
||
(+it "renders that element"
|
||
(should (equal (ert-bdd--render-test-name '("input"))
|
||
'input))))
|
||
|
||
(+describe "mix of symbols and strings in stack"
|
||
(+it "renders those element"
|
||
(should (equal (ert-bdd--render-test-name '("a" b "c"))
|
||
'c--b--a))))
|
||
|
||
(+describe "input sanitisation"
|
||
(+it "converts spaces to dashes"
|
||
(should (equal (ert-bdd--render-test-name '("a b"))
|
||
'a-b)))))
|
||
|
||
|
||
(+describe ert-bdd-compile
|
||
|
||
(+describe "input is a `+describe'"
|
||
|
||
(+describe "no body forms"
|
||
(+it "has no body forms in output"
|
||
(should (equal (ert-bdd-compile
|
||
'(+describe test-name))
|
||
nil))))
|
||
|
||
(+describe "one body form"
|
||
(+it "outputs those forms"
|
||
(should (equal (ert-bdd-compile
|
||
'(+describe test-name x))
|
||
'x))))
|
||
|
||
(+describe "many body forms"
|
||
(+it "outputs those forms"
|
||
(should (equal (ert-bdd-compile
|
||
'(+describe test-name x y))
|
||
'(progn x y)))))
|
||
|
||
(+describe "input contains no `+it' forms"
|
||
(let ((input '(let* ((x 1)
|
||
(y 2))
|
||
x
|
||
y
|
||
(defun foo ()
|
||
(+ x y)))))
|
||
(+it "does not transform its input"
|
||
(should (equal (ert-bdd-compile input) input))))))
|
||
|
||
|
||
(+describe "input is an `+it'"
|
||
|
||
(+describe "no body forms"
|
||
(+it "generated test has no body"
|
||
(should (equal (ert-bdd-compile
|
||
'(+it test-name))
|
||
'(ert-deftest test-name ())))))
|
||
|
||
(+describe "one body form"
|
||
(+it "generated test has that form as its body"
|
||
(should (equal (ert-bdd-compile
|
||
'(+it test-name x))
|
||
'(ert-deftest test-name ()
|
||
x)))))
|
||
|
||
(+describe "many body forms"
|
||
(+it "generated test has those body forms"
|
||
(should (equal (ert-bdd-compile
|
||
'(+it test-name x y))
|
||
'(ert-deftest test-name ()
|
||
x
|
||
y)))))
|
||
|
||
(+describe "test name is a symbol"
|
||
(+it "produces the expected test"
|
||
(should (equal
|
||
(ert-bdd-compile
|
||
'(+it test-name
|
||
(should (equal 1 2))))
|
||
'(ert-deftest test-name ()
|
||
(should (equal 1 2)))))))
|
||
|
||
(+describe "test name is a string"
|
||
(+it "produces the expected test"
|
||
(should (equal
|
||
(ert-bdd-compile
|
||
'(+it "test name"
|
||
(should (equal 1 2))))
|
||
'(ert-deftest test-name ()
|
||
(should (equal 1 2))))))))
|
||
|
||
|
||
(+describe "an +it is wrapped in another form"
|
||
(+it "is still macro-expanded"
|
||
(should (equal
|
||
|
||
(ert-bdd-compile
|
||
'(let ((x 1))
|
||
(+it "test name"
|
||
(should (= x 1)))))
|
||
|
||
|
||
'(let ((x 1))
|
||
(ert-deftest test-name ()
|
||
(should (= x 1)))))))))
|
||
|
||
|
||
(provide 'ert-bdd)
|
||
|
||
;;; ert-bdd.el ends here
|