2419 lines
101 KiB
EmacsLisp
2419 lines
101 KiB
EmacsLisp
|
;;; ert.el --- Emacs Lisp Regression Testing
|
||
|
|
||
|
;; Modified by Lennart Borgman 2008-07-13 to make all global symbols
|
||
|
;; use the "ert-" prefix.
|
||
|
|
||
|
;; Copyright (C) 2007, 2008 Christian M. Ohler
|
||
|
|
||
|
;; Author: Christian M. Ohler
|
||
|
;; Version: 0.2
|
||
|
;; Keywords: lisp, tools
|
||
|
|
||
|
;; This file is NOT part of GNU Emacs.
|
||
|
|
||
|
;; This program 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 program 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 program. If not, see `http://www.gnu.org/licenses/'.
|
||
|
|
||
|
;;; Commentary:
|
||
|
|
||
|
;; ERT is a tool for automated testing in Emacs Lisp. Its main
|
||
|
;; features are facilities for defining and running test cases and
|
||
|
;; reporting the results as well as for debugging test failures
|
||
|
;; interactively.
|
||
|
;;
|
||
|
;; The main entry points are `ert-deftest', which is similar to
|
||
|
;; `defun' but defines a test, and `ert-run-tests-interactively',
|
||
|
;; which runs tests and offers an interactive interface for inspecting
|
||
|
;; results and debugging. There is also `ert-run-tests-batch' for
|
||
|
;; non-interactive use.
|
||
|
;;
|
||
|
;; The body of `ert-deftest' forms resembles a function body, but the
|
||
|
;; additional operators `should', `should-not' and `should-error' are
|
||
|
;; available. `should' is similar to cl's `assert', but signals a
|
||
|
;; different error when its condition is violated that is caught and
|
||
|
;; processed by ERT. In addition, it analyzes its argument form and
|
||
|
;; records information that helps debugging (`assert' tries to do
|
||
|
;; something similar when its second argument SHOW-ARGS is true, but
|
||
|
;; `should' is more sophisticated). For information on `should-not'
|
||
|
;; and `should-error', see their docstrings.
|
||
|
;;
|
||
|
;; For example,
|
||
|
;;
|
||
|
;; ;; Define a test named `foo'.
|
||
|
;; (ert-deftest foo ()
|
||
|
;; (ert-should (= (+ 1 2) 4)))
|
||
|
;;
|
||
|
;; ;; Run it.
|
||
|
;; (ert-run-tests-interactively 'foo)
|
||
|
;;
|
||
|
;; generates the following output (in addition to some statistics) in
|
||
|
;; the *ert* results buffer:
|
||
|
;;
|
||
|
;; F foo
|
||
|
;; (ert-test-failed
|
||
|
;; ((ert-should
|
||
|
;; (=
|
||
|
;; (+ 1 2)
|
||
|
;; 4))
|
||
|
;; :form
|
||
|
;; (= 3 4)
|
||
|
;; :value nil))
|
||
|
;;
|
||
|
;; This indicates that the test failed. The `should' form that failed
|
||
|
;; was (ert-should (= (+ 1 2) 4)), because its inner form, after
|
||
|
;; evaluation of its arguments, was the function call (= 3 4), which
|
||
|
;; returned nil.
|
||
|
;;
|
||
|
;; Obviously, this is a bug in the test case, not in the functions `+'
|
||
|
;; or `='. In the results buffer, with point on the test result, the
|
||
|
;; key "." can be used to jump to the definition of the test to modify
|
||
|
;; it to correct the bug. After evaluating the modified definition
|
||
|
;; and switching back to the results buffer, the key "r" will re-run
|
||
|
;; the test and show the new result.
|
||
|
|
||
|
|
||
|
;; Test selectors
|
||
|
;;
|
||
|
;; Functions like `ert-run-tests-interactively' accept a test
|
||
|
;; selector, which is a Lisp expression specifying a set of tests.
|
||
|
;; Each test name is a selector that refers to that test, the selector
|
||
|
;; `t' refers to all tests, and the selector `:failed' refers to all
|
||
|
;; tests that failed; but more complex selectors are available. Test
|
||
|
;; selector syntax is similar to cl's type specifier syntax. See the
|
||
|
;; docstring of `ert-select-tests' for details.
|
||
|
|
||
|
|
||
|
;; Comparison with other testing tools
|
||
|
;;
|
||
|
;; ERT allows test-driven development similar to *Unit frameworks for
|
||
|
;; other languages. However, two common *Unit features are notably
|
||
|
;; absent from ERT: fixtures and test suites.
|
||
|
;;
|
||
|
;; Fixtures, as used e.g. in SUnit or JUnit, have two main purposes:
|
||
|
;; Setting up (and tearing down) an environment for a set of test
|
||
|
;; cases, and making that environment accessible through object
|
||
|
;; attributes that can be used like local variables.
|
||
|
;;
|
||
|
;; While fixtures are a great syntactic simplification in other
|
||
|
;; languages, they are not very useful in Lisp, where higher-order
|
||
|
;; functions and `unwind-protect' are available. One way to implement
|
||
|
;; and use a fixture in ERT is
|
||
|
;;
|
||
|
;; (defun my-fixture (body)
|
||
|
;; (unwind-protect
|
||
|
;; (progn ...set up...
|
||
|
;; (funcall body))
|
||
|
;; ...tear down...))
|
||
|
;;
|
||
|
;; (ert-deftest my-test ()
|
||
|
;; (my-fixture
|
||
|
;; (lambda ()
|
||
|
;; ...test code...)))
|
||
|
;;
|
||
|
;; (Another way would be a `with-my-fixture' macro.) This solves the
|
||
|
;; set-up and tear-down part, and additionally allows any test case to
|
||
|
;; use any combination of fixtures, so it is more general than what
|
||
|
;; other tools typically allow.
|
||
|
;;
|
||
|
;; If the test case needs access to the environment the fixture sets
|
||
|
;; up, the fixture can be modified to pass arguments to the body.
|
||
|
;;
|
||
|
;; These are standard Lisp idioms. Special syntax for them could be
|
||
|
;; added easily enough, but would provide only a minor simplification.
|
||
|
;;
|
||
|
;; (Note that splitting set-up and tear-down into separate functions,
|
||
|
;; like *Unit tools usually do, makes it impossible to establish
|
||
|
;; dynamic `let' bindings as part of the fixture. So, blindly
|
||
|
;; imitating the way fixtures are implemented in other languages would
|
||
|
;; be counter-productive in Lisp.)
|
||
|
;;
|
||
|
;;
|
||
|
;; The purpose of test suites is to group related test cases together.
|
||
|
;; The most common use of this is to run just the tests for one
|
||
|
;; particular module. Since symbol prefixes are the usual way of
|
||
|
;; separating module namespaces in Emacs Lisp, test selectors already
|
||
|
;; solve this by allowing regexp matching on test names; e.g., the
|
||
|
;; selector "^ert-" selects ERT's self-tests.
|
||
|
;;
|
||
|
;; If test suites containing arbitrary sets of tests are found to be
|
||
|
;; desirable, it would be easy to add a `define-test-selector'
|
||
|
;; mechanism that introduces a new selector, defined in terms of
|
||
|
;; existing ones; e.g.
|
||
|
;;
|
||
|
;; ;; Note that `define-test-selector' does not exist yet.
|
||
|
;; (define-test-selector my-test-suite () `(member foo-test bar-test))
|
||
|
;;
|
||
|
;; would define a test suite named `my-test-suite' consisting of
|
||
|
;; `foo-test' and `bar-test'. See also `deftype' in Common Lisp.
|
||
|
|
||
|
|
||
|
;; TODO: Add `skip' feature for tests that can't run in current environment.
|
||
|
|
||
|
|
||
|
;;; Code:
|
||
|
|
||
|
(eval-when-compile (require 'cl))
|
||
|
(require 'ewoc)
|
||
|
(require 'find-func)
|
||
|
(require 'debug)
|
||
|
|
||
|
(defvar ert-debug-on-error nil
|
||
|
"Non-nil means enter debugger when a test fails or terminates with an error.")
|
||
|
|
||
|
|
||
|
;;; Defining and locating tests.
|
||
|
|
||
|
;; The data structure that represents a test case.
|
||
|
(defstruct ert-test
|
||
|
(name nil)
|
||
|
(documentation nil)
|
||
|
(body (assert nil))
|
||
|
(most-recent-result nil)
|
||
|
(expected-result-type 'ert-test-passed))
|
||
|
|
||
|
(defun ert-test-boundp (symbol)
|
||
|
"Return non-nil if SYMBOL names a test."
|
||
|
(and (get symbol 'ert-test) t))
|
||
|
|
||
|
(defun ert-get-test (symbol)
|
||
|
"If SYMBOL names a test, return that. Signal an error otherwise."
|
||
|
(assert (ert-test-boundp symbol) t)
|
||
|
(get symbol 'ert-test))
|
||
|
|
||
|
(defun ert-set-test (symbol doc definition)
|
||
|
"Make SYMBOL name the test DEFINITION, and return DEFINITION."
|
||
|
(when doc
|
||
|
(put symbol 'ert-test-documentation doc))
|
||
|
(put symbol 'ert-test definition)
|
||
|
definition)
|
||
|
|
||
|
(defun ert-make-test-unbound (symbol)
|
||
|
"Make SYMBOL name no test. Return SYMBOL."
|
||
|
(remprop symbol 'ert-test)
|
||
|
symbol)
|
||
|
|
||
|
(defun ert-test-result-expected-p (test result)
|
||
|
"Return non-nil if RESULT matches the expected result type for TEST."
|
||
|
(typep result (ert-test-expected-result-type test)))
|
||
|
|
||
|
(defvar ert-find-test-regexp
|
||
|
(concat "^\\s-*(ert-deftest"
|
||
|
find-function-space-re
|
||
|
"%s\\(\\s-\\|$\\)")
|
||
|
"The regexp the `find-function' mechanisms use for locating test definitions.")
|
||
|
|
||
|
(eval-and-compile
|
||
|
(defun ert-parse-keys-and-body (docstr keys-and-body)
|
||
|
"Split KEYS-AND-BODY into keyword-and-value pairs and the remaining body.
|
||
|
|
||
|
KEYS-AND-BODY should have the form of a property list, with the
|
||
|
exception that only keywords are permitted as keys and that the
|
||
|
tail -- the body -- is a list of forms that does not start with a
|
||
|
keyword.
|
||
|
|
||
|
Returns a two-element list containing the keys-and-values plist
|
||
|
and the body."
|
||
|
(unless (stringp docstr)
|
||
|
(when docstr
|
||
|
(setq keys-and-body (cons docstr keys-and-body))
|
||
|
(setq docstr nil)))
|
||
|
(let ((extracted-key-accu '())
|
||
|
(remaining keys-and-body))
|
||
|
(while (and (consp remaining) (keywordp (first remaining)))
|
||
|
(let ((keyword (pop remaining)))
|
||
|
(unless (consp remaining)
|
||
|
(error "Value expected after keyword %S in %S"
|
||
|
keyword keys-and-body))
|
||
|
(when (assoc keyword extracted-key-accu)
|
||
|
(warn "Keyword %S appears more than once in %S" keyword
|
||
|
keys-and-body))
|
||
|
(push (cons keyword (pop remaining)) extracted-key-accu)))
|
||
|
(setq extracted-key-accu (nreverse extracted-key-accu))
|
||
|
(list (loop for (key . value) in extracted-key-accu
|
||
|
collect key
|
||
|
collect value)
|
||
|
docstr
|
||
|
remaining))))
|
||
|
|
||
|
(defvar ert-error-on-test-redefinition nil)
|
||
|
|
||
|
;;;###autoload
|
||
|
(defmacro* ert-deftest (name ()
|
||
|
&optional docstr
|
||
|
&body keys-and-body)
|
||
|
"Define NAME (a symbol) as a test.
|
||
|
|
||
|
\(fn NAME () [:documentation DOCSTRING] [:expected-result TYPE] BODY...)"
|
||
|
;; The :documentation would be unreadable. I have therefore added
|
||
|
;; docstr that will look like documentation use to in Emacs. Maybe
|
||
|
;; add function ert-describe-test?
|
||
|
(declare (indent 2)
|
||
|
(debug (&define :name test name sexp
|
||
|
[&optional [":documentation" stringp]]
|
||
|
[&optional [":expected-result" sexp]]
|
||
|
def-body)))
|
||
|
(destructuring-bind ((&key (expected-result nil expected-result-supplied-p)
|
||
|
(documentation nil documentation-supplied-p))
|
||
|
doc
|
||
|
body)
|
||
|
(ert-parse-keys-and-body docstr keys-and-body)
|
||
|
`(progn
|
||
|
;; Guard against missing/badly named tests:
|
||
|
(when (and ert-error-on-test-redefinition
|
||
|
(symbolp ',name)
|
||
|
(get ',name 'ert-test))
|
||
|
(with-output-to-temp-buffer "*Ert Error*"
|
||
|
(with-current-buffer "*Ert Error*"
|
||
|
(insert "Test "
|
||
|
(format "%s" ',name)
|
||
|
" is already defined in "
|
||
|
(format "%s" (find-definition-noselect ',name 'ert-deftest))
|
||
|
"\n\n"
|
||
|
"Tip: Use `ert-delete-all-tests' or `ert-delete-test' before redefining tests."
|
||
|
)))
|
||
|
(if (y-or-n-p "Do you want to call ert-delete-all-tests and then continue? ")
|
||
|
;; Fix-me: This does not work, why?
|
||
|
(ert-delete-all-tests)
|
||
|
(error "Test %s is already defined in %s"
|
||
|
',name
|
||
|
(find-definition-noselect ',name 'ert-deftest))))
|
||
|
(ert-set-test ',name
|
||
|
nil ;;doc
|
||
|
(make-ert-test
|
||
|
:name ',name
|
||
|
:body (lambda () ,@body)
|
||
|
,@(when expected-result-supplied-p
|
||
|
`(:expected-result-type ,expected-result))
|
||
|
,@(when documentation-supplied-p
|
||
|
`(:documentation ,documentation))))
|
||
|
;; This hack allows `symbol-file' to associate `ert-deftest'
|
||
|
;; forms with files, and therefore enables `find-function' to
|
||
|
;; work with tests. However, it leads to warnings in
|
||
|
;; `unload-feature', which doesn't know how to undefine tests
|
||
|
;; and has no mechanism for extension.
|
||
|
(push '(ert-deftest . ,name) current-load-list)
|
||
|
',name)))
|
||
|
|
||
|
(defun ert-read-test-name (prompt &optional default-value history)
|
||
|
"Read the name of a test and return it as a symbol.
|
||
|
Prompt with PROMPT. By default, return DEFAULT-VALUE."
|
||
|
(when (symbolp default-value) (setq default-value (symbol-name default-value)))
|
||
|
(intern (completing-read prompt obarray #'ert-test-boundp
|
||
|
t nil history default-value nil)))
|
||
|
|
||
|
(defun ert-find-test-other-window (test-name)
|
||
|
"Find, in another window, the definition of TEST-NAME."
|
||
|
(interactive (list (ert-read-test-name "Find test definition: ")))
|
||
|
(find-function-do-it test-name 'ert-deftest 'switch-to-buffer-other-window))
|
||
|
|
||
|
(defun ert-delete-test (test-name)
|
||
|
"An interactive interface to `ert-make-test-unbound'."
|
||
|
(interactive (list (let ((default (thing-at-point 'symbol)))
|
||
|
(when default
|
||
|
(set-text-properties 0 (length default) nil default)
|
||
|
(when (or (string= default "nil") (intern-soft default))
|
||
|
(setq default (intern default)))
|
||
|
(unless (ert-test-boundp default)
|
||
|
(setq default nil)))
|
||
|
(completing-read (if (null default)
|
||
|
"Delete test: "
|
||
|
(format "Delete test (default %s): "
|
||
|
default))
|
||
|
obarray #'ert-test-boundp
|
||
|
'really-require-match
|
||
|
nil nil default nil))))
|
||
|
(ert-make-test-unbound test-name))
|
||
|
|
||
|
(defun ert-delete-all-tests ()
|
||
|
"Make all symbols in `obarray' name no test."
|
||
|
(interactive)
|
||
|
(when (interactive-p)
|
||
|
(unless (y-or-n-p "Delete all tests? ")
|
||
|
(error "Aborted")))
|
||
|
(mapc #'ert-delete-test (mapcar #'ert-test-name (ert-select-tests t t)))
|
||
|
t)
|
||
|
|
||
|
|
||
|
(defun ert-make-end-marker (buffer must-exist)
|
||
|
"Return a marker to the end of buffer BUFFER.
|
||
|
BUFFER may be a string or a buffer. If BUFFER does not exist
|
||
|
return nil.
|
||
|
|
||
|
The buffer must exist if MUST-EXIST is non-nil.
|
||
|
|
||
|
See also:
|
||
|
`ert-end-of-messages'
|
||
|
`ert-end-of-warnings'"
|
||
|
(let ((buf (if must-exist
|
||
|
(get-buffer buffer)
|
||
|
(get-buffer-create buffer))))
|
||
|
(when (and buf
|
||
|
(bufferp buf)
|
||
|
(buffer-live-p buf))
|
||
|
(with-current-buffer buf
|
||
|
(save-restriction
|
||
|
(widen)
|
||
|
(point-max-marker))))))
|
||
|
|
||
|
(defun ert-end-of-messages ()
|
||
|
"Return a marker to the end of *Messages* buffer."
|
||
|
(ert-make-end-marker "*Messages*" nil))
|
||
|
|
||
|
(defun ert-end-of-warnings ()
|
||
|
"Return a marker to the end of *Warnings* buffer."
|
||
|
(ert-make-end-marker "*Warnings*" nil))
|
||
|
|
||
|
(defun ert-search-after (after regexp)
|
||
|
"Search after marker in AFTER for regular expression REGEXP.
|
||
|
Return a alist of position and matches. AFTER should have been
|
||
|
created with `ert-make-end-marker'.
|
||
|
|
||
|
This is supposed to be used for messages and trace buffers.
|
||
|
|
||
|
See also
|
||
|
`ert-get-messages'"
|
||
|
(let ((buf (marker-buffer after)))
|
||
|
(with-current-buffer buf
|
||
|
(let ((here (point))
|
||
|
res)
|
||
|
(goto-char after)
|
||
|
(save-match-data
|
||
|
(while (re-search-forward regexp nil t)
|
||
|
(setq res (cons (match-data) res))))
|
||
|
(goto-char here)
|
||
|
(reverse res)))))
|
||
|
;; fix-me: add a conventient way to look at the result of
|
||
|
;; `ert-search-after'. Probably this means adding something more to
|
||
|
;; the returned result.
|
||
|
|
||
|
(defvar ert-messages-mark)
|
||
|
(defun ert-get-messages (regexp)
|
||
|
"Search *Messages* buffer for regular expression REGEXP.
|
||
|
This should be used within `ert-deftest'. Search begins where
|
||
|
the buffer ended when test started.
|
||
|
|
||
|
See also:
|
||
|
`ert-get-warnings'
|
||
|
`ert-search-after'"
|
||
|
(ert-search-after ert-messages-mark regexp))
|
||
|
|
||
|
(defvar ert-warnings-mark)
|
||
|
(defun ert-get-warnings (regexp)
|
||
|
"Search *Warnings* buffer for regular expression REGEXP.
|
||
|
See `ert-get-messages' for more information."
|
||
|
(ert-search-after ert-warnings-mark regexp))
|
||
|
|
||
|
|
||
|
;;; Test selectors.
|
||
|
|
||
|
(defun ert-select-tests (selector universe)
|
||
|
"Select, from UNIVERSE, a set of tests according to SELECTOR.
|
||
|
|
||
|
UNIVERSE should be a list of tests, or t, which refers to all
|
||
|
tests named by symbols in `obarray'.
|
||
|
|
||
|
Returns the set of tests as a list.
|
||
|
|
||
|
Valid selectors:
|
||
|
|
||
|
nil -- Selects the empty set.
|
||
|
t -- Selects UNIVERSE.
|
||
|
:new -- Selects all tests that have not been run yet.
|
||
|
:failed, :passed, :error -- Select tests according to their most recent result.
|
||
|
:expected, :unexpected -- Select tests according to their most recent result.
|
||
|
a string -- Selects all tests that have a name that matches the string, a regexp.
|
||
|
a test -- Selects that test.
|
||
|
a symbol -- Selects the test that the symbol names, errors if none.
|
||
|
\(member TESTS...\) -- Selects TESTS, a list of tests or symbols naming tests.
|
||
|
\(eql TEST\) -- Selects TEST, a test or a symbol naming a test.
|
||
|
\(and SELECTORS...\) -- Selects the tests that match all SELECTORS.
|
||
|
\(or SELECTORS...\) -- Selects the tests that match any SELECTOR.
|
||
|
\(not SELECTOR\) -- Selects all tests that do not match SELECTOR.
|
||
|
\(satisfies PREDICATE\) -- Selects all tests that satisfy PREDICATE.
|
||
|
|
||
|
Only selectors that require a superset of tests, such
|
||
|
as (satisfies ...), strings, :new, etc. make use of UNIVERSE.
|
||
|
Selectors that do not, such as \(member ...\), just return the
|
||
|
set implied by them without checking whether it is really
|
||
|
contained in UNIVERSE."
|
||
|
;; This code needs to match the etypecase in
|
||
|
;; `ert-insert-human-readable-selector'.
|
||
|
(etypecase selector
|
||
|
((member nil) nil)
|
||
|
((member t) (etypecase universe
|
||
|
(list universe)
|
||
|
((member t) (ert-select-tests "" universe))))
|
||
|
((member :new) (ert-select-tests
|
||
|
`(satisfies ,(lambda (test)
|
||
|
(typep (ert-test-most-recent-result test)
|
||
|
'null)))
|
||
|
universe))
|
||
|
((member :failed) (ert-select-tests
|
||
|
`(satisfies ,(lambda (test)
|
||
|
(typep (ert-test-most-recent-result test)
|
||
|
'ert-test-failed)))
|
||
|
universe))
|
||
|
((member :passed) (ert-select-tests
|
||
|
`(satisfies ,(lambda (test)
|
||
|
(typep (ert-test-most-recent-result test)
|
||
|
'ert-test-passed)))
|
||
|
universe))
|
||
|
((member :error) (ert-select-tests
|
||
|
`(satisfies ,(lambda (test)
|
||
|
(typep (ert-test-most-recent-result test)
|
||
|
'ert-test-error)))
|
||
|
universe))
|
||
|
((member :expected) (ert-select-tests
|
||
|
`(satisfies
|
||
|
,(lambda (test)
|
||
|
(ert-test-result-expected-p
|
||
|
test
|
||
|
(ert-test-most-recent-result test))))
|
||
|
universe))
|
||
|
((member :unexpected) (ert-select-tests `(not :expected) universe))
|
||
|
(string
|
||
|
(etypecase universe
|
||
|
((member t) (mapcar #'ert-get-test
|
||
|
(apropos-internal selector #'ert-test-boundp)))
|
||
|
(list (remove-if-not (lambda (test)
|
||
|
(and (ert-test-name test)
|
||
|
(string-match selector (ert-test-name test))))
|
||
|
universe))))
|
||
|
(ert-test (list selector))
|
||
|
(symbol
|
||
|
(assert (ert-test-boundp selector))
|
||
|
(list (ert-get-test selector)))
|
||
|
(cons
|
||
|
(destructuring-bind (operator &rest operands) selector
|
||
|
(ecase operator
|
||
|
(member
|
||
|
(mapcar (lambda (purported-test)
|
||
|
(etypecase purported-test
|
||
|
(symbol (assert (ert-test-boundp purported-test))
|
||
|
(ert-get-test purported-test))
|
||
|
(ert-test purported-test)))
|
||
|
operands))
|
||
|
(eql
|
||
|
(assert (eql (length operands) 1))
|
||
|
(ert-select-tests `(member ,@operands) universe))
|
||
|
(and
|
||
|
;; Do these definitions of AND, NOT and OR satisfy de
|
||
|
;; Morgan's rules? Should they?
|
||
|
(case (length operands)
|
||
|
(0 (ert-select-tests 't universe))
|
||
|
(t (ert-select-tests `(and ,@(rest operands))
|
||
|
(ert-select-tests (first operands) universe)))))
|
||
|
(not
|
||
|
(assert (eql (length operands) 1))
|
||
|
(set-difference (ert-select-tests 't universe)
|
||
|
(ert-select-tests (first operands) universe)))
|
||
|
(or
|
||
|
(case (length operands)
|
||
|
(0 (ert-select-tests 'nil universe))
|
||
|
(t (union (ert-select-tests (first operands) universe)
|
||
|
(ert-select-tests `(or ,@(rest operands)) universe)))))
|
||
|
(satisfies
|
||
|
(assert (eql (length operands) 1))
|
||
|
(remove-if-not (first operands) (ert-select-tests 't universe))))))))
|
||
|
|
||
|
(defun ert-insert-human-readable-selector (selector)
|
||
|
"Insert a human-readable presentation of SELECTOR into the current buffer."
|
||
|
;; This is needed to avoid printing the (huge) contents of the
|
||
|
;; `backtrace' slot of the result objects in the
|
||
|
;; `most-recent-result' slots of test case objects in (eql ...) or
|
||
|
;; (member ...) selectors.
|
||
|
(labels ((rec (selector)
|
||
|
;; This code needs to match the etypecase in `ert-select-tests'.
|
||
|
(etypecase selector
|
||
|
((or (member nil t
|
||
|
:new :failed :passed :error
|
||
|
:expected :unexpected)
|
||
|
string
|
||
|
symbol)
|
||
|
selector)
|
||
|
(ert-test
|
||
|
(if (ert-test-name selector)
|
||
|
(make-symbol (format "<%S>" (ert-test-name selector)))
|
||
|
(make-symbol "<unnamed test>")))
|
||
|
(cons
|
||
|
(destructuring-bind (operator &rest operands) selector
|
||
|
(ecase operator
|
||
|
((member eql and not or)
|
||
|
`(,operator ,@(mapcar #'rec operands)))
|
||
|
(satisfies
|
||
|
selector)))))))
|
||
|
(insert (format "%S" (rec selector)))))
|
||
|
|
||
|
|
||
|
;;; Running tests.
|
||
|
|
||
|
(put 'ert-test-failed 'error-conditions '(error ert-test-failed))
|
||
|
(put 'ert-test-failed 'error-message "Test failed")
|
||
|
|
||
|
(defun ert-pass ()
|
||
|
"Terminate the current test and mark it passed. Does not return."
|
||
|
(throw 'ert-pass nil))
|
||
|
|
||
|
(defun ert-fail (data)
|
||
|
"Terminate the current test and mark it failed. Does not return.
|
||
|
DATA is displayed to the user and should state the reason of the failure."
|
||
|
(signal 'ert-test-failed (list data)))
|
||
|
|
||
|
;; The data structures that represent the result of running a test.
|
||
|
(defstruct ert-test-result
|
||
|
(messages nil)
|
||
|
)
|
||
|
(defstruct (ert-test-passed (:include ert-test-result)))
|
||
|
(defstruct (ert-test-result-with-condition (:include ert-test-result))
|
||
|
(condition (assert nil))
|
||
|
(backtrace (assert nil)))
|
||
|
(defstruct (ert-test-error (:include ert-test-result-with-condition)))
|
||
|
(defstruct (ert-test-quit (:include ert-test-result-with-condition)))
|
||
|
(defstruct (ert-test-failed (:include ert-test-result-with-condition)))
|
||
|
(defstruct (ert-test-aborted-with-non-local-exit (:include ert-test-result)))
|
||
|
|
||
|
|
||
|
(defun ert-record-backtrace ()
|
||
|
"Record the current backtrace (as a list) and return it."
|
||
|
;; Since the backtrace is stored in the result object, result
|
||
|
;; objects must only be printed with appropriate limits
|
||
|
;; (`print-level' and `print-length') in place. For interactive
|
||
|
;; use, the cost of ensuring this possibly outweighs the advantage
|
||
|
;; of storing the backtrace for
|
||
|
;; `ert-results-pop-to-backtrace-for-test-at-point' given that we
|
||
|
;; already have `ert-results-rerun-test-debugging-errors-at-point'.
|
||
|
;; For batch use, however, printing the backtrace may be useful.
|
||
|
(loop
|
||
|
;; 6 is the number of frames our own debugger adds (when
|
||
|
;; compiled; more when interpreted). FIXME: Need to describe a
|
||
|
;; procedure for determining this constant.
|
||
|
for i from 6
|
||
|
for frame = (backtrace-frame i)
|
||
|
while frame
|
||
|
collect frame))
|
||
|
|
||
|
;; A container for the state of the execution of a single test and
|
||
|
;; environment data needed during its execution.
|
||
|
(defstruct ert-test-execution-info
|
||
|
(test (assert nil))
|
||
|
(result (assert nil))
|
||
|
;; A thunk that may be called when RESULT has been set to its final
|
||
|
;; value and test execution should be terminated. Should not
|
||
|
;; return.
|
||
|
(exit-continuation (assert nil))
|
||
|
;; The binding of `debugger' outside of the execution of the test.
|
||
|
next-debugger
|
||
|
;; The binding of `ert-debug-on-error' that is in effect for the
|
||
|
;; execution of the current test. We store it to avoid being
|
||
|
;; affected by any new bindings the test itself may establish. (I
|
||
|
;; don't remember whether this feature is important.)
|
||
|
ert-debug-on-error)
|
||
|
|
||
|
(defun ert-run-test-debugger (info debugger-args)
|
||
|
"The function that `debugger' is bound to during the execution of tests.
|
||
|
|
||
|
Records failures and errors and either terminates the test
|
||
|
silently or calls the interactive debugger, as appropriate."
|
||
|
(destructuring-bind (first-debugger-arg &rest more-debugger-args) debugger-args
|
||
|
(ecase first-debugger-arg
|
||
|
((lambda debug t exit nil)
|
||
|
(apply (ert-test-execution-info-next-debugger info) debugger-args))
|
||
|
(error
|
||
|
(let* ((condition (first more-debugger-args))
|
||
|
(type (case (car condition)
|
||
|
((quit) 'quit)
|
||
|
((ert-test-failed) 'failed)
|
||
|
(otherwise 'error)))
|
||
|
(backtrace (ert-record-backtrace)))
|
||
|
(setf (ert-test-execution-info-result info)
|
||
|
(ecase type
|
||
|
(quit
|
||
|
(make-ert-test-quit :condition condition
|
||
|
:backtrace backtrace))
|
||
|
(failed
|
||
|
(make-ert-test-failed :condition condition
|
||
|
:backtrace backtrace))
|
||
|
(error
|
||
|
(make-ert-test-error :condition condition
|
||
|
:backtrace backtrace))))
|
||
|
;; Work around Emacs' heuristic (in eval.c) for detecting
|
||
|
;; errors in the debugger.
|
||
|
(incf num-nonmacro-input-events)
|
||
|
;; FIXME: We should probably implement more fine-grained
|
||
|
;; control a la non-t `debug-on-error' here.
|
||
|
(cond
|
||
|
((ert-test-execution-info-ert-debug-on-error info)
|
||
|
(apply (ert-test-execution-info-next-debugger info) debugger-args))
|
||
|
(t))
|
||
|
(funcall (ert-test-execution-info-exit-continuation info)))))))
|
||
|
|
||
|
(defun ert-run-test-internal (ert-test-execution-info)
|
||
|
(lexical-let ((info ert-test-execution-info))
|
||
|
(setf (ert-test-execution-info-next-debugger info) debugger
|
||
|
(ert-test-execution-info-ert-debug-on-error info) ert-debug-on-error)
|
||
|
(catch 'ert-pass
|
||
|
;; For now, each test gets its own temp buffer and its own
|
||
|
;; window excursion, just to be safe. If this turns out to be
|
||
|
;; too expensive, we can remove it.
|
||
|
(with-temp-buffer
|
||
|
(save-window-excursion
|
||
|
(let ((debugger (lambda (&rest debugger-args)
|
||
|
(ert-run-test-debugger info debugger-args)))
|
||
|
(debug-on-error t)
|
||
|
(debug-on-quit t)
|
||
|
;; FIXME: Do we need to store the old binding of this
|
||
|
;; and consider it in `ert-run-test-debugger'?
|
||
|
(debug-ignored-errors nil)
|
||
|
(ert-messages-mark (ert-end-of-messages))
|
||
|
(ert-warnings-mark (ert-end-of-warnings)))
|
||
|
(funcall (ert-test-body (ert-test-execution-info-test info))))))
|
||
|
(ert-pass))
|
||
|
(setf (ert-test-execution-info-result info) (make-ert-test-passed)))
|
||
|
nil)
|
||
|
|
||
|
(defun ert-make-marker-in-messages-buffer ()
|
||
|
(with-current-buffer (get-buffer-create "*Messages*")
|
||
|
(set-marker (make-marker) (point-max))))
|
||
|
|
||
|
(defun ert-force-message-log-buffer-truncation ()
|
||
|
(with-current-buffer (get-buffer-create "*Messages*")
|
||
|
;; This is a reimplementation of this part of message_dolog() in xdisp.c:
|
||
|
;; if (NATNUMP (Vmessage_log_max))
|
||
|
;; {
|
||
|
;; scan_newline (Z, Z_BYTE, BEG, BEG_BYTE,
|
||
|
;; -XFASTINT (Vmessage_log_max) - 1, 0);
|
||
|
;; del_range_both (BEG, BEG_BYTE, PT, PT_BYTE, 0);
|
||
|
;; }
|
||
|
(when (and (integerp message-log-max) (>= message-log-max 0))
|
||
|
(let ((begin (point-min))
|
||
|
(end (save-excursion
|
||
|
(goto-char (point-max))
|
||
|
(forward-line (- message-log-max))
|
||
|
(point))))
|
||
|
(delete-region begin end)))))
|
||
|
|
||
|
(defun ert-run-test (test)
|
||
|
"Run TEST. Return the result and store it in TEST's `most-recent-result' slot."
|
||
|
(setf (ert-test-most-recent-result test) nil)
|
||
|
(block error
|
||
|
(lexical-let* ((begin-marker (ert-make-marker-in-messages-buffer))
|
||
|
(info (make-ert-test-execution-info
|
||
|
:test test
|
||
|
:result (make-ert-test-aborted-with-non-local-exit)
|
||
|
:exit-continuation (lambda ()
|
||
|
(return-from error nil)))))
|
||
|
(unwind-protect
|
||
|
(let ((message-log-max t))
|
||
|
(ert-run-test-internal info))
|
||
|
(let ((result (ert-test-execution-info-result info)))
|
||
|
(setf (ert-test-result-messages result)
|
||
|
(with-current-buffer (get-buffer-create "*Messages*")
|
||
|
(buffer-substring begin-marker (point-max))))
|
||
|
(ert-force-message-log-buffer-truncation)
|
||
|
(setf (ert-test-most-recent-result test) result)))))
|
||
|
(ert-test-most-recent-result test))
|
||
|
|
||
|
|
||
|
;;; The `should' macros.
|
||
|
|
||
|
(eval-and-compile
|
||
|
(defun ert-special-operator-p (thing)
|
||
|
"Return non-nil if THING is a symbol naming a special operator."
|
||
|
(and (symbolp thing)
|
||
|
(let ((definition (indirect-function thing t)))
|
||
|
(and (subrp definition)
|
||
|
(eql (cdr (subr-arity definition)) 'unevalled)))))
|
||
|
(defun ert-expand-should (whole form env inner-expander)
|
||
|
"Helper function for the `should' macro and its variants.
|
||
|
|
||
|
Analyzes FORM and produces an expression that has the same
|
||
|
semantics under evaluation but records additional debugging
|
||
|
information. INNER-EXPANDER adds the actual checks specific to
|
||
|
the particular variant of `should'."
|
||
|
(let ((form (macroexpand form env)))
|
||
|
;; It's sort of a wart that `inner-expander' can't influence the
|
||
|
;; value the expansion returns.
|
||
|
(cond
|
||
|
((atom form)
|
||
|
(funcall inner-expander form `(list ',whole :form ',form :value ,form)))
|
||
|
((ert-special-operator-p (car form))
|
||
|
(let ((value (gensym "value-")))
|
||
|
`(let ((,value (make-symbol "ert-form-evaluation-aborted")))
|
||
|
,(funcall inner-expander
|
||
|
`(setq ,value ,form)
|
||
|
`(list ',whole :form ',form :value ,value))
|
||
|
,value)))
|
||
|
(t
|
||
|
(let ((fn-name (car form))
|
||
|
(arg-forms (cdr form)))
|
||
|
(assert (or (symbolp fn-name)
|
||
|
(and (consp fn-name)
|
||
|
(eql (car fn-name) 'lambda)
|
||
|
(listp (cdr fn-name)))))
|
||
|
(let ((fn (gensym "fn-"))
|
||
|
(args (gensym "args-"))
|
||
|
(value (gensym "value-"))
|
||
|
(default-value (gensym "ert-form-evaluation-aborted-")))
|
||
|
`(let ((,fn (function ,fn-name))
|
||
|
(,args (list ,@arg-forms)))
|
||
|
(let ((,value ',default-value))
|
||
|
,(funcall inner-expander
|
||
|
`(setq ,value (apply ,fn ,args))
|
||
|
`(nconc (list ',whole)
|
||
|
(list :form `(,,fn ,@,args))
|
||
|
(unless (eql ,value ',default-value)
|
||
|
(list :value ,value))
|
||
|
(let ((-explainer-
|
||
|
(and (symbolp ',fn-name)
|
||
|
(get ',fn-name
|
||
|
'ert-explainer))))
|
||
|
(when -explainer-
|
||
|
(list :explanation
|
||
|
(apply -explainer- ,args))))))
|
||
|
,value)))))))))
|
||
|
|
||
|
(defmacro* ert-should (form &environment env)
|
||
|
"Evaluate FORM. If it returns nil, abort the current test as failed.
|
||
|
|
||
|
Returns the value of FORM."
|
||
|
(ert-expand-should `(ert-should ,form) form env
|
||
|
(lambda (inner-form form-description-form)
|
||
|
`(unless ,inner-form
|
||
|
(ert-fail ,form-description-form)))))
|
||
|
|
||
|
(defmacro* ert-should-not (form &environment env)
|
||
|
"Evaluate FORM. If it returns non-nil, abort the current test as failed.
|
||
|
|
||
|
Returns nil."
|
||
|
(ert-expand-should `(ert-should-not ,form) form env
|
||
|
(lambda (inner-form form-description-form)
|
||
|
`(unless (not ,inner-form)
|
||
|
(ert-fail ,form-description-form)))))
|
||
|
|
||
|
(defun ert-should-error-handle-error (form-description-fn
|
||
|
condition type exclude-subtypes test)
|
||
|
"Helper function for `should-error'.
|
||
|
|
||
|
Determines whether CONDITION matches TYPE, EXCLUDE-SUBTYPES and
|
||
|
TEST, and aborts the current test as failed if it doesn't."
|
||
|
(let ((signalled-conditions (get (car condition) 'error-conditions))
|
||
|
(handled-conditions (etypecase type
|
||
|
(list type)
|
||
|
(symbol (list type)))))
|
||
|
(assert signalled-conditions)
|
||
|
(unless (intersection signalled-conditions handled-conditions)
|
||
|
(ert-fail (append
|
||
|
(funcall form-description-fn)
|
||
|
(list
|
||
|
:condition condition
|
||
|
:fail-reason (concat "the error signalled did not"
|
||
|
" have the expected type")))))
|
||
|
(when exclude-subtypes
|
||
|
(unless (member (car condition) handled-conditions)
|
||
|
(ert-fail (append
|
||
|
(funcall form-description-fn)
|
||
|
(list
|
||
|
:condition condition
|
||
|
:fail-reason (concat "the error signalled was a subtype"
|
||
|
" of the expected type"))))))
|
||
|
(unless (funcall test condition)
|
||
|
(ert-fail (append
|
||
|
(funcall form-description-fn)
|
||
|
(list
|
||
|
:condition condition
|
||
|
:fail-reason "the error signalled did not pass the test"))))))
|
||
|
|
||
|
;; FIXME: The expansion will evaluate the keyword args (if any) in
|
||
|
;; nonstandard order.
|
||
|
(defmacro* ert-should-error (form &rest keys &key type exclude-subtypes test
|
||
|
&environment env)
|
||
|
"Evaluate FORM. Unless it signals an error, abort the current test as failed.
|
||
|
|
||
|
The error signalled additionally needs to match TYPE and satisfy
|
||
|
TEST. TYPE should be a condition name or a list of condition
|
||
|
names. If EXCLUDE-SUBTYPES is nil, the error matches TYPE if one
|
||
|
of its condition names is an element of TYPE. If
|
||
|
EXCLUDE-SUBTYPES is non-nil, the error matches TYPE if it is an
|
||
|
element of TYPE. TEST should be a predicate."
|
||
|
;; Returns a gensym named `ert-form-evaluation-aborted-XXX', but
|
||
|
;; that's a wart, so let's not document it.
|
||
|
(unless type (setq type ''error))
|
||
|
(unless test (setq test '(lambda (condition) t)))
|
||
|
(ert-expand-should
|
||
|
`(ert-should-error ,form ,@keys)
|
||
|
form env
|
||
|
(lambda (inner-form form-description-form)
|
||
|
(let ((errorp (gensym "errorp"))
|
||
|
(form-description-fn (gensym "form-description-fn-")))
|
||
|
`(let ((,errorp nil)
|
||
|
(,form-description-fn (lambda () ,form-description-form)))
|
||
|
(condition-case -condition-
|
||
|
,inner-form
|
||
|
;; We can't use ,type here because we want to evaluate it.
|
||
|
(error
|
||
|
(setq ,errorp t)
|
||
|
(ert-should-error-handle-error ,form-description-fn
|
||
|
-condition-
|
||
|
,type ,exclude-subtypes ,test)
|
||
|
;; It would make sense to have the `should-error' form
|
||
|
;; return the error in this case, but `ert-expand-should'
|
||
|
;; doesn't allow that at the moment.
|
||
|
))
|
||
|
(unless ,errorp
|
||
|
(ert-fail (append
|
||
|
(funcall ,form-description-fn)
|
||
|
(list
|
||
|
:fail-reason "did not signal an error")))))))))
|
||
|
|
||
|
|
||
|
;;; Explanation of `should' failures.
|
||
|
|
||
|
(defun ert-proper-list-p (x)
|
||
|
"Return non-nil if X is a proper list, nil otherwise."
|
||
|
(loop
|
||
|
for firstp = t then nil
|
||
|
for fast = x then (cddr fast)
|
||
|
for slow = x then (cdr slow) do
|
||
|
(when (null fast) (return t))
|
||
|
(when (not (consp fast)) (return nil))
|
||
|
(when (null (cdr fast)) (return t))
|
||
|
(when (not (consp (cdr fast))) (return nil))
|
||
|
(when (and (not firstp) (eq fast slow)) (return nil))))
|
||
|
|
||
|
(defun ert-explain-not-equal (a b)
|
||
|
"Return a programmer-readable explanation of why A and B are not `equal'.
|
||
|
|
||
|
Returns nil if they are equal."
|
||
|
(if (not (equal (type-of a) (type-of b)))
|
||
|
`(different-types ,a ,b)
|
||
|
(etypecase a
|
||
|
(cons
|
||
|
(let ((a-proper-p (ert-proper-list-p a))
|
||
|
(b-proper-p (ert-proper-list-p b)))
|
||
|
(if (not (eql (not a-proper-p) (not b-proper-p)))
|
||
|
`(one-list-proper-one-improper ,a ,b)
|
||
|
(if a-proper-p
|
||
|
(if (not (equal (length a) (length b)))
|
||
|
;; This would be even more helpful if it showed
|
||
|
;; something like what `set-difference' would
|
||
|
;; return.
|
||
|
`(proper-lists-of-different-length ,a ,b)
|
||
|
(loop for i from 0
|
||
|
for ai in a
|
||
|
for bi in b
|
||
|
for xi = (ert-explain-not-equal ai bi)
|
||
|
do (when xi (return `(list-elt ,i ,xi)))))
|
||
|
(let ((car-x (ert-explain-not-equal (car a) (car b))))
|
||
|
(if car-x
|
||
|
`(car ,car-x)
|
||
|
(let ((cdr-x (ert-explain-not-equal (cdr a) (cdr b))))
|
||
|
(if cdr-x
|
||
|
`(cdr ,cdr-x))
|
||
|
nil)))))))
|
||
|
(array (if (not (equal (length a) (length b)))
|
||
|
`(arrays-of-different-length ,a ,b)
|
||
|
(loop for i from 0
|
||
|
for ai across a
|
||
|
for bi across b
|
||
|
for xi = (ert-explain-not-equal ai bi)
|
||
|
do (when xi (return `(array-elt ,i ,xi))))))
|
||
|
(atom (if (not (equal a b))
|
||
|
`(different-atoms ,a ,b)
|
||
|
nil)))))
|
||
|
(put 'equal 'ert-explainer 'ert-explain-not-equal)
|
||
|
|
||
|
|
||
|
;;; Results display.
|
||
|
|
||
|
;; The data structure that contains the set of tests being executed
|
||
|
;; during one particular test run, their results, the state of the
|
||
|
;; execution, and some statistics.
|
||
|
;;
|
||
|
;; The data about results and expected results of tests may seem
|
||
|
;; redundant here, since the test objects also carry such information.
|
||
|
;; However, the information in the test objects may be more recent, it
|
||
|
;; may correspond to a different test run. We need the information
|
||
|
;; that corresponds to this run in order to be able to update the
|
||
|
;; statistics correctly when a test is re-run interactively and has a
|
||
|
;; different result than before.
|
||
|
(defstruct ert-stats
|
||
|
(selector (assert nil))
|
||
|
;; The tests, in order.
|
||
|
(tests (assert nil) :type vector)
|
||
|
;; A map of test names (or the test objects themselves for unnamed
|
||
|
;; tests) to indices into the `tests' vector.
|
||
|
(test-map (assert nil) :type hash-table)
|
||
|
;; The results of the tests during this run, in order.
|
||
|
(test-results (assert nil) :type vector)
|
||
|
;; The expected result types of the tests, in order.
|
||
|
(test-results-expected (assert nil) :type vector)
|
||
|
(total (assert nil))
|
||
|
(passed-expected 0)
|
||
|
(passed-unexpected 0)
|
||
|
(failed-expected 0)
|
||
|
(failed-unexpected 0)
|
||
|
(error-expected 0)
|
||
|
(error-unexpected 0)
|
||
|
(start-time (assert nil))
|
||
|
(end-time nil)
|
||
|
(aborted-p nil)
|
||
|
(current-test nil))
|
||
|
|
||
|
;; An entry in the results buffer ewoc. There is one entry per test.
|
||
|
(defstruct ert-ewoc-entry
|
||
|
(test (assert nil))
|
||
|
(result nil)
|
||
|
;; If the result of this test was expected, its ewoc entry is hidden
|
||
|
;; initially.
|
||
|
(hidden-p (assert nil))
|
||
|
;; An ewoc entry may be collapsed to hide details such as the error
|
||
|
;; condition.
|
||
|
;;
|
||
|
;; I'm not sure the ability to expand and collapse entries is still
|
||
|
;; a useful feature.
|
||
|
(expanded-p t)
|
||
|
;; By default, the ewoc entry presents the error condition with
|
||
|
;; certain limits on how much to print (`print-level',
|
||
|
;; `print-length'). The user can interactively switch to a set of
|
||
|
;; higher limits.
|
||
|
(extended-printer-limits-p nil))
|
||
|
|
||
|
;; Variables local to the results buffer.
|
||
|
|
||
|
;; The ewoc.
|
||
|
(defvar ert-results-ewoc)
|
||
|
;; The stats object.
|
||
|
(defvar ert-results-stats)
|
||
|
;; A string with one character per test. Each character represents
|
||
|
;; the result of the corresponding test. The string is displayed near
|
||
|
;; the top of the buffer and serves as a progress bar.
|
||
|
(defvar ert-results-progress-bar-string)
|
||
|
;; The position where the progress bar button begins.
|
||
|
(defvar ert-results-progress-bar-button-begin)
|
||
|
;; The test result listener that updates the buffer when tests are run.
|
||
|
(defvar ert-results-listener)
|
||
|
|
||
|
;; The same as `ert-results-stats', but dynamically bound. Used for
|
||
|
;; the mode line progress indicator.
|
||
|
(defvar ert-current-run-stats nil)
|
||
|
|
||
|
(defun ert-format-time-iso8601 (time)
|
||
|
"Format TIME in the particular variant of ISO 8601 used for timestamps in ERT."
|
||
|
(format-time-string "%Y-%m-%d %T%z" time))
|
||
|
|
||
|
(defun ert-insert-test-name-button (test-name)
|
||
|
(insert-text-button (format "%S" test-name)
|
||
|
:type 'ert-test-name-button
|
||
|
'ert-test-name test-name))
|
||
|
|
||
|
(defun ert-results-update-ewoc-hf (ewoc stats)
|
||
|
"Update the header and footer of EWOC to show certain information from STATS.
|
||
|
|
||
|
Also sets `ert-results-progress-bar-button-begin'."
|
||
|
(let ((run-count (+ (ert-stats-passed-expected stats)
|
||
|
(ert-stats-passed-unexpected stats)
|
||
|
(ert-stats-failed-expected stats)
|
||
|
(ert-stats-failed-unexpected stats)
|
||
|
(ert-stats-error-expected stats)
|
||
|
(ert-stats-error-unexpected stats)))
|
||
|
(results-buffer (current-buffer)))
|
||
|
(ewoc-set-hf
|
||
|
ewoc
|
||
|
;; header
|
||
|
(with-temp-buffer
|
||
|
(insert "Selector: ")
|
||
|
(ert-insert-human-readable-selector (ert-stats-selector stats))
|
||
|
(insert "\n")
|
||
|
(insert
|
||
|
(format (concat "Passed: %s (%s unexpected)\n"
|
||
|
"Failed: %s (%s unexpected)\n"
|
||
|
"Error: %s (%s unexpected)\n"
|
||
|
"Total: %s/%s\n\n")
|
||
|
(+ (ert-stats-passed-expected stats)
|
||
|
(ert-stats-passed-unexpected stats))
|
||
|
(ert-stats-passed-unexpected stats)
|
||
|
(+ (ert-stats-failed-expected stats)
|
||
|
(ert-stats-failed-unexpected stats))
|
||
|
(ert-stats-failed-unexpected stats)
|
||
|
(+ (ert-stats-error-expected stats)
|
||
|
(ert-stats-error-unexpected stats))
|
||
|
(ert-stats-error-unexpected stats)
|
||
|
run-count
|
||
|
(ert-stats-total stats)))
|
||
|
(insert
|
||
|
(format "Started at: %s\n"
|
||
|
(ert-format-time-iso8601 (ert-stats-start-time stats))))
|
||
|
;; FIXME: This is ugly. Need to properly define invariants of
|
||
|
;; the `stats' data structure.
|
||
|
(let ((state (cond ((ert-stats-aborted-p stats)
|
||
|
'aborted)
|
||
|
((ert-stats-current-test stats)
|
||
|
'running)
|
||
|
((ert-stats-end-time stats)
|
||
|
'finished)
|
||
|
(t
|
||
|
'preparing))))
|
||
|
(ecase state
|
||
|
(preparing
|
||
|
(insert ""))
|
||
|
(aborted
|
||
|
(cond ((ert-stats-current-test stats)
|
||
|
(insert "Aborted during test: ")
|
||
|
(ert-insert-test-name-button
|
||
|
(ert-test-name (ert-stats-current-test stats))))
|
||
|
(t
|
||
|
(insert "Aborted."))))
|
||
|
(running
|
||
|
(assert (ert-stats-current-test stats))
|
||
|
(insert "Running test: ")
|
||
|
(ert-insert-test-name-button (ert-test-name
|
||
|
(ert-stats-current-test stats))))
|
||
|
(finished
|
||
|
(assert (not (ert-stats-current-test stats)))
|
||
|
(insert "Finished.")))
|
||
|
(insert "\n")
|
||
|
(if (ert-stats-end-time stats)
|
||
|
(insert
|
||
|
(format "%s%s\n"
|
||
|
(if (ert-stats-aborted-p stats)
|
||
|
"Aborted at: "
|
||
|
"Finished at: ")
|
||
|
(ert-format-time-iso8601 (ert-stats-end-time stats))))
|
||
|
(insert "\n"))
|
||
|
(insert "\n"))
|
||
|
(let ((progress-bar-string (with-current-buffer results-buffer
|
||
|
ert-results-progress-bar-string)))
|
||
|
(let ((progress-bar-button-begin
|
||
|
(insert-text-button (substring progress-bar-string 0 run-count)
|
||
|
:type 'ert-results-progress-bar-button)))
|
||
|
(with-current-buffer results-buffer
|
||
|
(set (make-local-variable 'ert-results-progress-bar-button-begin)
|
||
|
progress-bar-button-begin)))
|
||
|
(insert (substring progress-bar-string run-count)))
|
||
|
(insert "\n\n")
|
||
|
(buffer-string))
|
||
|
;; footer
|
||
|
;;
|
||
|
;; We actually want an empty footer, but that would trigger a bug
|
||
|
;; in ewoc, sometimes clearing the entire buffer.
|
||
|
"\n")))
|
||
|
|
||
|
(defun ert-results-update-stats-display (ewoc stats)
|
||
|
"Update EWOC and the mode line to show data from STATS."
|
||
|
(ert-results-update-ewoc-hf ewoc stats)
|
||
|
(force-mode-line-update)
|
||
|
(redisplay t))
|
||
|
|
||
|
(defun ert-char-for-test-result (result expectedp)
|
||
|
"Return a character that represents the test result RESULT."
|
||
|
(let ((char
|
||
|
(etypecase result
|
||
|
(ert-test-passed ?.)
|
||
|
(ert-test-failed ?f)
|
||
|
(ert-test-error ?e)
|
||
|
(null ?-)
|
||
|
(ert-test-aborted-with-non-local-exit ?a))))
|
||
|
(if expectedp
|
||
|
char
|
||
|
(upcase char))))
|
||
|
|
||
|
(defun ert-string-for-test-result (result expectedp)
|
||
|
"Return a string that represents the test result RESULT."
|
||
|
(etypecase result
|
||
|
(ert-test-passed "passed")
|
||
|
(ert-test-failed "failed")
|
||
|
(ert-test-error "error")
|
||
|
(null "unknown")
|
||
|
(ert-test-aborted-with-non-local-exit "aborted")))
|
||
|
|
||
|
(defun ert-tests-running-mode-line-indicator ()
|
||
|
(let* ((stats ert-current-run-stats)
|
||
|
(tests-total (ert-stats-total stats))
|
||
|
(tests-completed (+ (ert-stats-passed-expected stats)
|
||
|
(ert-stats-passed-unexpected stats)
|
||
|
(ert-stats-failed-expected stats)
|
||
|
(ert-stats-failed-unexpected stats)
|
||
|
(ert-stats-error-expected stats)
|
||
|
(ert-stats-error-unexpected stats))))
|
||
|
(if (>= tests-completed tests-total)
|
||
|
(format " ERT(%s/%s,finished)" tests-completed tests-total)
|
||
|
(format " ERT(%s/%s):%s"
|
||
|
(1+ tests-completed)
|
||
|
tests-total
|
||
|
(if (null (ert-stats-current-test stats))
|
||
|
"?"
|
||
|
(format "%S"
|
||
|
(ert-test-name (ert-stats-current-test stats))))))))
|
||
|
|
||
|
(defun ert-pp-with-indentation-and-newline (object)
|
||
|
"Pretty-print OBJECT, indenting it to the current column of point.
|
||
|
Ensures a final newline is inserted."
|
||
|
(let ((begin (point)))
|
||
|
(pp object (current-buffer))
|
||
|
(unless (bolp) (insert "\n"))
|
||
|
(save-excursion
|
||
|
(goto-char begin)
|
||
|
(indent-sexp))))
|
||
|
|
||
|
(defun ert-print-test-for-ewoc (entry)
|
||
|
"The ewoc print function for ewoc test entries."
|
||
|
(let* ((test (ert-ewoc-entry-test entry))
|
||
|
(result (ert-ewoc-entry-result entry))
|
||
|
(hiddenp (ert-ewoc-entry-hidden-p entry))
|
||
|
(expandedp (ert-ewoc-entry-expanded-p entry))
|
||
|
(extended-printer-limits-p (ert-ewoc-entry-extended-printer-limits-p
|
||
|
entry)))
|
||
|
(cond (hiddenp)
|
||
|
(t
|
||
|
(insert-text-button (format "%c"
|
||
|
(ert-char-for-test-result
|
||
|
result
|
||
|
(ert-test-result-expected-p test
|
||
|
result)))
|
||
|
:type 'ert-results-expand-collapse-button)
|
||
|
(insert " ")
|
||
|
(ert-insert-test-name-button (ert-test-name test))
|
||
|
(insert "\n")
|
||
|
(when (and expandedp (not (eql result 'nil)))
|
||
|
(etypecase result
|
||
|
(ert-test-passed
|
||
|
(insert " passed\n")
|
||
|
(insert ""))
|
||
|
(ert-test-result-with-condition
|
||
|
(insert " ")
|
||
|
(let ((print-escape-newlines t)
|
||
|
(print-level (if extended-printer-limits-p 10 5))
|
||
|
(print-length (if extended-printer-limits-p 100 10)))
|
||
|
(let ((begin (point)))
|
||
|
(ert-pp-with-indentation-and-newline
|
||
|
(ert-test-result-with-condition-condition result))
|
||
|
(save-restriction
|
||
|
(narrow-to-region begin (point))
|
||
|
;; Inhibit optimization in `debugger-make-xrefs'
|
||
|
;; that sometimes inserts unrelated backtrace
|
||
|
;; info into our buffer.
|
||
|
(let ((debugger-previous-backtrace nil))
|
||
|
(debugger-make-xrefs))))))
|
||
|
(ert-test-aborted-with-non-local-exit
|
||
|
(insert " aborted\n")))
|
||
|
(insert "\n")))))
|
||
|
nil)
|
||
|
|
||
|
(defun ert-setup-results-buffer (stats listener buffer-name)
|
||
|
"Set up a test results buffer."
|
||
|
(unless buffer-name (setq buffer-name "*ert*"))
|
||
|
(let ((buffer (let ((default-major-mode 'fundamental-mode))
|
||
|
(get-buffer-create buffer-name))))
|
||
|
(with-current-buffer buffer
|
||
|
(setq buffer-read-only t)
|
||
|
(let ((inhibit-read-only t))
|
||
|
(buffer-disable-undo)
|
||
|
(erase-buffer)
|
||
|
(ert-results-mode)
|
||
|
(set (make-local-variable 'ert-results-ewoc)
|
||
|
(ewoc-create 'ert-print-test-for-ewoc nil nil t))
|
||
|
(set (make-local-variable 'ert-results-stats) stats)
|
||
|
(set (make-local-variable 'ert-results-progress-bar-string)
|
||
|
(make-string (ert-stats-total stats)
|
||
|
(ert-char-for-test-result nil t)))
|
||
|
(set (make-local-variable 'ert-results-listener) listener)
|
||
|
(ert-results-update-ewoc-hf ert-results-ewoc ert-results-stats)
|
||
|
(goto-char (1- (point-max)))
|
||
|
buffer))))
|
||
|
|
||
|
(defun ert-run-or-rerun-test (stats test listener)
|
||
|
"Run the single test TEST and record the result using STATS and LISTENER."
|
||
|
(let ((ert-current-run-stats stats)
|
||
|
(pos (ert-stats-test-index stats test))
|
||
|
(results (ert-stats-test-results stats))
|
||
|
(expected (ert-stats-test-results-expected stats)))
|
||
|
;; Adjust stats to remove previous result.
|
||
|
(if (aref expected pos)
|
||
|
(etypecase (aref results pos)
|
||
|
(ert-test-passed (decf (ert-stats-passed-expected stats)))
|
||
|
(ert-test-failed (decf (ert-stats-failed-expected stats)))
|
||
|
(ert-test-error (decf (ert-stats-error-expected stats)))
|
||
|
(null)
|
||
|
(ert-test-aborted-with-non-local-exit))
|
||
|
(etypecase (aref results pos)
|
||
|
(ert-test-passed (decf (ert-stats-passed-unexpected stats)))
|
||
|
(ert-test-failed (decf (ert-stats-failed-unexpected stats)))
|
||
|
(ert-test-error (decf (ert-stats-error-unexpected stats)))
|
||
|
(null)
|
||
|
(ert-test-aborted-with-non-local-exit)))
|
||
|
(setf (aref results pos) nil)
|
||
|
;; Call listener after setting/before resetting
|
||
|
;; (ert-stats-current-test stats); the listener might refresh the
|
||
|
;; mode line display, and if the value is not set yet/any more
|
||
|
;; during this refresh, the mode line will flicker unnecessarily.
|
||
|
(setf (ert-stats-current-test stats) test)
|
||
|
(funcall listener 'test-started stats test)
|
||
|
(setf (ert-test-most-recent-result test) nil)
|
||
|
(unwind-protect
|
||
|
(ert-run-test test)
|
||
|
(let* ((result (ert-test-most-recent-result test))
|
||
|
(expectedp (typep result (ert-test-expected-result-type test))))
|
||
|
;; Adjust stats to add new result.
|
||
|
(if expectedp
|
||
|
(etypecase result
|
||
|
(ert-test-passed (incf (ert-stats-passed-expected stats)))
|
||
|
(ert-test-failed (incf (ert-stats-failed-expected stats)))
|
||
|
(ert-test-error (incf (ert-stats-error-expected stats)))
|
||
|
(null)
|
||
|
(ert-test-aborted-with-non-local-exit))
|
||
|
(etypecase result
|
||
|
(ert-test-passed (incf (ert-stats-passed-unexpected stats)))
|
||
|
(ert-test-failed (incf (ert-stats-failed-unexpected stats)))
|
||
|
(ert-test-error (incf (ert-stats-error-unexpected stats)))
|
||
|
(null)
|
||
|
(ert-test-aborted-with-non-local-exit)))
|
||
|
(setf (aref results pos) result
|
||
|
(aref expected pos) expectedp)
|
||
|
(funcall listener 'test-ended stats test result))
|
||
|
(setf (ert-stats-current-test stats) nil))))
|
||
|
|
||
|
(defun ert-run-tests (selector listener)
|
||
|
"Run the tests specified by SELECTOR, sending progress updates to LISTENER."
|
||
|
(let* ((tests (coerce (ert-select-tests selector t) 'vector))
|
||
|
(map (let ((map (make-hash-table :size (length tests))))
|
||
|
(loop for i from 0
|
||
|
for test across tests
|
||
|
for key = (or (ert-test-name test) test) do
|
||
|
(assert (not (gethash key map)))
|
||
|
(setf (gethash key map) i))
|
||
|
map))
|
||
|
(stats (make-ert-stats :selector selector
|
||
|
:tests tests
|
||
|
:test-map map
|
||
|
:test-results (make-vector (length tests) nil)
|
||
|
:test-results-expected (make-vector
|
||
|
(length tests) nil)
|
||
|
:total (length tests)
|
||
|
:start-time (current-time))))
|
||
|
(funcall listener 'run-started stats)
|
||
|
(let ((abortedp t))
|
||
|
(let ((ert-current-run-stats stats))
|
||
|
(force-mode-line-update)
|
||
|
(unwind-protect
|
||
|
(progn
|
||
|
(loop for test across tests do
|
||
|
(ert-run-or-rerun-test stats test listener))
|
||
|
(setq abortedp nil))
|
||
|
(setf (ert-stats-aborted-p stats) abortedp)
|
||
|
(setf (ert-stats-end-time stats) (current-time))
|
||
|
(funcall listener 'run-ended stats abortedp)))
|
||
|
stats)))
|
||
|
|
||
|
(defun ert-stats-test-index (stats test)
|
||
|
"Return the index of TEST in the run represented by STATS."
|
||
|
(gethash (or (ert-test-name test) test) (ert-stats-test-map stats)))
|
||
|
|
||
|
(defvar ert-selector-history nil
|
||
|
"List of recent test selectors read from terminal.")
|
||
|
|
||
|
;; Fix-me: return (regep (list of matches))?
|
||
|
;; Fix-me: Add prompt parameter?
|
||
|
(defun ert-read-test-selector ()
|
||
|
"Read a regexp for test selection from minibuffer.
|
||
|
The user can use TAB to see which tests match."
|
||
|
(let* ((all-tests
|
||
|
(mapcar (lambda (rec) (format "%s" (elt rec 1)))
|
||
|
(ert-select-tests "" t))
|
||
|
;;'("ert-group1-1" "ert-group1-2" "ert-other")
|
||
|
)
|
||
|
regexp
|
||
|
ret
|
||
|
(get-completions
|
||
|
(lambda ()
|
||
|
(let* ((ret (save-match-data
|
||
|
(mapcar (lambda (alt)
|
||
|
(when (string-match regexp alt)
|
||
|
alt))
|
||
|
all-tests))))
|
||
|
(setq ret (delq nil ret))
|
||
|
ret))))
|
||
|
(setq all-tests (append all-tests
|
||
|
'(":new"
|
||
|
":failed" ":passed" ":error"
|
||
|
)
|
||
|
nil))
|
||
|
(let ((mini-map (copy-keymap minibuffer-local-map)))
|
||
|
(define-key mini-map [?\t]
|
||
|
(lambda () (interactive)
|
||
|
(with-output-to-temp-buffer "*Completions*"
|
||
|
(display-completion-list
|
||
|
(progn
|
||
|
(setq regexp (minibuffer-contents))
|
||
|
(set-text-properties 0 (length regexp) nil regexp)
|
||
|
(funcall get-completions))))))
|
||
|
(setq regexp
|
||
|
(let* ((sym-here (thing-at-point 'symbol))
|
||
|
(test-here (when (and sym-here
|
||
|
(memq sym-here all-tests))
|
||
|
sym-here))
|
||
|
(default (if sym-here
|
||
|
(substring-no-properties sym-here)
|
||
|
(if ert-selector-history
|
||
|
(first ert-selector-history)
|
||
|
"t"))))
|
||
|
(read-from-minibuffer
|
||
|
(if (null default)
|
||
|
"Run tests, use TAB to see matches: "
|
||
|
(format "Run tests, use TAB to see matches (default %s): "
|
||
|
default))
|
||
|
nil ;; initial-contents
|
||
|
mini-map ;; keymap
|
||
|
nil ;; read
|
||
|
'ert-selector-history
|
||
|
default nil))))
|
||
|
(setq ret regexp)
|
||
|
(when (string= "t" ret)
|
||
|
(setq ret t))
|
||
|
ret))
|
||
|
|
||
|
;; Should OUTPUT-BUFFER-NAME and MESSAGE-FN really be arguments here?
|
||
|
;; They are needed only for our automated self-tests at the moment.
|
||
|
;; Or should there be some other mechanism?
|
||
|
;;;###autoload
|
||
|
(defun ert-run-tests-interactively (selector
|
||
|
&optional output-buffer-name message-fn)
|
||
|
"Run the tests specified by SELECTOR and display the results in a buffer."
|
||
|
(interactive
|
||
|
;;; (list (let ((default (if ert-selector-history
|
||
|
;;; (first ert-selector-history)
|
||
|
;;; "t")))
|
||
|
;;; (read-from-minibuffer (if (null default)
|
||
|
;;; "Run tests: "
|
||
|
;;; (format "Run tests (default %s): " default))
|
||
|
;;; ;;nil nil t 'ert-selector-history
|
||
|
;;; ;;
|
||
|
;;; ;; fix-me: seems like I am misunderstanding Christians intent here.
|
||
|
;;; nil nil nil 'ert-selector-history
|
||
|
;;; default nil))
|
||
|
;;; nil nil))
|
||
|
(list (ert-read-test-selector)
|
||
|
nil nil))
|
||
|
(unless message-fn (setq message-fn 'message))
|
||
|
(lexical-let ((output-buffer-name output-buffer-name)
|
||
|
buffer
|
||
|
listener
|
||
|
(message-fn message-fn))
|
||
|
(setq listener
|
||
|
(lambda (event-type &rest event-args)
|
||
|
(ecase event-type
|
||
|
(run-started
|
||
|
(destructuring-bind (stats) event-args
|
||
|
(setq buffer (ert-setup-results-buffer stats
|
||
|
listener
|
||
|
output-buffer-name))
|
||
|
(pop-to-buffer buffer)))
|
||
|
(run-ended
|
||
|
(destructuring-bind (stats abortedp) event-args
|
||
|
(funcall message-fn
|
||
|
"%sRan %s tests, %s results were as expected%s"
|
||
|
(if (not abortedp)
|
||
|
""
|
||
|
"Aborted: ")
|
||
|
(ert-stats-total stats)
|
||
|
(+ (ert-stats-passed-expected stats)
|
||
|
(ert-stats-failed-expected stats)
|
||
|
(ert-stats-error-expected stats))
|
||
|
(let ((unexpected
|
||
|
(+ (ert-stats-passed-unexpected stats)
|
||
|
(ert-stats-failed-unexpected stats)
|
||
|
(ert-stats-error-unexpected stats))))
|
||
|
(if (zerop unexpected)
|
||
|
""
|
||
|
(format ", %s unexpected" unexpected))))
|
||
|
(ert-results-update-stats-display (with-current-buffer buffer
|
||
|
ert-results-ewoc)
|
||
|
stats)))
|
||
|
(test-started
|
||
|
(destructuring-bind (stats test) event-args
|
||
|
(with-current-buffer buffer
|
||
|
(let* ((ewoc ert-results-ewoc)
|
||
|
(pos (ert-stats-test-index stats test))
|
||
|
(node (ewoc-nth ewoc pos)))
|
||
|
(unless node
|
||
|
;; FIXME: How expensive is this assertion?
|
||
|
(assert (or (zerop pos) (ewoc-nth ewoc (1- pos)))
|
||
|
t)
|
||
|
(setq node (ewoc-enter-last
|
||
|
ewoc
|
||
|
(make-ert-ewoc-entry :test test
|
||
|
:hidden-p t))))
|
||
|
(setf (ert-ewoc-entry-test (ewoc-data node)) test)
|
||
|
(setf (ert-ewoc-entry-result (ewoc-data node)) nil)
|
||
|
(aset ert-results-progress-bar-string pos
|
||
|
(ert-char-for-test-result nil t))
|
||
|
(ert-results-update-stats-display ewoc stats)
|
||
|
(ewoc-invalidate ewoc node)))))
|
||
|
(test-ended
|
||
|
(destructuring-bind (stats test result) event-args
|
||
|
(with-current-buffer buffer
|
||
|
(let* ((ewoc ert-results-ewoc)
|
||
|
(pos (ert-stats-test-index stats test))
|
||
|
(node (ewoc-nth ewoc pos)))
|
||
|
(setf (ert-ewoc-entry-result (ewoc-data node)) result)
|
||
|
(when (ert-ewoc-entry-hidden-p (ewoc-data node))
|
||
|
(setf (ert-ewoc-entry-hidden-p (ewoc-data node))
|
||
|
(ert-test-result-expected-p test result)))
|
||
|
(aset ert-results-progress-bar-string pos
|
||
|
(ert-char-for-test-result result
|
||
|
(ert-test-result-expected-p
|
||
|
test result)))
|
||
|
(ert-results-update-stats-display ewoc stats)
|
||
|
(ewoc-invalidate ewoc node))))))))
|
||
|
(ert-run-tests
|
||
|
selector
|
||
|
listener)))
|
||
|
|
||
|
(defvar ert-batch-backtrace-right-margin 70
|
||
|
"*The maximum line length for printing backtraces in `ert-run-tests-batch'.")
|
||
|
|
||
|
(defun ert-run-tests-batch (selector)
|
||
|
"Run the tests specified by SELECTOR, printing results to the terminal.
|
||
|
|
||
|
Returns the stats object."
|
||
|
(ert-run-tests
|
||
|
selector
|
||
|
(lambda (event-type &rest event-args)
|
||
|
(ecase event-type
|
||
|
(run-started
|
||
|
(destructuring-bind (stats) event-args
|
||
|
(message "Running %s tests (%s)"
|
||
|
(length (ert-stats-tests stats))
|
||
|
(ert-format-time-iso8601 (ert-stats-start-time stats)))))
|
||
|
(run-ended
|
||
|
(destructuring-bind (stats abortedp) event-args
|
||
|
(let ((unexpected (+ (ert-stats-passed-unexpected stats)
|
||
|
(ert-stats-failed-unexpected stats)
|
||
|
(ert-stats-error-unexpected stats))))
|
||
|
(message "\n%sRan %s tests, %s results were as expected%s (%s)\n"
|
||
|
(if (not abortedp)
|
||
|
""
|
||
|
"Aborted: ")
|
||
|
(ert-stats-total stats)
|
||
|
(+ (ert-stats-passed-expected stats)
|
||
|
(ert-stats-failed-expected stats)
|
||
|
(ert-stats-error-expected stats))
|
||
|
(if (zerop unexpected)
|
||
|
""
|
||
|
(format ", %s unexpected" unexpected))
|
||
|
(ert-format-time-iso8601 (ert-stats-end-time stats)))
|
||
|
(unless (zerop unexpected)
|
||
|
(message "%s unexpected results:" unexpected)
|
||
|
(loop for test across (ert-stats-tests stats)
|
||
|
for result = (ert-test-most-recent-result test) do
|
||
|
(when (not (ert-test-result-expected-p test result))
|
||
|
(message "%9s %S"
|
||
|
(ert-string-for-test-result result nil)
|
||
|
(ert-test-name test))))
|
||
|
(message "%s" "")))))
|
||
|
(test-started
|
||
|
)
|
||
|
(test-ended
|
||
|
(destructuring-bind (stats test result) event-args
|
||
|
(etypecase result
|
||
|
(ert-test-passed)
|
||
|
(ert-test-result-with-condition
|
||
|
(message "Test %S backtrace:" (ert-test-name test))
|
||
|
(with-temp-buffer
|
||
|
(ert-print-backtrace (ert-test-result-with-condition-backtrace result))
|
||
|
(goto-char (point-min))
|
||
|
(while (not (eobp))
|
||
|
(let ((start (point))
|
||
|
(end (progn (end-of-line) (point))))
|
||
|
(setq end (min end
|
||
|
(+ start ert-batch-backtrace-right-margin)))
|
||
|
(message "%s" (buffer-substring-no-properties
|
||
|
start end)))
|
||
|
(forward-line 1)))
|
||
|
(with-temp-buffer
|
||
|
(insert " ")
|
||
|
(let ((print-escape-newlines t)
|
||
|
(print-level 5)
|
||
|
(print-length 10))
|
||
|
(let ((begin (point)))
|
||
|
(ert-pp-with-indentation-and-newline
|
||
|
(ert-test-result-with-condition-condition result))))
|
||
|
(goto-char (1- (point-max)))
|
||
|
(assert (looking-at "\n"))
|
||
|
(delete-char 1)
|
||
|
(message "Test %S condition:" (ert-test-name test))
|
||
|
(message "%s" (buffer-string))))
|
||
|
(ert-test-aborted-with-non-local-exit))
|
||
|
(let* ((max (prin1-to-string (length (ert-stats-tests stats))))
|
||
|
(format-string (concat "%9s %"
|
||
|
(prin1-to-string (length max))
|
||
|
"s/" max " %S")))
|
||
|
(message format-string
|
||
|
(ert-string-for-test-result result
|
||
|
(ert-test-result-expected-p
|
||
|
test result))
|
||
|
(1+ (ert-stats-test-index stats test))
|
||
|
(ert-test-name test)))))))))
|
||
|
|
||
|
|
||
|
;;; Commands and button actions for the results buffer.
|
||
|
|
||
|
(define-derived-mode ert-results-mode fundamental-mode "ERT-Results"
|
||
|
"Major mode for viewing results of ERT test runs.")
|
||
|
|
||
|
(loop for (key binding) in
|
||
|
'(("j" ert-results-jump-between-summary-and-result)
|
||
|
("." ert-results-find-test-at-point-other-window)
|
||
|
("r" ert-results-rerun-test-at-point)
|
||
|
("d" ert-results-rerun-test-at-point-debugging-errors)
|
||
|
("b" ert-results-pop-to-backtrace-for-test-at-point)
|
||
|
("m" ert-results-pop-to-messages-for-test-at-point)
|
||
|
("p" ert-results-toggle-printer-limits-for-test-at-point)
|
||
|
("D" ert-delete-test)
|
||
|
([?\t] forward-button)
|
||
|
([backtab] backward-button)
|
||
|
)
|
||
|
do
|
||
|
(define-key ert-results-mode-map key binding))
|
||
|
|
||
|
(define-button-type 'ert-results-progress-bar-button
|
||
|
'action #'ert-results-progress-bar-button-action
|
||
|
'help-echo "mouse-2, RET: Reveal test result")
|
||
|
|
||
|
(define-button-type 'ert-test-name-button
|
||
|
'action #'ert-test-name-button-action
|
||
|
'help-echo "mouse-2, RET: Find test definition")
|
||
|
|
||
|
(define-button-type 'ert-results-expand-collapse-button
|
||
|
'action #'ert-results-expand-collapse-button-action
|
||
|
'help-echo "mouse-2, RET: Expand/collapse test result")
|
||
|
|
||
|
(defun ert-results-test-node-or-null-at-point ()
|
||
|
"If point is on a valid ewoc node, return it; return nil otherwise.
|
||
|
|
||
|
To be used in the ERT results buffer."
|
||
|
(let* ((ewoc ert-results-ewoc)
|
||
|
(node (ewoc-locate ewoc)))
|
||
|
;; `ewoc-locate' will return an arbitrary node when point is on
|
||
|
;; header or footer, or when all nodes are invisible. So we need
|
||
|
;; to validate its return value here.
|
||
|
(if (and (>= (point) (ewoc-location node))
|
||
|
(not (ert-ewoc-entry-hidden-p (ewoc-data node))))
|
||
|
node
|
||
|
nil)))
|
||
|
|
||
|
(defun ert-results-test-node-at-point ()
|
||
|
"If point is on a valid ewoc node, return it; signal an error otherwise.
|
||
|
|
||
|
To be used in the ERT results buffer."
|
||
|
(or (ert-results-test-node-or-null-at-point)
|
||
|
(error "No test at point")))
|
||
|
|
||
|
(defun ert-results-expand-collapse-button-action (button)
|
||
|
"Expand or collapse the test node BUTTON belongs to."
|
||
|
(let* ((ewoc ert-results-ewoc)
|
||
|
(node (save-excursion
|
||
|
(goto-char (ert-button-action-position))
|
||
|
(ert-results-test-node-at-point)))
|
||
|
(entry (ewoc-data node)))
|
||
|
(setf (ert-ewoc-entry-expanded-p entry)
|
||
|
(not (ert-ewoc-entry-expanded-p entry)))
|
||
|
(ewoc-invalidate ewoc node)))
|
||
|
|
||
|
(defun ert-results-find-test-at-point-other-window ()
|
||
|
"Find the definition of the test at point in another window.
|
||
|
|
||
|
To be used in the ERT results buffer."
|
||
|
(interactive)
|
||
|
(let* ((node (ert-results-test-node-at-point))
|
||
|
(entry (ewoc-data node))
|
||
|
(test (ert-ewoc-entry-test entry))
|
||
|
(name (ert-test-name test)))
|
||
|
(ert-find-test-other-window name)))
|
||
|
|
||
|
(defun ert-test-name-button-action (button)
|
||
|
"Find the definition of the test BUTTON belongs to, in another window."
|
||
|
(let ((name (button-get button 'ert-test-name)))
|
||
|
(ert-find-test-other-window name)))
|
||
|
|
||
|
(defun ert-ewoc-position (ewoc node)
|
||
|
"Return the position of NODE in EWOC, or nil if NODE is not in EWOC."
|
||
|
(loop for i from 0
|
||
|
for node-here = (ewoc-nth ewoc 0) then (ewoc-next ewoc node-here)
|
||
|
do (when (eql node node-here)
|
||
|
(return i))
|
||
|
finally (return nil)))
|
||
|
|
||
|
(defun ert-results-jump-between-summary-and-result ()
|
||
|
"Jump back and forth between the test run summary and individual test results.
|
||
|
|
||
|
From an ewoc node, jumps to the character that represents the
|
||
|
same test in the progress bar, and vice versa.
|
||
|
|
||
|
To be used in the ERT results buffer."
|
||
|
;; Maybe this command isn't actually needed much, but if it is, it
|
||
|
;; seems like an indication that the UI design is not optimal. If
|
||
|
;; jumping back and forth between a summary at the top of the buffer
|
||
|
;; and the error log in the remainder of the buffer is useful, then
|
||
|
;; the summary apparently needs to be easily accessible from the
|
||
|
;; error log, and perhaps it would be better to have it in a
|
||
|
;; separate buffer to keep it visible.
|
||
|
(interactive)
|
||
|
(let ((ewoc ert-results-ewoc)
|
||
|
(progress-bar-begin ert-results-progress-bar-button-begin))
|
||
|
(cond ((ert-results-test-node-or-null-at-point)
|
||
|
(let* ((node (ert-results-test-node-at-point))
|
||
|
(pos (ert-ewoc-position ewoc node)))
|
||
|
(goto-char (+ progress-bar-begin pos))))
|
||
|
((and (<= progress-bar-begin (point))
|
||
|
(< (point) (button-end (button-at progress-bar-begin))))
|
||
|
(let* ((node (ewoc-nth ewoc (- (point) progress-bar-begin)))
|
||
|
(entry (ewoc-data node)))
|
||
|
(when (ert-ewoc-entry-hidden-p entry)
|
||
|
(setf (ert-ewoc-entry-hidden-p entry) nil)
|
||
|
(ewoc-invalidate ewoc node))
|
||
|
(ewoc-goto-node ewoc node)))
|
||
|
(t
|
||
|
(goto-char progress-bar-begin)))))
|
||
|
|
||
|
(defun ert-button-action-position ()
|
||
|
"The buffer position where the last button action was triggered."
|
||
|
(cond ((integerp last-command-event)
|
||
|
(point))
|
||
|
((eventp last-command-event)
|
||
|
(posn-point (event-start last-command-event)))
|
||
|
(t (assert nil))))
|
||
|
|
||
|
(defun ert-results-progress-bar-button-action (button)
|
||
|
"Find the ewoc node that represents the same test as the character clicked on."
|
||
|
(goto-char (ert-button-action-position))
|
||
|
(ert-results-jump-between-summary-and-result))
|
||
|
|
||
|
(defun ert-results-rerun-test-at-point ()
|
||
|
"Re-run the test at point.
|
||
|
|
||
|
To be used in the ERT results buffer."
|
||
|
(interactive)
|
||
|
(let* ((ewoc ert-results-ewoc)
|
||
|
(node (ert-results-test-node-at-point))
|
||
|
(entry (ewoc-data node))
|
||
|
(old-test (ert-ewoc-entry-test entry))
|
||
|
(test-name (ert-test-name old-test))
|
||
|
;; FIXME: Write a test for this lookup.
|
||
|
(test (if test-name
|
||
|
(if (ert-test-boundp test-name)
|
||
|
(ert-get-test test-name)
|
||
|
(error "No such test: %S" test-name))
|
||
|
old-test))
|
||
|
(stats ert-results-stats)
|
||
|
(pos (gethash test (ert-stats-test-map stats)))
|
||
|
(progress-message (format "Running test %S" (ert-test-name test))))
|
||
|
;; Need to save and restore point manually here: When point is on
|
||
|
;; the first visible ewoc entry while the header is updated, point
|
||
|
;; moves to the top of the buffer. This is undesirable, and a
|
||
|
;; simple `save-excursion' doesn't prevent it.
|
||
|
(let ((point (point)))
|
||
|
(unwind-protect
|
||
|
(unwind-protect
|
||
|
(progn
|
||
|
(message "%s..." progress-message)
|
||
|
(ert-run-or-rerun-test stats test
|
||
|
ert-results-listener))
|
||
|
(ert-results-update-stats-display ewoc stats)
|
||
|
(message "%s...%s"
|
||
|
progress-message
|
||
|
(let ((result (ert-test-most-recent-result test)))
|
||
|
(ert-string-for-test-result
|
||
|
result (ert-test-result-expected-p test result)))))
|
||
|
(goto-char point)))))
|
||
|
|
||
|
(defun ert-results-rerun-test-at-point-debugging-errors ()
|
||
|
"Re-run the test at point with `ert-debug-on-error' bound to t.
|
||
|
|
||
|
To be used in the ERT results buffer."
|
||
|
(interactive)
|
||
|
(let ((ert-debug-on-error t))
|
||
|
(ert-results-rerun-test-at-point)))
|
||
|
|
||
|
(defun ert-print-backtrace (backtrace)
|
||
|
"Format the backtrace BACKTRACE to the current buffer."
|
||
|
;; This is essentially a reimplementation of Fbacktrace
|
||
|
;; (src/eval.c), but for a saved backtrace, not the current one.
|
||
|
(let ((print-escape-newlines t)
|
||
|
(print-level 8)
|
||
|
(print-length 50))
|
||
|
(dolist (frame backtrace)
|
||
|
(ecase (first frame)
|
||
|
((nil)
|
||
|
;; Special operator.
|
||
|
(destructuring-bind (special-operator &rest arg-forms)
|
||
|
(cdr frame)
|
||
|
(insert
|
||
|
(format " %S\n" (list* special-operator arg-forms)))))
|
||
|
((t)
|
||
|
;; Function call.
|
||
|
(destructuring-bind (fn &rest args) (cdr frame)
|
||
|
(insert (format " %S(" fn))
|
||
|
(loop for firstp = t then nil
|
||
|
for arg in args do
|
||
|
(unless firstp
|
||
|
(insert " "))
|
||
|
(insert (format "%S" arg)))
|
||
|
(insert ")\n")))))))
|
||
|
|
||
|
(defun ert-results-pop-to-backtrace-for-test-at-point ()
|
||
|
"Display the backtrace for the test at point.
|
||
|
|
||
|
To be used in the ERT results buffer."
|
||
|
(interactive)
|
||
|
(let* ((node (ert-results-test-node-at-point))
|
||
|
(entry (ewoc-data node))
|
||
|
(test (ert-ewoc-entry-test entry))
|
||
|
(result (ert-ewoc-entry-result entry)))
|
||
|
(etypecase result
|
||
|
(ert-test-passed (error "Test passed, no backtrace available"))
|
||
|
(ert-test-result-with-condition
|
||
|
(let ((backtrace (ert-test-result-with-condition-backtrace result))
|
||
|
(buffer
|
||
|
(let ((default-major-mode 'fundamental-mode))
|
||
|
(get-buffer-create "*ERT Backtrace*"))))
|
||
|
(pop-to-buffer buffer)
|
||
|
(setq buffer-read-only t)
|
||
|
(let ((inhibit-read-only t))
|
||
|
(erase-buffer)
|
||
|
;; Use unibyte because `debugger-setup-buffer' also does so.
|
||
|
(set-buffer-multibyte nil)
|
||
|
(setq truncate-lines t)
|
||
|
(ert-print-backtrace backtrace)
|
||
|
(debugger-make-xrefs)
|
||
|
(goto-char (point-min))))))))
|
||
|
|
||
|
(defun ert-results-pop-to-messages-for-test-at-point ()
|
||
|
"Display the part of the *Messages* buffer generated during the test at point.
|
||
|
|
||
|
To be used in the ERT results buffer."
|
||
|
(interactive)
|
||
|
(let* ((node (ert-results-test-node-at-point))
|
||
|
(entry (ewoc-data node))
|
||
|
(test (ert-ewoc-entry-test entry))
|
||
|
(result (ert-ewoc-entry-result entry)))
|
||
|
(let ((buffer
|
||
|
(let ((default-major-mode 'fundamental-mode))
|
||
|
(get-buffer-create "*ERT Messages*"))))
|
||
|
(pop-to-buffer buffer)
|
||
|
(setq buffer-read-only t)
|
||
|
(let ((inhibit-read-only t))
|
||
|
(erase-buffer)
|
||
|
(insert (ert-test-result-messages result))
|
||
|
(goto-char (point-min))
|
||
|
(insert "Messages for test `")
|
||
|
(ert-insert-test-name-button (ert-test-name test))
|
||
|
(insert "':\n")))))
|
||
|
|
||
|
(defun ert-results-toggle-printer-limits-for-test-at-point ()
|
||
|
"Toggle how much of the condition to print for the test at point.
|
||
|
|
||
|
To be used in the ERT results buffer."
|
||
|
(interactive)
|
||
|
(let* ((ewoc ert-results-ewoc)
|
||
|
(node (ert-results-test-node-at-point))
|
||
|
(entry (ewoc-data node)))
|
||
|
(setf (ert-ewoc-entry-extended-printer-limits-p entry)
|
||
|
(not (ert-ewoc-entry-extended-printer-limits-p entry)))
|
||
|
(ewoc-invalidate ewoc node)))
|
||
|
|
||
|
(defun ert-activate-font-lock-keywords ()
|
||
|
(font-lock-add-keywords
|
||
|
nil
|
||
|
'(("(\\(\\<ert-deftest\\)\\>\\s *\\(\\sw+\\)?"
|
||
|
(1 font-lock-keyword-face nil t)
|
||
|
(2 font-lock-function-name-face nil t)))))
|
||
|
|
||
|
(defun* ert-remove-from-list (list-var element &key key test)
|
||
|
"Remove ELEMENT from the value of LIST-VAR if present.
|
||
|
|
||
|
This is an inverse of `add-to-list'."
|
||
|
(unless key (setq key #'identity))
|
||
|
(unless test (setq test #'equal))
|
||
|
(setf (symbol-value list-var)
|
||
|
(remove* element
|
||
|
(symbol-value list-var)
|
||
|
:key key
|
||
|
:test test)))
|
||
|
|
||
|
|
||
|
;;; Actions on load/unload.
|
||
|
|
||
|
(add-to-list 'find-function-regexp-alist '(ert-deftest . ert-find-test-regexp))
|
||
|
(add-to-list 'minor-mode-alist '(ert-current-run-stats
|
||
|
(:eval
|
||
|
(ert-tests-running-mode-line-indicator))))
|
||
|
(add-to-list 'emacs-lisp-mode-hook 'ert-activate-font-lock-keywords)
|
||
|
|
||
|
(defun ert-unload-function ()
|
||
|
(ert-remove-from-list 'find-function-regexp-alist 'ert-deftest :key #'car)
|
||
|
(ert-remove-from-list 'minor-mode-alist 'ert-current-run-stats :key #'car)
|
||
|
(ert-remove-from-list 'emacs-lisp-mode-hook 'ert-activate-font-lock-keywords)
|
||
|
nil)
|
||
|
|
||
|
(defvar ert-unload-hook '())
|
||
|
(add-hook 'ert-unload-hook 'ert-unload-function)
|
||
|
|
||
|
|
||
|
;;; Self-tests.
|
||
|
|
||
|
(ert-delete-all-tests)
|
||
|
|
||
|
;; Test that test bodies are actually run.
|
||
|
(defvar ert-test-body-was-run)
|
||
|
(ert-deftest ert-test-body-runs ()
|
||
|
(setq ert-test-body-was-run t))
|
||
|
|
||
|
|
||
|
;; Test that nested test bodies run.
|
||
|
(ert-deftest ert-nested-test-body-runs ()
|
||
|
(lexical-let ((was-run nil))
|
||
|
(let ((test (make-ert-test :body (lambda ()
|
||
|
(setq was-run t)))))
|
||
|
(assert (not was-run))
|
||
|
(ert-run-test test)
|
||
|
(assert was-run))))
|
||
|
|
||
|
|
||
|
;; Test that pass/fail works.
|
||
|
(ert-deftest ert-test-pass ()
|
||
|
(let ((test (make-ert-test :body (lambda ()))))
|
||
|
(let ((result (ert-run-test test)))
|
||
|
(assert (typep result 'ert-test-passed)))))
|
||
|
|
||
|
(ert-deftest ert-test-fail ()
|
||
|
(let ((test (make-ert-test :body (lambda () (ert-fail "failure message")))))
|
||
|
(let ((result (let ((ert-debug-on-error nil))
|
||
|
(ert-run-test test))))
|
||
|
(assert (typep result 'ert-test-failed) t)
|
||
|
(assert (equal (ert-test-result-with-condition-condition result)
|
||
|
'(ert-test-failed "failure message"))
|
||
|
t))))
|
||
|
|
||
|
(ert-deftest ert-test-fail-debug-with-condition-case ()
|
||
|
(let ((test (make-ert-test :body (lambda () (ert-fail "failure message")))))
|
||
|
(condition-case condition
|
||
|
(progn
|
||
|
(let ((ert-debug-on-error t))
|
||
|
(ert-run-test test))
|
||
|
(assert nil))
|
||
|
((error)
|
||
|
(assert (equal condition '(ert-test-failed "failure message")) t)))))
|
||
|
|
||
|
(ert-deftest ert-test-fail-debug-with-debugger-1 ()
|
||
|
(let ((test (make-ert-test :body (lambda () (ert-fail "failure message")))))
|
||
|
(let ((debugger (lambda (&rest debugger-args)
|
||
|
(assert nil))))
|
||
|
(let ((ert-debug-on-error nil))
|
||
|
(ert-run-test test)))))
|
||
|
|
||
|
(ert-deftest ert-test-fail-debug-with-debugger-2 ()
|
||
|
(let ((test (make-ert-test :body (lambda () (ert-fail "failure message")))))
|
||
|
(block nil
|
||
|
(let ((debugger (lambda (&rest debugger-args)
|
||
|
(return-from nil nil))))
|
||
|
(let ((ert-debug-on-error t))
|
||
|
(ert-run-test test))
|
||
|
(assert nil)))))
|
||
|
|
||
|
(ert-deftest ert-test-fail-debug-nested-with-debugger ()
|
||
|
(let ((test (make-ert-test :body (lambda ()
|
||
|
(let ((ert-debug-on-error t))
|
||
|
(ert-fail "failure message"))))))
|
||
|
(let ((debugger (lambda (&rest debugger-args)
|
||
|
(assert nil nil "Assertion a"))))
|
||
|
(let ((ert-debug-on-error nil))
|
||
|
(ert-run-test test))))
|
||
|
(let ((test (make-ert-test :body (lambda ()
|
||
|
(let ((ert-debug-on-error nil))
|
||
|
(ert-fail "failure message"))))))
|
||
|
(block nil
|
||
|
(let ((debugger (lambda (&rest debugger-args)
|
||
|
(return-from nil nil))))
|
||
|
(let ((ert-debug-on-error t))
|
||
|
(ert-run-test test))
|
||
|
(assert nil nil "Assertion b")))))
|
||
|
|
||
|
(ert-deftest ert-test-error ()
|
||
|
(let ((test (make-ert-test :body (lambda () (error "error message")))))
|
||
|
(let ((result (let ((ert-debug-on-error nil))
|
||
|
(ert-run-test test))))
|
||
|
(assert (typep result 'ert-test-error) t)
|
||
|
(assert (equal (ert-test-result-with-condition-condition result)
|
||
|
'(error "error message"))
|
||
|
t))))
|
||
|
|
||
|
(ert-deftest ert-test-error-debug ()
|
||
|
(let ((test (make-ert-test :body (lambda () (error "error message")))))
|
||
|
(condition-case condition
|
||
|
(progn
|
||
|
(let ((ert-debug-on-error t))
|
||
|
(ert-run-test test))
|
||
|
(assert nil))
|
||
|
((error)
|
||
|
(assert (equal condition '(error "error message")) t)))))
|
||
|
|
||
|
|
||
|
;; Test that `should' works.
|
||
|
(ert-deftest ert-test-should ()
|
||
|
(let ((test (make-ert-test :body (lambda () (ert-should nil)))))
|
||
|
(let ((result (let ((ert-debug-on-error nil))
|
||
|
(ert-run-test test))))
|
||
|
(assert (typep result 'ert-test-failed) t)
|
||
|
(assert (equal (ert-test-result-with-condition-condition result)
|
||
|
'(ert-test-failed ((ert-should nil) :form nil :value nil)))
|
||
|
t)))
|
||
|
(let ((test (make-ert-test :body (lambda () (ert-should t)))))
|
||
|
(let ((result (ert-run-test test)))
|
||
|
(assert (typep result 'ert-test-passed) t))))
|
||
|
|
||
|
(ert-deftest ert-test-should-value ()
|
||
|
(ert-should (eql (ert-should 'foo) 'foo))
|
||
|
(ert-should (eql (ert-should 'bar) 'bar)))
|
||
|
|
||
|
(ert-deftest ert-test-should-not ()
|
||
|
(let ((test (make-ert-test :body (lambda () (ert-should-not t)))))
|
||
|
(let ((result (let ((ert-debug-on-error nil))
|
||
|
(ert-run-test test))))
|
||
|
(assert (typep result 'ert-test-failed) t)
|
||
|
(assert (equal (ert-test-result-with-condition-condition result)
|
||
|
'(ert-test-failed ((ert-should-not t) :form t :value t)))
|
||
|
t)))
|
||
|
(let ((test (make-ert-test :body (lambda () (ert-should-not nil)))))
|
||
|
(let ((result (ert-run-test test)))
|
||
|
(assert (typep result 'ert-test-passed)))))
|
||
|
|
||
|
|
||
|
(ert-deftest ert-test-should-error ()
|
||
|
;; No error.
|
||
|
(let ((test (make-ert-test :body (lambda () (ert-should-error (progn))))))
|
||
|
(let ((result (let ((ert-debug-on-error nil))
|
||
|
(ert-run-test test))))
|
||
|
(ert-should (typep result 'ert-test-failed))
|
||
|
(ert-should (equal (ert-test-result-with-condition-condition result)
|
||
|
'(ert-test-failed
|
||
|
((ert-should-error (progn))
|
||
|
:form (progn)
|
||
|
:value nil
|
||
|
:fail-reason "did not signal an error"))))))
|
||
|
;; A simple error.
|
||
|
(let ((test (make-ert-test :body (lambda () (ert-should-error (error "foo"))))))
|
||
|
(let ((result (ert-run-test test)))
|
||
|
(ert-should (typep result 'ert-test-passed))))
|
||
|
;; Error of unexpected type, no test.
|
||
|
(let ((test (make-ert-test :body (lambda ()
|
||
|
(ert-should-error (error "foo")
|
||
|
:type 'singularity-error)))))
|
||
|
(let ((result (ert-run-test test)))
|
||
|
(ert-should (typep result 'ert-test-failed))
|
||
|
(ert-should (equal
|
||
|
(ert-test-result-with-condition-condition result)
|
||
|
'(ert-test-failed
|
||
|
((ert-should-error (error "foo") :type 'singularity-error)
|
||
|
:form (error "foo")
|
||
|
:condition (error "foo")
|
||
|
:fail-reason
|
||
|
"the error signalled did not have the expected type"))))))
|
||
|
;; Error of the expected type, no test.
|
||
|
(let ((test (make-ert-test :body (lambda ()
|
||
|
(ert-should-error (signal 'singularity-error
|
||
|
nil)
|
||
|
:type 'singularity-error)))))
|
||
|
(let ((result (ert-run-test test)))
|
||
|
(ert-should (typep result 'ert-test-passed))))
|
||
|
;; Error that fails the test, no type.
|
||
|
(let ((test (make-ert-test :body (lambda ()
|
||
|
(ert-should-error
|
||
|
(error "foo")
|
||
|
:test (lambda (error) nil))))))
|
||
|
(let ((result (ert-run-test test)))
|
||
|
(ert-should (typep result 'ert-test-failed))
|
||
|
(ert-should (equal (ert-test-result-with-condition-condition result)
|
||
|
'(ert-test-failed
|
||
|
((ert-should-error (error "foo") :test (lambda (error) nil))
|
||
|
:form (error "foo")
|
||
|
:condition (error "foo")
|
||
|
:fail-reason
|
||
|
"the error signalled did not pass the test"))))))
|
||
|
;; Error that passes the test, no type.
|
||
|
(let ((test (make-ert-test :body (lambda ()
|
||
|
(ert-should-error (error "foo")
|
||
|
:test (lambda (error) t))))))
|
||
|
(let ((result (ert-run-test test)))
|
||
|
(ert-should (typep result 'ert-test-passed))))
|
||
|
;; Error that has the expected type but fails the test.
|
||
|
(let ((test (make-ert-test :body (lambda ()
|
||
|
(ert-should-error
|
||
|
(signal 'singularity-error nil)
|
||
|
:type 'singularity-error
|
||
|
:test (lambda (error) nil))))))
|
||
|
(let ((result (ert-run-test test)))
|
||
|
(ert-should (typep result 'ert-test-failed))
|
||
|
(ert-should (equal (ert-test-result-with-condition-condition result)
|
||
|
'(ert-test-failed
|
||
|
((ert-should-error (signal 'singularity-error nil)
|
||
|
:type 'singularity-error
|
||
|
:test (lambda (error) nil))
|
||
|
:form (signal singularity-error nil)
|
||
|
:condition (singularity-error)
|
||
|
:fail-reason
|
||
|
"the error signalled did not pass the test"))))))
|
||
|
;; Error that has the expected type and passes the test.
|
||
|
(let ((test (make-ert-test :body (lambda ()
|
||
|
(ert-should-error
|
||
|
(signal 'singularity-error nil)
|
||
|
:type 'singularity-error
|
||
|
:test (lambda (error) t))))))
|
||
|
(let ((result (ert-run-test test)))
|
||
|
(ert-should (typep result 'ert-test-passed))))
|
||
|
)
|
||
|
|
||
|
(ert-deftest ert-test-should-error-subtypes ()
|
||
|
(let ((test (make-ert-test
|
||
|
:body (lambda ()
|
||
|
(ert-should-error (signal 'singularity-error nil)
|
||
|
:type 'singularity-error
|
||
|
:exclude-subtypes t)))))
|
||
|
(let ((result (ert-run-test test)))
|
||
|
(ert-should (typep result 'ert-test-passed))))
|
||
|
(let ((test (make-ert-test
|
||
|
:body (lambda ()
|
||
|
(ert-should-error (signal 'arith-error nil)
|
||
|
:type 'singularity-error)))))
|
||
|
(let ((result (ert-run-test test)))
|
||
|
(ert-should (typep result 'ert-test-failed))
|
||
|
(ert-should (equal
|
||
|
(ert-test-result-with-condition-condition result)
|
||
|
'(ert-test-failed
|
||
|
((ert-should-error (signal 'arith-error nil)
|
||
|
:type 'singularity-error)
|
||
|
:form (signal arith-error nil)
|
||
|
:condition (arith-error)
|
||
|
:fail-reason
|
||
|
"the error signalled did not have the expected type"))))))
|
||
|
(let ((test (make-ert-test
|
||
|
:body (lambda ()
|
||
|
(ert-should-error (signal 'arith-error nil)
|
||
|
:type 'singularity-error
|
||
|
:exclude-subtypes t)))))
|
||
|
(let ((result (ert-run-test test)))
|
||
|
(ert-should (typep result 'ert-test-failed))
|
||
|
(ert-should (equal
|
||
|
(ert-test-result-with-condition-condition result)
|
||
|
'(ert-test-failed
|
||
|
((ert-should-error (signal 'arith-error nil)
|
||
|
:type 'singularity-error
|
||
|
:exclude-subtypes t)
|
||
|
:form (signal arith-error nil)
|
||
|
:condition (arith-error)
|
||
|
:fail-reason
|
||
|
"the error signalled did not have the expected type"))))))
|
||
|
(let ((test (make-ert-test
|
||
|
:body (lambda ()
|
||
|
(ert-should-error (signal 'singularity-error nil)
|
||
|
:type 'arith-error
|
||
|
:exclude-subtypes t)))))
|
||
|
(let ((result (ert-run-test test)))
|
||
|
(ert-should (typep result 'ert-test-failed))
|
||
|
(ert-should (equal
|
||
|
(ert-test-result-with-condition-condition result)
|
||
|
'(ert-test-failed
|
||
|
((ert-should-error (signal 'singularity-error nil)
|
||
|
:type 'arith-error
|
||
|
:exclude-subtypes t)
|
||
|
:form (signal singularity-error nil)
|
||
|
:condition (singularity-error)
|
||
|
:fail-reason
|
||
|
"the error signalled was a subtype of the expected type"))))))
|
||
|
)
|
||
|
|
||
|
;; Test that `should' errors contain the information we expect them to.
|
||
|
(defmacro ert-test-my-list (&rest args)
|
||
|
`(list ,@args))
|
||
|
|
||
|
(ert-deftest ert-test-should-failure-debugging ()
|
||
|
(loop for (body expected-condition) in
|
||
|
`((,(lambda () (let ((x nil)) (ert-should x)))
|
||
|
(ert-test-failed ((ert-should x) :form x :value nil)))
|
||
|
(,(lambda () (let ((x t)) (ert-should-not x)))
|
||
|
(ert-test-failed ((ert-should-not x) :form x :value t)))
|
||
|
(,(lambda () (let ((x t)) (ert-should (not x))))
|
||
|
(ert-test-failed ((ert-should (not x)) :form (not t) :value nil)))
|
||
|
(,(lambda () (let ((x nil)) (ert-should-not (not x))))
|
||
|
(ert-test-failed ((ert-should-not (not x)) :form (not nil) :value t)))
|
||
|
(,(lambda () (let ((x t) (y nil)) (ert-should-not (ert-test-my-list x y))))
|
||
|
(ert-test-failed
|
||
|
((ert-should-not (ert-test-my-list x y))
|
||
|
:form (list t nil)
|
||
|
:value (t nil))))
|
||
|
(,(lambda () (let ((x t)) (ert-should (error "foo"))))
|
||
|
(error "foo")))
|
||
|
do
|
||
|
(let ((test (make-ert-test :body body)))
|
||
|
(condition-case actual-condition
|
||
|
(progn
|
||
|
(let ((ert-debug-on-error t))
|
||
|
(ert-run-test test))
|
||
|
(assert nil))
|
||
|
((error)
|
||
|
(ert-should (equal actual-condition expected-condition)))))))
|
||
|
|
||
|
(ert-deftest ert-test-messages ()
|
||
|
(let* ((message-string "Test message")
|
||
|
(messages-buffer (get-buffer-create "*Messages*"))
|
||
|
(test (make-ert-test :body (lambda () (message "%s" message-string)))))
|
||
|
(with-current-buffer messages-buffer
|
||
|
(let ((result (ert-run-test test)))
|
||
|
(ert-should (equal (concat message-string "\n")
|
||
|
(ert-test-result-messages result)))))))
|
||
|
|
||
|
(defun ert-call-with-temporary-messages-buffer (thunk)
|
||
|
(lexical-let ((new-buffer-name (generate-new-buffer-name
|
||
|
"*Messages* orig buffer")))
|
||
|
(unwind-protect
|
||
|
(progn
|
||
|
(with-current-buffer (get-buffer-create "*Messages*")
|
||
|
(rename-buffer new-buffer-name))
|
||
|
(get-buffer-create "*Messages*")
|
||
|
(funcall thunk))
|
||
|
(kill-buffer "*Messages*")
|
||
|
(with-current-buffer new-buffer-name
|
||
|
(rename-buffer "*Messages*")))))
|
||
|
|
||
|
(ert-deftest ert-test-messages-on-log-truncation ()
|
||
|
(let ((test (make-ert-test
|
||
|
:body (lambda ()
|
||
|
;; Emacs would combine messages if we
|
||
|
;; generate the same message multiple
|
||
|
;; times.
|
||
|
(message "a")
|
||
|
(message "b")
|
||
|
(message "c")
|
||
|
(message "d")))))
|
||
|
(let (result)
|
||
|
(ert-call-with-temporary-messages-buffer
|
||
|
(lambda ()
|
||
|
(let ((message-log-max 2))
|
||
|
(setq result (ert-run-test test)))
|
||
|
(ert-should (equal (with-current-buffer "*Messages*"
|
||
|
(buffer-string))
|
||
|
"c\nd\n"))))
|
||
|
(ert-should (equal (ert-test-result-messages result) "a\nb\nc\nd\n")))))
|
||
|
|
||
|
;; Test `ert-select-tests'.
|
||
|
(ert-deftest ert-test-select-regexp ()
|
||
|
(ert-should (equal (ert-select-tests "^ert-test-select-regexp$" t)
|
||
|
(list (ert-get-test 'ert-test-select-regexp)))))
|
||
|
|
||
|
(ert-deftest ert-test-test-boundp ()
|
||
|
(ert-should (ert-test-boundp 'ert-test-test-boundp))
|
||
|
(ert-should-not (ert-test-boundp (make-symbol "ert-not-a-test"))))
|
||
|
|
||
|
(ert-deftest ert-test-select-member ()
|
||
|
(ert-should (equal (ert-select-tests '(member ert-test-select-member) t)
|
||
|
(list (ert-get-test 'ert-test-select-member)))))
|
||
|
|
||
|
(ert-deftest ert-test-select-test ()
|
||
|
(ert-should (equal (ert-select-tests (ert-get-test 'ert-test-select-test) t)
|
||
|
(list (ert-get-test 'ert-test-select-test)))))
|
||
|
|
||
|
(ert-deftest ert-test-select-symbol ()
|
||
|
(ert-should (equal (ert-select-tests 'ert-test-select-symbol t)
|
||
|
(list (ert-get-test 'ert-test-select-symbol)))))
|
||
|
|
||
|
(ert-deftest ert-test-select-and ()
|
||
|
(let ((test (make-ert-test
|
||
|
:name nil
|
||
|
:body nil
|
||
|
:most-recent-result (make-ert-test-failed
|
||
|
:condition nil
|
||
|
:backtrace nil))))
|
||
|
(ert-should (equal (ert-select-tests `(and (member ,test) :failed) t)
|
||
|
(list test)))))
|
||
|
|
||
|
|
||
|
;; Test utility functions.
|
||
|
(ert-deftest ert-proper-list-p ()
|
||
|
(ert-should (ert-proper-list-p '()))
|
||
|
(ert-should (ert-proper-list-p '(1)))
|
||
|
(ert-should (ert-proper-list-p '(1 2)))
|
||
|
(ert-should (ert-proper-list-p '(1 2 3)))
|
||
|
(ert-should (ert-proper-list-p '(1 2 3 4)))
|
||
|
(ert-should (not (ert-proper-list-p 'a)))
|
||
|
(ert-should (not (ert-proper-list-p '(1 . a))))
|
||
|
(ert-should (not (ert-proper-list-p '(1 2 . a))))
|
||
|
(ert-should (not (ert-proper-list-p '(1 2 3 . a))))
|
||
|
(ert-should (not (ert-proper-list-p '(1 2 3 4 . a))))
|
||
|
(let ((a (list 1)))
|
||
|
(setf (cdr (last a)) a)
|
||
|
(ert-should (not (ert-proper-list-p a))))
|
||
|
(let ((a (list 1 2)))
|
||
|
(setf (cdr (last a)) a)
|
||
|
(ert-should (not (ert-proper-list-p a))))
|
||
|
(let ((a (list 1 2 3)))
|
||
|
(setf (cdr (last a)) a)
|
||
|
(ert-should (not (ert-proper-list-p a))))
|
||
|
(let ((a (list 1 2 3 4)))
|
||
|
(setf (cdr (last a)) a)
|
||
|
(ert-should (not (ert-proper-list-p a))))
|
||
|
(let ((a (list 1 2)))
|
||
|
(setf (cdr (last a)) (cdr a))
|
||
|
(ert-should (not (ert-proper-list-p a))))
|
||
|
(let ((a (list 1 2 3)))
|
||
|
(setf (cdr (last a)) (cdr a))
|
||
|
(ert-should (not (ert-proper-list-p a))))
|
||
|
(let ((a (list 1 2 3 4)))
|
||
|
(setf (cdr (last a)) (cdr a))
|
||
|
(ert-should (not (ert-proper-list-p a))))
|
||
|
(let ((a (list 1 2 3)))
|
||
|
(setf (cdr (last a)) (cddr a))
|
||
|
(ert-should (not (ert-proper-list-p a))))
|
||
|
(let ((a (list 1 2 3 4)))
|
||
|
(setf (cdr (last a)) (cddr a))
|
||
|
(ert-should (not (ert-proper-list-p a))))
|
||
|
(let ((a (list 1 2 3 4)))
|
||
|
(setf (cdr (last a)) (cdddr a))
|
||
|
(ert-should (not (ert-proper-list-p a)))))
|
||
|
|
||
|
(ert-deftest ert-parse-keys-and-body ()
|
||
|
(ert-should (equal (ert-parse-keys-and-body "doc" '(foo))
|
||
|
'(nil "doc" (foo))))
|
||
|
(ert-should (equal (ert-parse-keys-and-body "doc" '(:bar foo))
|
||
|
'((:bar foo) "doc" nil)))
|
||
|
(ert-should (equal (ert-parse-keys-and-body nil '(:bar foo))
|
||
|
'((:bar foo) nil nil)))
|
||
|
(ert-should (equal (ert-parse-keys-and-body "doc" '(:bar foo))
|
||
|
'((:bar foo) "doc" nil)))
|
||
|
(ert-should (equal (ert-parse-keys-and-body nil '(:bar foo a (b)))
|
||
|
'((:bar foo) nil (a (b)))))
|
||
|
(ert-should (equal (ert-parse-keys-and-body nil '(:bar foo :a (b)))
|
||
|
'((:bar foo :a (b)) nil nil)))
|
||
|
(ert-should (equal (ert-parse-keys-and-body nil '(bar foo :a (b)))
|
||
|
'(nil nil (bar foo :a (b)))))
|
||
|
(ert-should-error (ert-parse-keys-and-body nil '(:bar foo :a))))
|
||
|
|
||
|
|
||
|
|
||
|
;; Test `ert-run-tests'.
|
||
|
(ert-deftest ert-test-run-tests ()
|
||
|
(let ((passing-test (make-ert-test :name 'passing-test
|
||
|
:body (lambda () (ert-pass))))
|
||
|
(failing-test (make-ert-test :name 'failing-test
|
||
|
:body (lambda () (ert-fail
|
||
|
"failure message"))))
|
||
|
)
|
||
|
(let ((ert-debug-on-error nil))
|
||
|
(let* ((buffer-name (generate-new-buffer-name " *ert-test-run-tests*"))
|
||
|
(messages nil)
|
||
|
(mock-message-fn
|
||
|
(lambda (format-string &rest args)
|
||
|
(push (apply #'format format-string args) messages))))
|
||
|
(save-window-excursion
|
||
|
(unwind-protect
|
||
|
(let ((case-fold-search nil))
|
||
|
(ert-run-tests-interactively
|
||
|
`(member ,passing-test ,failing-test) buffer-name
|
||
|
mock-message-fn)
|
||
|
(ert-should (equal messages `(,(concat
|
||
|
"Ran 2 tests, 1 results were "
|
||
|
"as expected, 1 unexpected"))))
|
||
|
(with-current-buffer buffer-name
|
||
|
(goto-char (point-min))
|
||
|
(ert-should (equal
|
||
|
(buffer-substring (point-min)
|
||
|
(save-excursion
|
||
|
(forward-line 5)
|
||
|
(point)))
|
||
|
(concat
|
||
|
"Selector: (member <passing-test> <failing-test>)\n"
|
||
|
"Passed: 1 (0 unexpected)\n"
|
||
|
"Failed: 1 (1 unexpected)\n"
|
||
|
"Error: 0 (0 unexpected)\n"
|
||
|
"Total: 2/2\n")))))
|
||
|
(when (get-buffer buffer-name)
|
||
|
(kill-buffer buffer-name))))))))
|
||
|
|
||
|
(ert-deftest ert-test-special-operator-p ()
|
||
|
(ert-should (ert-special-operator-p 'if))
|
||
|
(ert-should-not (ert-special-operator-p 'car))
|
||
|
(ert-should-not (ert-special-operator-p 'ert-special-operator-p))
|
||
|
(let ((b (gensym)))
|
||
|
(ert-should-not (ert-special-operator-p b))
|
||
|
(fset b 'if)
|
||
|
(ert-should (ert-special-operator-p b))))
|
||
|
|
||
|
;; This test attempts to demonstrate that there is no way to force
|
||
|
;; immediate truncation of the *Messages* buffer from Lisp (and hence
|
||
|
;; justifies the existence of
|
||
|
;; `ert-force-message-log-buffer-truncation'): The only way that came
|
||
|
;; to my mind was (message ""), which doesn't have the desired effect.
|
||
|
(ert-deftest ert-test-builtin-message-log-flushing ()
|
||
|
(ert-call-with-temporary-messages-buffer
|
||
|
(lambda ()
|
||
|
(with-current-buffer "*Messages*"
|
||
|
(let ((message-log-max 2))
|
||
|
(let ((message-log-max t))
|
||
|
(loop for i below 4 do
|
||
|
(message "%s" i))
|
||
|
(ert-should (eql (count-lines (point-min) (point-max)) 4)))
|
||
|
(ert-should (eql (count-lines (point-min) (point-max)) 4))
|
||
|
(message "")
|
||
|
(ert-should (eql (count-lines (point-min) (point-max)) 4))
|
||
|
(message "Test message")
|
||
|
(ert-should (eql (count-lines (point-min) (point-max)) 2)))))))
|
||
|
|
||
|
(ert-deftest ert-test-force-message-log-buffer-truncation ()
|
||
|
(labels ((body ()
|
||
|
(loop for i below 5 do
|
||
|
(message "%s" i)))
|
||
|
(c (x)
|
||
|
(ert-call-with-temporary-messages-buffer
|
||
|
(lambda ()
|
||
|
(let ((message-log-max x))
|
||
|
(body))
|
||
|
(with-current-buffer "*Messages*"
|
||
|
(buffer-string)))))
|
||
|
(lisp (x)
|
||
|
(ert-call-with-temporary-messages-buffer
|
||
|
(lambda ()
|
||
|
(let ((message-log-max t))
|
||
|
(body))
|
||
|
(let ((message-log-max x))
|
||
|
(ert-force-message-log-buffer-truncation))
|
||
|
(with-current-buffer "*Messages*"
|
||
|
(buffer-string))))))
|
||
|
(loop for x in '(0 1 2 3 4 5 6 t) do
|
||
|
(ert-should (equal (c x) (lisp x))))))
|
||
|
|
||
|
(defun ert-run-self-tests ()
|
||
|
;; Run tests and make sure they actually ran.
|
||
|
(let ((window-configuration (current-window-configuration)))
|
||
|
(let ((ert-test-body-was-run nil))
|
||
|
;; The buffer name chosen here should not compete with the default
|
||
|
;; results buffer name for completion in `switch-to-buffer'.
|
||
|
(let ((stats (ert-run-tests-interactively "^ert-" " *ert self-tests*")))
|
||
|
(assert ert-test-body-was-run)
|
||
|
(when (zerop (+ (ert-stats-passed-unexpected stats)
|
||
|
(ert-stats-failed-unexpected stats)
|
||
|
(ert-stats-error-unexpected stats)))
|
||
|
;; Hide results window only when everything went well.
|
||
|
(set-window-configuration window-configuration))))))
|
||
|
|
||
|
(provide 'ert)
|
||
|
|
||
|
;;; ert.el ends here
|