#lang racket/base ;;; @Package Overeasy ;;; @Subtitle Racket Language Test Engine ;;; @HomePage http://www.neilvandyke.org/overeasy/ ;;; @Author Neil Van Dyke ;;; @Version 0.1 ;;; @Date 2011-08-26 ;;; @PLaneT neil/overeasy:1:=0 ;; $Id: overeasy.rkt,v 1.52 2011/08/27 00:24:58 neilpair Exp $ ;;; @legal ;;; Copyright @copyright{} 2011 Neil Van Dyke. This program is Free Software; ;;; Software; you can redistribute it and/or modify it under the terms of the ;;; GNU Lesser General Public License as published by the Free Software ;;; Foundation; either version 3 of the License (LGPL 3), or (at your option) ;;; any later version. This program is distributed in the hope that it will be ;;; useful, but without any warranty; without even the implied warranty of ;;; merchantability or fitness for a particular purpose. See ;;; @indicateurl{http://www.gnu.org/licenses/} for details. For other licenses ;;; and consulting, please contact the author. ;;; @end legal (require (for-syntax racket/base syntax/parse) racket/port) ;;; @section Introduction ;;; Overeasy is a software test engine for the Racket programming language. It ;;; designed for all of: ;;; ;;; @itemize ;;; @item rapid interactive testing of expressions in the REPL; ;;; @item unit testing of individual modules; and ;;; @item running hierarchical sets of individual module unit tests at once. ;;; @end itemize ;;; ;;; An individual test case, or @i{test}, is specified by the programmer with ;;; the @i{test} syntax, and evaluation of that syntax causes the test to be ;;; run. Properties that are checked by tests are: ;;; ;;; @itemize ;;; @item values of expressions (single value, or multiple value); ;;; @item exceptions raised; and ;;; @item output to @code{current-output-port} and @code{current-error-port}. ;;; @end itemize ;;; ;;; Some checking is also done to help protect test suites from crashing due to ;;; errors in the setup of the test itself, such as errors in evaluating an ;;; expression that provides an expected value for a test. ;;; ;;; A future version of Overeasy might permit the properties that are ;;; tested to be extensible, such as for testing network state or other ;;; resources. ;;; ;;; For the properties checked by tests, in most cases, the programmer can ;;; specify both an expected value and a predicate, or @i{checker}, for ;;; comparing expected and actual values. Note that, if the predicate is not ;;; an equality predicate of some kind, then the ``expected'' would be a ;;; misnomer, and ``argument to the predicate'' would be more accurate. The ;;; actual @code{test} syntax does not include the word ``expected.'' ;;; Specification of expected exceptions is diferent from values and output ;;; ports, in that only the predicate is specified, with no separate expected ;;; or argument value. All these have have reasonable defaults whenever ;;; possible. ;;; @subsection Examples ;;; ;;; Here's a simple test, with the first argument the expression under test, and ;;; the other argument the expected value. ;;; ;;; @lisp ;;; (test (+ 1 2 3) 6) ;;; @end lisp ;;; ;;; How the results of tests are reported varies depending on how the tests are ;;; run. For purposes of these examples, we will pretend we are running tests in ;;; the simplest way. In this way, tests that fail produce one-line error-messages ;;; to @code{current-error-port}, which in DrRacket show up as red italic text by ;;; default. Tests that pass in this way do not produce any message at all. So, ;;; our first example above, does not produce any message. ;;; ;;; Now, for a test that fails: ;;; ;;; @lisp ;;; (test (+ 1 2 3) 7) ;;; @end lisp ;;; @example ;;; Test FAIL : Value 6 did not match expected value 7 by equal?. ;;; @end example ;;; ;;; That's a quick way to do a test in a REPL or when you're otherwise in a ;;; hurry, but if you're reviewing a report of failed tests for one or more ;;; modules, you'd probably like a more descriptive way of seeing which tests ;;; failed. That's what the @i{test ID} is for, and to specify it, we have to ;;; give keyword arguments in our @code{test}: ;;; ;;; @lisp ;;; (test (+ 1 2 3) ;;; 7 ;;; #:id 'simple-addition) ;;; @end lisp ;;; @example ;;; Test FAIL simple-addition : Value 6 did not match expected value 7 by equal?. ;;; @end example ;;; ;;; Quick note on syntax. The above is actually shorthand syntax. In the ;;; non-shorthand syntax, every argument to @code{test} has a keyword, so the ;;; above is actually shorthand for: ;;; ;;; @lisp ;;; (test #:code (+ 1 2 3) ;;; #:val 7 ;;; #:id 'simple-addition) ;;; @end lisp ;;; ;;; @code{#:code} and @code{#:val} are used so often that the keywords can be ;;; left off, so long as the values are positionally the first and second ;;; arguments of @code{test}. One reason you might want to use the ;;; non-shorthand is if you like to have an ID for each test and you like to ;;; have @code{#:id} as the first thing, like a label or heading: ;;; ;;; @lisp ;;; (test #:id 'simple-addition ;;; #:code (+ 1 2 3) ;;; #:val 7) ;;; @end lisp ;;; ;;; In the rest of these examples, we'll use the shorthand syntax, because it's ;;; quicker to type, and getting rid of the @code{#:code} and @code{#:val} ;;; keywords also makes less-common keyword arguments stand out. ;;; ;;; So far, we've been checking the values of code, and we haven't yet dealt in ;;; exceptions. Exceptions, such as due to programming errors in the code ;;; being tested, can also be reported: ;;; ;;; @lisp ;;; (test (+ 1 (error "help!") 3) ;;; 3) ;;; @end lisp ;;; @example ;;; Test FAIL : Got exception #(struct:exn:fail "help!"), but expected value 3. ;;; @end example ;;; ;;; And if an exception is the correct behavior, instead of specifying an expected value, we ;;; can use @code{#:exn} to specify predicate just like for @code{with-handlers}: ;;; ;;; @lisp ;;; (test (+ 1 (error "help!") 3) ;;; #:exn exn:fail?) ;;; @end lisp ;;; ;;; That test passed. But if our code under test doesn't throw an exception ;;; matched by our @code{#:exn} predicate, that's a test failure: ;;; ;;; @lisp ;;; (test (+ 1 2 3) ;;; #:exn exn:fail?) ;;; @end lisp ;;; @example ;;; Test FAIL : Got value 6, but expected exception matched by predicate exn:fail?. ;;; @end example ;;; ;;; Of course, when you want finer discrimination of exceptions than, say, ;;; @code{exn:fail?} or @code{exn:fail:filesystem?}, you can write a custom ;;; predicate that uses @code{exn-message} or other information, and supply it ;;; to @code{test}'s @code{#:exn}. ;;; ;;; Multiple values are supported: ;;; ;;; @lisp ;;; (test (begin 1 2 3) ;;; (values 1 2 3)) ;;; @end lisp ;;; @example ;;; Test FAIL : Value 3 did not match expected values (1 2 3) by equal?. ;;; @end example ;;; ;;; You might have noticed that a lot of the test failure messages say ``by ;;; equal?''. That's referring to the default predicate, so, the following ;;; test passes: ;;; ;;; @lisp ;;; (test (string-append "a" "b" "c") ;;; "abc") ;;; @end lisp ;;; ;;; But we let's say we wanted the expected and actual values to not only be ;;; @code{equal?} but to be @code{eq?} as well: ;;; ;;; @lisp ;;; (test (string-append "a" "b" "c") ;;; "abc" ;;; #:val-check eq?) ;;; @end lisp ;;; @example ;;; Test FAIL : Value "abc" did not match expected value "abc" by eq?. ;;; @end example ;;; ;;; As mentioned earlier, the checker does not have to be an equality ;;; predicate, and it can use whatever reasoning you like in rendering is ;;; verdict on whether the actual value should be considered OK. ;;; ;;; In addition to values an exceptions, @code{test} also intercepts and ;;; permits checking of @code{current-output-port} and ;;; @code{current-error-port}. By default, it assumes no output to either of ;;; those ports, which is especially good for catching programming errors like ;;; neglecting to specify an output port to a procedure for which the port is ;;; optional: ;;; ;;; @lisp ;;; (test (let ((o (open-output-string))) ;;; (display 'a o) (display 'b) (display 'c o) ;;; (get-output-string o)) ;;; "abc") ;;; @end lisp ;;; @example ;;; Test FAIL : Value "ac" did not match expected value "abc" by equal?. Out bytes #"b" did not match expected #"" by equal?. ;;; @end example ;;; ;;; Likewise, messages to @code{current-error-port}, such as warnings and ;;; errors from legacy code, are also caught by default: ;;; ;;; @lisp ;;; (test (begin (fprintf (current-error-port) ;;; "%W%SYS$FROBINATOR_OVERHEAT\n") ;;; 0) ;;; 42) ;;; @end lisp ;;; @example ;;; Test FAIL : Value 0 did not match expected value 42 by equal?. Err bytes #"%W%SYS$FROBINATOR_OVERHEAT\n" did not match expected #"" by equal?. ;;; @end example ;;; ;;; Now we know why we've started getting 0, which information might have gone ;;; unnoticed had our test engine not captured error port output: the ;;; frobinator is failing, after all these years of valiant service. ;;; ;;; With the @code{#:out-check} and @code{#:err-check} keyword arguments to ;;; @code{test}, you can specify predicates other than @code{equal?}. Also, by ;;; setting one of these predicates to @code{#f}, you can cause the output to ;;; be consumed but not stored and checked. This is useful if, for example, ;;; the code produces large amounts of debugging message output. ;;; ;;; @lisp ;;; (test (begin (display "blah") ;;; (display "blah") ;;; (display "blah") ;;; (* 44 2)) ;;; 88 ;;; #:out-check #f) ;;; @end lisp ;;; ;;; There are some more tricks you can do with @code{test}. Most notably, ;;; you'll sometimes want to set up state in the system -- Racket parameters, ;;; test input files, whatever. Because the @code{test} syntax can appear ;;; anywhere normal Racket code can, you can set up this state using normal ;;; Racket code. No special forms for setup and tear-down are required, nor ;;; are they provided. ;;; ;;; @subsection Report Backends ;;; ;;; The architecture of Overeasy is designed to permit different backends ;;; for reporting test results to be plugged in. Currently implemented backends ;;; are for: ;;; ;;; @itemize ;;; @item quick one-line error messages for tests that fail; and ;;; @item more verbose textual report of all test cases run. ;;; @end itemize ;;; ;;; @noindent ;;; In the future, Web front-end and GUI backends might also be implemented. ;;; The backend is dynamic context, so no changes to the files containing test ;;; code is required to run the tests with a different backend. ;;; ;;; @subsection Test Contexts and Test Sections ;;; ;;; The architectural notion that permits the backends to be plugged in is ;;; called the @i{test context}. Test context are nested dynamically, with ;;; each introduced context having the previous context as a parent. The same ;;; test context notion that permits backends for reporting to be introduced ;;; also permits @i{test sections} for grouping tests to be nested dynamically. ;;; The dynamic nesting of test sections facilitates reporting of test results ;;; when running unit tests for multiple modules together. Plugging in a ;;; backend for reporting simply means establishing it as the first or topmost ;;; test context. ;;; ;;; By default, if a test is run without a test context, then the one-line ;;; error messages are used. If a test section context is introduced without a ;;; parent context, such as would usually be the case for an individual ;;; module's unit tests, then the text report backend is plugged in by default. ;;; ;;; One place you'll want to use a section is for the unit tests for a ;;; particular module. This groups the tests together if the module's unit ;;; tests are run in the context of a larger test suite, and it also provides a ;;; default report context when the unit tests are run by themselves. You ;;; might want to package the module's unit tests in a procedure, for ease of ;;; use as part of a test suite. (Unless you have rigged up something ;;; different, like by having @code{require} or @code{dynamic-require} simply ;;; run the tests, without needing to then invoke a provided procedure. For ;;; illustration in this document, we'll use procedures.) For example, if you ;;; have a @code{fruits} module, in file @code{fruits.rkt}, then you might want ;;; to put its unit tests in a procedure in file @code{test-fruits.rkt}, like ;;; so: ;;; ;;; @lisp ;;; (define (test-fruits) ;;; (with-test-section ;;; #:id 'fruits ;;; (test #:id 'apple #:code (+ 1 2 3) #:val 6) ;;; (test #:id 'banana #:code (+ 4 5 6) #:val 6) ;;; (test #:id 'cherry #:code (+ 7 8 9) #:val 24))) ;;; @end lisp ;;; ;;; Notice that we put all the tests for module in @code{fruits} in ;;; @code{with-test-section} here, and gave it an ID. The ID didn't have to be ;;; @code{fruits} like the module name; we could have called it ;;; @code{fruity-unit-tests}, @code{fructose}, or any other symbol. ;;; ;;; Then let's say we have a @code{cars} module, so in file @code{some-cars-tests.rkt}, we put ;;; this procedure: ;;; ;;; @lisp ;;; (define (test-drive-cars) ;;; (with-test-section ;;; #:id 'cars ;;; (test (+ 77 11) 88 #:id 'delorean) ;;; (test (or (and #f 'i-cant-drive) 55) 55 #:id 'ferrari) ;;; (test (+ 300 8) 308 #:id 'magnum))) ;;; @end lisp ;;; ;;; Those unit test suites are used independently. Later, those modules are ;;; integrated into a larger system, COLOSSUS. For running all the unit tests ;;; for the modules of COLOSSUS, we add another module, which @code{require}s ;;; the other test modules, and invokes the each unit test procedure within its ;;; own test section: ;;; ;;; @lisp ;;; (with-test-section ;;; #:id 'colossus-components ;;; (test-fruits) ;;; (test-drive-cars)) ;;; @end lisp ;;; ;;; Unless this is done within another test context, the result will be to ;;; execute the tests in the default text report context. This produces a ;;; report like: ;;; ;;; @smallexample ;;; ;; START-TESTS ;;; ;; ;;; ;; START-TEST-SECTION colossus-components ;;; ;; ;;; ;; START-TEST-SECTION fruits ;;; ;; ;;; ;; TEST apple ;;; ;; (+ 1 2 3) ;;; ;; OK ;;; ;; ;;; ;; TEST banana ;;; ;; (+ 4 5 6) ;;; ;; *FAIL* Value 15 did not match expected value 6 by equal?. ;;; ;; ;;; ;; TEST cherry ;;; ;; (+ 7 8 9) ;;; ;; OK ;;; ;; ;;; ;; END-TEST-SECTION fruits ;;; ;; ;;; ;; START-TEST-SECTION cars ;;; ;; ;;; ;; TEST delorean ;;; ;; (+ 77 11) ;;; ;; OK ;;; ;; ;;; ;; TEST ferrari ;;; ;; (or (and #f (quote i-cant-drive)) 55) ;;; ;; OK ;;; ;; ;;; ;; TEST magnum ;;; ;; (+ 300 8) ;;; ;; OK ;;; ;; ;;; ;; END-TEST-SECTION cars ;;; ;; ;;; ;; END-TEST-SECTION colossus-components ;;; ;; ;;; ;; END-TESTS ;;; ;; OK: 5 FAIL: 1 BROKEN: 0 ;;; ;; SOME TESTS *FAILED*! ;;; @end smallexample ;;; ;;; The test sections here are nested only two deep, but test sections may be ;;; nested to arbitrary depth. You can use test sections at each nested ;;; subsystem, to organize the unit tests for a module into groups, to group ;;; variations of generated test cases (e.g., if evaluating the same ;;; @code{test} form multiple times, with different values or state each time), ;;; or other purposes. ;;; ;;; @subsection Project Status and History ;;; ;;; Work is ongoing, but Overeasy should be useful already. It is being ;;; developed both as a useful tool, and as input to discussion in the Racket ;;; developer community about unifying the various test engines. ;;; ;;; This package does not yet provide an interface so that additional reporting ;;; backends can be added. This is intentional, so that we can be comfortable ;;; that the interface won't be changing soon before others start developing to ;;; it. ;; TODO: Discuss related projects, including RackUnit, eli-tester, and DrDr. ;;; As a historical note, Overeasy is much superior to the author's 2005 ;;; lightweight unit testing library, ;;; @uref{http://www.neilvandyke.org/testeez/, Testeez}. ;;; @section Interface ;; @section Test Specs ;; TODO: Maybe rename "spec" to "decl" or "defn" or something, to avoid ;; confusion with software test specification documents. (define (%test-spec-custom-write spec out mode) (fprintf out "#<test-spec:~S>" (test-spec-id spec))) (define-struct test-spec ;; TODO: Maybe code-sexp should be code-stx? ;; ;; TODO: add value-sexp and exception-sexp? ;; ;; TODO: Include stx of the "test" form? (id code-sexp code-thunk expected-exn expected-vals vals-check expected-out expected-err out-check err-check notes) #:property prop:custom-write %test-spec-custom-write) (define-syntax %make-test-spec/kw (syntax-rules () ((_ #:id id #:code-sexp code-sexp #:code-thunk code-thunk #:expected-exn expected-exn #:expected-vals expected-vals #:vals-check vals-check #:expected-out expected-out #:expected-err expected-err #:out-check out-check #:err-check err-check #:notes notes) (make-test-spec id code-sexp code-thunk expected-exn expected-vals vals-check expected-out expected-err out-check err-check notes)))) ;; @section Test Results (define-struct test-result (spec actual-exn actual-vals actual-out actual-err exn-ok? vals-ok? out-ok? err-ok? ok?)) (define (%make-test-result/kw #:spec spec #:actual-exn actual-exn #:actual-vals actual-vals #:actual-out actual-out #:actual-err actual-err #:exn-ok? exn-ok? #:vals-ok? vals-ok? #:out-ok? out-ok? #:err-ok? err-ok? #:ok? ok?) (make-test-result spec actual-exn actual-vals actual-out actual-err exn-ok? vals-ok? out-ok? err-ok? ok?)) (define (%write-test-result-textual-problem-summary result out) (let* ((spec (test-result-spec result)) (expected-exn (test-spec-expected-exn spec)) (actual-exn (test-result-actual-exn result))) (if expected-exn (or (test-result-exn-ok? result) (if actual-exn (fprintf out " Exception ~A was not matched by expected exception predicate ~A." (%pretty-exn-string actual-exn) (%pretty-proc-name-string expected-exn)) (fprintf out " Got ~A, but expected exception matched by predicate ~A." (%pretty-vals (test-result-actual-vals result)) (%pretty-proc-name-string expected-exn)))) (or (test-result-vals-ok? result) (let ((expected-vals (test-spec-expected-vals spec))) (if actual-exn (fprintf out " Got exception ~A, but expected ~A." (%pretty-exn-string actual-exn) (%pretty-vals expected-vals)) (fprintf out " ~A did not match expected ~A by ~A." (%pretty-vals (test-result-actual-vals result) #:capitalized? #t) (%pretty-vals expected-vals) (%pretty-proc-name-string (test-spec-vals-check spec))))))) (let-syntax ((do-err/out (syntax-rules () ((_ STR SPEC-CHECK RESULT-OK? RESULT-ACTUAL SPEC-EXPECTED) (cond ((SPEC-CHECK spec) => (lambda (check) (or (RESULT-OK? result) (fprintf out " ~A bytes ~S did not match expected ~S by ~A." STR (RESULT-ACTUAL result) (SPEC-EXPECTED spec) (%pretty-proc-name-string check)))))))))) (do-err/out "Out" test-spec-out-check test-result-out-ok? test-result-actual-out test-spec-expected-out) (do-err/out "Err" test-spec-err-check test-result-err-ok? test-result-actual-err test-spec-expected-err)))) ;; @section Test Contexts (define-struct test-context (parent id handle-start handle-end handle-child-start handle-child-end handle-test-start handle-test-end handle-test-broken)) (define (make-test-context/kw #:parent parent #:id id #:handle-start handle-start #:handle-end handle-end #:handle-child-start handle-child-start #:handle-child-end handle-child-end #:handle-test-start handle-test-start #:handle-test-end handle-test-end #:handle-test-broken handle-test-broken) (make-test-context parent id handle-start handle-end handle-child-start handle-child-end handle-test-start handle-test-end handle-test-broken)) (define %current-test-context (make-parameter #f)) (define (%start-test-context construct-from-parent) (let* ((parent-context (%current-test-context)) (new-context (construct-from-parent parent-context))) (and parent-context ((test-context-handle-child-start parent-context) new-context)) ((test-context-handle-start new-context)) (%current-test-context new-context))) (define (%end-test-context) (let* ((old-context (or (%current-test-context) (error '%end-test-context "no current test context"))) (parent-context (test-context-parent old-context))) ((test-context-handle-end old-context)) (and parent-context ((test-context-handle-child-end parent-context) old-context)) (%current-test-context parent-context))) (define-syntax %with-test-context/construct-from-parent (syntax-rules () ((_ CONSTRUCT-FROM-PARENT BODY0 BODYn ...) (dynamic-wind (lambda () (%start-test-context CONSTRUCT-FROM-PARENT)) (lambda () BODY0 BODYn ...) (lambda () (%end-test-context)))))) ;; @subsection Text Test Context (define (make-text-report-context parent) (let ((out (current-output-port)) (ok-count 0) (fail-count 0) (broken-count 0)) (make-test-context/kw #:parent (and parent (error 'make-text-report-context "unexpected parent ~S" parent)) #:id '<text-report> #:handle-start (lambda () (fprintf out "\n;; START-TESTS\n")) #:handle-end (lambda () (fprintf out ";;\n;; END-TESTS\n") (fprintf out ";; OK: ~S FAIL: ~S BROKEN: ~S\n" ok-count fail-count broken-count) (fprintf out ";; ~A!\n\n" (cond ((zero? (+ ok-count fail-count broken-count)) "*NO TESTS*") ((not (zero? broken-count)) "SOME TESTS *BROKEN*") ((zero? fail-count) "ALL TESTS *PASSED*") ((zero? ok-count) "ALL TESTS *FAILED*") (else "SOME TESTS *FAILED*")))) #:handle-child-start (lambda (child-context) (fprintf out ";;\n;; START-TEST-SECTION ~S\n" (test-context-id child-context))) #:handle-child-end (lambda (child-context) (fprintf out ";;\n;; END-TEST-SECTION ~S\n" (test-context-id child-context))) #:handle-test-start void #:handle-test-end (lambda (result) (let ((spec (test-result-spec result))) (fprintf out ";;\n;; TEST ~S\n;; ~S\n;; ~A\n" (or (test-spec-id spec) '<unspecified-id>) (test-spec-code-sexp spec) ;; TODO: Show actual returned values, exceptions, outputs, ;; and annotate those when differ from expected. (if (test-result-ok? result) (begin (set! ok-count (+ 1 ok-count)) "OK") (let ((os (open-output-string))) (set! fail-count (+ 1 fail-count)) (display "*FAIL*" os) (%write-test-result-textual-problem-summary result os) (get-output-string os)))))) #:handle-test-broken (lambda (exn) (set! broken-count (+ 1 broken-count)) (fprintf out "TEST BROKEN!~A\n" (%space-value-if-true (%test-setup-exn-id exn)) (exn-message exn)))))) ;; @subsection Single Test Context (define (%space-value-if-true val) (if val (format " ~A" val) "")) (define (%pretty-proc-name-string proc) (let ((name (cond ((object-name proc) => symbol->string) (else (let ((name (call-with-output-string (lambda (out) (write proc out))))) (cond ((regexp-match #rx"^#<procedure:(.*)>$" name) => cadr) (else name))))))) (cond ((regexp-match-positions #rx"\\.(?:rkt|ss|scm):[0-9]+:[0-9]$" name) => (lambda (m) (string-append "#<procedure:" name ">"))) (else name)))) (define (%pretty-exn-string exn) (let ((str (format "~S" exn))) (cond ((regexp-match "^(#\\(struct:.*) #<continuation-mark-set>\\)$" str) => (lambda (m) (string-append (cadr m) ")"))) (else str)))) (define (%pretty-vals vals #:capitalized? (capitalized? #f)) (let ((first-letter (if capitalized? "V" "v"))) (if (null? (cdr vals)) (format "~Aalue ~S" first-letter (car vals)) (format "~Aalues ~S" first-letter vals)))) (define (make-single-test-context parent) (let ((out (current-error-port))) (make-test-context/kw #:parent parent #:id '<single-test> #:handle-start void #:handle-end void #:handle-child-start (lambda (child-context) (error '<make-single-test-context> "handle-child-start called")) #:handle-child-end (lambda (child-context) (error '<make-single-test-context> "handle-child-end called")) #:handle-test-start void #:handle-test-end (lambda (result) (or (test-result-ok? result) (let ((os (open-output-string)) (spec (test-result-spec result))) (display "Test FAIL" os) (cond ((test-spec-id spec) => (lambda (id) (write-char #\space os) (display id os)))) (display " :" os) (%write-test-result-textual-problem-summary result os) (displayln (get-output-string os) out)))) #:handle-test-broken (lambda (exn) (fprintf out "TEST BROKEN!~A\n" (%space-value-if-true (%test-setup-exn-id exn)) (exn-message exn)))))) ;; @subsection Section Test Context (define %current-default-parent-test-context-for-sections (make-parameter #f)) ;; TODO: THIS CONTEXT STUFF IS A LITTLE MESSY. SIMPLIFY. Maybe part of this ;; is making default behavior for the handlers to be to propagate (value symbol ;; "propagate-to-parent" or "default" instead of a proc. ;;; @defsyntax with-test-section #:id id body ... ;;; ;;; See above. (define-syntax with-test-section (syntax-rules () ((_ #:id ID BODYn ...) (let ((id ID)) (%call-with-current-test-context-or-construct (%current-default-parent-test-context-for-sections) (lambda (parent) (%with-test-context/construct-from-parent (lambda (parent) ;; TODO: !!! CHANGE ALL THESE KEYWORD ARGS FOR SECTIONS !!!!!!!!!!!!!!!!!!!!!!! (let ((this-context (make-test-context/kw #:parent parent #:id id #:handle-start void ;; !!! #:handle-end void ;; !!! #:handle-child-start (lambda (child-context) ((test-context-handle-child-start parent) child-context)) #:handle-child-end (lambda (child-context) ((test-context-handle-child-end parent) child-context)) #:handle-test-start (lambda (spec) ((test-context-handle-test-start parent) spec)) #:handle-test-end (lambda (result) ((test-context-handle-test-end parent) result)) #:handle-test-broken (lambda (exn) ((test-context-handle-test-broken parent) exn))))) this-context)) BODYn ...))))))) ;; TODO: Is this safe? (%current-default-parent-test-context-for-sections make-text-report-context) ;; ;; (define (start-test-section #:id (id #f) ;; #:notes (notes #f)) ;; (let* ((parent-context (or (%current-test-context) ;; ((%current-default-parent-test-context-for-sections) #f))) ;; (new-context (make-test-context parent-context ;; id ;; notes)) ;; (start-handler '!!!) ;; ;; ( ;; ;; '!!! ;; ) ;; (make-test-context/kw #:parent parent ;; #:handle-start handle-start ;; #:handle-end handle-end ;; ;; #:handle-child-start handle-child-start ;; #:handle-child-end handle-child-end ;; #:handle-test-start handle-test-start ;; #:handle-test-end handle-test-end) ;; ;; ;; !!! ;; ))) ;; @section Tests (define %current-default-test-context-maker-for-tests (make-parameter #f)) (%current-default-test-context-maker-for-tests make-single-test-context) (define-struct (%test-setup-exn exn:fail) (id)) (define-syntax %test-setup-values (syntax-rules () ;; TODO: Can put the WHAT-STRING in at syntax expansion time. ((_ ID WHAT-STRING EXPR) (with-handlers ((exn:fail? (lambda (orig-exn) (raise (make-%test-setup-exn (format "Exception from ~A during test setup: ~S" WHAT-STRING (exn-message orig-exn)) (exn-continuation-marks orig-exn) ID))))) EXPR)))) ;; TODO: !!! make %test-setup-value/proc and use it for "expected-exn" and for ;; some of the checkers. And "/proc-or-false" for out/err checkers. (define-syntax %test-setup-value/non-false (syntax-rules () ((_ ID WHAT-STRING EXPR) ;; TODO: Can put the WHAT-STRING in at syntax expansion time. ;; ;; TODO: Error-check that it's not multiple-value. (or (%test-setup-values ID WHAT-STRING EXPR) (raise (make-%test-setup-exn (format "Invalid ~A during test setup: #f" WHAT-STRING) (current-continuation-marks) ID)))))) ;; TODO: When writing "code-sexp", normalize the writing procedure. ;; (define (%finish-executing-test-spec-after-eval #:spec spec ;; #:actual-exn actual-exn ;; #:actual-value actual-value) ;; ;; ) (define (%false x) #f) (define (%open-output-null) ;; Note: This definition is taken from the Racket 5.1.2 documentation for ;; "make-output-port". (make-output-port 'null always-evt (lambda (s start end non-block? breakable?) (- end start)) void (lambda (special non-block? breakable?) #t) (lambda (s start end) (wrap-evt always-evt (lambda (x) (- end start)))) (lambda (special) always-evt))) (define (%call-with-current-test-context-or-single proc) (cond ((%current-test-context) => proc) (else (%with-test-context/construct-from-parent make-single-test-context (proc (%current-test-context)))))) (define (%call-with-current-test-context-or-construct construct proc) (cond ((%current-test-context) => proc) (else (%with-test-context/construct-from-parent construct (proc (%current-test-context)))))) ;; (define-struct %not-applicable ()) ;; (define %not-applicable (make-%not-applicable)) (define (%execute-test-spec spec) (%call-with-current-test-context-or-single (lambda (context) ((test-context-handle-test-start context) spec) (let* ((out-check (test-spec-out-check spec)) (err-check (test-spec-err-check spec)) (out-op (if out-check (open-output-bytes) (%open-output-null))) (err-op (if err-check (open-output-bytes) (%open-output-null)))) (parameterize ((current-output-port out-op) (current-error-port err-op)) (let*-values (((expected-exn) (test-spec-expected-exn spec)) ((actual-exn actual-vals exn-ok? vals-ok?) (with-handlers (((or expected-exn %false) (lambda (actual-exn) (values actual-exn 'not-applicable #t 'not-applicable))) (exn:fail? (lambda (actual-exn) (values actual-exn 'not-applicable #f ; TODO: not-applicable? #f)))) (let ((actual-vals (call-with-values (test-spec-code-thunk spec) list))) (if expected-exn (values #f actual-vals #f 'not-applicable) (let ((expected-vals (test-spec-expected-vals spec))) (values #f actual-vals 'not-applicable ((test-spec-vals-check spec) actual-vals expected-vals))))))) ;; TODO: Make a macro for output ports. ((actual-out out-ok?) (if out-check (let ((actual-bytes (get-output-bytes out-op))) (values actual-bytes (out-check actual-bytes (test-spec-expected-out spec)))) (values 'not-applicable 'not-applicable))) ((actual-err err-ok?) (if err-check (let ((actual-bytes (get-output-bytes err-op))) (values actual-bytes (err-check actual-bytes (test-spec-expected-err spec)))) (values 'not-applicable 'not-applicable))) ((ok?) (and (if expected-exn exn-ok? vals-ok?) (or (not out-check) out-ok?) (or (not err-check) err-ok?) #t)) ((result) (%make-test-result/kw #:spec spec #:actual-exn actual-exn #:actual-vals actual-vals #:actual-out actual-out #:actual-err actual-err #:exn-ok? exn-ok? #:vals-ok? vals-ok? #:out-ok? out-ok? #:err-ok? err-ok? #:ok? ok?))) ((test-context-handle-test-end context) result) (void))))))) (define (make-exn-with-message-predicate pred msg) (let ((<make-exn-with-message-predicate> (lambda (e) (and (pred e) (equal? (exn-message e) msg))))) <make-exn-with-message-predicate>)) (define (make-exn-with-message-starts-with-predicate pred msg) (let ((rx (string-append "^" (regexp-quote msg)))) (let ((<make-exn-with-message-starts-with-predicate> (lambda (e) (and (pred e) (regexp-match? rx (exn-message e)))))) <make-exn-with-message-starts-with-predicate>))) ;;; @defsyntax test ... ;;; ;;; See above. (define-syntax (test stx) (syntax-parse stx ((_ CODE:expr EXPECTED-VALS:expr RESTn ...) #'(test #:code CODE #:val EXPECTED-VALS RESTn ...)) ((_ CODE:expr RESTn ...) #'(test #:code CODE RESTn ...)) ((_ (~or (~optional (~seq #:id ID:expr) #:name "#:id option") (~once (~seq #:code CODE:expr) #:name "#:code option") (~once (~or (~seq #:val EXPECTED-VALS:expr) #:name "#:val option" (~seq #:exn EXPECTED-EXN:expr) #:name "#:exn option")) (~optional (~seq #:val-check VALS-CHECK:expr) #:name "#:val-check option") (~optional (~seq #:out EXPECTED-OUT:expr) #:name "#:out option") (~optional (~seq #:out-check OUT-CHECK:expr) #:name "#:out-check option") (~optional (~seq #:err EXPECTED-ERR:expr) #:name "#:err option") (~optional (~seq #:err-check ERR-CHECK:expr) #:name "#:err-check option") (~optional (~seq #:notes NOTES:expr))) ...) ;; TODO: Don't let specify "#:val-check" if "#:exn". Is it ;; possible to do this with the "~or", "~seq", etc. alone? (with-syntax ((ID (or (attribute ID) #'#f)) (EXPECTED-VALS (or (attribute EXPECTED-VALS) #''not-applicable)) (EXPECTED-EXN (or (attribute EXPECTED-EXN) #'#f)) (VALS-CHECK (or (attribute VALS-CHECK) #'equal?)) (EXPECTED-OUT (or (attribute EXPECTED-OUT) #'#"")) (EXPECTED-ERR (or (attribute EXPECTED-ERR) #'#"")) (OUT-CHECK (or (attribute OUT-CHECK) #'equal?)) (ERR-CHECK (or (attribute ERR-CHECK) #'equal?)) (NOTES (or (attribute NOTES) #'#f)) ;; (EXN-TEST-SETUP-VAL (cond ((attribute EXPECTED-VALS) #'%test-setup-values) ((attribute EXPECTED-EXN) #'%test-setup-value/non-false) (else (error 'test "internal error: expect-which setting"))))) #`(with-handlers ((%test-setup-exn? %handle-test-setup-exn)) (let ((id (%test-setup-values #f "#:id" ID))) (%execute-test-spec (%make-test-spec/kw #:id id #:code-sexp (%test-setup-values id "#:code" (quote CODE)) #:code-thunk (%test-setup-values id "#:code" (lambda () CODE)) #:expected-exn (EXN-TEST-SETUP-VAL id "#:exn" EXPECTED-EXN) #:expected-vals (%test-setup-values id "#:val" (call-with-values (lambda () EXPECTED-VALS) list)) #:vals-check (%test-setup-values id "#:val-check" VALS-CHECK) #:expected-out (%test-setup-values id "#:out" EXPECTED-OUT) #:expected-err (%test-setup-values id "#:err" EXPECTED-ERR) #:out-check (%test-setup-values id "#:out-check" OUT-CHECK) #:err-check (%test-setup-values id "#:err-check" ERR-CHECK) #:notes (%test-setup-values id "#:notes" NOTES))))))))) (define (%handle-test-setup-exn exn) (%call-with-current-test-context-or-single (lambda (context) ((test-context-handle-test-broken context) exn) (void)))) ;; TODO: for out and err, force expected to bytes (convert using utf-8 if ;; string), and also ensure that port encoding is utf-8. or ucs-whatever, if ;; utf-8 doesn't make sense. ;;; @section History ;;; @table @asis ;;; ;;; @item Version 0.1 --- 2011-08-26 -- PLaneT @code{(1 0)} ;;; Initial release. ;;; ;;; @end table (provide test with-test-section)