Source

missbehave / missbehave-matchers.scm

;; 
;; %%HEADER%%
;; 

(module missbehave-matchers
( expect-at-least-n-applications
  expect-at-most-n-applications
  expect-exactly-n-applications
  ignore-arguments 
  match-arguments
  make-call-matcher
  message-from-predicate-form
  be close-to any-of none-of
  list-including
  have-type
  match-string
  call
  raise
  make-error-matcher
  )

(import chicken scheme extras data-structures irregex ports)
(require-extension missbehave advice (only srfi-1 every))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Procedure-Expections
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(define (format-times n)
  (if (= n 1) "once" (sprintf "~A times" n))) 

(define ((expect-at-least-n-applications proc n) applications)
  (values
   (>= applications n)
   (sprintf "Expected ~A to be called at least ~A, but was called ~A" proc (format-times n) (format-times applications))))

(define ((expect-at-most-n-applications proc n) applications)
  (values
   (<= applications n)
   (sprintf "Expected ~A to be called at most ~A, but was called ~A" proc (format-times n) (format-times applications))))

(define ((expect-exactly-n-applications proc n) applications)
  (values
   (= applications n)
   (sprintf "Expected ~A to be called ~A, but was called ~A" proc (format-times n) (format-times applications))))

(define ((ignore-arguments) . _)
  (values  #t ""))

(define ((match-arguments proc . args) arguments)
  (values
   (equal? arguments args)
   (sprintf "Expected ~A to be called with ~A, but was called with ~A" proc args arguments)))


(define-syntax call
  (syntax-rules (once twice times time never with)
    ((_ proc (with arg arg+ ...))
     (make-call-matcher proc
                        (expect-at-least-n-applications (quote proc) 1)
                        (match-arguments (quote proc) arg arg+ ...)))

    ((_ proc (with arg arg+ ...) once)
     (make-call-matcher proc
                        (expect-exactly-n-applications (quote proc) 1)
                        (match-arguments (quote proc) arg arg+ ...)))
    ((_ proc (with arg arg+ ...) twice)
     (make-call-matcher proc
                        (expect-exactly-n-applications (quote proc) 2)
                        (match-arguments (quote proc) arg arg+ ...)))

    ((_ proc (with arg arg+ ...) never)
     (make-call-matcher proc
                        (expect-exactly-n-applications (quote proc) 0)
                        (match-arguments (quote proc) arg arg+ ...)))

    ((_ proc (with arg arg+ ...) (n time))
     (make-call-matcher proc
                        (expect-exactly-n-applications (quote proc) n)
                        (match-arguments (quote proc) arg arg+ ...)))

    ((_ proc (with arg arg+ ...) (n times))
     (make-call-matcher proc
                        (expect-exactly-n-applications (quote proc) n)
                        (match-arguments (quote proc) arg arg+ ...)))
    ((_ proc never)
     (call proc (0 times)))
    ((_ proc once)
     (call proc (1 time)))
    ((_ proc twice)
     (call proc (2 times)))
    ((_ proc (n times))
     (make-call-matcher proc
                        (expect-exactly-n-applications (quote proc) n)
                        (ignore-arguments)))
    ((_ proc (n time))
     (make-call-matcher proc
                        (expect-exactly-n-applications (quote proc) n)
                        (ignore-arguments)))))




(define (make-call-matcher  procedure application-count-matcher argument-matcher)
  (let* ((applications 0)
         (arguments (unspecified))
         (counting-advice (lambda (proc  args)
                            (set! applications (+ 1 applications))
                            (apply proc args)))
         (arguments-advice (lambda (proc  args)
                             (set! arguments args)
                             (apply proc args))))
    (make-matcher
     check: (lambda (subject)
              (dynamic-wind
                  (lambda ()
                    (advise 'around procedure counting-advice)
                    (advise 'around procedure arguments-advice))
                  (lambda ()
                    (force subject)
                    (and (application-count-matcher applications) (argument-matcher arguments)))
                  (lambda () (unadvise procedure))))
     
     failure-message-generator: (lambda (form subject negate)
                                  (receive (count-matched count-message) (application-count-matcher applications)
                                      (receive (arguments-matched argument-message) (argument-matcher arguments)
                                          (cond
                                           ((not count-matched)
                                            (if (not arguments-matched)
                                                 (sprintf "~A.  Additionally: ~A" count-message argument-message)
                                                 (sprintf "~A" count-message)))
                                           (else (sprintf "~A" argument-message)))))))))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Regex
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;




(define (match-string what #!key (with-matches #f))
  (let ((missmatch #f))
    (matcher
     (check (subject)
       (if with-matches
           (let ((matches (irregex-match (force subject) what)))
             (call-with-current-continuation
              (lambda (return)
                (for-each
                 (lambda (submatch)
                   (unless matches
                     (return #f))
                   (unless (and  (irregex-match-valid-index? matches (car submatch))
                                 (equal? (irregex-match-substring matches (car submatch)) (cdr submatch)))
                     (set! missmatch submatch)
                     
                     (return #f)))
                 with-matches)
                #t)))
           (irregex-match (force subject) what)))
     (message (form subject negate)
       (if with-matches
           (if missmatch
               (if negate
                   (sprintf "Expected ~A not to include submatch ~A when matched against ~S" form missmatch what)
                   (sprintf "Expected ~A to include submatch ~A when matched against ~S" form missmatch what))
               (if negate
                   (sprintf "Exepcted ~A not to match ~S" form what)
                   (sprintf "Expected ~A to match ~S" form what)))
           (if negate
               (sprintf "Expected ~A not to match ~S" form what)
               (sprintf "Expected ~A to match ~S" form what)))))))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Be
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(define (message-from-predicate-form form)
  (if (list? form)
      (let ((name (symbol->string (car form))))
        (with-output-to-string
          (lambda ()
            (display (string-translate name "-" " "))
            (display " ")
            (for-each (cut printf "~A " <>) (cdr form)))))
      form))

(define-syntax be
  (syntax-rules (a an true false)
    ((_ true)
     (be #t))
    ((_ false)
     (be #f))
    ((_ a type)
     (have-type type))
    ((_ an type)
     (have-type type))
    ((_ pred-or-value)
     (matcher
      (check (subject)
        (if (procedure? pred-or-value)
            (pred-or-value (force subject))
            (equal? pred-or-value (force subject))))
      (message (form subject negate)
        (if negate
            (if (procedure? pred-or-value)
                (sprintf "Expected ~S not to be ~A" (force subject) (message-from-predicate-form (quote pred-or-value)))
                (sprintf "Expected ~S not to be ~S" (force subject) pred-or-value))
            (if (procedure? pred-or-value)
                (sprintf "Expected ~S to be ~A" (force subject) (message-from-predicate-form (quote pred-or-value)))
                (sprintf "Expected ~S to be ~S" (force subject) pred-or-value))))))
    ((_ pred value more-values ...)
     (matcher
      (check (subject)
         (apply pred (list (force subject) value more-values ...)))
      (message (form subject negate)
        (with-output-to-string
          (lambda ()
            (if negate
                (printf "Expected ~S not to be ~S" (force subject) (quote pred))
                (printf "Expected ~S to be ~S" (force subject) (quote pred)))
            (for-each
             (lambda (val)
               (printf " ~S" val))
             (list value more-values ...)))))))))




(define-syntax have-type
  (lambda (form rename env)
    (let* ((type (cadr form))
           (type-pred (string->symbol (conc (symbol->string type) "?")))
           (%make-matcher (rename 'make-matcher)))
      `(,%make-matcher check: (lambda (subject)
                                (,type-pred (force subject)))
                       failure-message-generator: (lambda (form subject negate)
                                                    (if negate
                                                        (sprintf "Expected ~S to not be a ~A" (force subject) (quote ,type))
                                                        (sprintf "Expected ~S to be a ~A" (force subject) (quote ,type))))))))


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Be helpers
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(define ((close-to what #!key (delta 0.3)) actual)
  (<= (abs (- what actual)) delta))

(define ((any-of item . more-items) subject)
  (member subject (cons item more-items)))

(define ((none-of item . more-items) subject)
  (not (member subject (cons item more-items))))

(define ((list-including item . more-items) subject)
  (and (list? subject)
       (every (cut member <> subject) (cons item more-items))))

(define ((vector-including item . more-items) subject) #t)
(define ((hash-table-including item . more-items) subject) #t)

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Have/Has
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; raise
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(define-syntax raise
  (syntax-rules (error errors with)
    ((_ error)
     (make-error-matcher))
    ((_ errors)
     (make-error-matcher))
    ((_ (kind more-kinds ...))
     (make-error-matcher kinds: '(kind more-kinds ...)))))

(define (make-error-matcher #!key (kinds #f) (properties #f))
  (let ((message "")
        (negative-message ""))
    (make-matcher
     check: (lambda (code)
              (handle-exceptions exn
                  (let* ((condition (condition->list exn))
                         (exn-kinds (map car condition)))
                    (cond
                     ((and kinds properties) #t)
                     (kinds
                      (if (every (cut member <> exn-kinds) kinds)
                          #t
                          (begin
                            (set! message (sprintf "Expected exn of kinds ~A but got ~A" kinds exn-kinds))
                                        ;FIXME find proper wording
                            (set! negative-message (sprintf "Expected exn not of kinds ~A but got ~A" kinds exn-kinds))
                            #f)))
                     (properties #t)
                     (else
                      (set! message            (sprintf "Expecte errors but didn't get one"))
                      (set! negative-message   (sprintf "Expected no errors but got one"))
                      #t)))
                (force code)
                #f))
     failure-message-generator: (lambda (form subject negate)
                                  (if negate
                                      negative-message
                                      message)))))

)