1 ;;;; -*- Mode: Lisp; indent-tabs-mode: nil -*-
3 (in-package :it.bese.fiveam)
7 ;;;; At the lowest level testing the system requires that certain
8 ;;;; forms be evaluated and that certain post conditions are met: the
9 ;;;; value returned must satisfy a certain predicate, the form must
10 ;;;; (or must not) signal a certain condition, etc. In FiveAM these
11 ;;;; low level operations are called 'checks' and are defined using
12 ;;;; the various checking macros.
14 ;;;; Checks are the basic operators for collecting results. Tests and
15 ;;;; test suites on the other hand allow grouping multiple checks into
16 ;;;; logic collections.
18 (defvar *test-dribble* t)
20 (defmacro with-*test-dribble* (stream &body body)
21 `(let ((*test-dribble* ,stream))
22 (declare (special *test-dribble*))
25 (eval-when (:compile-toplevel :load-toplevel :execute)
26 (def-special-environment run-state ()
30 ;;;; ** Types of test results
32 ;;;; Every check produces a result object.
34 (defclass test-result ()
35 ((reason :accessor reason :initarg :reason :initform "no reason given")
36 (test-expr :accessor test-expr :initarg :test-expr)
37 (test-case :accessor test-case
39 :initform (with-run-state (current-test)
41 (:documentation "All checking macros will generate an object of
44 (defgeneric test-result-p (object)
45 (:method ((o test-result)) t)
46 (:method ((o t)) nil))
48 (defclass test-passed (test-result)
50 (:documentation "Class for successful checks."))
52 (defgeneric test-passed-p (object)
54 (:method ((o test-passed)) t))
56 ;; if a condition could inhert from a class we could avoid duplicating
57 ;; these slot definitions...
59 (define-condition check-failure (error)
60 ((reason :accessor reason :initarg :reason :initform "no reason given")
61 (test-expr :accessor test-expr :initarg :test-expr)
62 (test-case :accessor test-case
64 :initform (with-run-state (current-test)
66 (:documentation "Signaled when a check fails.")
67 (:report (lambda (c stream)
68 (format stream "The following check failed: ~S~%~A."
72 (defmacro process-failure (&rest args)
74 (restartable-check-failure ,@args)
75 (add-result 'test-failure ,@args)))
77 (defun restartable-check-failure (&rest check-failure-args)
78 (with-simple-restart (ignore-failure "Continue the test run.")
79 (apply #'error 'check-failure check-failure-args)))
81 (defclass test-failure (test-result)
83 (:documentation "Class for unsuccessful checks."))
85 (defgeneric test-failure-p (object)
87 (:method ((o test-failure)) t))
89 (defclass unexpected-test-failure (test-failure)
90 ((actual-condition :accessor actual-condition :initarg :condition))
91 (:documentation "Represents the result of a test which neither
92 passed nor failed, but signaled an error we couldn't deal
95 Note: This is very different than a SIGNALS check which instead
96 creates a TEST-PASSED or TEST-FAILURE object."))
98 (defclass test-skipped (test-result)
100 (:documentation "A test which was not run. Usually this is due
101 to unsatisfied dependencies, but users can decide to skip test
104 (defgeneric test-skipped-p (object)
105 (:method ((o t)) nil)
106 (:method ((o test-skipped)) t))
108 (defun add-result (result-type &rest make-instance-args)
109 "Create a TEST-RESULT object of type RESULT-TYPE passing it the
110 initialize args MAKE-INSTANCE-ARGS and adds the resulting object to
111 the list of test results.
113 If RESULT-TYPE is already a TEST-RESULT object it is used as is and
114 the MAKE-INSTANCE-ARGS are ignored."
115 (with-run-state (result-list)
116 (let ((result (if (test-result-p result-type)
118 (apply #'make-instance result-type make-instance-args))))
120 (test-passed (format *test-dribble* "."))
121 (unexpected-test-failure (format *test-dribble* "X"))
122 (test-failure (format *test-dribble* "f"))
123 (test-skipped (format *test-dribble* "s")))
124 (push result result-list))))
126 ;;;; ** The check operators
128 ;;;; *** The IS check
130 (defmacro is (test &rest reason-args)
131 "The DWIM checking operator.
133 If TEST returns a true value a test-passed result is generated,
134 otherwise a test-failure result is generated. The reason, unless
135 REASON-ARGS is provided, is generated based on the form of TEST:
137 `(predicate expected actual)`::
139 Means that we want to check whether, according to PREDICATE, the
140 ACTUAL value is in fact what we EXPECTED. `expected` can also be a
141 values form, `(cl:values &rest values)`. In this case the values
142 returned by `actual` will be converted to a list and that list will be
143 compared, via `predicate` to the list `values`.
145 `(predicate value)`::
147 Means that we want to ensure that VALUE satisfies PREDICATE.
149 Wrapping the TEST form in a NOT simply produces a negated reason
153 "Argument to IS must be a list, not ~S" test)
154 (let (bindings effective-test failure-init-args)
155 (with-gensyms (e a v)
156 (flet ((process-entry (predicate expected actual &optional negatedp)
157 ;; make sure EXPECTED is holding the entry that starts with 'values
158 (when (and (consp actual)
159 (eq (car actual) 'values))
160 (assert (not (and (consp expected)
161 (eq (car expected) 'values))) ()
162 "Both the expected and actual part is a values expression.")
163 (rotatef expected actual))
165 (if (and (consp expected)
166 (eq (car expected) 'values))
168 (setf expected (copy-list expected))
169 (setf setf-forms (loop for cell = (rest expected) then (cdr cell)
172 when (eq (car cell) '*)
173 collect `(setf (elt ,a ,i) nil)
174 and do (setf (car cell) nil)))
175 (setf bindings (list (list e `(list ,@(rest expected)))
176 (list a `(multiple-value-list ,actual)))))
177 (setf bindings (list (list e expected)
179 (setf effective-test `(progn
182 `(not (,predicate ,e ,a))
183 `(,predicate ,e ,a))))
185 (list-match-case test
186 ((not (?predicate ?expected ?actual))
187 (multiple-value-bind (expected-value actual-value)
188 (process-entry ?predicate ?expected ?actual t)
189 (setf failure-init-args `('is-negated-binary-failure
190 :predicate ',?predicate
191 :expected-form ',?expected
192 :expected-value ,expected-value
193 :actual-form ',?actual
194 :actual-value ,actual-value))))
195 ((not (?satisfies ?value))
196 (setf bindings (list (list v ?value))
197 effective-test `(not (,?satisfies ,v))
198 failure-init-args `('is-negated-unary-failure
199 :predicate ',?satisfies
200 :expected-form ',?value
201 :expected-value ,v)))
202 ((?predicate ?expected ?actual)
203 (multiple-value-bind (expected-value actual-value)
204 (process-entry ?predicate ?expected ?actual)
205 (setf failure-init-args `('is-binary-failure
206 :predicate ',?predicate
207 :expected-form ',?expected
208 :expected-value ,expected-value
209 :actual-value ,actual-value
210 :actual-form ',?actual))))
212 (setf bindings (list (list v ?value))
213 effective-test `(,?satisfies ,v)
214 failure-init-args `('is-unary-failure
215 :predicate ',?satisfies
216 :expected-form ',?value
217 :expected-value ,v)))
221 failure-init-args `('test-failure
222 :reason (format nil "~2&~S~2% returned NIL." ',test)
223 :test-expr ',test)))))
225 (setf failure-init-args (list* :result `(format nil ,@reason-args) failure-init-args)))
228 (add-result 'test-passed :test-expr ',test)
229 (let ((failure (make-instance ,@failure-init-args)))
230 (restartable-check-failure :reason (reason failure) :test-expr ',test)
231 (add-result failure)))))))
233 (defclass is-failure-mixin ()
234 ((predicate :initarg :predicate :accessor predicate)
235 (actual-form :initarg :actual-form :accessor actual-form)
236 (actual-value :initarg :actual-value :accessor actual-value)))
238 (defclass is-binary-failure-mixin (is-failure-mixin)
239 ((expected-value :initarg :expected-value :accessor expected-value)
240 (expected-form :initarg :expected-form :accessor expected-form)))
242 (defclass is-failure (test-failure)
243 ((reason :initform nil :initarg :reason)))
245 (defmethod reason :around ((result is-failure))
246 (or (slot-value result 'reason)
249 (defclass is-binary-failure (is-failure is-binary-failure-mixin)
252 (defmethod reason ((result is-binary-failure))
254 "~2&~S~2% evaluated to ~2&~S~2% which is ~2&~S~2%to ~2&~S~2% (it should not be)"
256 (actual-value result)
258 (expected-value result)))
260 (defclass is-negated-binary-failure (is-failure is-binary-failure-mixin)
263 (defmethod reason ((result is-binary-failure))
265 "~2&~S~2% evaluated to ~2&~S~2% which is not ~2&~S~2%to ~2&~S~2% (it should be)"
267 (actual-value result)
269 (expected-value result)))
271 (defclass is-unary-failure (is-failure is-failure-mixin)
274 (defmethod reason ((result is-unary-failure))
276 "~2&~S~2% evaluated to ~2&~S~2% which satisfies ~2&~S~2% (it should not)"
278 (actual-value result)
281 (defclass is-negated-unary-failure (is-failure is-failure-mixin)
284 (defmethod reason ((result is-negated-unary-failure))
286 "~2&~S~2% evaluated to ~2&~S~2% which does not satisfy ~2&~S~2%"
288 (actual-value result)
291 ;;;; *** Other checks
293 (defmacro is-every (predicate &body clauses)
294 "Tests that all the elements of CLAUSES are equal, according to PREDICATE.
296 If every element of CLAUSES is a cons we assume the `first` of each
297 element is the expected value, and the `second` of each element is the
298 actual value and generate a call to `IS` accordingly.
300 If not every element of CLAUSES is a cons then we assume that each
301 element is a value to pass to predicate (the 1 argument form of `IS`)"
303 ,@(if (every #'consp clauses)
304 (loop for (expected actual . reason) in clauses
305 collect `(is (,predicate ,expected ,actual) ,@reason))
307 (assert (evenp (list-length clauses)))
308 (loop for (expr value) on clauses by #'cddr
309 collect `(is (,predicate ,expr ,value)))))))
311 (defmacro is-true (condition &rest reason-args)
312 "Like IS this check generates a pass if CONDITION returns true
313 and a failure if CONDITION returns false. Unlike IS this check
314 does not inspect CONDITION to determine how to report the
317 (add-result 'test-passed :test-expr ',condition)
319 :reason ,(if reason-args
320 `(format nil ,@reason-args)
321 `(format nil "~S did not return a true value" ',condition))
322 :test-expr ',condition)))
324 (defmacro is-false (condition &rest reason-args)
325 "Generates a pass if CONDITION returns false, generates a
326 failure otherwise. Like IS-TRUE, and unlike IS, IS-FALSE does
327 not inspect CONDITION to determine what reason to give it case
330 (with-gensyms (value)
331 `(let ((,value ,condition))
334 :reason ,(if reason-args
335 `(format nil ,@reason-args)
336 `(format nil "~S returned the value ~S, which is true" ',condition ,value ))
337 :test-expr ',condition)
338 (add-result 'test-passed :test-expr ',condition)))))
340 (defmacro signals (condition-spec
342 "Generates a pass if `BODY` signals a condition of type
343 `CONDITION`. `BODY` is evaluated in a block named `NIL`, `CONDITION`
345 (let ((block-name (gensym)))
346 (destructuring-bind (condition &optional reason-control reason-args)
347 (ensure-list condition-spec)
349 (handler-bind ((,condition (lambda (c)
351 ;; ok, body threw condition
352 (add-result 'test-passed
353 :test-expr ',condition)
354 (return-from ,block-name t))))
358 :reason ,(if reason-control
359 `(format nil ,reason-control ,@reason-args)
360 `(format nil "Failed to signal a ~S" ',condition))
361 :test-expr ',condition)
362 (return-from ,block-name nil)))))
364 (defmacro finishes (&body body)
365 "Generates a pass if BODY executes to normal completion.
367 In other words if body signals a condition (which is then handled),
368 return-froms or throws this test fails."
375 (add-result 'test-passed :test-expr ',body)
377 :reason (format nil "Test didn't finish")
378 :test-expr ',body)))))
380 (defmacro pass (&rest message-args)
382 `(add-result 'test-passed
383 :test-expr ',message-args
385 `(:reason (format nil ,@message-args)))))
387 (defmacro fail (&rest message-args)
390 :test-expr ',message-args
392 `(:reason (format nil ,@message-args)))))
394 (defmacro skip (&rest message-args)
395 "Generates a SKIP result."
397 (format *test-dribble* "s")
398 (add-result 'test-skipped :reason (format nil ,@message-args))))
400 ;; Copyright (c) 2002-2003, Edward Marco Baringer
401 ;; All rights reserved.
403 ;; Redistribution and use in source and binary forms, with or without
404 ;; modification, are permitted provided that the following conditions are
407 ;; - Redistributions of source code must retain the above copyright
408 ;; notice, this list of conditions and the following disclaimer.
410 ;; - Redistributions in binary form must reproduce the above copyright
411 ;; notice, this list of conditions and the following disclaimer in the
412 ;; documentation and/or other materials provided with the distribution.
414 ;; - Neither the name of Edward Marco Baringer, nor BESE, nor the names
415 ;; of its contributors may be used to endorse or promote products
416 ;; derived from this software without specific prior written permission.
418 ;; THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
419 ;; "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
420 ;; LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
421 ;; A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
422 ;; OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
423 ;; SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
424 ;; LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
425 ;; DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
426 ;; THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
427 ;; (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
428 ;; OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE