1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386
|
;;; test-simple.el --- Simple Unit Test Framework for Emacs Lisp -*- lexical-binding: t -*-
;; Rewritten from Phil Hagelberg's behave.el by rocky
;; Copyright (C) 2015, 2016 Free Software Foundation, Inc
;; Author: Rocky Bernstein <rocky@gnu.org>
;; URL: http://github.com/rocky/emacs-test-simple
;; Keywords: unit-test
;; Package-Requires: ((cl-lib "0"))
;; Version: 1.2.0
;; 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:
;; test-simple.el is:
;;
;; * Simple. No need for
;; - context macros,
;; - enclosing specifications,
;; - required test tags.
;;
;; But if you want, you still can enclose tests in a local scope,
;; add customized assert failure messages, or add summary messages
;; before a group of tests.
;;
;; * Accommodates both interactive and non-interactive use.
;; - For interactive use, one can use `eval-last-sexp', `eval-region',
;; and `eval-buffer'. One can `edebug' the code.
;; - For non-interactive use, run:
;; emacs --batch --no-site-file --no-splash --load <test-lisp-code.el>
;;
;; Here is an example using gcd.el found in the examples directory.
;;
;; (require 'test-simple)
;; (test-simple-start) ;; Zero counters and start the stop watch.
;;
;; ;; Use (load-file) below because we want to always to read the source.
;; ;; Also, we don't want no stinking compiled source.
;; (assert-t (load-file "./gcd.el")
;; "Can't load gcd.el - are you in the right directory?" )
;;
;; (note "degenerate cases")
;;
;; (assert-nil (gcd 5 -1) "using positive numbers")
;; (assert-nil (gcd -4 1) "using positive numbers, switched order")
;; (assert-raises error (gcd "a" 32)
;; "Passing a string value should raise an error")
;;
;; (note "GCD computations")
;; (assert-equal 1 (gcd 3 5) "gcd(3,5)")
;; (assert-equal 8 (gcd 8 32) "gcd(8,32)")
;; (end-tests) ;; Stop the clock and print a summary
;;
;; Edit (with Emacs of course) gcd-tests.el and run M-x eval-current-buffer
;;
;; You should see in buffer *test-simple*:
;;
;; gcd-tests.el
;; ......
;; 0 failures in 6 assertions (0.002646 seconds)
;;
;; Now let us try from a command line:
;;
;; $ emacs --batch --no-site-file --no-splash --load gcd-tests.el
;; Loading /src/external-vcs/emacs-test-simple/example/gcd.el (source)...
;; *scratch*
;; ......
;; 0 failures in 6 assertions (0.000723 seconds)
;;; To do:
;; FIXME: Namespace is all messed up!
;; Main issues: more expect predicates
(require 'time-date)
;;; Code:
(eval-when-compile
(byte-compile-disable-warning 'cl-functions)
;; Somehow disabling cl-functions causes the erroneous message:
;; Warning: the function `reduce' might not be defined at runtime.
;; FIXME: isolate, fix and/or report back to Emacs developers a bug
;; (byte-compile-disable-warning 'unresolved)
(require 'cl)
)
(require 'cl)
(defgroup test-simple nil
"Simple Unit Test Framework for Emacs Lisp"
:group 'lisp)
(defcustom test-simple-runner-interface (if (fboundp 'bpr-spawn)
'bpr-spawn
'compile)
"Function with one string argument when running tests non-interactively.
Command line started with `emacs --batch' is passed as the argument.
`bpr-spawn', which is in bpr package, is preferable because of no window popup.
If bpr is not installed, fall back to `compile'."
:type 'function
:group 'test-simple)
(defcustom test-simple-runner-key "C-x C-z"
"Key to run non-interactive test after defining command line by `test-simple-run'."
:type 'string
:group 'test-simple)
(defvar test-simple-debug-on-error nil
"If non-nil raise an error on the first failure.")
(defvar test-simple-verbosity 0
"The greater the number the more verbose output.")
(defstruct test-info
description ;; description of last group of tests
(assert-count 0) ;; total number of assertions run
(failure-count 0) ;; total number of failures seen
(start-time (current-time)) ;; Time run started
)
(defvar test-simple-info (make-test-info)
"Variable to store testing information for a buffer.")
(defun note (description &optional test-info)
"Add a name to a group of tests."
(if (getenv "USE_TAP")
(test-simple-msg (format "# %s" description) 't)
(if (> test-simple-verbosity 0)
(test-simple-msg (concat "\n" description) 't))
(unless test-info
(setq test-info test-simple-info))
(setf (test-info-description test-info) description)
))
;;;###autoload
(defmacro test-simple-start (&optional test-start-msg)
`(test-simple-clear nil
(or ,test-start-msg
(if (and (functionp '__FILE__) (__FILE__))
(file-name-nondirectory (__FILE__))
(buffer-name)))
))
;;;###autoload
(defun test-simple-clear (&optional test-info test-start-msg)
"Initialize and reset everything to run tests.
You should run this before running any assertions. Running more than once
clears out information from the previous run."
(interactive)
(unless test-info
(unless test-simple-info
(make-variable-buffer-local (defvar test-simple-info (make-test-info))))
(setq test-info test-simple-info))
(setf (test-info-description test-info) "none set")
(setf (test-info-start-time test-info) (current-time))
(setf (test-info-assert-count test-info) 0)
(setf (test-info-failure-count test-info) 0)
(with-current-buffer (get-buffer-create "*test-simple*")
(let ((old-read-only inhibit-read-only))
(setq inhibit-read-only 't)
(delete-region (point-min) (point-max))
(if test-start-msg (insert (format "%s\n" test-start-msg)))
(setq inhibit-read-only old-read-only)))
(unless noninteractive
(message "Test-Simple: test information cleared")))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Assertion tests
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defmacro assert-raises (error-condition body &optional fail-message)
(let ((fail-message (or fail-message
(format "assert-raises did not get expected %s"
error-condition))))
(list 'condition-case nil
(list 'progn body
(list 'assert-t nil fail-message))
(list error-condition '(assert-t t)))))
(defun assert-op (op expected actual &optional fail-message test-info)
"Expectation is that ACTUAL should be equal to EXPECTED."
(unless test-info (setq test-info test-simple-info))
(cl-incf (test-info-assert-count test-info))
(if (not (funcall op actual expected))
(let* ((fail-message
(if fail-message
(format "Message: %s" fail-message)
""))
(expect-message
(format "\n Expected: %S\n Got: %S" expected actual))
(test-info-mess
(if (boundp 'test-info)
(test-info-description test-info)
"unset")))
(test-simple--add-failure (format "assert-%s" op) test-info-mess
(concat fail-message expect-message)))
(test-simple--ok-msg fail-message)))
(defun assert-equal (expected actual &optional fail-message test-info)
"Expectation is that ACTUAL should be equal to EXPECTED."
(assert-op 'equal expected actual fail-message test-info))
(defun assert-eq (expected actual &optional fail-message test-info)
"Expectation is that ACTUAL should be EQ to EXPECTED."
(assert-op 'eql expected actual fail-message test-info))
(defun assert-eql (expected actual &optional fail-message test-info)
"Expectation is that ACTUAL should be EQL to EXPECTED."
(assert-op 'eql expected actual fail-message test-info))
(defun assert-matches (expected-regexp actual &optional fail-message test-info)
"Expectation is that ACTUAL should match EXPECTED-REGEXP."
(unless test-info (setq test-info test-simple-info))
(cl-incf (test-info-assert-count test-info))
(if (not (string-match expected-regexp actual))
(let* ((fail-message
(if fail-message
(format "\n\tMessage: %s" fail-message)
""))
(expect-message
(format "\tExpected Regexp: %s\n\tGot: %s"
expected-regexp actual))
(test-info-mess
(if (boundp 'test-info)
(test-info-description test-info)
"unset")))
(test-simple--add-failure "assert-equal" test-info-mess
(concat expect-message fail-message)))
(progn (test-simple-msg ".") t)))
(defun assert-t (actual &optional fail-message test-info)
"expectation is that ACTUAL is not nil."
(assert-nil (not actual) fail-message test-info))
(defun assert-nil (actual &optional fail-message test-info)
"expectation is that ACTUAL is nil. FAIL-MESSAGE is an optional
additional message to be displayed."
(unless test-info (setq test-info test-simple-info))
(cl-incf (test-info-assert-count test-info))
(if actual
(let* ((fail-message
(if fail-message
(format "\n\tMessage: %s" fail-message)
""))
(test-info-mess
(if (boundp 'test-simple-info)
(test-info-description test-simple-info)
"unset")))
(test-simple--add-failure "assert-nil" test-info-mess
fail-message test-info))
(test-simple--ok-msg fail-message)))
(defun test-simple--add-failure (type test-info-msg fail-msg
&optional test-info)
(unless test-info (setq test-info test-simple-info))
(cl-incf (test-info-failure-count test-info))
(let ((failure-msg
(format "\nDescription: %s, type %s\n%s" test-info-msg type fail-msg))
)
(save-excursion
(test-simple--not-ok-msg fail-msg)
(test-simple-msg failure-msg 't)
(unless noninteractive
(if test-simple-debug-on-error
(signal 'test-simple-assert-failed failure-msg)
;;(message failure-msg)
)))))
(defun end-tests (&optional test-info)
"Give a tally of the tests run."
(interactive)
(unless test-info (setq test-info test-simple-info))
(test-simple-describe-failures test-info)
(if noninteractive
(progn
(switch-to-buffer "*test-simple*")
(message "%s" (buffer-substring (point-min) (point-max)))
)
(switch-to-buffer-other-window "*test-simple*")
))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Reporting
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defun test-simple-msg(msg &optional newline)
(switch-to-buffer "*test-simple*")
(let ((inhibit-read-only t))
(insert msg)
(if newline (insert "\n"))
(switch-to-buffer nil)
))
(defun test-simple--ok-msg (fail-message &optional test-info)
(unless test-info (setq test-info test-simple-info))
(let ((msg (if (getenv "USE_TAP")
(if (equal fail-message "")
(format "ok %d\n" (test-info-assert-count test-info))
(format "ok %d - %s\n"
(test-info-assert-count test-info)
fail-message))
".")))
(test-simple-msg msg))
't)
(defun test-simple--not-ok-msg (_fail-message &optional test-info)
(unless test-info (setq test-info test-simple-info))
(let ((msg (if (getenv "USE_TAP")
(format "not ok %d\n" (test-info-assert-count test-info))
"F")))
(test-simple-msg msg))
nil)
(defun test-simple-summary-line(info)
(let*
((failures (test-info-failure-count info))
(asserts (test-info-assert-count info))
(problems (concat (number-to-string failures) " failure"
(unless (= 1 failures) "s")))
(tests (concat (number-to-string asserts) " assertion"
(unless (= 1 asserts) "s")))
(elapsed-time (time-since (test-info-start-time info)))
)
(if (getenv "USE_TAP")
(format "1..%d" asserts)
(format "\n%s in %s (%g seconds)" problems tests
(float-time elapsed-time))
)))
(defun test-simple-describe-failures(&optional test-info)
(unless test-info (setq test-info test-simple-info))
(goto-char (point-max))
(test-simple-msg (test-simple-summary-line test-info)))
;;;###autoload
(defun test-simple-run (&rest command-line-formats)
"Register command line to run tests non-interactively and bind key to run test.
After calling this function, you can run test by key specified by `test-simple-runner-key'.
It is preferable to write at the first line of test files as a comment, e.g,
;;;; (test-simple-run \"emacs -batch -L %s -l %s\" (file-name-directory (locate-library \"test-simple.elc\")) buffer-file-name)
Calling this function interactively, COMMAND-LINE-FORMATS is set above."
(interactive)
(setq command-line-formats
(or command-line-formats
(list "emacs -batch -L %s -l %s"
(file-name-directory (locate-library "test-simple.elc"))
buffer-file-name)))
(let ((func (lambda ()
(interactive)
(funcall test-simple-runner-interface
(apply 'format command-line-formats)))))
(global-set-key (kbd test-simple-runner-key) func)
(funcall func)))
(defun test-simple-noninteractive-kill-emacs-hook ()
"Emacs exits abnormally when noninteractive test fails."
(when (and noninteractive test-simple-info
(<= 1 (test-info-failure-count test-simple-info)))
(let (kill-emacs-hook)
(kill-emacs 1))))
(when noninteractive
(add-hook 'kill-emacs-hook 'test-simple-noninteractive-kill-emacs-hook))
(provide 'test-simple)
;;; test-simple.el ends here
|