Generic test runner for Emacs with a treemacs-based results UI.
Verdict provides a framework for running tests and displaying results in a tree view. Language-specific backends plug in via a simple registration API.
- verdict-dart — Dart and Flutter
M-x package-install RET verdict RET(use-package verdict)- Install and configure a verdict backend (e.g.
verdict-dart). - Run tests with the keybindings below.
By default, all commands are available under the C-c t prefix when
verdict-mode is active. This can be configured by setting
verdict-keymap-prefix before verdict is loaded.
| Key | Command | Description |
|---|---|---|
C-c C-t t |
verdict-run-test-at-point |
Run test at point |
C-c C-t g |
verdict-run-group-at-point |
Run enclosing group |
C-c C-t f |
verdict-run-file |
Run current file |
C-c C-t m |
verdict-run-module |
Run current module |
C-c C-t p |
verdict-run-project |
Run all project tests |
C-c C-t r |
verdict-run-last |
Rerun last test run |
C-c C-t ! |
verdict-rerun-failed |
Rerun only failed tests |
C-c C-t k |
verdict-kill |
Kill running test process |
C-c C-t T |
verdict-debug-test-at-point |
Debug test at point |
C-c C-t G |
verdict-debug-group-at-point |
Debug enclosing group |
C-c C-t F |
verdict-debug-file |
Debug current file |
C-c C-t M |
verdict-debug-module |
Debug current module |
C-c C-t P |
verdict-debug-project |
Debug all project tests |
C-c C-t R |
verdict-debug-last |
Debug last test run |
In the results buffer:
| Key | Action |
|---|---|
| TAB | Expand/collapse group, or show test output |
| RET | Visit test file/line |
| click | Expand/collapse group, or show test output |
| dbl-click | Visit test file/line |
r |
Rerun test/group at point |
R |
Rerun last test run |
! |
Rerun only failed tests |
k |
Kill running test process |
| Variable | Default | Description |
|---|---|---|
verdict-keymap-prefix |
C-c C-t |
Prefix key for all verdict keybindings (set before loading) |
verdict-project-root-fn |
project-current |
Function to find the project root directory |
verdict-save-before-run |
nil (ask) |
Whether to save current buffer before run: yes, no, nil |
verdict-icon-font |
auto-detected | Font for status icons (Braille-capable preferred) |
verdict-spinner-style |
braille or ascii |
Spinner animation style while tests run |
verdict-icon-height |
1.0 |
Relative height of status icons |
verdict-icon-passed |
✓ |
Icon for passed tests |
verdict-icon-failed |
✗ |
Icon for failed tests |
verdict-icon-error |
! |
Icon for errored tests |
verdict-icon-skipped |
- |
Icon for skipped tests |
A verdict backend connects a language-specific test runner to the verdict UI.
To create one, you implement three functions and register them with
verdict-register-backend.
(verdict-register-backend PREDICATE CONTEXT-FN COMMAND-FN LINE-HANDLER)PREDICATE determines when this backend is active. It can be:
- A symbol — a major-mode name; matched with
derived-mode-p. Example:'python-ts-mode - A string — a regexp; matched against
buffer-name. Example:"_test\\.py$" - A function — called with no arguments; non-nil means match.
Example:
(lambda () (and (derived-mode-p 'python-ts-mode) (project-current)))
The most recently registered backend takes precedence when multiple predicates match.
(context-fn SCOPE) -> plist
Called in the user's source buffer (so buffer-file-name, point, etc. are
available). Returns a backend-specific context plist that will be passed to
command-fn.
SCOPE is one of:
:test-at-point— run the test at point:group-at-point— run the group at point:file— run the current file:module— run the current module:project— run all project tests(:tests . FILE-TESTS)— rerun specific tests.FILE-TESTSis an alist of(FILE . (NAME ...))entries, provided when the user invokesverdict-rerun-failedor reruns a specific node.
The returned plist is opaque to verdict — it is passed through to command-fn
unchanged. Include whatever your backend needs (project root, file paths, test
names, etc.).
The context is reused when the user wants to run the same tests again.
(command-fn CONTEXT DEBUG) -> plist
Called with the context plist from context-fn and a boolean DEBUG flag.
Returns a plist with the following keys:
| Key | Type | Required | Description |
|---|---|---|---|
:command |
list of strings or function | yes | Process arguments, or a function for custom launch |
:directory |
string | yes | Working directory for the process |
:name |
string | yes | Display name for the run |
:header |
string | no | Header text shown at the top of the verdict buffer |
When :command is a list of strings, it is used as the :command argument to make-process. Verdict will manage
the process (start, filter, sentinel).
When :command is a function, verdict calls it and the function is responsible for its own process management. The
function must call verdict-stop when the run is complete.
(line-handler LINE)
Called once per complete line of process output (newline-stripped). Parse the
line and call verdict-event with event plists to update the UI.
Events are plists with a :type key. Call verdict-event with each event.
Declares a test group (suite, describe block, etc.).
| Key | Type | Required | Description |
|---|---|---|---|
:type |
:group |
yes | |
:id |
any (unique) | yes | Globally unique identifier |
:name |
string | yes | Display name |
:label |
string | no | Overrides :name for display |
:file |
string or nil | no | Absolute file path |
:line |
integer or nil | no | 1-based line number |
:parent-id |
id or nil | no | Parent group ID |
If :parent-id references a non-existent node, the group becomes a root node.
Declares and starts a test.
| Key | Type | Required | Description |
|---|---|---|---|
:type |
:test-start |
yes | |
:id |
any (unique) | yes | Globally unique identifier |
:name |
string | yes | Display name |
:label |
string | no | Overrides :name for display |
:file |
string | yes | Absolute file path |
:line |
integer | yes | 1-based line number |
:parent-id |
id | yes | Parent group ID |
Appends output to a node.
| Key | Type | Required | Description |
|---|---|---|---|
:type |
:log |
yes | |
:id |
id (existing) | yes | Target node |
:severity |
info or error |
yes | Controls face styling |
:message |
string | yes | Log text |
error messages are displayed with verdict-error-face. info messages are
plain.
Marks a test as finished.
| Key | Type | Required | Description |
|---|---|---|---|
:type |
:test-done |
yes | |
:id |
id (existing) | yes | Target test |
:result |
passed, failed, error, or skipped |
yes | Final status |
Signals the entire test run is complete. No additional keys.
(verdict-event '(:type :done))- User triggers a run (e.g.
C-c C-t f). - Verdict calls
context-fnin the user's source buffer. - Verdict calls
command-fnwith the context. - Verdict starts the process (or calls the custom launcher).
- Each output line is passed to
line-handler. line-handlercallsverdict-eventwith parsed events.- The UI updates after each event.
- The
:doneevent finalizes the run.
A minimal backend skeleton:
(defun my-backend-context (scope)
(list :project (verdict--default-project-root)
:file (buffer-file-name)
:scope scope))
(defun my-backend-command (context _debug)
(list :command (list "my-test-runner" "--json" (plist-get context :file))
:directory (plist-get context :project)
:name (file-name-nondirectory (plist-get context :file))))
(defun my-backend-line-handler (line)
;; Parse JSON, emit verdict events
(when-let* ((data (ignore-errors (json-parse-string line :object-type 'plist))))
(pcase (plist-get data :event)
("testStart"
(verdict-event (list :type :test-start
:id (plist-get data :id)
:name (plist-get data :name)
:file (plist-get data :file)
:line (plist-get data :line)
:parent-id (plist-get data :group))))
("testDone"
(verdict-event (list :type :test-done
:id (plist-get data :id)
:result (intern (plist-get data :result)))))
("done"
(verdict-event '(:type :done))))))
(verdict-register-backend 'my-test-mode
#'my-backend-context
#'my-backend-command
#'my-backend-line-handler)
(add-hook 'my-test-mode-hook #'verdict-mode)- All node IDs must be globally unique within a run.
- Status transitions:
running->passed|failed|error|skipped|stopped. - Groups have
:children(list of child IDs). Leaf nodes (tests) do not. - Only one test run can be active at a time.
PRs are welcome. Please open an issue first to discuss larger changes.
GPL-3.0-or-later
