Skip to content

Commit 4f365d9

Browse files
committed
Redesign check-fail to provide a tree-based API
1 parent 4c85b39 commit 4f365d9

File tree

3 files changed

+144
-266
lines changed

3 files changed

+144
-266
lines changed

rackunit-doc/rackunit/scribblings/check.scrbl

Lines changed: 28 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -462,84 +462,45 @@ message.}
462462

463463
Custom checks such as those created by @racket[define-check] can contain a fair
464464
amount of logic. Consequently, custom checks can be buggy and should be tested.
465-
RackUnit provides a handful of checks explicitly designed for testing the
466-
behavior of other checks; they allow verifying checks pass and fail when
467-
expected or that checks add certain information to the check information stack.
468-
These bindings are provided by @racketmodname[rackunit/meta], not
469-
@racketmodname[rackunit].
470-
471-
@defproc[(check-fail [fail-exn-predicate
472-
(or/c (-> exn:test:check? any/c) regexp?)]
465+
RackUnit provides a few checks explicitly designed for testing the behavior of
466+
other checks; they allow verifying checks pass and fail when expected or that
467+
checks add certain information to the check information stack. These bindings
468+
are provided by @racketmodname[rackunit/meta], not @racketmodname[rackunit].
469+
470+
@defproc[(check-fail [assertion-tree
471+
(treeof (or/c (-> exn:test:check? any/c)
472+
regexp?
473+
check-info?))]
473474
[thunk (-> any)]
474475
[message string? ""])
475476
void?]{
476-
Checks that @racket[thunk] evaluates a failing check and that
477-
@racket[fail-exn-predicate], if it's a function, returns a true value when
478-
given the check failure exception. If @racket[fail-exn-predicate] is a regexp,
479-
instead checks that the regexp matches the check failure exception's message.
480-
Note that a check failure exception's message is the message given to
481-
@racket[fail-check], not the optional @racket[message] argument that all checks
482-
accept. See also @racket[check-exn] and @racket[check-fail/error].
477+
Checks that @racket[thunk] raises a check failure and that the failure
478+
satisfies @racket[assertion-tree]. The tree is checked in the following manner:
479+
480+
@(itemlist
481+
@item{If the tree is a predicate, it must return a true value when applied to
482+
the raised check failure.}
483+
@item{If the tree is a regexp, it must match the check failure's message (as
484+
provided by @racket[fail-check]).}
485+
@item{If the tree is a @racket[check-info] value, the check failure's
486+
@racket[exn:test:check-stack] value must contain the expected info value.}
487+
@item{If the tree is a list, every assertion in the list is checked.})
483488

484489
@(examples
485490
#:eval rackunit-eval
486-
(check-fail values (λ () (check-equal? 'foo 'bar)))
491+
(check-fail '() (λ () (check-equal? 'foo 'bar)))
487492
(check-fail number? (λ () (check-equal? 'foo 'bar)))
488-
(check-fail values (λ () (check-equal? 'foo 'foo))))
493+
(check-fail (list string? (check-info 'info 10))
494+
(λ () (check-equal? 'foo 'foo))))
489495

490-
@history[#:added "1.8"]}
491-
492-
@defproc[(check-fail* [thunk (-> any)] [message string? ""]) void?]{
493-
Like @racket[check-fail], but only checks that @racket[thunk] evaluates a
494-
failing check without testing the failure against a predicate or regexp.
495-
496-
@(examples
497-
#:eval rackunit-eval
498-
(check-fail* (λ () (check-equal? 'foo 'bar)))
499-
(check-fail* (λ () (check-equal? 'foo 'foo))))
500-
501-
@history[#:added "1.8"]}
502-
503-
@defproc[(check-fail/info [info check-info?]
504-
[thunk (-> any)]
505-
[message string? ""])
506-
void?]{
507-
Like @racket[check-fail], but instead of checking that the failure matches a
508-
predicate or regexp checks that the failure contains a check info value equal
509-
to @racket[info]. Note that the check info stack of the failure may contain
510-
multiple infos with the same name as @racket[info] but different values; in
511-
that case the check passes as long as at least one info is equal to the
512-
expected info.
513-
514-
@(examples
515-
#:eval rackunit-eval
516-
(define foo-info (make-check-info 'foo 'foo))
517-
(define-check (fail-foo) (with-heck-info* (list foo-info) fail-check))
518-
(check-fail/info foo-info fail-foo)
519-
(check-fail/info foo-info void)
520-
(check-fail/info foo-info fail))
521-
522-
@history[#:added "1.8"]}
523-
524-
@defproc[(check-fail/error [fail-exn-predicate
525-
(or/c (-> exn:test:check? any/c) regexp?)]
526-
[thunk (-> any)]
527-
[message string? ""])
528-
void?]{
529-
Checks that @racket[thunk] evaluates a check that raises an error value instead
530-
of passing or failing, and checks that the raised value satisfies
531-
@racket[fail-exn-predicate]. Satisfies means that @racket[fail-exn-predicate]
532-
return true when given the raised value if @racket[fail-exn-predicate] is a
533-
function. If it's a predicate, satisfies means that the raised value is an
534-
exception whose message matches the regexp. See also @racket[check-fail] and
535-
@racket[check-exn].
496+
Additionally, a failure is reported if @racket[thunk] raises something other
497+
than an @racket[exn:test:check] value. The optional @racket[message] argument
498+
is included in the output if the check fails.
536499

537500
@(examples
538501
#:eval rackunit-eval
539-
(define-check (error-check)
540-
(raise (make-exn:fail "Kaboom!!!" (current-continuation-marks)))
541-
(fail-check "Doesn't get here"))
542-
(check-fail/error #rx"boom" error-check))
502+
(check-fail '() (λ () (raise 'foo)))
503+
(check-fail number? (λ () (check-equal? 'foo 'bar)) "my message"))
543504

544505
@history[#:added "1.8"]}
545506

rackunit-lib/rackunit/meta.rkt

Lines changed: 85 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -1,105 +1,58 @@
11
#lang racket/base
22

3-
(provide check-fail/error
4-
check-fail
5-
check-fail*
6-
check-fail/info)
3+
(provide check-fail)
74

85
(require (for-syntax racket/base)
96
racket/function
107
racket/list
118
rackunit/log
129
syntax/parse/define
1310
rackunit
14-
rackunit/private/check-info)
11+
(only-in rackunit/private/check-info
12+
current-check-info
13+
pretty-info))
1514

1615

17-
(define-check (check-fail pred-or-msg chk-thnk)
18-
(contract-pred-or-msg! 'check-fail pred-or-msg)
16+
(define-check (check-fail tree chk-thnk)
17+
(contract-tree! 'check-fail tree)
1918
(contract-thunk! 'check-fail chk-thnk)
2019
(define failure (check-raise-value chk-thnk))
21-
(with-expected pred-or-msg
22-
(assert-failure failure)
23-
(assert-check-failure failure)
24-
(if (procedure? pred-or-msg)
25-
(unless (pred-or-msg failure)
26-
(with-actual failure
27-
(fail-check "Wrong exception raised")))
28-
(let ([msg (exn-message failure)])
29-
(unless (regexp-match? pred-or-msg msg)
30-
(with-actual failure
31-
(with-check-info (['actual-msg msg])
32-
(fail-check "Wrong exception raised"))))))))
33-
34-
(define-check (check-fail/info expected-info chk-thnk)
35-
(contract-info! 'check-fail/info expected-info)
36-
(contract-thunk! 'check-fail/info chk-thnk)
37-
(define failure (check-raise-value chk-thnk))
38-
(with-expected expected-info
39-
(assert-failure failure)
40-
(assert-check-failure failure)
41-
(define (has-expected-name? info)
42-
(equal? (check-info-name info) (check-info-name expected-info)))
43-
(define infos (exn:test:check-stack failure))
44-
(define info-names (map check-info-name infos))
45-
(define matching-infos (filter has-expected-name? infos))
46-
(when (empty? matching-infos)
47-
(with-check-info (['actual-info-names info-names])
48-
(fail-check "Check failure did not contain the expected info")))
49-
(unless (member expected-info matching-infos)
50-
(with-check-info* (map make-check-actual matching-infos)
51-
(λ () (fail-check "Check failure contained info(s) with matching name but unexpected value"))))))
52-
53-
(define-check (check-fail* chk-thnk)
54-
(contract-thunk! 'check-fail* chk-thnk)
55-
(define failure (check-raise-value chk-thnk))
56-
(assert-failure failure)
57-
(assert-check-failure failure))
58-
59-
(define-check (check-fail/error pred-or-msg chk-thnk)
60-
(contract-pred-or-msg! 'check-fail/error pred-or-msg)
61-
(contract-thunk! 'check-fail/error chk-thnk)
62-
(define failure (check-raise-value chk-thnk))
63-
(with-expected pred-or-msg
64-
(assert-failure failure)
65-
(assert-not-check-failure failure)
66-
(cond
67-
[(procedure? pred-or-msg)
68-
(unless (pred-or-msg failure)
69-
(with-actual failure
70-
(fail-check "Wrong error raised")))]
71-
[(exn? failure)
72-
(define msg (exn-message failure))
73-
(unless (regexp-match? pred-or-msg msg)
74-
(with-actual failure
75-
(with-check-info (['actual-msg msg])
76-
(fail-check "Wrong error raised"))))]
77-
[else
78-
(with-actual failure
79-
(fail-check "Wrong error raised"))])))
20+
(unless (exn:test:check? failure)
21+
(with-actual failure
22+
(fail-check "Check raised error instead of failing")))
23+
(check-tree-assert tree failure))
8024

8125
;; Shorthands for adding infos
8226

8327
(define-simple-macro (with-actual act:expr body:expr ...)
84-
(with-check-info* (list (make-check-actual act)) (λ () body ...)))
28+
(with-check-info* (error-info act) (λ () body ...)))
8529

86-
(define-simple-macro (with-expected exp:expr body:expr ...)
87-
(with-check-info* (list (make-check-expected exp)) (λ () body ...)))
30+
(define (list/if . vs) (filter values vs))
8831

89-
;; Pseudo-contract helpers, to be replaced with real check contracts eventually
32+
(define (error-info raised)
33+
(list/if (make-check-actual raised)
34+
(and (exn? raised)
35+
(make-check-info 'actual-message (exn-message raised)))
36+
(and (exn:test:check? raised)
37+
(make-check-info 'actual-info
38+
(nested-info
39+
(exn:test:check-stack raised))))))
9040

91-
(define (contract-pred-or-msg! name pred-or-msg)
92-
(unless (or (and (procedure? pred-or-msg)
93-
(procedure-arity-includes? pred-or-msg 1))
94-
(regexp? pred-or-msg))
95-
(define ctrct "(or/c (-> any/c boolean?) regexp?)")
96-
(raise-argument-error name ctrct pred-or-msg)))
41+
;; Pseudo-contract helpers, to be replaced with real check contracts eventually
9742

9843
(define (contract-thunk! name thnk)
99-
(unless (procedure? thnk) (raise-argument-error name "(-> any)" thnk)))
100-
101-
(define (contract-info! name info)
102-
(unless (check-info? info) (raise-argument-error name "check-info?" info)))
44+
(unless (and (procedure? thnk)
45+
(procedure-arity-includes? thnk 0))
46+
(raise-argument-error name "(-> any)" thnk)))
47+
48+
(define (contract-tree! name tree)
49+
(for ([v (in-list (flatten tree))])
50+
(unless (or (and (procedure? v)
51+
(procedure-arity-includes? v 1))
52+
(regexp? v)
53+
(check-info? v))
54+
(define ctrct "(or/c (-> any/c boolean?) regexp? check-info?)")
55+
(raise-argument-error name ctrct v))))
10356

10457
;; Extracting raised values from checks
10558

@@ -109,22 +62,58 @@
10962
;; instead of writing to stdout / stderr, 2) the inner check doesn't log
11063
;; any pass or fail information to rackunit/log, and 3) the inner check's info
11164
;; stack is independent of the outer check's info stack.
112-
(parameterize ([current-check-handler raise]
113-
[test-log-enabled? #f]
114-
[current-check-info (list)])
115-
(with-handlers ([(negate exn:break?) values]) (chk-thnk) #f)))
65+
(or (parameterize ([current-check-handler raise]
66+
[test-log-enabled? #f]
67+
[current-check-info (list)])
68+
(with-handlers ([(negate exn:break?) values]) (chk-thnk) #f))
69+
(fail-check "Check passed unexpectedly")))
11670

11771
;; Assertion helpers
11872

119-
(define (assert-failure maybe-failure)
120-
(unless maybe-failure (fail-check "No check failure occurred")))
121-
122-
(define (assert-check-failure failure)
123-
(unless (exn:test:check? failure)
124-
(with-actual failure
125-
(fail-check "A value other than a check failure was raised"))))
126-
127-
(define (assert-not-check-failure failure)
128-
(when (exn:test:check? failure)
129-
(with-actual failure
130-
(fail-check "Wrong error raised"))))
73+
(struct failure (type expected) #:transparent)
74+
75+
(define (assert-pred raised pred)
76+
(and (not (pred raised))
77+
(failure 'predicate pred)))
78+
79+
(define (assert-regexp exn rx)
80+
(and (not (regexp-match? rx (exn-message exn)))
81+
(failure 'message rx)))
82+
83+
(define (assert-info exn info)
84+
(and (not (member info (exn:test:check-stack exn)))
85+
(failure 'info info)))
86+
87+
(define (assert assertion raised)
88+
((cond [(procedure? assertion) assert-pred]
89+
[(regexp? assertion) assert-regexp]
90+
[(check-info? assertion) assert-info])
91+
raised assertion))
92+
93+
(define (assertions-adjust assertions raised)
94+
(define is-exn? (exn? raised))
95+
(define has-regexps? (ormap regexp? assertions))
96+
(define adjust-regexps? (and has-regexps? (not is-exn?)))
97+
(if adjust-regexps?
98+
(cons exn? (filter-not regexp? assertions))
99+
assertions))
100+
101+
(define (assertion-tree-apply tree raised)
102+
(define assertions (assertions-adjust (flatten tree) raised))
103+
(filter-map (λ (a) (assert a raised)) assertions))
104+
105+
(define (failure-list->info failures)
106+
(define vs
107+
(if (equal? (length failures) 1)
108+
(pretty-info (failure-expected (first failures)))
109+
(nested-info (for/list ([f (in-list failures)])
110+
(make-check-info (failure-type f)
111+
(pretty-info (failure-expected f)))))))
112+
(make-check-info 'expected vs))
113+
114+
(define (check-tree-assert tree raised)
115+
(with-actual raised
116+
(define failures (assertion-tree-apply tree raised))
117+
(unless (empty? failures)
118+
(with-check-info* (list (failure-list->info failures))
119+
fail-check))))

0 commit comments

Comments
 (0)