mirror of
https://git.savannah.gnu.org/git/guile.git
synced 2025-04-29 19:30:36 +02:00
The specification mandates a string, but with rationale suggesting symbols would be a more natural fit. > In some ways using symbols would be preferable. However, we want > human-readable names, and standard Scheme does not provide a way to include > spaces or mixed-case text in literal symbols. Add support for symbols as an implementation extension and for backwards compatibility with the reference implementation. * module/srfi/srfi-64.scm (%cmp-group-name): New procedure. (test-end): Use it. Signed-off-by: Ludovic Courtès <ludo@gnu.org> Reported-by: Daniel Llorens <lloda@sarc.name>
1008 lines
34 KiB
Scheme
1008 lines
34 KiB
Scheme
;;; Copyright (C) 2024 Tomas Volf <~@wolfsden.cz>
|
||
|
||
;;
|
||
;; This library is free 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, or (at your option) any later version.
|
||
;;
|
||
;; This library 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 the GNU
|
||
;; Lesser General Public License for more details.
|
||
;;
|
||
;; You should have received a copy of the GNU Lesser General Public
|
||
;; License along with this library; if not, write to the Free Software
|
||
;; Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||
|
||
;;; Commentary:
|
||
|
||
;;; Implementation of the SRFI-64. In contrast to the reference
|
||
;;; implementation of @samp{(srfi srfi-64)} it aims to implement the
|
||
;;; standard fully and correctly.
|
||
|
||
;;; Code:
|
||
|
||
(define-module (srfi srfi-64)
|
||
#:use-module (ice-9 exceptions)
|
||
#:use-module (ice-9 format)
|
||
#:use-module (ice-9 match)
|
||
#:use-module (ice-9 pretty-print)
|
||
#:use-module (srfi srfi-1)
|
||
#:use-module (srfi srfi-9)
|
||
#:use-module (srfi srfi-26)
|
||
#:use-module (srfi srfi-71)
|
||
#:export
|
||
(
|
||
;; Going by individual sections of the specification, top to bottom:
|
||
;; Simple test-cases
|
||
test-approximate
|
||
test-assert
|
||
test-eq
|
||
test-equal
|
||
test-eqv
|
||
;; Tests for catching errors
|
||
test-error
|
||
;; Testing syntax
|
||
test-read-eval-string
|
||
;; Test groups and paths
|
||
test-begin
|
||
test-end
|
||
test-group
|
||
;; Handling set-up and cleanup
|
||
test-group-with-cleanup
|
||
;; Test specifiers
|
||
test-match-all
|
||
test-match-any
|
||
test-match-name
|
||
test-match-nth
|
||
;; Skipping selected tests
|
||
test-expect-fail
|
||
test-skip
|
||
;; Test-runner
|
||
test-runner-create
|
||
test-runner-current
|
||
test-runner-factory
|
||
test-runner-get
|
||
test-runner-null
|
||
test-runner-simple
|
||
test-runner?
|
||
;; Running specific tests with a specified runner
|
||
test-apply
|
||
test-with-runner
|
||
;; Result kind
|
||
test-passed?
|
||
test-result-kind
|
||
;; Test result properties
|
||
test-result-alist
|
||
test-result-clear
|
||
test-result-ref
|
||
test-result-remove
|
||
test-result-set!
|
||
;; Call-back hooks
|
||
test-runner-on-bad-count
|
||
test-runner-on-bad-count!
|
||
test-runner-on-bad-end-name
|
||
test-runner-on-bad-end-name!
|
||
test-runner-on-final
|
||
test-runner-on-final!
|
||
test-runner-on-group-begin
|
||
test-runner-on-group-begin!
|
||
test-runner-on-group-end
|
||
test-runner-on-group-end!
|
||
test-runner-on-test-begin
|
||
test-runner-on-test-begin!
|
||
test-runner-on-test-end
|
||
test-runner-on-test-end!
|
||
;; Simple runner call-back functions
|
||
test-on-bad-count-simple
|
||
test-on-bad-end-name-simple
|
||
test-on-group-begin-simple
|
||
test-on-group-end-simple
|
||
test-on-test-begin-simple
|
||
test-on-test-end-simple
|
||
;; Test-runner components
|
||
test-runner-aux-value
|
||
test-runner-aux-value!
|
||
test-runner-fail-count
|
||
test-runner-group-path
|
||
test-runner-group-stack
|
||
test-runner-pass-count
|
||
test-runner-reset
|
||
test-runner-skip-count
|
||
test-runner-test-name
|
||
test-runner-xfail-count
|
||
test-runner-xpass-count
|
||
|
||
;; Additional functionality not in SRFI-64:
|
||
define-test
|
||
test-procedure?
|
||
test-thunk
|
||
|
||
&bad-end-name
|
||
bad-end-name?
|
||
bad-end-name-begin-name
|
||
bad-end-name-end-name))
|
||
|
||
(define (set-documentation! symbol docstring)
|
||
"Set the docstring for @var{symbol} in current module to @var{docstring}.
|
||
|
||
Do not use this procedure for forms that already support setting the
|
||
docstring. Should directly follow the definition of @var{symbol}.
|
||
|
||
Example:
|
||
|
||
@lisp
|
||
(define answer 42)
|
||
(set-documentation! 'answer
|
||
\"The answer to life, the universe, and everything.\")
|
||
@end lisp"
|
||
(set-object-property! (module-ref (current-module) symbol)
|
||
'documentation
|
||
docstring))
|
||
|
||
(cond-expand-provide (current-module) '(srfi-64))
|
||
|
||
(define-record-type <test-runner>
|
||
(%make-test-runner)
|
||
test-runner?
|
||
;; Test result properties
|
||
(result-alist test-runner-result-alist test-runner-result-alist!)
|
||
;; Call-back hooks
|
||
(on-bad-count test-runner-on-bad-count test-runner-on-bad-count!)
|
||
(on-bad-end-name test-runner-on-bad-end-name test-runner-on-bad-end-name!)
|
||
(on-final test-runner-on-final test-runner-on-final!)
|
||
(on-group-begin test-runner-on-group-begin test-runner-on-group-begin!)
|
||
(on-group-end test-runner-on-group-end test-runner-on-group-end!)
|
||
(on-test-begin test-runner-on-test-begin test-runner-on-test-begin!)
|
||
(on-test-end test-runner-on-test-end test-runner-on-test-end!)
|
||
;; Test-runner components
|
||
(counts test-runner-counts test-runner-counts!)
|
||
|
||
(test-name test-runner-test-name test-runner-test-name!)
|
||
|
||
(group-stack test-runner-group-stack test-runner-group-stack!)
|
||
|
||
(aux-value test-runner-aux-value test-runner-aux-value!)
|
||
|
||
;; Implementation details
|
||
(fail-list test-runner-fail-list test-runner-fail-list!)
|
||
(groups test-runner-groups test-runner-groups!)
|
||
(run-list test-runner-run-list test-runner-run-list!)
|
||
(skip-list test-runner-skip-list test-runner-skip-list!))
|
||
|
||
(define (test-runner-reset runner)
|
||
(test-runner-result-alist! runner '())
|
||
|
||
(test-runner-counts! runner '())
|
||
|
||
(test-runner-test-name! runner #f)
|
||
|
||
(test-runner-group-stack! runner '())
|
||
|
||
(test-runner-fail-list! runner '())
|
||
(test-runner-groups! runner '())
|
||
;; run-list is not documented as part of the test-runner, so it should *not*
|
||
;; be cleared.
|
||
(test-runner-skip-list! runner '()))
|
||
|
||
(define (test-runner-group-path runner)
|
||
"Return list of names of groups we're nested in, with the outermost group
|
||
first."
|
||
(reverse (test-runner-group-stack runner)))
|
||
|
||
(define (test-runner-fail-count r)
|
||
"Return the number of tests that failed, but were expected to pass."
|
||
(or (assq-ref (test-runner-counts r) 'fail) 0))
|
||
|
||
(define (test-runner-pass-count r)
|
||
"Return the number of tests that passed, and were expected to pass."
|
||
(or (assq-ref (test-runner-counts r) 'pass) 0))
|
||
|
||
(define (test-runner-skip-count r)
|
||
"Return the number of tests or test groups that were skipped."
|
||
(or (assq-ref (test-runner-counts r) 'skip) 0))
|
||
|
||
(define (test-runner-xfail-count r)
|
||
"Return the number of tests that failed, and were expected to fail."
|
||
(or (assq-ref (test-runner-counts r) 'xfail) 0))
|
||
|
||
(define (test-runner-xpass-count r)
|
||
"Return the number of tests that passed, but were expected to fail."
|
||
(or (assq-ref (test-runner-counts r) 'xpass) 0))
|
||
|
||
|
||
;;;
|
||
;;; Test specifiers
|
||
;;;
|
||
(define (test-match-name name)
|
||
"Return a specifier matching the current test name against @var{name}."
|
||
(λ (runner)
|
||
(equal? name (test-runner-test-name runner))))
|
||
|
||
(define* (test-match-nth n #:optional (count 1))
|
||
"Return a stateful predicate. A counter keeps track of how many times it
|
||
has been called. The predicate matches the @var{n}'th time it is
|
||
called (where 1 is the first time), and the next @code{(- @var{count} 1)}
|
||
times, where @var{count} defaults to 1."
|
||
(let ((i 0)
|
||
(m (+ n count -1)))
|
||
(λ (runner)
|
||
(set! i (1+ i))
|
||
(and (>= i n) (<= i m)))))
|
||
|
||
(define (obj->specifier obj)
|
||
"Convert an object to a specifier accounting for the convenience
|
||
short-hands."
|
||
(match obj
|
||
((? procedure? spec)
|
||
spec)
|
||
((? string? name)
|
||
(test-match-name name))
|
||
((? integer? count)
|
||
(test-match-nth 1 count))))
|
||
|
||
(define (test-match-any . specifiers)
|
||
"Return specifier matching if any specifier in @var{specifiers} matches.
|
||
Each specifier is applied, in order, so side-effects from a later specifier
|
||
happen even if an earlier specifier is true."
|
||
(let ((specifiers (map obj->specifier specifiers)))
|
||
(λ (runner)
|
||
(fold (λ (specifier seed)
|
||
(or (specifier runner) seed))
|
||
#f
|
||
specifiers))))
|
||
|
||
(define (test-match-all . specifiers)
|
||
"Return specifier matching if all @var{specifiers} match. Each specifier is
|
||
applied, in order, so side-effects from a later specifier happen even if an
|
||
earlier specifier is true."
|
||
(let ((specifiers (map obj->specifier specifiers)))
|
||
(λ (runner)
|
||
(fold (λ (specifier seed)
|
||
(and (specifier runner) seed))
|
||
#t
|
||
specifiers))))
|
||
|
||
|
||
;;;
|
||
;;; Skipping selected tests
|
||
;;;
|
||
(define (test-skip specifier)
|
||
"Evaluating test-skip adds the resulting specifier to the set of currently
|
||
active skip-specifiers. Before each test (or test-group) the set of active
|
||
skip-specifiers are applied to the active test-runner. If any specifier
|
||
matches, then the test is skipped.
|
||
|
||
@var{specifier} can be a predicate of one argument (the test runner), a
|
||
string (used as if @code{(test-match-name @var{specifier})}) or an
|
||
integer (used as if @code{(test-match-nth 1 @var{specifier})})."
|
||
(let ((r (test-runner-current)))
|
||
(test-runner-skip-list! r (cons (obj->specifier specifier)
|
||
(test-runner-skip-list r)))))
|
||
|
||
(define (any-specifier-matches? specifiers)
|
||
"Does any specifier in @var{specifiers} match current test?
|
||
|
||
All specifiers are always evaluated."
|
||
(let ((r (test-runner-current)))
|
||
(fold (λ (specifier seed)
|
||
(or (specifier r) seed))
|
||
#f
|
||
specifiers)))
|
||
|
||
(define (should-skip?)
|
||
"Should current test be skipped?"
|
||
(any-specifier-matches? (test-runner-skip-list (test-runner-current))))
|
||
|
||
|
||
;;;
|
||
;;; Expected failures
|
||
;;;
|
||
(define (test-expect-fail specifier)
|
||
"Matching tests (where matching is defined as in test-skip) are expected to
|
||
fail. This only affects test reporting, not test execution."
|
||
(let ((r (test-runner-current)))
|
||
(test-runner-fail-list! r (cons (obj->specifier specifier)
|
||
(test-runner-fail-list r)))))
|
||
|
||
(define (should-fail?)
|
||
"Should the current test fail?"
|
||
(any-specifier-matches? (test-runner-fail-list (test-runner-current))))
|
||
|
||
|
||
;;;
|
||
;;; Test result properties
|
||
;;;
|
||
(define* (test-result-ref runner pname #:optional default)
|
||
"Returns the property value associated with the @var{pname} property name.
|
||
If there is no value associated with @var{pname} return @var{default}, or
|
||
@code{#f} if @var{default} is not specified."
|
||
(or (assoc-ref (test-runner-result-alist runner) pname)
|
||
default))
|
||
|
||
(define (test-result-set! runner pname value)
|
||
"Sets the property value associated with the @var{pname} property name to
|
||
@var{value}."
|
||
(test-runner-result-alist! runner
|
||
(assoc-set! (test-runner-result-alist runner)
|
||
pname
|
||
value)))
|
||
|
||
(define (test-result-remove runner pname)
|
||
"Remove the property with the name @var{pname}."
|
||
(test-runner-result-alist! runner
|
||
(assoc-remove! (test-runner-result-alist runner)
|
||
pname)))
|
||
|
||
(define (test-result-clear runner)
|
||
"Remove all result properties."
|
||
;; Standard says the following for test-result-alist:
|
||
;; > However, a test-result-clear does not modify the returned alist.
|
||
;;
|
||
;; Therefore we assign a new empty list instead of removing all entries.
|
||
(test-runner-result-alist! runner '()))
|
||
|
||
(define test-result-alist test-runner-result-alist)
|
||
(set-documentation! 'test-result-alist
|
||
"Returns an association list of the current result properties. It is
|
||
unspecified if the result shares state with the test-runner. The result
|
||
should not be modified; on the other hand, the result may be implicitly
|
||
modified by future @code{test-result-set!} or @code{test-result-remove} calls.
|
||
However, a @code{test-result-clear} does not modify the returned alist.")
|
||
|
||
|
||
;;;
|
||
;;; Result kind
|
||
;;;
|
||
(define* (test-result-kind #:optional (runner (test-runner-current)))
|
||
"Result code of most recent test. Returns @code{#f} if no tests have been run yet.
|
||
If we have started on a new test, but do not have a result yet, then the
|
||
result kind is @code{'xfail} if the test is expected to fail, @code{'skip} if
|
||
the test is supposed to be skipped, or @code{#f} otherwise."
|
||
(test-result-ref runner 'result-kind))
|
||
|
||
(define* (test-passed? #:optional (runner (test-runner-current)))
|
||
"Is the value of @code{(test-result-kind [runner])} one of @code{'pass} or
|
||
@code{'xpass}?
|
||
|
||
This function is of little use, since @code{'xpass} is type of failure. You
|
||
should write your own wrapper checking @code{'pass} and @code{'xfail}
|
||
instead."
|
||
(let ((result (test-result-kind runner)))
|
||
(or (eq? result 'pass)
|
||
(eq? result 'xpass))))
|
||
|
||
|
||
;;;
|
||
;;; Simple test runner
|
||
;;;
|
||
(define (test-on-bad-count-simple runner actual-count expected-count)
|
||
"Log the discrepancy between expected and actual test counts."
|
||
(format #t "*** Expected to run ~a tests, but ~a was executed. ***~%"
|
||
expected-count actual-count))
|
||
|
||
(define (test-on-bad-end-name-simple runner begin-name end-name)
|
||
"Log the discrepancy between the -begin and -end suite names."
|
||
(format #t "*** Suite name mismatch: test-begin (~a) != test-end (~a) ***~%"
|
||
begin-name end-name))
|
||
|
||
(define (test-on-final-simple runner)
|
||
"Display summary of the test suite."
|
||
(display "*** Test suite finished. ***\n")
|
||
(for-each (λ (x)
|
||
(let ((count ((cdr x) runner)))
|
||
(when (> count 0)
|
||
(format #t "*** # of ~a: ~a~%" (car x) count))))
|
||
`(("expected passes " . ,test-runner-pass-count)
|
||
("expected failures " . ,test-runner-xfail-count)
|
||
("unexpected passes " . ,test-runner-xpass-count)
|
||
("unexpected failures" . ,test-runner-fail-count)
|
||
("skips " . ,test-runner-skip-count))))
|
||
|
||
(define (test-on-group-begin-simple runner suite-name count)
|
||
"Log that the group is beginning."
|
||
(format #t "*** Entering test group: ~a~@[ (# of tests: ~a) ~] ***~%"
|
||
suite-name count))
|
||
|
||
(define (test-on-group-end-simple runner)
|
||
"Log that the group is ending."
|
||
;; There is no portable way to get the test group name.
|
||
(format #t "*** Leaving test group: ~a ***~%"
|
||
(car (test-runner-group-stack runner))))
|
||
|
||
(define (test-on-test-begin-simple runner)
|
||
"Do nothing."
|
||
#f)
|
||
|
||
(define (test-on-test-end-simple runner)
|
||
"Log that test is done."
|
||
(define (maybe-print-prop prop pretty?)
|
||
(let* ((val (test-result-ref runner prop))
|
||
(val (string-trim-both
|
||
(with-output-to-string
|
||
(λ ()
|
||
(if pretty?
|
||
(pretty-print val #:per-line-prefix " ")
|
||
(display val)))))))
|
||
(when val
|
||
(format #t "~a: ~a~%" prop val))))
|
||
|
||
(let ((result-kind (test-result-kind runner)))
|
||
;; Skip tests not executed due to run list.
|
||
(when result-kind
|
||
(format #t "* ~:@(~a~): ~a~%"
|
||
result-kind
|
||
(test-runner-test-name runner))
|
||
(unless (member result-kind '(pass xfail))
|
||
(maybe-print-prop 'source-file #f)
|
||
(maybe-print-prop 'source-line #f)
|
||
(maybe-print-prop 'source-form #t)
|
||
(maybe-print-prop 'expected-value #f)
|
||
(maybe-print-prop 'expected-error #t)
|
||
(maybe-print-prop 'actual-value #f)
|
||
(maybe-print-prop 'actual-error #t)))))
|
||
|
||
(define (test-runner-simple)
|
||
"Creates a new simple test-runner, that prints errors and a summary on the
|
||
standard output port."
|
||
(let ((r (%make-test-runner)))
|
||
(test-runner-reset r)
|
||
|
||
(test-runner-on-bad-count! r test-on-bad-count-simple)
|
||
(test-runner-on-bad-end-name! r test-on-bad-end-name-simple)
|
||
(test-runner-on-final! r test-on-final-simple)
|
||
(test-runner-on-group-begin! r test-on-group-begin-simple)
|
||
(test-runner-on-group-end! r test-on-group-end-simple)
|
||
(test-runner-on-test-begin! r test-on-test-begin-simple)
|
||
(test-runner-on-test-end! r test-on-test-end-simple)
|
||
|
||
(test-runner-run-list! r (make-parameter #f))
|
||
r))
|
||
|
||
|
||
;;;
|
||
;;; Test runner
|
||
;;;
|
||
|
||
(define test-runner-current (make-parameter #f))
|
||
(set-documentation! 'test-runner-current
|
||
"Parameter representing currently installed test runner.")
|
||
|
||
(define (test-runner-get)
|
||
"Get current test runner if any, raise an exception otherwise."
|
||
(or (test-runner-current)
|
||
(throw 'no-test-runner)))
|
||
|
||
(define test-runner-factory (make-parameter test-runner-simple))
|
||
(set-documentation! 'test-runner-factory
|
||
"Factory producing new test runner. Has to be a procedure of arity 0
|
||
returning new test runner. Defaults to @code{test-runner-simple}.")
|
||
|
||
(define (test-runner-create)
|
||
"Create a new test-runner. Equivalent to @code{((test-runner-factory))}."
|
||
((test-runner-factory)))
|
||
|
||
(define (test-runner-null)
|
||
(let ((r (%make-test-runner))
|
||
(dummy-1 (λ (_) #f))
|
||
(dummy-3 (λ (_ __ ___) #f)))
|
||
(test-runner-reset r)
|
||
|
||
(test-runner-on-bad-count! r dummy-3)
|
||
(test-runner-on-bad-end-name! r dummy-3)
|
||
(test-runner-on-final! r dummy-1)
|
||
(test-runner-on-group-begin! r dummy-3)
|
||
(test-runner-on-group-end! r dummy-1)
|
||
(test-runner-on-test-begin! r dummy-1)
|
||
(test-runner-on-test-end! r dummy-1)
|
||
|
||
(test-runner-run-list! r (make-parameter #f))
|
||
r))
|
||
|
||
|
||
;;;
|
||
;;; Test groups and paths
|
||
;;;
|
||
(define-record-type <group>
|
||
(make-group name count executed-count installed-runner? previous-skip-list)
|
||
group?
|
||
(name group-name)
|
||
(count group-count)
|
||
(executed-count group-executed-count group-executed-count!)
|
||
(installed-runner? group-installed-runner?)
|
||
(previous-skip-list group-previous-skip-list))
|
||
|
||
(define (increment-executed-count r)
|
||
"Increment executed count of the first group."
|
||
(let ((groups (test-runner-groups r)))
|
||
(unless (null? groups)
|
||
(let ((group (car groups)))
|
||
(group-executed-count! group
|
||
(1+ (group-executed-count group)))))))
|
||
|
||
(define* (test-begin suite-name #:optional count)
|
||
"Enter a new test group.
|
||
|
||
As implementation extension, in addition to strings, symbols are also
|
||
supported as @var{suite-name}."
|
||
(let* ((r (test-runner-current))
|
||
(r install? (if r
|
||
(values r #f)
|
||
(values (test-runner-create) #t)))
|
||
(group (make-group suite-name
|
||
count
|
||
0
|
||
install?
|
||
(test-runner-skip-list r))))
|
||
(when install?
|
||
(test-runner-current r))
|
||
|
||
(test-runner-test-name! r suite-name)
|
||
(test-runner-groups! r (cons group (test-runner-groups r)))
|
||
;; Per-strict reading of SRFI-64, -group-stack is required to be
|
||
;; non-copying, hence non-computed. So duplicate the information already
|
||
;; present in -groups here.
|
||
(test-runner-group-stack! r (cons suite-name (test-runner-group-stack r)))
|
||
|
||
((test-runner-on-group-begin r) r suite-name count)))
|
||
|
||
(define (%cmp-group-name a b)
|
||
(match (list a b)
|
||
(((? string?) (? string?))
|
||
(string=? a b))
|
||
(((? symbol?) (? symbol?))
|
||
(eq? a b))
|
||
(_ #f)))
|
||
|
||
(define* (test-end #:optional suite-name)
|
||
"Leave the current test group."
|
||
(let* ((r (test-runner-current))
|
||
(group (car (test-runner-groups r))))
|
||
|
||
(let ((begin-name (car (test-runner-group-stack r)))
|
||
(end-name suite-name))
|
||
(when (and end-name (not (%cmp-group-name begin-name end-name)))
|
||
((test-runner-on-bad-end-name r) r begin-name end-name)
|
||
(raise-exception (make-bad-end-name begin-name end-name))))
|
||
|
||
(let ((expected-count (group-count group))
|
||
(actual-count (group-executed-count group)))
|
||
(when (and expected-count (not (= expected-count actual-count)))
|
||
((test-runner-on-bad-count r) r actual-count expected-count)))
|
||
|
||
((test-runner-on-group-end r) r)
|
||
|
||
(test-runner-groups! r (cdr (test-runner-groups r)))
|
||
(test-runner-group-stack! r (cdr (test-runner-group-stack r)))
|
||
(test-runner-skip-list! r (group-previous-skip-list group))
|
||
|
||
(if (null? (test-runner-group-stack r))
|
||
((test-runner-on-final r) r)
|
||
(increment-executed-count r))
|
||
|
||
(when (group-installed-runner? group)
|
||
(test-runner-current #f))))
|
||
|
||
(define-syntax test-group
|
||
(syntax-rules ()
|
||
"Execute @var{decl-or-expr ...} in a named test group. The whole group is
|
||
skipped if it matches an active test-skip."
|
||
((_ suite-name decl-or-expr ...)
|
||
(let ((r (test-runner-current))
|
||
(name suite-name))
|
||
;; Since test-runner stores skip state, if we do not have test-runner,
|
||
;; the test cannot be on skip list (it does not exist).
|
||
(when (or (not r)
|
||
(begin
|
||
;; Specifiers are using -test-name, so we need to do this
|
||
;; here and not rely on test-begin.
|
||
(test-runner-test-name! r name)
|
||
(not (should-skip?))))
|
||
(dynamic-wind
|
||
(λ () (test-begin name))
|
||
(λ () decl-or-expr ...)
|
||
(λ () (test-end name))))))))
|
||
|
||
|
||
;;;
|
||
;;; Handling set-up and cleanup
|
||
;;;
|
||
(define-syntax test-group-with-cleanup
|
||
(syntax-rules ()
|
||
"Execute each of the @var{decl-or-expr} forms in order, and then execute
|
||
the @var{cleanup-form}. The latter shall be executed even if one of a
|
||
@var{decl-or-expr} forms raises an exception."
|
||
((_ suite-name decl-or-expr ... cleanup-form)
|
||
(dynamic-wind
|
||
(λ () #t)
|
||
(λ () (test-group suite-name decl-or-expr ...))
|
||
(λ () cleanup-form)))))
|
||
|
||
|
||
;;;
|
||
;;; Simple test-cases
|
||
;;;
|
||
(define (syntax->source-properties form)
|
||
"Extract properties of syntax @var{form} and return them as a alist with
|
||
keys compatible with Guile's SRFI-64 implementation."
|
||
(let* ((source (syntax-source form))
|
||
(file (and=> source (cut assq-ref <> 'filename)))
|
||
(line (and=> source (cut assq-ref <> 'line)))
|
||
;; I do not care about column. Tests are not nested enough.
|
||
(file-alist (if file
|
||
`((source-file . ,file))
|
||
'()))
|
||
(line-alist (if line
|
||
`((source-line . ,(1+ line))) ; 1st line should be 1.
|
||
'())))
|
||
(datum->syntax form
|
||
`((source-form . ,(syntax->datum form))
|
||
,@file-alist
|
||
,@line-alist))))
|
||
|
||
(define (preliminary-result-kind! r fail? skip?)
|
||
"Set result-kind before the test was run based on @var{fail?} and
|
||
@var{skip?}."
|
||
(test-result-set! r 'result-kind (cond
|
||
;; I think this order is stupid, but it is
|
||
;; what SRFI demands.
|
||
(fail? 'xfail)
|
||
(skip? 'skip)
|
||
(else #f))))
|
||
|
||
(define (final-result-kind! r match? fail-expected?)
|
||
"Set the final result-kind based on @var{match?} and @var{fail-expected?}."
|
||
(test-result-set! r 'result-kind (cond ((and match? fail-expected?)
|
||
'xpass)
|
||
(match?
|
||
'pass)
|
||
(fail-expected?
|
||
'xfail)
|
||
(else
|
||
'fail))))
|
||
|
||
(define (fail-on-exception thunk)
|
||
"Run the thunk and return the result. If exception occurs, record it and
|
||
return @code{#f}."
|
||
(with-exception-handler
|
||
(λ (exc)
|
||
(test-result-set! (test-runner-current) 'actual-error exc)
|
||
#f)
|
||
(λ () (thunk))
|
||
#:unwind? #t))
|
||
|
||
(define (increment-test-count r)
|
||
"Increment the test count for the current 'result-kind."
|
||
(let* ((kind (test-result-kind r))
|
||
(counts (test-runner-counts r))
|
||
(c (or (assq-ref counts kind) 0)))
|
||
(test-runner-counts! r (assq-set! counts kind (1+ c)))))
|
||
|
||
(define (test-thunk test-name properties thunk)
|
||
"Run test @var{thunk} while taking into account currently active skip list
|
||
and such. The result alist is initially set to @var{properties}, however
|
||
@var{thunk} is expected to make additions (actual, expected values, ...).
|
||
|
||
@var{thunk} must return @code{#f} to indicate test failure. Otherwise the
|
||
test is considered successful."
|
||
(let ((r (test-runner-current)))
|
||
;; Since skip checks are using -test-name, set it first.
|
||
(test-runner-test-name! r (or test-name ""))
|
||
(test-runner-result-alist! r properties)
|
||
|
||
(let ((fail? (should-fail?))
|
||
(run? (should-run?))
|
||
(skip? (should-skip?)))
|
||
(preliminary-result-kind! r fail? skip?)
|
||
((test-runner-on-test-begin r) r)
|
||
(when run?
|
||
(if skip?
|
||
(test-result-set! r 'result-kind 'skip)
|
||
(begin
|
||
(final-result-kind! r (fail-on-exception thunk) fail?)
|
||
(increment-executed-count r))))
|
||
((test-runner-on-test-end r) r)
|
||
(increment-test-count r))))
|
||
|
||
(define-syntax %test-assert
|
||
(λ (x)
|
||
(syntax-case x ()
|
||
((_ syn test-name expression)
|
||
#`(test-thunk (let () test-name)
|
||
'#,(syntax->source-properties #'syn)
|
||
(λ ()
|
||
(let ((r (test-runner-current))
|
||
(a (let () expression)))
|
||
(test-result-set! r 'actual-value a)
|
||
a)))))))
|
||
|
||
(define-syntax test-assert
|
||
(λ (x)
|
||
(syntax-case x ()
|
||
((_ test-name expression)
|
||
#`(%test-assert #,x test-name expression))
|
||
((_ expression)
|
||
#`(%test-assert #,x #f expression)))))
|
||
(set-documentation! 'test-assert
|
||
"@defspec test-assert test-name expression
|
||
@defspecx test-assert expression
|
||
Evaluate the @var{expression}, the test passes if the result is true.
|
||
|
||
@var{test-name} and @var{expression} are evaluated just once. It is an error
|
||
to invoke @code{test-assert} if there is no current test runner.
|
||
|
||
@end defspec")
|
||
|
||
(define-syntax %%test-2
|
||
(λ (x)
|
||
(syntax-case x ()
|
||
((_ syn test-proc test-name expected test-expr)
|
||
#`(test-thunk (let () test-name)
|
||
'#,(syntax->source-properties #'syn)
|
||
(λ ()
|
||
(let ((r (test-runner-current))
|
||
(e (let () expected))
|
||
(a (let () test-expr)))
|
||
(test-result-set! r 'expected-value e)
|
||
(test-result-set! r 'actual-value a)
|
||
(test-proc e a))))))))
|
||
|
||
(define-syntax %test-2
|
||
(syntax-rules ()
|
||
((_ name test-proc)
|
||
(define-syntax name
|
||
(λ (x)
|
||
(syntax-case x ()
|
||
((_ test-name expected test-expr)
|
||
#`(%%test-2 #,x test-proc test-name expected test-expr))
|
||
((_ expected test-expr)
|
||
#`(%%test-2 #,x test-proc #f expected test-expr))))))))
|
||
|
||
(%test-2 test-eq eq?)
|
||
(%test-2 test-eqv eqv?)
|
||
(%test-2 test-equal equal?)
|
||
|
||
(set-documentation! 'test-eq
|
||
"@defspec test-eq test-name expected test-expr
|
||
@defspecx test-eq expected test-expr
|
||
Test whether result of @var{test-expr} matches @var{expected} using
|
||
@code{eq?}.
|
||
|
||
@end defspec")
|
||
(set-documentation! 'test-eqv
|
||
"@defspec test-eqv test-name expected test-expr
|
||
@defspecx test-eqv expected test-expr
|
||
Test whether result of @var{test-expr} matches @var{expected} using
|
||
@code{eqv?}.
|
||
|
||
@end defspec")
|
||
(set-documentation! 'test-equal
|
||
"@defspec test-equal test-name expected test-expr
|
||
@defspecx test-equal expected test-expr
|
||
Test whether result of @var{test-expr} matches @var{expected} using
|
||
@code{equal?}.
|
||
|
||
@end defspec")
|
||
|
||
(define (within-epsilon ε)
|
||
(λ (expected actual)
|
||
(and (>= actual (- expected ε))
|
||
(<= actual (+ expected ε)))))
|
||
|
||
(define-syntax %test-approximate
|
||
(λ (x)
|
||
(syntax-case x ()
|
||
((_ syn test-name expected test-expr error)
|
||
#`(test-thunk (let () test-name)
|
||
'#,(syntax->source-properties #'syn)
|
||
(λ ()
|
||
(let ((r (test-runner-current))
|
||
(e (let () expected))
|
||
(a (let () test-expr))
|
||
(ε (let () error)))
|
||
(test-result-set! r 'expected-value e)
|
||
(test-result-set! r 'actual-value a)
|
||
(test-result-set! r 'epsilon ε)
|
||
((within-epsilon ε) e a))))))))
|
||
|
||
(define-syntax test-approximate
|
||
(λ (x)
|
||
(syntax-case x ()
|
||
((_ test-name expected test-expr error)
|
||
#`(%test-approximate #,x test-name expected test-expr error))
|
||
((_ expected test-expr error)
|
||
#`(%test-approximate #,x #f expected test-expr error)))))
|
||
(set-documentation! 'test-approximate
|
||
"@defspec test-approximate test-name expected test-expr error
|
||
@defspecx test-approximate expected test-expr error
|
||
Test whether result of @var{test-expr} is within @var{error} of
|
||
@var{expected}.
|
||
|
||
@end defspec")
|
||
|
||
(define-syntax %test-error
|
||
(λ (x)
|
||
(syntax-case x ()
|
||
((_ syn test-name error-type test-expr)
|
||
#`(test-thunk (let () test-name)
|
||
'#,(syntax->source-properties #'syn)
|
||
(λ ()
|
||
(let ((r (test-runner-current))
|
||
(e-type (let () error-type)))
|
||
(test-result-set! r 'expected-error e-type)
|
||
(with-exception-handler
|
||
(λ (exc)
|
||
(test-result-set! r 'actual-error exc)
|
||
(match e-type
|
||
(#t #t)
|
||
(#f #f)
|
||
((? symbol? sym)
|
||
(eq? sym (exception-kind exc)))
|
||
((? procedure? proc)
|
||
(proc exc))
|
||
((? exception-type? exc-type)
|
||
((exception-predicate exc-type) exc))))
|
||
(λ ()
|
||
test-expr
|
||
(not e-type))
|
||
#:unwind? #t))))))))
|
||
|
||
(define-syntax test-error
|
||
(λ (x)
|
||
(syntax-case x ()
|
||
((_ test-name error-type test-expr)
|
||
#`(%test-error #,x test-name error-type test-expr))
|
||
((_ error-type test-expr)
|
||
#`(%test-error #,x #f error-type test-expr))
|
||
((_ test-expr)
|
||
#`(%test-error #,x #f #t test-expr)))))
|
||
(set-documentation! 'test-error
|
||
"@defspec test-error test-name error-type test-expr
|
||
@defspecx test-error error-type test-expr
|
||
@defspecx test-error test-expr
|
||
Evaluating @var{test-expr} is expected to signal an error. The kind of error
|
||
is indicated by @var{error-type}. It is always evaluated (even when no
|
||
exception is raised) and can be one of the following.
|
||
|
||
@table @code
|
||
@item #t
|
||
Per specification, this matches any exception.
|
||
|
||
@item #f
|
||
Pass if no exception is raised.
|
||
|
||
@item symbol?
|
||
Symbols can be used to match against exceptions created using
|
||
@code{throw} and @code{error}.
|
||
|
||
@item procedure?
|
||
The exception object is passed to the predicate procedure. Example
|
||
would be @code{external-error?}.
|
||
|
||
@item exception-type?
|
||
Exception type like for example @code{&external-error}.
|
||
|
||
@end table
|
||
|
||
@end defspec")
|
||
|
||
|
||
;;;
|
||
;;; Testing syntax
|
||
;;;
|
||
(define (test-read-eval-string string)
|
||
"Parse the @var{string} (using @code{read}), evaluate and return the
|
||
result.
|
||
|
||
An error is signaled if there are unread characters after the @code{read} is
|
||
done."
|
||
(with-input-from-string string
|
||
(λ ()
|
||
(let ((exp (read)))
|
||
(unless (eof-object? (read-char))
|
||
(error "read did not consume whole string"))
|
||
(eval exp (current-module))))))
|
||
|
||
|
||
;;;
|
||
;;; Running specific tests with a specified runner
|
||
;;;
|
||
(define-syntax test-with-runner
|
||
(syntax-rules ()
|
||
"Execute each @var{decl-or-expr} in order in a context where the current
|
||
test-runner is @var{runner}."
|
||
((_ runner decl-or-expr ...)
|
||
(parameterize ((test-runner-current runner))
|
||
#t
|
||
decl-or-expr ...))))
|
||
|
||
(define (should-run?)
|
||
"Should current test be considered for execution according to currently
|
||
active run list?"
|
||
(let ((run-list ((test-runner-run-list (test-runner-current)))))
|
||
(if run-list
|
||
(any-specifier-matches? run-list)
|
||
#t)))
|
||
|
||
(define test-apply
|
||
(match-lambda*
|
||
(((? test-runner? r) specifiers ... thunk)
|
||
(test-with-runner r
|
||
(parameterize (((test-runner-run-list r)
|
||
(if (null? specifiers)
|
||
#f
|
||
(map obj->specifier specifiers))))
|
||
(thunk))))
|
||
((specifiers ... thunk)
|
||
(apply test-apply
|
||
(or (test-runner-current)
|
||
(test-runner-create))
|
||
`(,@specifiers ,thunk)))))
|
||
(set-documentation! 'test-apply
|
||
"@defunx test-apply runner specifier ... procedure
|
||
@defunx test-apply specifier ... procedure
|
||
|
||
Call @var{procedure} with no arguments using the specified @var{runner} as the
|
||
current test-runner. If runner is omitted, then @code{(test-runner-current)}
|
||
is used. If there is no current runner, one is created as in
|
||
@code{test-begin}. If one or more @var{specifiers} are listed then only tests
|
||
matching the @var{specifiers} are executed. A specifier has the same form as
|
||
one used for @code{test-skip}. A test is executed if it matches any of the
|
||
specifiers in the @code{test-apply} and does not match any active
|
||
@code{test-skip} specifiers.")
|
||
|
||
|
||
;;;
|
||
;;; Additional functionality not covered by the SRFI.
|
||
;;;
|
||
|
||
(define %define-test-property 'srfi-64-extra/proc-for-test)
|
||
|
||
(define-syntax define-test
|
||
(λ (x)
|
||
(syntax-case x ()
|
||
((_ name e ...)
|
||
(let* ((binding-syn
|
||
(datum->syntax x
|
||
(string->symbol
|
||
(string-append "test-procedure-"
|
||
(syntax->datum #'name))))))
|
||
#`(begin
|
||
(define (#,binding-syn)
|
||
(test-begin name)
|
||
e ...
|
||
(test-end name))
|
||
(set-procedure-property! #,binding-syn
|
||
%define-test-property #t)))))))
|
||
(set-documentation! 'define-test
|
||
"@defspec define-test name form ...
|
||
Introduce a top-level procedure (using @code{define}) with body equivalent to
|
||
|
||
@lisp
|
||
(test-begin @var{name})
|
||
@var{form ...}
|
||
(test-end @var{name})
|
||
@end lisp
|
||
|
||
Due to the procedure name being derived from @var{name}, the @var{name} should
|
||
be unique per-module.
|
||
|
||
The procedure has @code{%define-test-property} procedure property set to
|
||
@code{#t}. This can be used by test driver to discover all test procedures in
|
||
the module.
|
||
|
||
@end defspec")
|
||
|
||
(define (test-procedure? obj)
|
||
"Return whether @var{obj} is a procedure defined by define-test."
|
||
(and (procedure? obj)
|
||
(procedure-property obj %define-test-property)))
|
||
|
||
(define-exception-type &bad-end-name &programming-error
|
||
make-bad-end-name bad-end-name?
|
||
(begin-name bad-end-name-begin-name)
|
||
(end-name bad-end-name-end-name))
|
||
(set-documentation! '&bad-end-name
|
||
"Exception type raised when @var{suite-name} in @code{test-end} differs from
|
||
matching @code{test-begin}.")
|