changelog shortlog graph tags branches changeset files revisions annotate raw help

Mercurial > core / lisp/lib/rt/pkg.lisp

changeset 96: 301fd45bbe73
child: ac7ef5ddcaab
author: ellis <ellis@rwest.io>
date: Wed, 13 Dec 2023 20:02:36 -0500
permissions: -rw-r--r--
description: big refactor of lisp code
1 ;;; rt.lisp --- regression testing
2 
3 ;; Regression Testing framework. inspired by PCL, the original CMUCL
4 ;; code, and the SBCL port.
5 
6 ;;; Commentary:
7 
8 ;; - :rt https://www.merl.com/publications/docs/TR91-04.pdf Chapter 1
9 ;; - :com.gigamonkeys.test https://github.com/gigamonkey/monkeylib-test-framework
10 ;; - :sb-rt https://github.com/sbcl/sbcl/blob/master/contrib/sb-rt/rt.lisp
11 
12 ;; This package is intended to provide a modernized Lisp testing
13 ;; library with features found in some of the test frameworks listed
14 ;; below.
15 
16 ;; - :it.bese.fiveam https://github.com/lispci/fiveam
17 ;; - :try https://github.com/melisgl/try
18 ;; - :rove https://github.com/fukamachi/rove
19 
20 ;;; TODO:
21 #|
22 
23 - [ ] benchmark support: do-bench, test-count,
24 
25 - [ ] fixtures api
26 
27 - [ ] profiling
28 |#
29 ;;; Code:
30 #+x86-64
31 (eval-when (:compile-toplevel :load-toplevel :execute)
32  (require 'sb-sprof)
33  (require 'sb-cover))
34 
35 (defpackage :std/rt
36  (:use
37  :cl :std :sxp :log
38  :sb-aprof #+x86-64 :sb-sprof)
39  (:import-from :sb-cover :store-coverage-data)
40  (:nicknames :rt)
41  (:export
42  :*default-test-opts*
43  :*compile-tests*
44  :*coverage-directory*
45  :*catch-test-errors*
46  :*test-suffix*
47  :*default-test-suite-name*
48  :*test-suite*
49  :*test-suite-list*
50  ;; TODO 2023-09-04: :*test-profiler-list* not yet
51  :*testing*
52  :test-suite-designator
53  :check-suite-designator
54  :make-test
55  :make-suite
56  :test-name=
57  :do-test
58  :do-tests
59  :reset-tests
60  :continue-testing
61  :with-test-env
62  :ensure-suite
63  :test-fixture
64  :fixture-prototype
65  :make-fixture-prototype
66  :make-fixture
67  :with-fixture
68  :test-result
69  :test-pass-p
70  :test-fail-p
71  :test-skip-p
72  :test-failed
73  :fail!
74  :is
75  :signals
76  :deftest
77  :defsuite
78  :in-suite
79  :eval-test
80  :compile-test
81  :locked-tests
82  :push-test
83  :pop-test
84  :delete-test
85  :find-test
86  :do-suite
87  :test-object
88  :test
89  :test-fixture
90  :test-suite
91  :test-name
92  :tests
93  :test-form
94  :test-results
95  :enable-coverage
96  :disable-coverage
97  :with-coverage
98  :cover-report))
99 
100 (in-package :rt)
101 (in-readtable :std)
102 
103 ;;; Vars
104 (defvar *default-test-opts* '(optimize sb-c::instrument-consing (debug 3)))
105 (defvar *compile-tests* t
106  "When nil do not compile tests. With a value of t, tests are compiled
107 with default optimizations else the value is used to configure
108 compiler optimizations.")
109 (defvar *coverage-directory* #P"/tmp/rt/")
110 (defvar *catch-test-errors* t "When non-nil, cause errors in a test to be caught.")
111 (defvar *test-suffix* "-test" "A suffix to append to every `test' defined with `deftest'.")
112 (defvar *test-suite-list* nil "List of available `test-suite' objects.")
113 (defvar *test-suite* nil "A 'test-suite-designator' which identifies the current `test-suite'.")
114 (eval-when (:compile-toplevel :load-toplevel :execute)
115  (defvar *default-test-suite-name* "default"))
116 (declaim (type (or stream boolean string) *test-input*))
117 (defvar *test-input* nil "When non-nil, specifies an input stream or buffer for `*testing*'.")
118 (defvar *default-bench-count* 100 "Default number of iterations to repeat a bench test for. This value is
119 used when the slot value of :BENCH is t.")
120 (defvar *testing* nil "Testing state var.")
121 
122 ;;; Utils
123 (eval-when (:compile-toplevel :load-toplevel :execute)
124  (defun make-test (&rest slots)
125  (apply #'make-instance 'test slots))
126  (defun make-suite (&rest slots)
127  (apply #'make-instance 'test-suite slots)))
128 
129 ;; TODO 2023-09-04: optimize
130 (declaim (inline do-tests))
131 (defun do-tests (&optional (suite *test-suite*) (output *standard-output*))
132  (if (pathnamep output)
133  (with-open-file (stream output :direction :output)
134  (do-suite (ensure-suite suite) :stream stream))
135  (do-suite (ensure-suite suite) :stream output)))
136 
137 (defun reset-tests ()
138  (setq *testing* nil
139  *test-suite* nil
140  *test-suite-list* nil
141  *test-input* nil))
142 
143 ;; this assumes that *test-suite* is re-initialized correctly to the
144 ;; correct test-suite object.
145 (defun continue-testing ()
146  (if-let ((test *testing*))
147  (throw '%in-test test)
148  (do-suite *test-suite*)))
149 
150 ;; NOTE 2023-09-01: `pushnew' does not return an indication of whether
151 ;; place is changed - it returns place. This is functionally sound but
152 ;; means that if we want to do something else in the event that place
153 ;; is unchanged, we run into some friction,
154 ;; https://stackoverflow.com/questions/56228832/adapting-common-lisp-pushnew-to-return-success-failure
155 (defun spush (item lst &key (test #'equal))
156  "Substituting `push'"
157  (declare (type function test))
158  (cond
159  ((null lst) (push item lst))
160  ((list lst)
161  (if-let ((found (member item lst
162  :test test)))
163  (progn
164  (rplaca found item)
165  lst)
166  (push item lst)))
167  #|(or nil '(t (cons item lst)))|#))
168 
169 ;; FIX 2023-08-31: spush, replace with `add-test' method.
170 ;; (declaim (inline normalize-test-name))
171 (defun normalize-test-name (a)
172  "Return the normalized `test-suite-designator' of A."
173  (etypecase a
174  (string a)
175  (symbol (string-downcase (symbol-name a)))
176  (test-object (test-name a))
177  (t (format nil "~A" a))))
178 
179 (defun test-name= (a b)
180  "Return t if A and B are similar `test-suite-designator's."
181  (let ((a (normalize-test-name a))
182  (b (normalize-test-name b)))
183  (string= a b)))
184 
185 ;; (declaim (inline assert-suite ensure-suite))
186 (defun ensure-suite (name)
187  (if-let ((ok (member name *test-suite-list* :test #'test-name=)))
188  (car ok)
189  (when (or (eq name t) (null name)) (make-suite :name *default-test-suite-name*))))
190 
191 (defun check-suite-designator (suite) (check-type suite test-suite-designator))
192 
193 (defun assert-suite (name)
194  (check-suite-designator name)
195  (assert (ensure-suite name)))
196 
197 (declaim (inline test-opt-key-p test-opt-valid-p))
198 (defun test-opt-key-p (k)
199  "Test if K is a `test-opt-key'."
200  (member k '(:profile :save :stream)))
201 
202 (defun test-opt-valid-p (f)
203  "Test if F is a valid `test-opt' form. If so, return F else nil."
204  (when (test-opt-key-p (car f))
205  f))
206 
207 ;;; Conditions
208 (define-condition test-failed (error)
209  ((reason :accessor fail-reason :initarg :reason :initform "unknown")
210  (name :accessor fail-name :initarg :name)
211  (form :accessor fail-form :initarg :form))
212  (:documentation "Signaled when a test fails.")
213  (:report (lambda (c s)
214  (format s "The following expression failed: ~S~%~A."
215  (fail-form c)
216  (fail-reason c)))))
217 
218 ;;; Protocol
219 (defgeneric eval-test (self)
220  (:documentation "Eval a `test'."))
221 
222 (defgeneric compile-test (self &key &allow-other-keys)
223  (:documentation "Compile a `test'."))
224 
225 (defgeneric locked-tests (self)
226  (:documentation "Return a list of locked tests in `test-suite' object SELF."))
227 
228 (defgeneric push-test (self place)
229  (:documentation
230  "Push `test' SELF to the value of slot ':tests' in `test-suite' object PLACE."))
231 
232 (defgeneric pop-test (self)
233  (:documentation
234  "Pop the first `test' from the slot-value of ':tests' in `test-suite' object SELF."))
235 
236 (defgeneric push-result (self place)
237  (:documentation
238  "Push object SELF to the value of slot ':results' in object PLACE."))
239 
240 (defgeneric pop-result (self)
241  (:documentation
242  "Pop the first `test-result' from the slot-value of ':tests' from object SELF."))
243 
244 (defgeneric push-fixture (self place)
245  (:documentation
246  "Push object SELF to the value of slot ':results' in object PLACE."))
247 
248 (defgeneric delete-test (self &key &allow-other-keys)
249  (:documentation "Delete `test' object specified by `test-object' SELF and optional keys."))
250 
251 (defgeneric find-test (self name &key &allow-other-keys)
252  (:documentation "Find `test' object specified by name and optional keys."))
253 
254 (defgeneric do-test (self &optional test)
255  (:documentation
256  "Run `test' SELF, printing results to `*standard-output*'. The second
257 argument is an optional fixture.
258 
259 SELF can also be a `test-suite', in which case the TESTS slot is
260 queried for the value of TEST. If TEST is not provided, pops the car
261 from TESTS."))
262 
263 (defgeneric do-suite (self &key &allow-other-keys)
264  (:documentation
265  "Perform actions on `test-suite' object SELF with optional keys."))
266 
267 ;;;; Results
268 (deftype result-tag ()
269  '(or (member :pass :fail :skip) null))
270 
271 (declaim (inline %make-test-result))
272 (defstruct (test-result (:constructor %make-test-result)
273  (:conc-name tr-))
274  (tag nil :type result-tag :read-only t)
275  (form nil :type form))
276 
277 (defun make-test-result (tag &optional form)
278  (%make-test-result :tag tag :form form))
279 
280 (defmethod test-pass-p ((res test-result))
281  (when (eq :pass (tr-tag res)) t))
282 
283 (defmethod test-fail-p ((res test-result))
284  (when (eq :fail (tr-tag res)) t))
285 
286 (defmethod test-skip-p ((res test-result))
287  (when (eq :skip (tr-tag res)) t))
288 
289 (defmethod print-object ((self test-result) stream)
290  (print-unreadable-object (self stream)
291  (format stream "~A ~A"
292  (tr-tag self)
293  (tr-form self))))
294 
295 ;;; Objects
296 (defclass test-object ()
297  ((name :initarg :name :initform (required-argument) :type string :accessor test-name)
298  #+nil (cached :initarg :cache :allocation :class :accessor test-cached-p :type boolean))
299  (:documentation "Super class for all test-related objects."))
300 
301 (defmethod print-object ((self test-object) stream)
302  "test"
303  (print-unreadable-object (self stream :type t :identity t)
304  (format stream "~A"
305  (test-name self))))
306 
307 ;;;; Tests
308 ;; HACK 2023-08-31: inherit sxp?
309 
310 (defclass test (test-object)
311  ((fn :type symbol :accessor test-fn)
312  (bench :type (or boolean fixnum) :accessor test-bench :initform nil :initarg :bench)
313  (profile :type list :accessor test-profile :initform nil :initarg :profile)
314  (args :type list :accessor test-args :initform nil :initarg :args)
315  (decl :type list :accessor test-decl :initform nil :initarg :decl)
316  (form :initarg :form :initform nil :type function-lambda-expression :accessor test-form)
317  (doc :initarg :doc :type string :accessor test-doc)
318  (lock :initarg :lock :type boolean :accessor test-lock-p)
319  (persist :initarg :persist :initform nil :type boolean :accessor test-persist-p)
320  (results :initarg :results :type (array test-result) :accessor test-results))
321  (:documentation "Test class typically made with `deftest'."))
322 
323 (defmethod test-bench-p ((self test))
324  (when (test-bench self) t))
325 
326 (defmethod get-bench-count ((self test))
327  (when-let ((v (test-bench self)))
328  (cond
329  ((typep v 'fixnum) v)
330  ((eq v t) *default-bench-count*)
331  ;; unknown value
332  (t nil))))
333 
334 (defmethod initialize-instance ((self test) &key name)
335  ;; (debug! "building test" name)
336  (setf (test-fn self)
337  (make-symbol
338  (format nil "~A~A"
339  name
340  (gensym *test-suffix*))))
341  (setf (test-lock-p self) t)
342  ;; TODO 2023-09-21: we should count how many checks are in the :form
343  ;; slot and infer the array dimensions.
344  (setf (test-results self) (make-array 0 :element-type 'test-result))
345  (call-next-method))
346 
347 (defmethod print-object ((self test) stream)
348  (print-unreadable-object (self stream :type t :identity t)
349  (format stream "~A :fn ~A :args ~A :persist ~A"
350  (test-name self)
351  (test-fn self)
352  (test-args self)
353  (test-persist-p self))))
354 
355 ;; TODO 2023-09-01: use sxp?
356 ;; (defun validate-form (form))
357 
358 (defmethod push-result ((self test-result) (place test))
359  (with-slots (results) place
360  (push self results)))
361 
362 (defmethod pop-result ((self test))
363  (pop (test-results self)))
364 
365 (defmethod eval-test ((self test))
366  `(progn ,@(test-form self)))
367 
368 (defmethod compile-test ((self test) &key declare &allow-other-keys)
369  (compile
370  (test-fn self)
371  `(lambda ()
372  ,@(when declare `((declare ,declare)))
373  ,@(test-form self))))
374 
375 (defun fail! (form &optional fmt &rest args)
376  (let ((reason (and fmt (apply #'format nil fmt args))))
377  (with-simple-restart (ignore-fail "Continue testing.")
378  (error 'test-failed :reason reason :form form))))
379 
380 (defmacro with-test-env (self &body body)
381  `(catch '%in-test
382  (setf (test-lock-p ,self) t)
383  (let* ((*testing* ,self)
384  (bail nil)
385  r)
386  (block bail
387  ,@body
388  (setf (test-lock-p ,self) bail))
389  r)))
390 
391 (defmethod do-test ((self test) &optional fx)
392  (declare (ignorable fx))
393  (with-test-env self
394  (debug! "running test: " *testing*)
395  (flet ((%do ()
396  (if-let ((opt *compile-tests*))
397  ;; RESEARCH 2023-08-31: with-compilation-unit?
398  (progn
399  (if (eq opt t)
400  (setq opt *default-test-opts*)
401  (setq opt (push *default-test-opts* opt)))
402  ;; TODO 2023-09-21: handle failures here
403  (funcall (compile-test self :declare opt))
404  (setf r (make-test-result :pass (test-fn self))))
405  (progn
406  (eval-test self)
407  (setf r (make-test-result :pass (test-name self)))))))
408  (if *catch-test-errors*
409  (handler-bind
410  ((style-warning #'muffle-warning)
411  (error
412  #'(lambda (c)
413  (setf bail t)
414  (setf r (make-test-result :fail c))
415  (return-from bail r))))
416  (%do))
417  (%do)))))
418 
419 (defmacro bench (iter &body body)
420  `(loop for i from 1 to ,iter
421  do ,@body))
422 
423 (defmethod do-bench ((self test) &optional fx)
424  (declare (ignorable fx))
425  (with-test-env self
426  (flet ((%do ()
427  (if-let ((opt *compile-tests*))
428  (progn
429  (when (eq opt t) (setq opt *default-test-opts*))
430  ;; TODO 2023-09-21: handle failures here
431  (let ((fn (compile-test self :declare opt)))
432  (bench (test-bench self) (funcall fn)))
433  (setf r (make-test-result :pass (test-fn self))))
434  (progn
435  (bench (test-bench self) (eval-test self))
436  (setf r (make-test-result :pass (test-name self)))))))
437  (if *catch-test-errors*
438  (handler-bind
439  ((style-warning #'muffle-warning)
440  (error
441  #'(lambda (c)
442  (setf bail t)
443  (setf r (make-test-result :fail c))
444  (return-from bail r))))
445  (%do))
446  (%do)))))
447 
448 ;;;; Fixtures
449 
450 ;; Our fixtures are just closures - with a pandoric environment. You
451 ;; might call it a domain-specific object protocol.
452 
453 ;; You can build fixtures inside a test or use the push-fixture
454 ;; method on a `test-suite' object.
455 
456 (deftype fixture () 'form)
457 
458 (declaim (inline %make-fixture-prototype))
459 (defstruct (fixture-prototype (:constructor %make-fixture-prototype)
460  (:conc-name fxp))
461  (kind :empty :type keyword)
462  (form nil :type form))
463 
464 (defun make-fixture-prototype (kind form)
465  (%make-fixture-prototype :kind kind :form form))
466 
467 (defmacro make-fixture (letargs &body ds)
468  (let ((letargs (let-binding-transform letargs)))
469  `(let (,@letargs)
470  (dlambda ,@ds))))
471 
472 (defmacro with-fixture ((var fx) &body body)
473  `(let ((,var ,fx))
474  ,@body))
475 
476 ;;;; Suites
477 (defclass test-suite (test-object)
478  ((tests :initarg :set :initform nil :type list :accessor tests
479  :documentation "test-suite tests")
480  (results :initarg :results :initform nil :type list :accessor test-results
481  :documentation "test-suite results")
482  (stream :initarg :stream :initform *standard-output* :type stream :accessor test-stream)
483  (fixtures :initarg :fixtures :initform nil :type list :accessor test-fixtures))
484  (:documentation "A class for collections of related `test' objects."))
485 
486 (defmethod print-object ((self test-suite) stream)
487  (print-unreadable-object (self stream :type t :identity t)
488  (format stream "~A [~d+~d:~d:~d:~d]"
489  (test-name self)
490  (count t (map-tests self (lambda (x) (not (test-bench-p x)))))
491  (count t (map-tests self #'test-bench-p))
492  (count t (map-tests self #'test-lock-p))
493  (count t (map-tests self #'test-persist-p))
494  (length (test-results self)))))
495 
496 ;; (defmethod reinitialize-instance ((self test-suite) &rest initargs &key &allow-other-keys))
497 
498 (deftype test-suite-designator ()
499  "Either nil, a symbol, a string, or a `test-suite' object."
500  '(or null symbol string test-suite test keyword))
501 
502 (defmethod map-tests ((self test-suite) function)
503  (mapcar function (tests self)))
504 
505 (defmethod push-test ((self test) (place test-suite))
506  (push self (tests place)))
507 
508 (defmethod pop-test ((self test-suite))
509  (pop (tests self)))
510 
511 (defmethod push-result ((self test-result) (place test-suite))
512  (with-slots (results) place
513  (push self results)))
514 
515 (defmethod pop-result ((self test-suite))
516  (pop (test-results self)))
517 
518 (defmethod find-test ((self test-suite) name &key (test #'test-name=))
519  (declare (type (or string symbol) name)
520  (type function test))
521  (find name (the list (tests self)) :test test))
522 
523 (defmethod do-test ((self test-suite) &optional test)
524  (push-result
525  (if test
526  (do-test (find-test self (test-name test)))
527  (do-test (pop-test self)))
528  self))
529 
530 ;; HACK 2023-09-01: find better method of declaring failures from
531 ;; within the body of `deftest'.
532 (defmethod do-suite ((self test-suite) &key stream)
533  (when stream (setf (test-stream self) stream))
534  (with-slots (name stream) self
535  (format stream "in suite ~x with ~A/~A tests:~%"
536  name
537  (count t (tests self)
538  :key (lambda (x) (or (test-lock-p x) (test-persist-p x))))
539  (length (tests self)))
540  ;; loop over each test, calling `do-test' if locked or persistent
541  (map-tests self
542  (lambda (x)
543  (when (or (test-lock-p x) (test-persist-p x))
544  (let ((res (do-test x)))
545  (push-result res self)
546  (format stream "~@[~<~%~:;~:@(~S~) ~>~]~%" res)))))
547  ;; compare locked vs expected
548  (let ((locked (remove-if #'null (map-tests self (lambda (x) (when (test-lock-p x) x)))))
549  (fails
550  ;; collect if locked test not expected
551  (loop for r in (test-results self)
552  unless (test-pass-p r)
553  collect r)))
554  (if (null locked)
555  (format stream "~&No tests failed.~%")
556  (progn
557  ;; RESEARCH 2023-09-04: print fails ??
558  (format stream "~&~A out of ~A ~
559  total tests failed: ~
560  ~:@(~{~<~% ~1:;~S~>~
561  ~^, ~}~)."
562  (length locked)
563  (length (tests self))
564  locked)
565  (unless (null fails)
566  (format stream "~&~A unexpected failures: ~
567  ~:@(~{~<~% ~1:;~S~>~
568  ~^, ~}~)."
569  (length fails)
570  fails))))
571  ;; close stream
572  (finish-output stream)
573  ;; return values (PASS? LOCKED)
574  (values (not fails) locked))))
575 
576 ;;; Checks
577 (flet ((%test (val form)
578  (let ((r
579  (if val
580  (make-test-result :pass form)
581  (make-test-result :fail form))))
582  (debug! r)
583  r)))
584  (defmacro is (test &rest args)
585  "The DWIM Check.
586 
587 (is (= 1 1) :test 100) ;=> #S(TEST-RESULT :TAG :PASS :FORM (= 1 1))
588 If TEST returns a truthy value, return a PASS test-result, else return
589 a FAIL. The TEST is parameterized by ARGS which is a plist or nil.
590 
591 If ARGS is nil, TEST is bound to to the RESULT slot of the test-result
592 and evaluated 'as-is'.
593 
594 (nyi!)
595 ARGS may contain the following keywords followed by a corresponding
596 value:
597 
598 :EXPECTED
599 
600 :TIMEOUT
601 
602 :THEN
603 
604 All other values are treated as let bindings.
605 "
606  (with-gensyms (form)
607  `(if ,(null args)
608  (if *testing*
609  (push-result (funcall ,#'%test ,test ',test) *testing*)
610  (funcall ,#'%test ,test ',test))
611  (macrolet ((,form (test) `(let ,,(group args 2) ,,test)))
612  ;; TODO 2023-09-21: does this work...
613  (if *testing*
614  (push-result (funcall ,#'%test (,form ,test) ',test) *testing*)
615  (funcall ,#'%test (,form ,test) ',test)))))))
616 
617 (defmacro signals (condition-spec &body body)
618  "Generates a passing TEST-RESULT if body signals a condition of type
619 CONDITION-SPEC. BODY is evaluated in a block named NIL, CONDITION-SPEC
620 is not evaluated."
621  (let ((block-name (gensym)))
622  (destructuring-bind (condition &optional reason-control &rest reason-args)
623  (ensure-list condition-spec)
624  `(block ,block-name
625  (handler-bind ((,condition (lambda (c)
626  ;; ok, body threw condition
627  ;; TODO 2023-09-05: result collectors
628  ;; (add-result 'test-passed
629  ;; :test-expr ',condition)
630  (return-from ,block-name (make-test-result :pass ',body)))))
631  (block nil
632  ,@body))
633  (fail!
634  ',condition
635  ,@(if reason-control
636  `(,reason-control ,@reason-args)
637  `("Failed to signal a ~S" ',condition)))
638  (return-from ,block-name nil)))))
639 
640 ;;; Macros
641 (defmacro deftest (name props &body body)
642  "Build a test with NAME, parameterized by LAMBDA-LIST and with a test form of BODY."
643  (destructuring-bind (pr doc dec fn)
644  (multiple-value-bind (forms dec doc)
645  ;; parse body with docstring allowed
646  (sb-int:parse-body
647  (or body) t)
648  `(,props ',doc ',dec ',forms))
649  ;; TODO 2023-09-21: parse plist
650  `(let ((obj (make-test
651  :name ',(format nil "~A" name)
652  ;; note: we could leave these unbound if we want,
653  ;; personal preference
654  :form ,fn
655  ,@(when-let ((v (getf pr :persist))) `(:persist ,v))
656  ,@(when-let ((v (getf pr :args))) `(:args ,v))
657  ,@(when-let ((v (getf pr :bench))) `(:bench ,v))
658  ,@(when-let ((v (getf pr :profile))) `(:profile ,v))
659  ,@(when doc `(:doc ,doc))
660  ,@(when dec `(:decl ,dec)))))
661  (push-test obj *test-suite*)
662  obj)))
663 
664 (defmacro defsuite (suite-name &rest props)
665  "Define a `test-suite' with provided keys. The object returned can be
666 enabled using the `in-suite' macro, similiar to the `defpackage' API."
667  (check-type suite-name (or symbol string))
668  `(eval-when (:compile-toplevel :load-toplevel :execute)
669  (let ((obj (make-suite
670  :name (format nil "~A" ',suite-name)
671  ,@(when-let ((v (getf props :stream))) `(:stream ,v)))))
672  (setq *test-suite-list* (spush obj *test-suite-list* :test #'test-name=))
673  obj)))
674 
675 (defmacro in-suite (name)
676  "Set `*test-suite*' to the `test-suite' referred to by symbol
677 NAME. Return the `test-suite'."
678  (assert-suite name)
679  `(progn
680  (setq *test-suite* (ensure-suite ,name))))
681 
682 ;;; Coverage
683 (defmacro enable-coverage ()
684  `(declaim (optimize store-coverage-data)))
685 
686 (defun disable-coverage ()
687  `(declaim (optimize (sb-cover:store-coverage-data 0))))
688 
689 (defmacro with-coverage (&body body)
690  `(progn
691  (enable-coverage)
692  ,@body
693  (disable-coverage)))
694 
695 (defun coverage-report ()
696  "Generate a coverage report."
697  (sb-cover:report *coverage-directory*))
698 
699 (provide :rt)