1 ;;; el-expectations.el --- minimalist unit testing framework
2 ;; $Id: el-expectations.el,v 1.47 2008/08/28 19:28:37 rubikitch Exp $
4 ;; Copyright (C) 2008 rubikitch
6 ;; Author: rubikitch <rubikitch@ruby-lang.org>
7 ;; Keywords: lisp, testing, unittest
8 ;; URL: http://www.emacswiki.org/cgi-bin/wiki/download/el-expectations.el
10 ;; This file is free software; you can redistribute it and/or modify
11 ;; it under the terms of the GNU General Public License as published by
12 ;; the Free Software Foundation; either version 2, or (at your option)
15 ;; This file is distributed in the hope that it will be useful,
16 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
17 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 ;; GNU General Public License for more details.
20 ;; You should have received a copy of the GNU General Public License
21 ;; along with GNU Emacs; see the file COPYING. If not, write to
22 ;; the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
23 ;; Boston, MA 02110-1301, USA.
27 ;; Emacs Lisp Expectations framework is a minimalist unit testing
28 ;; framework in Emacs Lisp.
30 ;; I love Jay Fields' expectations unit testing framework in Ruby. It
31 ;; provides one syntax and can define various assertions. So I created
32 ;; Emacs Lisp Expectations modeled after expectations in Ruby.
33 ;; Testing policy is same as the original expectations in Ruby. Visit
34 ;; expectations site in rubyforge.
35 ;; http://expectations.rubyforge.org/
37 ;; With Emacs Lisp Mock (el-mock.el), Emacs Lisp Expectations supports
38 ;; mock and stub, ie. behavior based testing.
39 ;; You can get it from EmacsWiki
40 ;; http://www.emacswiki.org/cgi-bin/wiki/download/el-mock.el
44 ;; 1. Evaluate an expectations sexp.
45 ;; 2. `M-x expectations-execute' to execute a test.
46 ;; 3. If there are any errors, use M-x next-error (C-x `) and M-x previous-error
47 ;; to go to expect sexp in error.
49 ;; If you evaluated expectations by C-M-x, it is automatically executed.
50 ;; If you type C-u C-u C-M-x, execute expectations with batch-mode.
52 ;; For further information: see docstring of `expectations'.
53 ;; [EVAL IT] (describe-function 'expectations)
57 ;; Batch mode can be used with this shell script (el-expectations).
58 ;; Of course, EMACS/OPTIONS/OUTPUT can be customized.
60 ;; ATTENTION! This script is slightly changed since v1.32.
64 ;; OPTIONS="-L . -L $HOME/emacs/lisp"
65 ;; OUTPUT=/tmp/.el-expectations
66 ;; $EMACS -q --no-site-file --batch $OPTIONS -l el-expectations -f batch-expectations $OUTPUT "$@"
72 ;; $ el-expectations el-expectations-failure-sample.el
76 ;; You can embed test using `fboundp' and `dont-compile'. dont-compile
77 ;; is needed to prevent unit tests from being byte-compiled.
80 ;; (when (fboundp 'expectations)
88 ;; * `expectations-execute' can execute one test (sexp).
92 ;; Example code is in the EmacsWiki.
94 ;; Success example http://www.emacswiki.org/cgi-bin/wiki/download/el-expectations-success-sample.el
95 ;; Failure example http://www.emacswiki.org/cgi-bin/wiki/download/el-expectations-failure-sample.el
99 ;; $Log: el-expectations.el,v $
100 ;; Revision 1.47 2008/08/28 19:28:37 rubikitch
101 ;; not-called assertion
103 ;; Revision 1.46 2008/08/28 19:06:24 rubikitch
104 ;; `exps-padding': use `window-width'
106 ;; Revision 1.45 2008/08/24 20:36:37 rubikitch
107 ;; mention `dont-compile'
109 ;; Revision 1.44 2008/08/22 20:48:52 rubikitch
110 ;; fixed a stupid bug
112 ;; Revision 1.43 2008/08/22 20:43:00 rubikitch
113 ;; non-nil (true) assertion
115 ;; Revision 1.42 2008/04/14 07:54:27 rubikitch
116 ;; *** empty log message ***
118 ;; Revision 1.41 2008/04/14 06:58:20 rubikitch
119 ;; *** empty log message ***
121 ;; Revision 1.40 2008/04/14 06:52:39 rubikitch
124 ;; Revision 1.39 2008/04/13 11:49:08 rubikitch
125 ;; C-u M-x expectations-execute -> batch-expectations-in-emacs
127 ;; Revision 1.38 2008/04/13 11:39:51 rubikitch
128 ;; better result display.
130 ;; Revision 1.37 2008/04/13 11:30:17 rubikitch
131 ;; expectations-eval-defun
132 ;; batch-expectations-in-emacs
134 ;; Revision 1.36 2008/04/12 18:44:24 rubikitch
135 ;; extend `type' assertion to use predicates.
137 ;; Revision 1.35 2008/04/12 14:10:00 rubikitch
138 ;; updated el-mock info.
140 ;; Revision 1.34 2008/04/12 14:08:28 rubikitch
141 ;; * (require 'el-mock nil t)
142 ;; * updated `expectations' docstring
144 ;; Revision 1.33 2008/04/12 09:49:27 rubikitch
145 ;; *** empty log message ***
147 ;; Revision 1.32 2008/04/12 09:44:23 rubikitch
148 ;; batch-mode: handle multiple lisp files.
150 ;; Revision 1.31 2008/04/12 09:34:32 rubikitch
151 ;; colorize result summary
153 ;; Revision 1.30 2008/04/12 09:19:42 rubikitch
154 ;; show result summary at the top.
156 ;; Revision 1.29 2008/04/12 03:19:06 rubikitch
157 ;; Execute all expectations in batch mode.
159 ;; Revision 1.28 2008/04/12 03:07:43 rubikitch
162 ;; Revision 1.27 2008/04/10 17:02:40 rubikitch
163 ;; *** empty log message ***
165 ;; Revision 1.26 2008/04/10 14:27:47 rubikitch
169 ;; Revision 1.25 2008/04/10 12:45:57 rubikitch
172 ;; Revision 1.24 2008/04/10 08:46:19 rubikitch
173 ;; integration of `stub' in el-mock.el
175 ;; Revision 1.23 2008/04/10 07:11:40 rubikitch
176 ;; error data is evaluated.
178 ;; Revision 1.22 2008/04/10 06:14:12 rubikitch
179 ;; added finish message with current time.
181 ;; Revision 1.21 2008/04/09 20:45:41 rubikitch
182 ;; error assertion: with error data
184 ;; Revision 1.20 2008/04/09 20:02:46 rubikitch
185 ;; error-message assertion
187 ;; Revision 1.19 2008/04/09 15:07:29 rubikitch
188 ;; expectations-execute-at-once, eval-defun advice
190 ;; Revision 1.18 2008/04/09 08:57:37 rubikitch
191 ;; Batch Mode documentation
193 ;; Revision 1.17 2008/04/09 08:52:34 rubikitch
194 ;; * (eval-when-compile (require 'cl))
196 ;; * count expectations/failures/errors
197 ;; * exitstatus = failures + errors (batch mode)
199 ;; Revision 1.16 2008/04/09 04:03:11 rubikitch
200 ;; batch-expectations: use command-line-args-left
202 ;; Revision 1.15 2008/04/09 03:54:00 rubikitch
204 ;; batch-expectations
206 ;; Revision 1.14 2008/04/08 17:54:02 rubikitch
209 ;; Revision 1.13 2008/04/08 17:45:08 rubikitch
211 ;; renamed: expectations.el -> el-expectations.el
213 ;; Revision 1.12 2008/04/08 16:54:50 rubikitch
214 ;; changed output format slightly
216 ;; Revision 1.11 2008/04/08 16:37:53 rubikitch
219 ;; Revision 1.10 2008/04/08 15:52:14 rubikitch
222 ;; Revision 1.9 2008/04/08 15:39:06 rubikitch
223 ;; *** empty log message ***
225 ;; Revision 1.8 2008/04/08 15:38:03 rubikitch
226 ;; reimplementation of exps-assert-*
228 ;; Revision 1.7 2008/04/08 15:06:42 rubikitch
229 ;; better failure handling
231 ;; Revision 1.6 2008/04/08 14:45:58 rubikitch
236 ;; Revision 1.5 2008/04/08 13:16:16 rubikitch
237 ;; removed elk-test dependency
239 ;; Revision 1.4 2008/04/08 12:55:15 rubikitch
240 ;; next-error/occur-like interface
242 ;; Revision 1.3 2008/04/08 09:08:54 rubikitch
243 ;; prettier `desc' display
245 ;; Revision 1.2 2008/04/08 08:45:46 rubikitch
246 ;; exps-last-filename
248 ;; Revision 1.1 2008/04/08 07:52:30 rubikitch
254 (eval-when-compile (require 'cl
))
255 (require 'el-mock nil t
)
257 (defgroup el-expectations nil
258 "Emacs Lisp Expectations - minimalist unit testing framework."
261 (defvar exps-last-testcase nil
)
262 (defvar exps-last-filename nil
)
263 (defvar expectations-result-buffer
"*expectations result*")
265 (defcustom expectations-execute-at-once t
266 "If non-nil, execute selected expectation when pressing C-M-x"
267 :group
'el-expectations
)
268 (defmacro expectations
(&rest body
)
269 "Define a expectations test case.
270 Use `expect' and `desc' to verify the code.
271 Note that these are neither functions nor macros.
272 These are keywords in expectations Domain Specific Language(DSL).
275 * (expect EXPECTED-VALUE BODY ...)
276 Assert that the evaluation result of BODY is `equal' to EXPECTED-VALUE.
278 Description of a test. It is treated only as a delimiter comment.
280 Synopsis of EXPECTED-VALUE:
283 Any non-nil value, eg. t, 1, '(1).
285 * (buffer BUFFER-NAME)
286 Body should eq buffer object of BUFFER-NAME.
289 (expect (buffer \"*scratch*\")
290 (with-current-buffer \"*scratch*\"
293 Body should match REGEXP.
296 (expect (regexp \"o\")
299 Body should be a TYPE-SYMBOL.
300 TYPE-SYMBOL may be one of symbols returned by `type-of' function.
301 `symbol', `integer', `float', `string', `cons', `vector',
302 `char-table', `bool-vector', `subr', `compiled-function',
303 `marker', `overlay', `window', `buffer', `frame', `process',
304 `window-configuration'
305 Otherwise using predicate naming TYPE-SYMBOL and \"p\".
306 For example, `(type sequence)' uses `sequencep' predicate.
307 `(type char-or-string)' uses `char-or-string-p' predicate.
310 (expect (type buffer)
312 (expect (type sequence)
314 (expect (type char-or-string)
318 Body should raise any error.
323 * (error ERROR-SYMBOL)
324 Body should raise ERROR-SYMBOL error.
327 (expect (error arith-error)
329 * (error ERROR-SYMBOL ERROR-DATA)
330 Body should raise ERROR-SYMBOL error with ERROR-DATA.
331 ERROR-DATA is 2nd argument of `signal' function.
334 (expect (error wrong-number-of-arguments '(= 3))
336 * (error-message ERROR-MESSAGE)
337 Body should raise any error with ERROR-MESSAGE.
340 (expect (error-message \"ERROR!!\")
343 * (mock MOCK-FUNCTION-SPEC => MOCK-RETURN-VALUE)
344 Body should call MOCK-FUNCTION-SPEC and returns MOCK-RETURN-VALUE.
345 Mock assertion depends on `el-mock' library.
346 If available, you do not have to require it: el-expectations detects it.
348 Synopsis of MOCK-FUNCTION-SPEC:
349 (FUNCTION ARGUMENT ...)
350 MOCK-FUNCTION-SPEC is almost same as normal function call.
351 If you should specify `*' as ARGUMENT, any value is accepted.
352 Otherwise, body should call FUNCTION with specified ARGUMENTs.
355 (expect (mock (foo * 3) => nil)
358 * (not-called FUNCTION)
359 Body should not call FUNCTION.
360 Not-called assertion depends on `el-mock' library.
361 If available, you do not have to require it: el-expectations detects it.
364 (expect (not-called hoge)
368 Body should equal (eval SEXP).
374 Extending EXPECTED-VALUE is easy. See el-expectations.el source code.
378 (desc \"simple expectation\")
382 (concat \"ho\" \"ge\"))
384 (set-buffer (get-buffer-create \"tmp\"))
389 (desc \"extended expectation\")
390 (expect (buffer \"*scratch*\")
391 (with-current-buffer \"*scratch*\"
393 (expect (regexp \"o\")
395 (expect (type integer)
398 (desc \"error expectation\")
399 (expect (error arith-error)
403 (desc \"mock with stub\")
404 (expect (mock (foo 5 * 7) => nil)
405 ;; Stub function `hoge', which accepts any arguments and returns 3.
407 (foo (+ 2 (hoge 10)) 6 7))
411 `(setq exps-last-testcase
412 ',(append exps-last-testcase
413 '((new-expectations 1))
415 exps-last-filename nil
)
416 `(setq exps-last-testcase
',body
417 exps-last-filename
,(or load-file-name buffer-file-name
))))
419 (defun exps-execute-test (test)
420 (destructuring-bind (expect expected . actual
)
425 (exps-assert expected actual
)
426 (error (cons 'error e
))))
428 (cons 'desc expected
))
430 (cons 'desc
(concat "+++++ New expectations +++++"))))))
433 (defun expectations-execute (&optional testcase
)
434 "Execute last-defined `expectations' test.
435 With prefix argument, do `batch-expectations-in-emacs'."
437 (if current-prefix-arg
438 (batch-expectations-in-emacs)
440 (loop for test in
(or testcase exps-last-testcase
)
441 collecting
(exps-execute-test test
)))))
444 (defvar exps-assert-functions
445 '(exps-assert-non-nil
451 exps-assert-error-message
453 exps-assert-not-called
454 exps-assert-equal-eval
))
456 (defun exps-do-assertion (expected actual symbol evalp test-func msg-func
&optional expected-get-func
)
457 (and (consp expected
)
458 (eq symbol
(car expected
))
459 (exps-do-assertion-1 (funcall (or expected-get-func
#'cadr
) expected
)
460 actual evalp test-func msg-func
)))
462 (defun exps-do-assertion-1 (expected actual evalp test-func msg-func
)
463 (if evalp
(setq actual
(exps-eval-sexps actual
)))
464 (if (funcall test-func expected actual
)
466 (cons 'fail
(funcall msg-func expected actual
))))
468 (defun exps-eval-sexps (sexps)
469 (let ((fn (lambda () (eval `(progn ,@sexps
)))))
470 (if (fboundp 'mock-protect
)
474 (defun exps-assert-non-nil (expected actual
)
476 expected actual
'non-nil t
477 (lambda (e a
) (not (null a
)))
478 (lambda (e a
) (format "FAIL: Expected non-nil but was nil"))))
480 (defun exps-assert-true (expected actual
)
482 expected actual
'true t
483 (lambda (e a
) (not (null a
)))
484 (lambda (e a
) (format "FAIL: Expected non-nil but was nil"))))
485 (defun exps-assert-buffer (expected actual
)
487 expected actual
'buffer t
488 (lambda (e a
) (eq (get-buffer e
) a
))
489 (lambda (e a
) (format "FAIL: Expected <#<buffer %s>> but was <%S>" e a
))))
491 (defun exps-assert-regexp (expected actual
)
493 expected actual
'regexp t
494 (lambda (e a
) (string-match e a
))
495 (lambda (e a
) (format "FAIL: %S should match /%s/" a e
))))
497 (defun exps-assert-type (expected actual
)
499 expected actual
'type t
500 (lambda (e a
) (or (eq (type-of a
) e
)
501 (let* ((name (symbol-name e
))
503 (concat name
(if (string-match "-" name
)
508 (lambda (e a
) (format "FAIL: %S is not a %s" a e
))))
510 (defun exps-assert-error (expected actual
)
511 (let (actual-error actual-errdata
)
513 expected actual
'error nil
516 (progn (exps-eval-sexps a
) nil
)
518 (setq actual-error err
)
519 (cond ((consp (cadr e
))
520 (and (eq (car e
) (car err
))
521 (equal (setq actual-errdata
(eval (cadr e
)))
528 (let ((error-type (car e
))
531 (format ", but raised <%S>" actual-error
)
532 ", but no error was raised")))
533 (cond ((and error-type
(eq error-type
(car actual-error
)))
534 (format "FAIL: Expected errdata <%S>, but was <%S>" actual-errdata
(cdr actual-error
)))
536 (format "FAIL: should raise <%s>%s" error-type actual-err-string
))
538 (format "FAIL: should raise any error%s" actual-err-string
)))))
541 (defun exps-assert-error-message (expected actual
)
542 (let (actual-error-string)
544 expected actual
'error-message nil
547 (progn (exps-eval-sexps a
) nil
)
549 (setq actual-error-string
(error-message-string err
))
550 (equal e actual-error-string
))))
552 (if actual-error-string
553 (format "FAIL: Expected errmsg <%s>, but was <%s>" e actual-error-string
)
554 (format "FAIL: Expected errmsg <%s>, but no error was raised" e
))))))
557 (defun exps-assert-mock (expected actual
)
560 expected actual
'mock nil
567 (eval `(progn ,@a
))))
569 (mock-error (setq err me
) nil
))
572 (if (eq 'not-called
(cadr err
))
573 (format "FAIL: Expected function call <%S>" e
)
574 (destructuring-bind (_ e-args a-args
) err
575 (format "FAIL: Expected call <%S>, but was <%S>" e-args a-args
))))
578 (defun exps-assert-not-called (expected actual
)
581 expected actual
'not-called nil
587 (eval `(not-called ,@e
))
588 (eval `(progn ,@a
))))
590 (mock-error (setq err me
) nil
))
593 (if (eq 'called
(cadr err
))
594 (format "FAIL: Expected not-called <%S>" e
)))
596 (defun exps-assert-equal-eval (expected actual
)
598 (eval expected
) actual t
599 (lambda (e a
) (equal e a
))
600 (lambda (e a
) (format "FAIL: Expected <%S> but was <%S>" expected a
))))
602 (defun exps-assert (expected actual
)
603 (run-hook-with-args-until-success 'exps-assert-functions expected actual
))
605 ;;;; next-error interface / occur-mode-like interface
606 (define-derived-mode exps-display-mode fundamental-mode
"EXPECT"
607 (buffer-disable-undo)
608 (setq next-error-function
'exps-next-error
)
609 (setq next-error-last-buffer
(current-buffer))
610 (define-key exps-display-mode-map
"\C-m" 'exps-goto-expect
)
611 (define-key exps-display-mode-map
"\C-c\C-c" 'exps-goto-expect
))
613 (defun exps-padding (desc &optional default-width
)
616 (or default-width
(string-to-number (or (getenv "WIDTH") "60")))
617 (window-width (get-buffer-window (current-buffer) t
)))))
618 (make-string (floor (/ (- width
8 (length desc
)) 2)) ?
=)))
620 (defun exps-desc (desc &optional default-width
)
621 (let ((padding (exps-padding desc default-width
)))
622 (format "%s %s %s" padding desc padding
)))
624 (defface expectations-red
625 '((t (:foreground
"Red" :bold t
)))
626 "Face for expectations with failure."
627 :group
'el-expectations
)
628 (defface expectations-green
629 '((t (:foreground
"Green" :bold t
)))
630 "Face for successful expectations."
631 :group
'el-expectations
)
632 (defvar exps-red-face
'expectations-red
)
633 (defvar exps-green-face
'expectations-green
)
634 (defun exps-result-string (s f e
)
635 (let ((msg1 (format "%d expectations, %d failures, %d errors\n"
637 (msg2 (format "Expectations finished at %s\n" (current-time-string))))
638 (put-text-property 0 (length msg1
) 'face
644 (defun exps-display (results)
645 (set-buffer (get-buffer-create expectations-result-buffer
))
647 (display-buffer (current-buffer))
649 (insert (format "Executing expectations in %s...\n" exps-last-filename
))
650 (loop for result in results
659 (error (format "ERROR: %s" (cdr result
)))
660 (desc (exps-desc (cdr result
)))
664 (loop for result in results
665 for status
= (car result
)
666 when
(eq 'pass status
) collecting result into successes
667 when
(eq 'fail status
) collecting result into failures
668 when
(eq 'error status
) collecting result into errors
671 (destructuring-bind (s f e
)
672 (mapcar #'length
(list successes failures errors
))
673 (setq summary
(exps-result-string s f e
))
675 (goto-char (point-min))
678 (goto-char (point-min))
681 (defun exps-goto-expect ()
683 ;; assumes that current-buffer is *expectations result*
686 (looking-at "^[0-9]+")
687 (string-to-number (match-string 0)))))
688 (when exps-last-filename
689 (with-current-buffer (find-file-noselect exps-last-filename
)
690 (pop-to-buffer (current-buffer))
691 (goto-char (point-min))
692 (search-forward "(expectations\n" nil t
)
694 (forward-sexp -
1)))))
696 (defun exps-next-error (&optional argp reset
)
697 "Move to the Nth (default 1) next failure/error in *expectations result* buffer.
698 Compatibility function for \\[next-error] invocations."
700 ;; we need to run exps-find-failure from within the *expectations result* buffer
702 ;; Choose the buffer and make it current.
703 (if (next-error-buffer-p (current-buffer))
705 (next-error-find-buffer nil nil
707 (eq major-mode
'exps-display-mode
))))
708 (goto-char (cond (reset (point-min))
709 ((< argp
0) (line-beginning-position))
710 ((> argp
0) (line-end-position))
718 ;; In case the *expectations result* buffer is visible in a nonselected window.
719 (let ((win (get-buffer-window (current-buffer) t
)))
720 (if win
(set-window-point win
(point))))
723 (defun exps-find-failure (n search-func errmsg
)
725 (unless (funcall search-func
"^[0-9]+ *:\\(ERROR\\|FAIL\\)" nil t
)
729 (put 'expect
'lisp-indent-function
1)
730 (put 'expectations
'lisp-indent-function
0)
732 ;; (edit-list (quote font-lock-keywords-alist))
733 (font-lock-add-keywords
735 '(("\\<\\(expectations\\|expect\\)\\>" 0 font-lock-keyword-face
)
736 (exps-font-lock-desc 0 font-lock-warning-face prepend
)
737 (exps-font-lock-expected-value 0 font-lock-function-name-face prepend
)))
739 (defun exps-font-lock-desc (limit)
740 (when (re-search-forward "(desc\\s " limit t
)
742 (set-match-data (list (point) (progn (forward-sexp 1) (point))))
745 ;; I think expected value is so-called function name of `expect'.
746 (defun exps-font-lock-expected-value (limit)
747 (when (re-search-forward "(expect\\s " limit t
)
751 (set-match-data (list (point) e
))
754 (defun expectations-eval-defun (arg)
756 If `expectations-execute-at-once' is non-nil, execute expectations if it is an expectations form."
759 (when expectations-execute-at-once
762 (and (looking-at "(expectations\\|(.+(fboundp 'expectations)\\|(dont-compile\n.*expectations")
763 (expectations-execute)))))
765 (substitute-key-definition 'eval-defun
'expectations-eval-defun
emacs-lisp-mode-map)(substitute-key-definition 'eval-defun
'expectations-eval-defun
lisp-interaction-mode-map)
768 (defun batch-expectations ()
769 (if (not noninteractive
)
770 (error "`batch-expectations' is to be used only with -batch"))
771 (destructuring-bind (output-file . lispfiles
)
772 command-line-args-left
773 (dolist (lispfile lispfiles
)
774 (load lispfile nil t
))
775 (let ((fail-and-errors (expectations-execute)))
776 (with-current-buffer expectations-result-buffer
777 (write-region (point-min) (point-max) output-file nil
'nodisp
))
778 (kill-emacs fail-and-errors
))))
780 (defun batch-expectations-in-emacs ()
781 "Execute expectations in current file with batch mode."
783 (shell-command (concat "el-expectations " exps-last-filename
)
784 expectations-result-buffer
)
785 (with-current-buffer expectations-result-buffer
786 (goto-char (point-min))
787 (while (re-search-forward "^[0-9].+\\([0-9]\\) failures, \\([0-9]+\\) errors" nil t
)
788 (put-text-property (match-beginning 0) (match-end 0)
790 (if (and (string= "0" (match-string 1))
791 (string= "0" (match-string 2)))
794 (provide 'el-expectations
)
796 ;; How to save (DO NOT REMOVE!!)
797 ;; (emacswiki-post "el-expectations.el")
798 ;;; el-expectations.el ends here