-
Notifications
You must be signed in to change notification settings - Fork 4
/
should-test.lisp
executable file
·213 lines (185 loc) · 7.58 KB
/
should-test.lisp
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
;;;;; SHOULD-TEST core: package definition and main functions
;;;;; (c) 2013-2018 Vsevolod Dyomkin
(cl:defpackage #:should-test
(:nicknames #:st)
(:use #:common-lisp #:rtl #:local-time)
(:export #:deftest
#:should
#:should-check
#:should-checked
#:should-erred
#:should-failed
#:should-format
#:should-test-error
#:should-test-redefinition-warning
#:test
#:undeftest
#:*test-output*
#:*verbose*
#:*xml-output*
#:test-for-xunit))
(in-package #:should-test)
(named-readtables:in-readtable rutils-readtable)
(defvar *test-output* *standard-output*
"Stream to print test results.")
(defparameter *verbose* t)
(define-condition should-test-error (simple-error) ())
(define-condition should-checked ()
((rez :initarg :rez :reader should-checked-rez)))
(define-condition should-failed (should-checked) ())
(define-condition should-erred (should-checked) ())
(define-condition should-test-redefinition-warning (style-warning)
((name :initarg :name))
(:report (lambda (c stream)
(format stream "Redefining test: ~A" (slot-value c 'name)))))
(defmacro deftest (name () &body body)
"Define a NAMEd test which is a function
that treats each form in its BODY as an assertion to be checked
and prints some information to the output.
The result of this function is a boolean indicating
if any of the assertions has failed.
In case of failure second value is a list of failure descriptions,
returned from assertions,
and the third value is a list of uncaught errors if any."
(with-gensyms (failed erred)
`(progn
(when (get ',name 'test)
(warn 'should-test-redefinition-warning :name ',name))
(setf (get ',name 'test)
(lambda ()
(format *test-output* "Test ~A: " ',name)
(let (,failed ,erred)
(handler-bind
((should-failed #`(push (should-checked-rez %) ,failed))
(should-erred #`(push (should-checked-rez %) ,erred)))
,@body)
(if (or ,failed ,erred)
(progn
(format *test-output* " FAILED~%")
(values nil
,failed
,erred))
(progn
(format *test-output* " OK~%")
t))))))))
(defun undeftest (name)
"Remove test from symbol NAME."
(when (get name 'test)
(not (void (get name 'test)))))
(defun test (&key (package *package*) test failed)
"Run a scpecific TEST or all tests defined in PACKAGE (defaults to current).
Returns T if all tests pass or 3 values:
- NIL
- a hash-table of failed tests with their failed assertions' lists
- a hash-table of tests that have signalled uncaught errors with these errors
If FAILED is set reruns only tests failed at last run."
(if test
(if-it (get test 'test)
(funcall it)
(error 'should-test-error
:format-control (fmt "No test defined for ~A" test)))
(let ((failures (make-hash-table))
(errors (make-hash-table)))
(dolist (sym (package-internal-symbols package))
(when-it (and (or (not failed)
(get sym 'test-failed))
(get sym 'test))
(mv-bind (success? failed erred) (funcall it)
(if success?
(setf (get sym 'test-failed) nil)
(progn
(setf (get sym 'test-failed) t)
(when failed
(set# sym failures failed))
(when erred
(set# sym errors erred)))))))
(or (zerop (+ (hash-table-count failures)
(hash-table-count errors)))
(values nil
failures
errors)))))
(defmacro should (key test &rest expected-and-testee)
"Define an individual test from:
- a comparison TEST
- EXPECTED values
- an operation that needs to be tested (TESTEE)
KEY is used to determine, which kind of results processing is needed
(implemented by generic function SHOULD-CHECK methods).
The simplest key is BE that just checks for equality.
Another pre-defined key is SIGNAL, which intercepts conditions."
(with-gensyms (success? failed e)
(mv-bind (expected operation) (butlast2 expected-and-testee)
`(handler-case
(mv-bind (,success? ,failed)
(should-check ,(mkeyw key) ',test
(lambda () ,operation) ,@expected)
(or (when ,success?
(signal 'should-checked)
t)
(when *verbose*
(format *test-output*
"~&~A FAIL~%expect:~{ ~A~}~%actual:~{ ~A~}~%"
',operation
(if ',expected
(mapcar #'should-format (list ,@expected))
(list (should-format ',test)))
(mklist (should-format ,failed))))
(signal 'should-failed :rez ,failed)
(values nil
(list ',operation ',expected ,failed))))
(error (,e)
(when *verbose*
(format *test-output* "~&~A FAIL~%error: ~A~%"
',operation (should-format ,e)))
(signal 'should-erred :rez ,e))))))
(defgeneric should-check (key test fn &rest expected)
(:documentation
"Specific processing for SHOULD based on KEY.
FN's output values are matched to EXPECTED values (if they are given).
Up to 2 values are returned:
- if the test passed (T or NIL)
- in case of failure - actual result"))
(defmethod should-check ((key (eql :be)) test fn &rest expected)
(let ((rez (multiple-value-list (funcall fn))))
(or (if expected
(and (>= (length rez) (length expected))
(every test rez (mklist expected)))
(every test rez))
(values nil
rez))))
(defmethod should-check ((key (eql :signal)) test fn &rest expected)
(declare (ignore expected))
(handler-case (progn (funcall fn)
(values nil
nil))
(condition (c)
(or (eql (mkeyw test) (mkeyw (class-name (class-of c))))
(values nil
c)))))
(defmethod should-check ((key (eql :print-to)) stream-sym fn &rest expected)
(let ((original-value (symbol-value stream-sym)))
(unwind-protect
(progn (setf (symbol-value stream-sym)
(make-string-output-stream))
(funcall fn)
(let ((rez (get-output-stream-string (symbol-value stream-sym))))
(or (string= (first expected) rez)
(values nil
rez))))
(setf (symbol-value stream-sym) original-value))))
(defgeneric should-format (obj)
(:documentation "Format appropriately for test output.")
(:method :around (obj)
(let ((*print-length* 3)) (call-next-method)))
(:method (obj)
(handler-case (fmt "~S" obj)
(error () (fmt "~A" obj))))
(:method ((obj hash-table))
(with-output-to-string (out) (print-ht obj out)))
(:method ((obj list))
(cond ((null obj)
(fmt "NIL"))
((listp (cdr obj))
(mapcar #'should-format obj))
(t (fmt "(~A . ~A)"
(should-format (car obj)) (should-format (cdr obj)))))))