244 lines
7.6 KiB
Scheme
244 lines
7.6 KiB
Scheme
|
|
;; Safsaf, a Guile web framework
|
||
|
|
;; Copyright (C) 2026 Christopher Baines <mail@cbaines.net>
|
||
|
|
|
||
|
|
;; This program 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 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 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 program. If not, see
|
||
|
|
;; <https://www.gnu.org/licenses/>.
|
||
|
|
|
||
|
|
(define-module (tests support)
|
||
|
|
#:use-module (ice-9 format)
|
||
|
|
#:use-module (srfi srfi-1)
|
||
|
|
#:export (is
|
||
|
|
test
|
||
|
|
suite
|
||
|
|
suite-thunk
|
||
|
|
define-suite
|
||
|
|
test-runner*
|
||
|
|
test?
|
||
|
|
suite?
|
||
|
|
suite-thunk?
|
||
|
|
run-tests))
|
||
|
|
|
||
|
|
;;;
|
||
|
|
;;; Minimal SRFI-269 implementation for Guile.
|
||
|
|
;;;
|
||
|
|
;;; Three definition primitives — is, test, suite — construct first-class
|
||
|
|
;;; entities (alists) and deliver them to a pluggable test runner via
|
||
|
|
;;; message passing. Definition is separated from execution: the runner
|
||
|
|
;;; decides when and how to run things.
|
||
|
|
;;;
|
||
|
|
|
||
|
|
;;; --- Parameter ---
|
||
|
|
|
||
|
|
(define test-runner* (make-parameter #f))
|
||
|
|
|
||
|
|
;;; --- Predicates ---
|
||
|
|
|
||
|
|
(define (test? obj)
|
||
|
|
(and (pair? obj)
|
||
|
|
(assq 'test/body-thunk obj)
|
||
|
|
(assq 'test/description obj)
|
||
|
|
#t))
|
||
|
|
|
||
|
|
(define (suite? obj)
|
||
|
|
(and (pair? obj)
|
||
|
|
(assq 'suite/body-thunk obj)
|
||
|
|
(assq 'suite/description obj)
|
||
|
|
#t))
|
||
|
|
|
||
|
|
(define (suite-thunk? obj)
|
||
|
|
(procedure? obj))
|
||
|
|
|
||
|
|
;;; --- is ---
|
||
|
|
|
||
|
|
(define-syntax is
|
||
|
|
(lambda (x)
|
||
|
|
(syntax-case x ()
|
||
|
|
;; Predicate form: (is (pred arg ...))
|
||
|
|
((_ (pred arg ...))
|
||
|
|
(with-syntax ((src (datum->syntax x (syntax-source x))))
|
||
|
|
#'(%run-assert
|
||
|
|
(lambda () (pred arg ...))
|
||
|
|
'(pred arg ...)
|
||
|
|
'src
|
||
|
|
(lambda () (list arg ...)))))
|
||
|
|
;; Simple form: (is expr)
|
||
|
|
((_ expr)
|
||
|
|
(with-syntax ((src (datum->syntax x (syntax-source x))))
|
||
|
|
#'(%run-assert
|
||
|
|
(lambda () expr)
|
||
|
|
'expr
|
||
|
|
'src
|
||
|
|
#f))))))
|
||
|
|
|
||
|
|
(define (%run-assert body-thunk body-datum source args-thunk)
|
||
|
|
(let* ((entity `((assert/body-thunk . ,body-thunk)
|
||
|
|
(assert/body . ,body-datum)
|
||
|
|
(assert/location . ,source)
|
||
|
|
,@(if args-thunk
|
||
|
|
`((assert/args-thunk . ,args-thunk))
|
||
|
|
'()))))
|
||
|
|
((test-runner*)
|
||
|
|
`((type . runner/run-assert)
|
||
|
|
(assert . ,entity)))))
|
||
|
|
|
||
|
|
;;; --- test ---
|
||
|
|
|
||
|
|
(define-syntax test
|
||
|
|
(syntax-rules (quote)
|
||
|
|
((_ desc (quote metadata) meta body ...)
|
||
|
|
(%load-test desc 'meta (lambda () body ... (values))))
|
||
|
|
((_ desc body ...)
|
||
|
|
(%load-test desc '() (lambda () body ... (values))))))
|
||
|
|
|
||
|
|
(define (%load-test description metadata body-thunk)
|
||
|
|
((test-runner*)
|
||
|
|
`((type . runner/load-test)
|
||
|
|
(test . ((test/body-thunk . ,body-thunk)
|
||
|
|
(test/description . ,description)
|
||
|
|
(test/metadata . ,metadata))))))
|
||
|
|
|
||
|
|
;;; --- suite ---
|
||
|
|
|
||
|
|
(define-syntax suite
|
||
|
|
(syntax-rules (quote)
|
||
|
|
((_ desc (quote metadata) meta body ...)
|
||
|
|
(%load-suite desc 'meta (lambda () body ... (values))))
|
||
|
|
((_ desc body ...)
|
||
|
|
(%load-suite desc '() (lambda () body ... (values))))))
|
||
|
|
|
||
|
|
(define (%load-suite description metadata body-thunk)
|
||
|
|
((test-runner*)
|
||
|
|
`((type . runner/load-suite)
|
||
|
|
(suite . ((suite/body-thunk . ,body-thunk)
|
||
|
|
(suite/description . ,description)
|
||
|
|
(suite/metadata . ,metadata))))))
|
||
|
|
|
||
|
|
;;; --- suite-thunk ---
|
||
|
|
|
||
|
|
(define-syntax suite-thunk
|
||
|
|
(syntax-rules (quote)
|
||
|
|
((_ desc (quote metadata) meta body ...)
|
||
|
|
(lambda ()
|
||
|
|
(%load-suite desc 'meta (lambda () body ... (values)))))
|
||
|
|
((_ desc body ...)
|
||
|
|
(lambda ()
|
||
|
|
(%load-suite desc '() (lambda () body ... (values)))))))
|
||
|
|
|
||
|
|
;;; --- define-suite ---
|
||
|
|
|
||
|
|
(define-syntax define-suite
|
||
|
|
(syntax-rules (quote)
|
||
|
|
((_ name (quote metadata) meta body ...)
|
||
|
|
(define name
|
||
|
|
(suite-thunk (symbol->string 'name) (quote metadata) meta body ...)))
|
||
|
|
((_ name body ...)
|
||
|
|
(define name
|
||
|
|
(suite-thunk (symbol->string 'name) body ...)))))
|
||
|
|
|
||
|
|
|
||
|
|
;;;
|
||
|
|
;;; Simple immediate-execution test runner.
|
||
|
|
;;;
|
||
|
|
|
||
|
|
(define %depth 0)
|
||
|
|
(define %pass-count 0)
|
||
|
|
(define %fail-count 0)
|
||
|
|
(define %error-count 0)
|
||
|
|
(define %test-failed? #f)
|
||
|
|
|
||
|
|
(define (indent)
|
||
|
|
(make-string (* 2 %depth) #\space))
|
||
|
|
|
||
|
|
(define (format-location loc)
|
||
|
|
"Return a string like \"file.scm:42\" from a source location alist,
|
||
|
|
or #f if location info is unavailable."
|
||
|
|
(and loc
|
||
|
|
(let ((file (assq-ref loc 'filename))
|
||
|
|
(line (assq-ref loc 'line)))
|
||
|
|
(and file line
|
||
|
|
(format #f "~a:~a" file (+ line 1))))))
|
||
|
|
|
||
|
|
(define (simple-test-runner message)
|
||
|
|
(let ((type (assq-ref message 'type)))
|
||
|
|
(case type
|
||
|
|
|
||
|
|
((runner/load-suite)
|
||
|
|
(let* ((s (assq-ref message 'suite))
|
||
|
|
(desc (assq-ref s 'suite/description))
|
||
|
|
(body (assq-ref s 'suite/body-thunk)))
|
||
|
|
(format #t "~a~a~%" (indent) desc)
|
||
|
|
(set! %depth (+ %depth 1))
|
||
|
|
(body)
|
||
|
|
(set! %depth (- %depth 1))))
|
||
|
|
|
||
|
|
((runner/load-test)
|
||
|
|
(let* ((t (assq-ref message 'test))
|
||
|
|
(desc (assq-ref t 'test/description))
|
||
|
|
(body (assq-ref t 'test/body-thunk)))
|
||
|
|
(set! %test-failed? #f)
|
||
|
|
(with-exception-handler
|
||
|
|
(lambda (exn)
|
||
|
|
(set! %error-count (+ %error-count 1))
|
||
|
|
(format #t "~aERROR ~a~%" (indent) desc)
|
||
|
|
(format #t "~a ~a~%" (indent) exn))
|
||
|
|
(lambda ()
|
||
|
|
(body)
|
||
|
|
(if %test-failed?
|
||
|
|
(begin
|
||
|
|
(set! %fail-count (+ %fail-count 1))
|
||
|
|
(format #t "~aFAIL ~a~%" (indent) desc))
|
||
|
|
(begin
|
||
|
|
(set! %pass-count (+ %pass-count 1))
|
||
|
|
(format #t "~aok ~a~%" (indent) desc))))
|
||
|
|
#:unwind? #t)))
|
||
|
|
|
||
|
|
((runner/run-assert)
|
||
|
|
(let* ((a (assq-ref message 'assert))
|
||
|
|
(body-thunk (assq-ref a 'assert/body-thunk))
|
||
|
|
(body-datum (assq-ref a 'assert/body))
|
||
|
|
(loc (assq-ref a 'assert/location)))
|
||
|
|
(let ((result (body-thunk)))
|
||
|
|
(unless result
|
||
|
|
(set! %test-failed? #t)
|
||
|
|
(format #t "~a FAIL: ~s" (indent) body-datum)
|
||
|
|
(let ((loc-str (format-location loc)))
|
||
|
|
(when loc-str
|
||
|
|
(format #t " at ~a" loc-str)))
|
||
|
|
(newline)
|
||
|
|
;; Show evaluated arguments for predicate assertions.
|
||
|
|
(let ((args-thunk (assq-ref a 'assert/args-thunk)))
|
||
|
|
(when args-thunk
|
||
|
|
(with-exception-handler
|
||
|
|
(lambda (_) #f)
|
||
|
|
(lambda ()
|
||
|
|
(let ((args (args-thunk)))
|
||
|
|
(format #t "~a args: ~s~%" (indent) args)))
|
||
|
|
#:unwind? #t))))
|
||
|
|
result))))))
|
||
|
|
|
||
|
|
(define (run-tests thunk)
|
||
|
|
"Set up the simple test runner, call THUNK (typically a suite-thunk),
|
||
|
|
print a summary, and exit with 0 on success or 1 on failure."
|
||
|
|
(set! %depth 0)
|
||
|
|
(set! %pass-count 0)
|
||
|
|
(set! %fail-count 0)
|
||
|
|
(set! %error-count 0)
|
||
|
|
(parameterize ((test-runner* simple-test-runner))
|
||
|
|
(thunk))
|
||
|
|
(newline)
|
||
|
|
(let ((total (+ %pass-count %fail-count %error-count)))
|
||
|
|
(format #t "~a passed, ~a failed, ~a errors (of ~a)~%"
|
||
|
|
%pass-count %fail-count %error-count total)
|
||
|
|
(exit (if (and (zero? %fail-count) (zero? %error-count)) 0 1))))
|