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.


README.org

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:

  1. Insert the examples into the Help (or Helpful) buffer of the specific function the user is looking up.
  2. 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's README.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 an examples.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:

  1. Your dev/examples.el file will have readable examples that double as ERT tests.
  2. Your elisp file will have code (defuns, defvars, defcustoms) plus its documentation.
  3. Your README.org will use both this documentation and the examples from dev/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 your dev/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 the README.org exactly as they are.
  • Hierarchical headers in the elisp file (;;;+) will be automatically translated as hierarchical headings for the Commands and Functions headings of your README.org.
  • Your README.org is generated from a readme-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 and Background headings you're reading right now).

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.

Flexibility for adding your own equality operators

You'll probably do fine with just equal or any of those in the exemplify-ert-ops-default alist shown above. But if you need more, just setq your (equality-symbol . equality-function) in your examples.el and you're done.

An examples.el template

Just launch M-x exemplify-ert-make-new and you don't have to start from scratch, nor fiddle with adapting dash-defs.el. Your examples.el will be created from a template, with some instructions and examples already there.

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.

Functions

From examples to ERT

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.

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.

See News

exemplify-ert-see-news ()

See the News in exemplify-ert's README.org file.

Contributing

See my page Software for information about how to contribute to any of my Emacs packages.

News

0.6.3

Changes

Improved documentation
  • Note about complementarity provided by Exemplify-Align and Exemplify-Eval.
  • Note about notation (evaluation versus equivalence).
  • Added example of H=> operator.

0.6.2 (untagged)

Changes

Small code and documentation improvements

0.6.1

Changes

Obsolescence
Customizable variables
This is now obsolete

0.6.0

Exemplify-ERT News

This release brings new commands and new equality operators.

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.

New equality operators
K=> for h-kvl, which was missing
New in templates
Command to run all tests was added

Fixes

defgroup's group has been fixed
A few fixes in requires

0.5.0

Minor public release

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