Exemplify-ERT — Clean examples that double as ERT declarations (Emacs package)
Below you find the latest version of (1) the package's README and (2) its main source file.
For the git repository and issue tracker, see the project's page on sr.ht.
For more packages, see Software.
Overview
Exemplify-ERT helps you write clean-looking examples that double as regression tests.
Features
Clean and readable
Write your tests as clean-looking equalities that don't have the noise of shoulds
and should-nots
— but still use ERT in the background.
So instead of this:
(ert-deftest + () (should (equal (+ 21 21) 42)) (should (equal (+ 30 12) 42)) (should-error (eval '(+ "4" 2) nil) :type 'error)) (ert-deftest car () (should (equal (car '(:a 1)) :a)) (should-error (eval '(car) nil) :type 'wrong-number-of-arguments))
you simply write this:
(exemplify-ert + (+ 21 21) => 42 (+ 30 12) => 42 (+ "4" 2) !!> error) (exemplify-ert car (car '(:a 1)) => :a (car) !!> wrong-number-of-arguments)
So:
- Easier to write tests.
- Easier to read and understand tests at a glance.
- Tests easily exportable as they are to be shown as usage examples.
One keypress to (re)insert and align evaluations
Exemplify-Align, Exemplify-Eval, and Exemplify-ERT are independent libraries that complement each other:
- Exemplify-Align — align arrowheads (easier to compare and make sense of results).
- Exemplify-Eval — (re)insert arrow and evaluation of one or more sexps — with a single keypress.
- Exemplify-ERT — run tests represented by pairs such as
(+ 30 10 2) => 42
.
Ready for your README
The format is compatible with OrgReadme-fy, which can export your tests/examples into a nicely-formatted README.org
. Like this:
#+name: examples-+ #+begin_src emacs-lisp (+ 21 21) => 42 (+ 30 12) => 42 (+ "4" 2) !!> error #+end_src #+name: examples-car #+begin_src emacs-lisp (car '(:a 1)) => :a (car) !!> wrong-number-of-arguments #+end_src
Ready for Help buffers and library overview
The format is compatible with Democratize, which can import all your examples and, among other things:
- Insert the examples into the Help (or Helpful) buffer of the specific function the user is looking up.
- Show, in a new buffer, an Org tree with all functions for which there are examples, with examples source blocks included in the leaves.
This second one is a "cheat sheet": it gives the user a browsable overview of the library's functions and usage examples — which is great for studying the library.
Several equality functions
If only should-equal
and should-error
aren't enough for you, Exemplify-ERT offers a list of other pre-defined equality functions you can use.
'(( => . equal ) ; regular equality (Hp==> . h-pr== ) ; equality for hash tables ├> Available in ( Hp=> . h-pr= ) ; equality for hash tables │ package 'xht ( H==> . h== ) ; equality for hash tables │ by the same ( H=> . h= ) ; equality for hash tables │ author. They ( H_=> . h_= ) ; equality for hash tables │ allow you to ( H~=> . h~= ) ; equality for hash tables │ compare objs ( O=> . h-orgtbl=) ; equality for org table strings │ with same kv ( A=> . h-alist= ) ; equality for association lists │ pairs. ( P=> . h-plist= ) ; equality for property lists │ ( L=> . h-lol= ) ; equality for lists of lists │ ( T=> . h-tsv= ) ; equality for TSVs │ ( C=> . h-csv= ) ; equality for CSVs │ ( S=> . h-ssv= ) ; equality for SSVs │ ( J=> . h-json= ) ; equality for JSONs │ ( K=> . h-kvl= ) ; equality for key–value lines │ ( ~> . exemplify-ert-approx=)) ; equality for floats
Let's see a couple of usage examples for these operators.
(Nevermind the two test names I use here — see note about test names further below.)
Example: plist equality
This:
(exemplify-ert are-these-plists-equivalent-p '(:a 1 :b (:c 3)) P=> '(:b (:c 3) :a 1) '(:a 1 :b 2) P=> '(:b 2 :a 1) '(:a 1 :b 2) P=> '(:b 2 :a 1 :a 4))
expands to this:
(ert-deftest are-these-plists-equivalent-p nil (should (h-plist= '(:a 1 :b (:c 3)) '(:b (:c 3) :a 1))) (should (h-plist= '(:a 1 :b 2) '(:b 2 :a 1))) (should (h-plist= '(:a 1 :b 2) '(:b 2 :a 1 :a 4))))
where h-plist=
is a function from xht that tests the equivalence of plists.
Because, you see:
(plist-get '(:a 1 :b 2) :a) => 1 (plist-get '(:b 2 :a 1 :a 4) :a) => 1 (plist-get '(:a 1 :b 2) :b) => 2 (plist-get '(:b 2 :a 1 :a 4) :b) => 2
Example: hash table equality
Since you simply can't test hash table equality with #'equal
, you may find H=>
(which corresponds to xht's function #'h=
) to be particularly useful:
(equal #s(hash-table) #s(hash-table)) => nil ;!! (h= #s(hash-table) #s(hash-table)) => t (h= #s(hash-table data (:a 1 :b 2)) #s(hash-table data (:a 1 :b 2))) => t (h= (h-new) #s(hash-table test equal)) => t
(exemplify-ert are-these-hash-tables-equivalent-p #s(hash-table) H=> #s(hash-table) (h* :a 1 :b 2) H=> (h* :a 1 :b 2) (h-new) H=> #s(hash-table test equal))
A note about test names
OrgReadme-Fy and Democratize need your test to be named after the function it's supposed to be testing. Keep this in mind if you want your Exemplify-ERT tests to integrate with either (or both) of these libraries.
So in spite of the names I used to illustrate the previous examples, this is how you'd usually want to name your tests:
(exemplify-ert + (+ 21 21) => 42 (+ 30 12) => 42 (+ "4" 2) !!> error) (exemplify-ert -uniq (-> "abracadabra" (append nil) -uniq concat) => "abrcd" (-uniq '(4 2 1 4 2 2 5 2 4 1 1 4 5 2)) => '(4 2 1 5) (-uniq '(1 1 1 1 1 1)) => '(1) (-uniq '()) => '() (-uniq 42) !!> error) (exemplify-ert h-new ;;;; Equivalence with native hash table functions (h-new) H=> (make-hash-table :test 'equal) (h-new nil 'eq) H=> (make-hash-table :test 'eq) (h-new nil 'eql) H=> (make-hash-table) ;;;; Actual evaluations (h-new) H=> #s(hash-table test equal) (h-new nil 'eq) H=> #s(hash-table test eq) (h-new nil 'eql) H=> #s(hash-table))
A note about notation
The Emacs Lisp Manual uses this notation:
Arrow | Meaning |
---|---|
⇒ |
final evaluation |
↦ |
macro expansion |
≡ |
two forms that produce identical results |
⊣ |
printed text |
error→ |
errors |
We can think of the first two as being special cases of the third.
This is the corresponding notation in Exemplify-ERT:
Arrow | Meaning |
---|---|
=> |
two forms that produce identical results |
!!> |
errors |
Usually, you will want to show the final evaluation.
But sometimes you want to exemplify something else.
Sometimes you want to show that two expressions evaluate to the same value.
The =>
operator represents #'equal
, so it offers some flexibility.
These three are valid examples that would pass your tests:
(exemplify-ert caddr (caddr '(a b c)) => 'c ; final evaluation (caddr '(a b c)) => (car (cdr (cdr '(a b c)))) ; macro expansion (caddr '(a b c)) => (nth 2 '(a b c))) ; other equivalence
because the right side of the second and third would also evaluate to 'c
— which evaluates to itself.
Is Exemplify-ERT for you?
More so in proportion to how many of the following questions you can answer ‘yes’ to:
- Are you going to write regression tests for your Emacs project?
- Can most of them be expressed as equalities (or error)?
- Would you like to be able to export them automatically to your library's README (with OrgReadme-fy)?
- Does the way that the examples are represented in
xht
'sREADME.org
please you?
Background
The idea for this package came from seeing how clean Dash's ERT tests looked, then understanding what was going on by looking at dev/dash-defs.el
. I saw that all the functions' descriptions and examples in Dash's README.md
were being automatically generated from dash.el
and dev/examples.el
.
I found that interesting. Much later, when writing xht
(an extensive hash tables library for Emacs), I slightly adapted it so that I could more easily and cleanly write my own ERT tests for it.
It worked quite well. My xht/dev/examples.el
ended up with more than 11000 lines: I personally wrote more than 2300 such tests for it in this format.
I realized that dash-defs.el
was doing two different things that were closely related but fundamentally independent:
- Providing macros for writing cleaner ERT tests that look like, and double as, examples.
- Providing functions for automatically generating documentation from these examples and from functions' docstrings.
To build on and further develop these ideas, I wrote two packages: Exemplify-ERT and OrgReadme-fy.
Exemplify-ERT and OrgReadme-fy
These two packages are independent from, but complementary to, each other:
- Exemplify-ERT is about making it more pleasant to write ERT tests for your project. You can use it just for that, even if you don't plan to ever write a README.
- OrgReadme-fy is about bootstrapping a
README.org
from what you already have. If you do have anexamples.el
created with Exemplify-ERT, it will use that to fetch your examples. If you don't, that's fine: you can use it just for generating the initial skeleton and (more importantly) for automatically producing tables and subtrees with the information that is already there in your package's functions and docstrings.
If you use them together for your library, the effect will be that:
- Your
dev/examples.el
file will have readable examples that double as ERT tests. - Your elisp file will have code (defuns, defvars, defcustoms) plus its documentation.
- Your
README.org
will use both this documentation and the examples fromdev/examples.el
.
Expanding the above:
- The elisp file will be the single source of truth for documentation about the functions and commands. This documentation will be seen in context, either inside (as doc strings) or before (as
orgreadme-fy-describe
strings) the functions' code. - This very same documentation can be automatically translated to your
README.org
. But here, instead of the functions' code, you'll have examples of usage of the functions, coming straight from yourdev/examples.el
file. - Your
dev/examples.el
file doubles as ERT tests and examples of usage. Because of the clean syntax they use, these examples are imported to theREADME.org
exactly as they are. - Hierarchical headers in the elisp file (
;;;+
) will be automatically translated as hierarchical headings for theCommands
andFunctions
headings of yourREADME.org
. - Your
README.org
is generated from areadme-template.org
. This file has the code blocks that import both the documentation and the examples.
- You can configure these code blocks to your liking: what to exclude, at what depth the headings start, whether to include summary tables, where to insert them, spacing, etc.
- You write there as well the contents of all other headings that you want to include in your README — everything that doesn't seem fit to be in the elisp file (as is the case of the
Overview
andBackground
headings you're reading right now).
- You can configure these code blocks to your liking: what to exclude, at what depth the headings start, whether to include summary tables, where to insert them, spacing, etc.
Differences from Dash's implementation
In Exemplify-ERT some things are different.
Documentation structure and contents happens in the main library file, not in examples.el
I decided to rename the macro defexamples
to exemplify-ert
and leave them at top level in examples.el after removing def-example-groups
. See OrgReadme-fy's README for a longer explanation why.
Other than that, the format is the same
(exemplify-ert -uniq (-> "abracadabra" (append nil) -uniq concat) => "abrcd" (-uniq '(4 2 1 4 2 2 5 2 4 1 1 4 5 2)) => '(4 2 1 5) (-uniq '(1 1 1 1 1 1)) => '(1) (-uniq '()) => '() (-uniq 42) !!> error)
So the above exemplifies the use of -uniq
to the reader and are ERT tests.
Installation
See my page Software for the most up-to-date instructions on how to download and install any of my Emacs packages.
Having downloaded and installed the package and its dependencies, adapt the configurations below to your init.el
file.
(use-package exemplify-ert :demand t :config (global-exemplify-ert-fontify-mode))
Alternatively, if you don’t have use-package
:
(require 'exemplify-ert)
Functions and commands
Here's an overview of this package's functions that are likely to be of your interest:
Function | Summary |
---|---|
exemplify-ert | Define ERT tests for function FUN and its EXAMPLES. |
exemplify-ert-make-new | Create an examples/tests file for your new project. |
exemplify-ert-fontify-mode | Toggle fontification for Exemplify-Ert. |
global-exemplify-ert-fontify-mode | Toggle Exemplify-Ert-Fontify mode in all buffers. |
exemplify-ert-see-readme | Open exemplify-ert's README.org file. |
exemplify-ert-see-news | See the News in exemplify-ert's README.org file. |
They are described in more detail below.
Commands
Create examples/tests file
exemplify-ert-make-new ()
Create an examples/tests file for your new project.
Run it interactively and confirm filename and directory.
(If in doubt, go with ./dev/examples.el as suggested.)
It will also write (if not yet existent) run-tests.sh. You can use
it to run the tests from the terminal in batch mode instead of, or
in addition to, running them interactively.
Minor mode
exemplify-ert-fontify-mode (&optional arg)
Toggle fontification for Exemplify-Ert.
This is a buffer-local minor mode intended for Emacs Lisp buffers.
Enabling it causes syntax highlighting of exemplify-ert's macros and
equality operators.
See also exemplify-ert-fontify-mode-lighter
and
global-exemplify-ert-fontify-mode.
This is a minor mode. If called interactively, toggle the
`Exemplify-Ert-Fontify mode' mode. If the prefix argument is positive,
enable the mode, and if it is zero or negative, disable the mode.
If called from Lisp, toggle the mode if ARG is toggle
. Enable the
mode if ARG is nil, omitted, or is a positive number. Disable the mode
if ARG is a negative number.
To check whether the minor mode is enabled in the current buffer,
evaluate the variable exemplify-ert-fontify-mode.
The mode's hook is called both when the mode is enabled and when it is
disabled.
global-exemplify-ert-fontify-mode (&optional arg)
Toggle Exemplify-Ert-Fontify mode in all buffers.
With prefix ARG, enable Global Exemplify-Ert-Fontify mode if ARG is positive;
otherwise, disable it.
If called from Lisp, toggle the mode if ARG is toggle
.
Enable the mode if ARG is nil, omitted, or is a positive number.
Disable the mode if ARG is a negative number.
Exemplify-Ert-Fontify mode is enabled in all buffers where
exemplify-ert--turn-on-fontify-mode
would do it.
See exemplify-ert-fontify-mode for more information on Exemplify-Ert-Fontify
mode.
See README
exemplify-ert-see-readme (&optional heading narrow)
Open exemplify-ert's README.org file.
Search for the file in exemplify-ert.el's directory.
If found, open it read-only.
If optional argument HEADING is passed, try to navigate to the
heading after opening it. HEADING should be a string.
If optional argument NARROW is non-nil, narrow to that heading.
This argument has no effect if HEADING is nil or not found.
Contributing
See my page Software for information about how to contribute to any of my Emacs packages.
News
0.6.1
Changes
Obsolescence
Customizable variables
exemplify-ert-enable-fontlock
Use global-exemplify-ert-fontify-mode instead.
0.6.0
New features
New commands
exemplify-ert-see-readme
to open this library's local README.org.
exemplify-ert-see-news
to open README.org, narrow into the News heading, skip to the latest, recenter, and show branches.
License
This project follows the REUSE Specification (FAQ), which in turn is built upon SPDX.
Therefore, license and copyright information can be found in:
- each file's comment header, or
- an adjacent file of the same name with the additional extension
.license
, or - the
.reuse/dep5
file
The full text of the licenses can be found in the LICENSES
subdirectory.
exemplify-ert.el
Structure
;;; exemplify-ert.el --- Clean examples that double as ERT declarations -*- lexical-binding: t -*- ;;; Commentary: ;;;; For all the details, please do see the README ;;; Acknowledgments: ;;; Code: ;;;; Libraries ;;;; Obsolete symbols ;;;; Package metadata ;;;; Customizable variables ;;;; Other variables ;;;; Functions ;;;;; Equality-defining functions ;;;;; From examples to ERT ;;;;; Support ;;;; Commands ;;;;; Create examples/tests file ;;;;; Minor mode ;;;;; See README ;;;;; See News ;;;; Wrapping up ;;; exemplify-ert.el ends here
Contents
;;; exemplify-ert.el --- Clean examples that double as ERT declarations -*- lexical-binding: t -*- ;; SPDX-FileCopyrightText: © flandrew <https://flandrew.srht.site/listful> ;;--------------------------------------------------------------------------- ;; Author: flandrew ;; Created: 2022-03-31 ;; Updated: 2025-08-23 ;; Keywords: maint, tools, lisp ;; Homepage: <https://flandrew.srht.site/listful/software.html> ;;--------------------------------------------------------------------------- ;; Package-Version: 0.6.3 ;; Package-Requires: ((emacs "25.1") (dash "2.14") (s "1.12") (f "0.20")) ;;--------------------------------------------------------------------------- ;; SPDX-License-Identifier: GPL-3.0-or-later ;; This file 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 file 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 file. If not, see <https://www.gnu.org/licenses/>. ;;; Commentary: ;; ;; Exemplify-ERT helps you write regression tests that double as clean-looking ;; examples. Features: ;; ;; - Write your tests as clean-looking equalities that don't have the noise of ;; shoulds and should-nots — but still use ERT behind the scenes. ;; ;; - If should-equal and should-error aren't enough for you, there's a list of ;; other pre-defined equality functions you can use. ;; ;; ;; Exemplify-ERT is independent from, but goes hand in hand with: ;; ;; - Exemplify-Align, with which you can align the arrowheads of evaluations. ;; ;; - Exemplify-Eval, with which you can easily (re)insert arrow and evaluation ;; of one or more sexps — with a single keypress. ;; ;; - OrgReadme-fy, in case you wish to export your tests/examples into a ;; nicely-formatted README.org. ;; ;; - Democratize, which can make your examples show up in Help buffers. ;; ;;;; For all the details, please do see the README ;; ;; Open it easily with: ;; (find-file-read-only "README.org") <--- C-x C-e here¹ ;; ;; or from any buffer: ;; M-x exemplify-ert-see-readme ;; ;; or read it online: ;; <https://flandrew.srht.site/listful/sw-emacs-exemplify-ert.html> ;; ;; ¹ or the key that ‘eval-last-sexp’ is bound to, if not C-x C-e. ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;; Acknowledgments: ;; ;; The idea for this library came after seeing how clean Dash's ERT tests ;; looked in Dash's dev/examples.el. It uses code from there. ;; ;; After using the examples structure myself while writing XHT, I decided to ;; further develop the idea. The result was two independent packages that go ;; well together: Exemplify-ERT and OrgReadme-fy. ;; ;; A few of this library's functions have borrowed and changed code used in ;; Dash development, most notably the ‘exemplify-ert’ macro itself, ;; ‘exemplify-ert-approx=’, ‘exemplify-ert-example->test’, and the ;; ‘exemplify-ert-fontify-mode’. ;; ;; My thanks to Magnar Sveen and the other Dash contributors. ;; ;; dash ;; SPDX-FileCopyrightText: © 2012 Free Software Foundation, Inc. ;; SPDX-License-Identifier: GPL-3.0-or-later ;; Author: Magnar Sveen ;; Homepage: <https://github.com/magnars/dash.el> ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;; Code: ;;;; Libraries (require 'f) (require 's) (require 'rx) (require 'ert) (require 'dash) (require 'lisp-mnt) ; ‘lm-summary’, ‘lm-homepage’, ‘lm-version’, ‘lm-header’ ;;;; Obsolete symbols (define-obsolete-function-alias 'approx= 'exemplify-ert-approx= "0.6.0") (make-obsolete-variable 'exemplify-ert-enable-fontlock #'global-exemplify-ert-fontify-mode "0.6.1") ;;;; Package metadata (defvar exemplify-ert--name "Exemplify-ERT") (defvar exemplify-ert--dot-el (format "%s.el" (file-name-sans-extension (eval-and-compile (or load-file-name buffer-file-name))))) (defvar exemplify-ert--readme-org (expand-file-name "README.org" (file-name-directory exemplify-ert--dot-el))) (defvar exemplify-ert--summary (lm-summary exemplify-ert--dot-el)) (defvar exemplify-ert--homepage (lm-homepage exemplify-ert--dot-el)) (defvar exemplify-ert--version (lm-with-file exemplify-ert--dot-el (or (lm-header "package-version") (lm-version)))) ;;;; Customizable variables (defgroup exemplify-ert nil (format "%s." exemplify-ert--summary) :group 'lisp :link '(emacs-library-link :tag "Lisp file" "exemplify-ert.el") :link `(file-link :tag "README.org" ,exemplify-ert--readme-org) :link `(url-link :tag "Homepage" ,exemplify-ert--homepage)) (defcustom exemplify-ert-dev-subdir "dev" "Subdirectory that will have your examples file. The examples file's name is defined by ‘exemplify-ert-examples-filename’." :package-version '(exemplify-ert "0.5.0") :type 'string) (defcustom exemplify-ert-examples-filename "examples.el" "Name of your examples file. It will be stored under ‘exemplify-ert-dev-subdir’." :package-version '(exemplify-ert "0.5.0") :type 'string) ;; See also, under Minor mode: ;; ‘exemplify-ert-fontlock-add-equality-operators’, ;; ‘exemplify-ert-fontlock-add-exemplify-ert-macros’, ;; ‘exemplify-ert-fontify-mode-lighter’. ;; ‘exemplify-ert-enable-fontlock’ ;;;; Other variables (defvar exemplify-ert-ops-default '(( => . equal ) ; regular equality (Hp==> . h-pr== ) ; equality for hash tables ├> Available in ( Hp=> . h-pr= ) ; equality for hash tables │ package 'xht ( H==> . h== ) ; equality for hash tables │ by the same ( H=> . h= ) ; equality for hash tables │ author. They ( H_=> . h_= ) ; equality for hash tables │ allow you to ( H~=> . h~= ) ; equality for hash tables │ compare objs ( O=> . h-orgtbl=) ; equality for org table strings │ with same kv ( A=> . h-alist= ) ; equality for association lists │ pairs. ( P=> . h-plist= ) ; equality for property lists │ ( L=> . h-lol= ) ; equality for lists of lists │ ( T=> . h-tsv= ) ; equality for TSVs │ ( C=> . h-csv= ) ; equality for CSVs │ ( S=> . h-ssv= ) ; equality for SSVs │ ( J=> . h-json= ) ; equality for JSONs │ ( K=> . h-kvl= ) ; equality for key–value lines │ ( ~> . exemplify-ert-approx=)) ; equality for floats "An alist of equality operators that can be used for your examples. To use any of these starting with h- you'll need to (require \\='xht) in your examples.el. They allow the comparison of hash tables, alists and others. You don't need to use them, and they being on this list will cause no errors. The most common one is ‘=>’, which will use ‘equal’ to compare both sides of the equality. The ‘~>’ one allows small differences between floats. Use ‘!!>’ to signal error. You may add new ones, or even override these, by customizing the variable ‘exemplify-ert-ops-user’.") (defvar exemplify-ert-ops-user nil "An alist of user-added equality operators. You may add new ones according to what you normally use. This list will be checked before ‘exemplify-ert-ops-default’, so you may override the defaults if you want (though I'd suggest to pick some different symbol instead). To add your own operators, do it directly in the examples.el file. It should look somewhat like this: (setq exemplify-ert-ops-user \\='(( str=> . string= ) (char=> . char-equal))) After which you can now add examples such as: (myfunc1 x y str=> \"xy\") (myfunc2 ?x ?y char=> ?x) \(Although note that plain ‘equal’, which is behind the ‘=>’, could accomplish the above already, so you probably don't need these two new equalities.)") (defvar exemplify-ert-epsilon 1e-15 "Epsilon used in ‘exemplify-ert-approx=’.") ;;;; Functions ;;;;; Equality-defining functions (defun exemplify-ert-approx= (u v) "Like ‘=’, but compares floats U and V within ‘exemplify-ert-epsilon’. This allows approximate comparison of floats to work around differences in implementation between systems. Used in place of ‘equal’ when testing actual and expected values with ‘~>’." (or (= u v) (< (/ (abs (- u v)) (max (abs u) (abs v))) exemplify-ert-epsilon))) ;;;;; From examples to ERT (defun exemplify-ert-example->test (example) "Return an ERT assertion form based on EXAMPLE." (pcase example (`(,actual !!> ,(and (pred symbolp) expected)) ;; FIXME: Tests fail on Emacs 24-25 without ‘eval’ for some reason. `(should-error (eval ',actual ,lexical-binding) :type ',expected)) (`(,actual !!> ,expected) `(should (equal (should-error ,actual) ',expected))) (_ (-let [(actual op expected) example] (let* ((alist (append exemplify-ert-ops-user exemplify-ert-ops-default)) (fun (cdr (assoc op alist))) (ops (-map #'car alist))) (unless (member op ops) (error "Invalid test case: %S" example)) `(should (,fun ,actual ,expected))))))) (defmacro exemplify-ert (fun &rest examples) "Define ERT tests for function FUN and its EXAMPLES. This is the macro to use in your examples.el file." (declare (indent defun)) (setq examples (-partition 3 examples)) `(ert-deftest ,fun () ,@(mapcar #'exemplify-ert-example->test examples))) ;;;;; Support (defun exemplify-ert-libname->libfile (libname) "Given LIBNAME, load library and return filename it loaded from." (require (if (symbolp libname) libname (intern libname))) (--> load-history (-map #'car it) (car (-filter (lambda (lib) (when (string-match (format "/%s.el[cn]*$" libname) lib) lib)) it)))) (defun exemplify-ert--rw-template (from to repl) "Read template and make replacements. Read template in file FROM, then write to TO after replacing $PACKAGE with REPL." (--> from f-read (s-replace "$PACKAGE" repl it) (f-write it 'utf-8 to))) ;;;; Commands ;;;;; Create examples/tests file ;;;###autoload (defun exemplify-ert-make-new () "Create an examples/tests file for your new project. Run it interactively and confirm filename and directory. \(If in doubt, go with ./dev/examples.el as suggested.) It will also write (if not yet existent) run-tests.sh. You can use it to run the tests from the terminal in batch mode instead of, or in addition to, running them interactively." (interactive) (let* ((this (exemplify-ert-libname->libfile 'exemplify-ert)) (tmpl (->> this f-parent (f-expand "templates/examples"))) (rnts (->> this f-parent (f-expand "templates/run-tests"))) (exfl (read-file-name "Create examples file in... " nil nil nil (concat exemplify-ert-dev-subdir "/" exemplify-ert-examples-filename) nil)) (pack (read-from-minibuffer "Confirm package name: " (-> exfl f-parent f-parent f-base))) (libf (format "%s/%s.el" (->> exfl f-parent f-parent ;; in case the folder has .el (s-chop-suffix ".el")) ;; guess: libfile has same name as parent folder pack)) rtf) (mkdir (f-parent exfl) '-p) ;; run-tests.sh (setq rtf (->> exfl f-parent f-parent (f-expand "run-tests.sh"))) (unless (f-exists? rtf) (exemplify-ert--rw-template rnts rtf pack) (set-file-modes rtf #o744)) ;; examples.el (if (not (or (not (f-exists? exfl)) (and (y-or-n-p (s-lex-format "File ${exfl} exists. OVERWRITE?")) (y-or-n-p (s-lex-format "Will overwrite ${exfl}. Are you sure?"))))) (message "Ok, nothing done") (--> tmpl f-read (s-replace-all `(("$PACKAGE" . ,pack) ("$NAME" . ,(if (f-exists? libf) (--> libf lm-authors (-map #'car it) (s-join ", " it)) "$NAME")) ("$YEAR" . ,(->> (decode-time) (nth 5) (format "%s")))) it) (or (f-write it 'utf-8 exfl) (find-file exfl)))))) ;;;;; Minor mode (defcustom exemplify-ert-fontlock-add-equality-operators t "If non-nil, fontify equality-operators for def-examples." :package-version '(exemplify-ert "0.5.0") :type 'boolean) (defcustom exemplify-ert-fontlock-add-exemplify-ert-macros t "If non-nil, fontify ‘exemplify-ert’." :package-version '(exemplify-ert "0.5.0") :type 'boolean) (defun exemplify-ert--ops () "Equality operators." (->> (append exemplify-ert-ops-user exemplify-ert-ops-default) (-map #'car) (cons '!!>) -uniq)) (defun exemplify-ert--fontlock-make-keywords () "Make keywords for fontlock." (-non-nil (list (when exemplify-ert-fontlock-add-exemplify-ert-macros (exemplify-ert--fontlock-rx-exemplify-ert-macros)) (when exemplify-ert-fontlock-add-equality-operators (exemplify-ert--fontlock-make-rx (exemplify-ert--ops)))))) (defun exemplify-ert--fontlock-rx-exemplify-ert-macros () "The rx for ‘exemplify-ert’ macros." `(,(rx ?\( (group (| "exemplify-ert")) symbol-end (+ (in "\t ")) (group (* (| (syntax word) (syntax symbol) (: ?\\ nonl))))) (1 font-lock-keyword-face) (2 font-lock-function-name-face))) (defun exemplify-ert--fontlock-make-rx (symbols-list) "Make rx using SYMBOLS-LIST." (let* ((strings (-map #'symbol-name (-uniq symbols-list))) (rx-1 `(| ,@strings)) (rx `(rx symbol-start ,rx-1 symbol-end))) (eval rx))) (defcustom exemplify-ert-fontify-mode-lighter nil "Mode line lighter for function ‘exemplify-ert-fontify-mode’. Either a string to display in the mode line when function ‘exemplify-ert-fontify-mode’ is on, or nil to display nothing (the default)." :package-version '(exemplify-ert "0.5.0") :type '(choice (string :tag "Lighter" :value "") (const :tag "Nothing" nil))) ;;;###autoload (define-minor-mode exemplify-ert-fontify-mode "Toggle fontification for Exemplify-Ert. This is a buffer-local minor mode intended for Emacs Lisp buffers. Enabling it causes syntax highlighting of exemplify-ert's macros and equality operators. See also ‘exemplify-ert-fontify-mode-lighter’ and ‘global-exemplify-ert-fontify-mode’." :lighter exemplify-ert-fontify-mode-lighter (if exemplify-ert-fontify-mode (font-lock-add-keywords nil (exemplify-ert--fontlock-make-keywords) t) (font-lock-remove-keywords nil (exemplify-ert--fontlock-make-keywords))) (font-lock-flush)) (defun exemplify-ert--turn-on-fontify-mode () "Enable variable ‘exemplify-ert-fontify-mode’ if in an Emacs Lisp buffer." (when (derived-mode-p #'emacs-lisp-mode) (exemplify-ert-fontify-mode))) ;;;###autoload (define-globalized-minor-mode global-exemplify-ert-fontify-mode exemplify-ert-fontify-mode exemplify-ert--turn-on-fontify-mode) ;; Obsoleted (defcustom exemplify-ert-enable-fontlock nil "If t, fontify Exemplify-ERT's equality operators and ‘exemplify-ert’." :package-version '(exemplify-ert "0.5.0") :type 'boolean :set (lambda (sym val) (set-default sym val) (global-exemplify-ert-fontify-mode (if val 1 0)))) ;;;;; See README ;;;###autoload (defun exemplify-ert-see-readme (&optional heading narrow) "Open exemplify-ert's README.org file. Search for the file in exemplify-ert.el's directory. If found, open it read-only. If optional argument HEADING is passed, try to navigate to the heading after opening it. HEADING should be a string. If optional argument NARROW is non-nil, narrow to that heading. This argument has no effect if HEADING is nil or not found." (interactive) (let ((readme exemplify-ert--readme-org)) (if (file-exists-p readme) (let ((pr (make-progress-reporter (format "Opening %s ... " (abbreviate-file-name readme))))) (find-file-read-only readme) (when heading (exemplify-ert--goto-org-heading heading narrow)) (progress-reporter-done pr)) (message "Couldn't find %s's README.org" exemplify-ert--name)))) (defun exemplify-ert--goto-org-heading (heading &optional narrow) "Navigate to org HEADING and optionally NARROW to it." (let* ((hrx (format "^[*]+ %s" heading)) (pos (save-match-data (save-excursion (save-restriction (widen) (goto-char (point-max)) (re-search-backward hrx nil t 1)))))) (when pos (widen) (goto-char pos) (if (and narrow (fboundp 'org-narrow-to-subtree)) (org-narrow-to-subtree) (recenter-top-bottom 1)) (when (fboundp 'outline-show-subtree) (outline-show-subtree)) (when (fboundp 'org-flag-drawer) (save-excursion (forward-line 1) (org-flag-drawer t)))))) ;;;;; See News ;;;###autoload (defun exemplify-ert-see-news () "See the News in exemplify-ert's README.org file." (interactive) (exemplify-ert-see-readme "News" 'narrow) (exemplify-ert--display-org-subtree)) (defun exemplify-ert--display-org-subtree () "Selectively display org subtree." (let ((cmds '(outline-hide-subtree outline-show-children outline-next-heading outline-show-branches))) (and (fboundp (nth 0 cmds)) (fboundp (nth 1 cmds)) (fboundp (nth 2 cmds)) (fboundp (nth 3 cmds)) (mapc #'funcall cmds)))) ;;;; Wrapping up (provide 'exemplify-ert) ;; Local Variables: ;; coding: utf-8 ;; indent-tabs-mode: nil ;; sentence-end-double-space: nil ;; outline-regexp: ";;;;* " ;; End: ;;; exemplify-ert.el ends here
📆 2022-03-31