From 94d2fc1815a919734353c942f224db1de4b4fcb8 Mon Sep 17 00:00:00 2001 From: Tom Willemsen Date: Mon, 7 Mar 2011 09:04:49 +0100 Subject: Django, org * Added nxhtml, mostly for django support. * Changed some org settings. --- emacs.d/nxhtml/tests/ert.el | 2418 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 2418 insertions(+) create mode 100644 emacs.d/nxhtml/tests/ert.el (limited to 'emacs.d/nxhtml/tests/ert.el') diff --git a/emacs.d/nxhtml/tests/ert.el b/emacs.d/nxhtml/tests/ert.el new file mode 100644 index 0000000..491d79f --- /dev/null +++ b/emacs.d/nxhtml/tests/ert.el @@ -0,0 +1,2418 @@ +;;; 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 ""))) + (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 + '(("(\\(\\\\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 )\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 -- cgit v1.2.3-54-g00ecf