aboutsummaryrefslogtreecommitdiff
path: root/testiere.lisp
blob: c7b8934aa5287b7e975c124f0dee82b081574f4b (plain)
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
;;;; testiere.lisp

(in-package #:testiere)

(defun stub-names (stubs) (mapcar 'first stubs))

(defun build-test (name spec)
  (match spec
    ((list :fails inputs)
     `(assert
       (handler-case (progn (,name ,@inputs) nil)
         (error (e) (declare (ignore e)) t))))

    ((list :signals inputs condition)
     `(assert
       (handler-case (progn (,name ,@inputs) nil)
         (,condition (c) (declare (ignore c)) t)
         (condition (c) (declare (ignore c)) nil))))

    ((list* :with-stubs redefs more-specs)
     (let* ((assoc-vars
              (loop for (stub-name . more) in redefs
                    collect (list stub-name (gensym (symbol-name stub-name)))))
            (cache-binding-forms
              (loop for (stub-name tmp-var) in assoc-vars
                    collect `(,tmp-var (fdefinition ',stub-name))))
            (redef-forms
              (loop for (stub-name lambda-list . body) in redefs
                    collect `(setf (fdefinition ',stub-name)
                                   (function (lambda ,lambda-list ,@body)))))
            (clean-up
              (loop for (stub-name . more) in redefs
                    for binding = (assoc stub-name assoc-vars)
                    collect `(setf (fdefinition ',stub-name) ,(second binding)))))
       `(let ,cache-binding-forms
          (unwind-protect
               (progn
                 ,@redef-forms
                 ,@(mapcar (lambda (s) (build-test name s)) more-specs))
            ,@clean-up))))
    ((list* :with-bindings bindings more-specs)
     `(let ,bindings
        ,@(mapcar (lambda (s) (build-test name s)) more-specs)))
    ((list :afterp inputs thunk-test)
     `(progn (,name ,@inputs)
             (assert (funcall ,thunk-test))))
    ((list :outputp inputs output-test)
     `(assert (funcall ,output-test (,name ,@inputs))))
    ((list comparator-function inputs expected-output)
     `(assert (,comparator-function (,name ,@inputs) ,expected-output)))))

(defun extract-tests (name body)
  (let ((end-pos (position :end body))
        (start-pos (position :tests body)))
    (when (and end-pos start-pos)
      (let ((specs (subseq body (1+  start-pos) end-pos))
            (before (subseq body 0 start-pos))
            (after (nthcdr (1+ end-pos) body)))
        (list (mapcar (lambda (spec) (build-test name spec)) specs)
              (append before after))))))

(defmacro defun+ (name lambda-list &body body)
  "Like regular DEFUN, but with embedded unit tests. If those tests
  would fail, the function fails to be defined. "
  (destructuring-bind (tests function-body) (extract-tests name body)
    `(labels ((,name ,lambda-list ,@function-body))
       (handler-case
           (progn ,@tests
                  (defun ,name ,lambda-list ,@function-body))
         (error (e)
           (format t "~a~%Not defining ~a" e ',name))))))

(defun parse-defmethod (args)
  (match args
    ((guard (list* qualifier lambda-list body)
            (and (not (listp qualifier))
                 (listp lambda-list)))
     (list (list  qualifier lambda-list) body))
    ((list* lambda-list body)
     (list (list lambda-list) body))

    (_ (error "Malformed DEFMETHOD: ~a " args))))

(defmacro defmethod+ (name &rest args)
  "Like regular DEFMETHOD, but with embedded unit tests. If those
  test would fail, the method fails to be defined. "
  (destructuring-bind (qual-and-lambda-list body) (parse-defmethod args)
    (destructuring-bind  (tests function-body) (extract-tests name body)
      `(progn
         (defmethod ,name ,@qual-and-lambda-list ,@function-body)
         ,@tests))))