teachpacks/testing.rkt
#lang racket

(require racket/gui/dynamic
         "../private/planet.rkt"
         (cce scheme)
         (for-syntax (cce scheme))
         (prefix-in htdp: lang/private/teachprims)
         (prefix-in engine:
                    (combine-in (lib "racket-tests.rkt" "test-engine")
                                (lib "test-info.scm" "test-engine"))))

(provide check-expect check-within check-error generate-report generate-report!)

(define-syntax (generate-report stx)
  (case (syntax-local-context)
    [(expression) (raise-syntax-error #f
                    "may not be used as an expression"
                    stx)])
  (syntax-case stx ()
    [(_) #'(begin)]
    [_ (raise-syntax-error #f
         "expected no arguments"
         stx)]))

(define (real-generate-report!)
  (match [engine:scheme-test-data]
    [(list rep event-space display)
     (let* ([engine (test-engine)])
       (send engine run)
       (when display
         (send engine refine-display-class display))
       (send engine setup-display rep event-space)
       (send engine summarize-results (current-output-port))
       (send engine reset-info))]))

(define-syntax-rule (generate-report!)
  (define-values []
    (begin
      (real-generate-report!)
      (values))))

(define (test-engine)
  (unless the-engine
    (set! the-engine (engine:builder)))
  the-engine)

(define the-engine #f)

(define-syntax (check-expect stx)
  (syntax-case stx ()
    [(_ actual expected)
     (with-syntax ([src (src->list stx)])
       (syntax/loc stx
         (define-values []
           (let ([engine (test-engine)]
                 [test (lambda () actual)])
             (send engine add-test
               (lambda ()
                 (check-expect-procedure test expected 'src engine)))
             (values)))))]
    [_ (raise-syntax-error #f
         "expected two arguments: expression to test and expected result"
         stx)]))

(define-syntax (check-within stx)
  (syntax-case stx ()
    [(_ actual expected delta)
     (with-syntax ([src (src->list stx)])
       (syntax/loc stx
         (define-values []
           (let ([engine (test-engine)]
                 [test (lambda () actual)])
             (send engine add-test
               (lambda ()
                 (check-within-procedure test expected delta 'src engine)))
             (values)))))]
    [_ (raise-syntax-error #f
         (string-append "expected three arguments: "
                        "expression to test, "
                        "expected result, and "
                        "acceptable difference")
         stx)]))

(define-syntax (check-error stx)
  (syntax-case stx ()
    [(_ actual message)
     (with-syntax ([src (src->list stx)])
       (syntax/loc stx
         (define-values []
           (let ([engine (test-engine)]
                 [test (lambda () actual)])
             (send engine add-test
               (lambda ()
                 (check-error-procedure test message 'src engine)))
             (values)))))]
    [_ (raise-syntax-error #f
         (string-append "expected two arguments: "
                        "expression to test and "
                        "expected error message")
         stx)]))

(define (check-expect-procedure thunk expected src engine)
  (notify engine)
  (let* ([result (with-handlers ([exn:fail? values]) (thunk))])
    (cond
     [(exn? result)
      (fail engine
            (engine:make-unexpected-error src
                                          (engine:test-format)
                                          expected
                                          (exn-message result)
                                          result)
            result)]
     [(htdp:beginner-equal? result expected) #t]
     [else
      (fail engine
            (engine:make-unequal src (engine:test-format) result expected)
            #f)])))

(define (check-within-procedure thunk expected delta src engine)
  (check rational? delta
         "check-within requires a rational number as the range; got ~a instead"
         delta)
  (notify engine)
  (let* ([result (with-handlers ([exn:fail? values]) (thunk))])
    (cond
     [(exn? result)
      (fail engine
            (engine:make-unexpected-error src
                                          (engine:test-format)
                                          expected
                                          (exn-message result)
                                          result)
            result)]
     [(htdp:beginner-equal~? result expected delta) #t]
     [else
      (fail engine
            (engine:make-outofrange src
                                    (engine:test-format)
                                    result
                                    expected
                                    delta)
            #f)])))

(define (check-error-procedure thunk message src engine)
  (check string? message
         "check-error requires a string as the expected message; got ~a instead"
         message)
  (notify engine)
  (let* ([result (with-handlers ([exn:fail? values]) (thunk))])
    (cond
     [(not (exn? result))
      (fail engine
            (engine:make-expected-error src
                                        (engine:test-format)
                                        message
                                        result)
            #f)]
     [(string=? (exn-message result) message) #t]
     [else
      (fail engine
            (engine:make-incorrect-error src
                                         (engine:test-format)
                                         message
                                         (exn-message result)
                                         result)
            result)])))

(define (check pred value fmt . args)
  (unless (pred value)
    (raise (make-exn:fail:contract (apply format fmt args)
                                   (current-continuation-marks)))))

(define (notify engine)
  (send (send engine get-info) add-check))

(define (fail engine failure exn)
  (send (send engine get-info) check-failed
    failure (engine:check-fail-src failure) exn)
  (and exn (raise exn)))