2016-01-06 11:55:25 +01:00
|
|
|
;; Common definitions for writing tests.
|
|
|
|
;;
|
|
|
|
;; Copyright (C) 2016 g10 Code GmbH
|
|
|
|
;;
|
|
|
|
;; This file is part of GnuPG.
|
|
|
|
;;
|
|
|
|
;; GnuPG is free software; you can redistribute it and/or modify
|
|
|
|
;; it under the terms of the GNU General Public License as published by
|
|
|
|
;; the Free Software Foundation; either version 3 of the License, or
|
|
|
|
;; (at your option) any later version.
|
|
|
|
;;
|
|
|
|
;; GnuPG 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 General Public License for more details.
|
|
|
|
;;
|
|
|
|
;; You should have received a copy of the GNU General Public License
|
|
|
|
;; along with this program; if not, see <http://www.gnu.org/licenses/>.
|
|
|
|
|
|
|
|
;; Trace displays and returns the given value. A debugging aid.
|
|
|
|
(define (trace x)
|
|
|
|
(display x)
|
|
|
|
(newline)
|
|
|
|
x)
|
|
|
|
|
|
|
|
;; Stringification.
|
|
|
|
(define (stringify expression)
|
|
|
|
(let ((p (open-output-string)))
|
|
|
|
(write expression p)
|
|
|
|
(get-output-string p)))
|
|
|
|
|
|
|
|
;; Reporting.
|
2016-06-21 12:21:10 +02:00
|
|
|
(define (echo . msg)
|
|
|
|
(for-each (lambda (x) (display x) (display " ")) msg)
|
|
|
|
(newline))
|
|
|
|
|
|
|
|
(define (info . msg)
|
|
|
|
(apply echo msg)
|
2016-01-06 11:55:25 +01:00
|
|
|
(flush-stdio))
|
|
|
|
|
2016-11-07 16:21:21 +01:00
|
|
|
(define (log . msg)
|
|
|
|
(if (> (*verbose*) 0)
|
|
|
|
(apply info msg)))
|
|
|
|
|
2016-12-06 15:21:30 +01:00
|
|
|
(define (fail . msg)
|
2016-06-21 12:21:10 +02:00
|
|
|
(apply info msg)
|
2016-01-06 11:55:25 +01:00
|
|
|
(exit 1))
|
|
|
|
|
2016-06-21 12:21:10 +02:00
|
|
|
(define (skip . msg)
|
|
|
|
(apply info msg)
|
2016-01-06 11:55:25 +01:00
|
|
|
(exit 77))
|
|
|
|
|
|
|
|
(define (make-counter)
|
|
|
|
(let ((c 0))
|
|
|
|
(lambda ()
|
|
|
|
(let ((r c))
|
|
|
|
(set! c (+ 1 c))
|
|
|
|
r))))
|
|
|
|
|
|
|
|
(define *progress-nesting* 0)
|
|
|
|
|
|
|
|
(define (call-with-progress msg what)
|
|
|
|
(set! *progress-nesting* (+ 1 *progress-nesting*))
|
|
|
|
(if (= 1 *progress-nesting*)
|
|
|
|
(begin
|
|
|
|
(info msg)
|
|
|
|
(display " > ")
|
|
|
|
(flush-stdio)
|
|
|
|
(what (lambda (item)
|
|
|
|
(display item)
|
|
|
|
(display " ")
|
|
|
|
(flush-stdio)))
|
|
|
|
(info "< "))
|
|
|
|
(begin
|
|
|
|
(what (lambda (item) (display ".") (flush-stdio)))
|
|
|
|
(display " ")
|
|
|
|
(flush-stdio)))
|
|
|
|
(set! *progress-nesting* (- *progress-nesting* 1)))
|
|
|
|
|
2016-12-08 15:39:05 +01:00
|
|
|
(define (for-each-p msg proc lst . lsts)
|
|
|
|
(apply for-each-p' `(,msg ,proc ,(lambda (x . xs) x) ,lst ,@lsts)))
|
2016-01-06 11:55:25 +01:00
|
|
|
|
2016-12-08 15:39:05 +01:00
|
|
|
(define (for-each-p' msg proc fmt lst . lsts)
|
2016-01-06 11:55:25 +01:00
|
|
|
(call-with-progress
|
|
|
|
msg
|
|
|
|
(lambda (progress)
|
2016-12-08 15:39:05 +01:00
|
|
|
(apply for-each
|
|
|
|
`(,(lambda args
|
|
|
|
(progress (apply fmt args))
|
|
|
|
(apply proc args))
|
|
|
|
,lst ,@lsts)))))
|
2016-01-06 11:55:25 +01:00
|
|
|
|
|
|
|
;; Process management.
|
|
|
|
(define CLOSED_FD -1)
|
|
|
|
(define (call-with-fds what infd outfd errfd)
|
|
|
|
(wait-process (stringify what) (spawn-process-fd what infd outfd errfd) #t))
|
|
|
|
(define (call what)
|
|
|
|
(call-with-fds what
|
|
|
|
CLOSED_FD
|
2016-07-26 15:53:50 +02:00
|
|
|
(if (< (*verbose*) 0) STDOUT_FILENO CLOSED_FD)
|
|
|
|
(if (< (*verbose*) 0) STDERR_FILENO CLOSED_FD)))
|
2016-01-06 11:55:25 +01:00
|
|
|
|
|
|
|
;; Accessor functions for the results of 'spawn-process'.
|
|
|
|
(define :stdin car)
|
|
|
|
(define :stdout cadr)
|
|
|
|
(define :stderr caddr)
|
|
|
|
(define :pid cadddr)
|
|
|
|
|
|
|
|
(define (call-with-io what in)
|
|
|
|
(let ((h (spawn-process what 0)))
|
|
|
|
(es-write (:stdin h) in)
|
|
|
|
(es-fclose (:stdin h))
|
|
|
|
(let* ((out (es-read-all (:stdout h)))
|
|
|
|
(err (es-read-all (:stderr h)))
|
|
|
|
(result (wait-process (car what) (:pid h) #t)))
|
|
|
|
(es-fclose (:stdout h))
|
|
|
|
(es-fclose (:stderr h))
|
2016-07-26 15:53:50 +02:00
|
|
|
(if (> (*verbose*) 2)
|
|
|
|
(begin
|
|
|
|
(echo (stringify what) "returned:" result)
|
|
|
|
(echo (stringify what) "wrote to stdout:" out)
|
|
|
|
(echo (stringify what) "wrote to stderr:" err)))
|
2016-01-06 11:55:25 +01:00
|
|
|
(list result out err))))
|
|
|
|
|
|
|
|
;; Accessor function for the results of 'call-with-io'. ':stdout' and
|
|
|
|
;; ':stderr' can also be used.
|
|
|
|
(define :retcode car)
|
|
|
|
|
2016-07-07 16:18:10 +02:00
|
|
|
(define (call-check what)
|
|
|
|
(let ((result (call-with-io what "")))
|
|
|
|
(if (= 0 (:retcode result))
|
|
|
|
(:stdout result)
|
2016-11-18 13:36:23 +01:00
|
|
|
(throw (string-append (stringify what) " failed")
|
|
|
|
(:stderr result)))))
|
2016-07-07 16:18:10 +02:00
|
|
|
|
2016-01-06 11:55:25 +01:00
|
|
|
(define (call-popen command input-string)
|
|
|
|
(let ((result (call-with-io command input-string)))
|
|
|
|
(if (= 0 (:retcode result))
|
|
|
|
(:stdout result)
|
|
|
|
(throw (:stderr result)))))
|
|
|
|
|
|
|
|
;;
|
|
|
|
;; estream helpers.
|
|
|
|
;;
|
|
|
|
|
|
|
|
(define (es-read-all stream)
|
|
|
|
(let loop
|
|
|
|
((acc ""))
|
|
|
|
(if (es-feof stream)
|
|
|
|
acc
|
|
|
|
(loop (string-append acc (es-read stream 4096))))))
|
|
|
|
|
|
|
|
;;
|
|
|
|
;; File management.
|
|
|
|
;;
|
2016-06-21 12:21:10 +02:00
|
|
|
(define (file-exists? name)
|
|
|
|
(call-with-input-file name (lambda (port) #t)))
|
|
|
|
|
2016-01-06 11:55:25 +01:00
|
|
|
(define (file=? a b)
|
|
|
|
(file-equal a b #t))
|
|
|
|
|
|
|
|
(define (text-file=? a b)
|
|
|
|
(file-equal a b #f))
|
|
|
|
|
|
|
|
(define (file-copy from to)
|
|
|
|
(catch '() (unlink to))
|
|
|
|
(letfd ((source (open from (logior O_RDONLY O_BINARY)))
|
|
|
|
(sink (open to (logior O_WRONLY O_CREAT O_BINARY) #o600)))
|
|
|
|
(splice source sink)))
|
|
|
|
|
|
|
|
(define (text-file-copy from to)
|
|
|
|
(catch '() (unlink to))
|
|
|
|
(letfd ((source (open from O_RDONLY))
|
|
|
|
(sink (open to (logior O_WRONLY O_CREAT) #o600)))
|
|
|
|
(splice source sink)))
|
|
|
|
|
2016-07-05 16:25:21 +02:00
|
|
|
(define (path-join . components)
|
|
|
|
(let loop ((acc #f) (rest (filter (lambda (s)
|
|
|
|
(not (string=? "" s))) components)))
|
|
|
|
(if (null? rest)
|
|
|
|
acc
|
|
|
|
(loop (if (string? acc)
|
|
|
|
(string-append acc "/" (car rest))
|
|
|
|
(car rest))
|
|
|
|
(cdr rest)))))
|
|
|
|
(assert (string=? (path-join "foo" "bar" "baz") "foo/bar/baz"))
|
|
|
|
(assert (string=? (path-join "" "bar" "baz") "bar/baz"))
|
|
|
|
|
2016-11-16 12:02:03 +01:00
|
|
|
;; Is PATH an absolute path?
|
|
|
|
(define (absolute-path? path)
|
|
|
|
(or (char=? #\/ (string-ref path 0))
|
|
|
|
(and *win32* (char=? #\\ (string-ref path 0)))
|
|
|
|
(and *win32*
|
|
|
|
(char-alphabetic? (string-ref path 0))
|
|
|
|
(char=? #\: (string-ref path 1))
|
|
|
|
(or (char=? #\/ (string-ref path 2))
|
|
|
|
(char=? #\\ (string-ref path 2))))))
|
|
|
|
|
|
|
|
;; Make PATH absolute.
|
2016-01-06 11:55:25 +01:00
|
|
|
(define (canonical-path path)
|
2016-11-16 12:02:03 +01:00
|
|
|
(if (absolute-path? path) path (path-join (getcwd) path)))
|
2016-01-06 11:55:25 +01:00
|
|
|
|
2016-07-22 17:42:17 +02:00
|
|
|
(define (in-srcdir . names)
|
|
|
|
(canonical-path (apply path-join (cons (getenv "srcdir") names))))
|
2016-01-06 11:55:25 +01:00
|
|
|
|
2016-07-19 16:17:22 +02:00
|
|
|
;; Try to find NAME in PATHS. Returns the full path name on success,
|
|
|
|
;; or raises an error.
|
|
|
|
(define (path-expand name paths)
|
|
|
|
(let loop ((path paths))
|
2016-01-06 11:55:25 +01:00
|
|
|
(if (null? path)
|
2016-07-19 16:17:22 +02:00
|
|
|
(throw "Could not find" name "in" paths)
|
2016-10-07 12:53:25 +02:00
|
|
|
(let* ((qualified-name (path-join (car path) name))
|
2016-01-06 11:55:25 +01:00
|
|
|
(file-exists (call-with-input-file qualified-name
|
|
|
|
(lambda (x) #t))))
|
|
|
|
(if file-exists
|
|
|
|
qualified-name
|
|
|
|
(loop (cdr path)))))))
|
|
|
|
|
2016-07-19 16:17:22 +02:00
|
|
|
;; Expand NAME using the gpgscm load path. Use like this:
|
|
|
|
;; (load (with-path "library.scm"))
|
|
|
|
(define (with-path name)
|
|
|
|
(catch name
|
|
|
|
(path-expand name (string-split (getenv "GPGSCM_PATH") *pathsep*))))
|
|
|
|
|
2016-01-06 11:55:25 +01:00
|
|
|
(define (basename path)
|
|
|
|
(let ((i (string-index path #\/)))
|
|
|
|
(if (equal? i #f)
|
|
|
|
path
|
|
|
|
(basename (substring path (+ 1 i) (string-length path))))))
|
|
|
|
|
2016-06-21 18:12:03 +02:00
|
|
|
(define (basename-suffix path suffix)
|
|
|
|
(basename
|
|
|
|
(if (string-suffix? path suffix)
|
|
|
|
(substring path 0 (- (string-length path) (string-length suffix)))
|
|
|
|
path)))
|
|
|
|
|
2016-01-06 11:55:25 +01:00
|
|
|
;; Helper for (pipe).
|
|
|
|
(define :read-end car)
|
|
|
|
(define :write-end cadr)
|
|
|
|
|
|
|
|
;; let-like macro that manages file descriptors.
|
|
|
|
;;
|
|
|
|
;; (letfd <bindings> <body>)
|
|
|
|
;;
|
|
|
|
;; Bind all variables given in <bindings> and initialize each of them
|
|
|
|
;; to the given initial value, and close them after evaluting <body>.
|
2016-12-22 14:42:50 +01:00
|
|
|
(define-macro (letfd bindings . body)
|
|
|
|
(let bind ((bindings' bindings))
|
|
|
|
(if (null? bindings')
|
|
|
|
`(begin ,@body)
|
|
|
|
(let* ((binding (car bindings'))
|
|
|
|
(name (car binding))
|
|
|
|
(initializer (cadr binding)))
|
|
|
|
`(let ((,name ,initializer))
|
|
|
|
(finally (close ,name)
|
|
|
|
,(bind (cdr bindings'))))))))
|
|
|
|
|
|
|
|
(define-macro (with-working-directory new-directory . expressions)
|
|
|
|
(let ((new-dir (gensym))
|
|
|
|
(old-dir (gensym)))
|
|
|
|
`(let* ((,new-dir ,new-directory)
|
|
|
|
(,old-dir (getcwd)))
|
|
|
|
(dynamic-wind
|
|
|
|
(lambda () (if ,new-dir (chdir ,new-dir)))
|
|
|
|
(lambda () ,@expressions)
|
|
|
|
(lambda () (chdir ,old-dir))))))
|
2016-01-06 11:55:25 +01:00
|
|
|
|
2016-08-10 11:54:11 +02:00
|
|
|
;; Make a temporary directory. If arguments are given, they are
|
|
|
|
;; joined using path-join, and must end in a component ending in
|
|
|
|
;; "XXXXXX". If no arguments are given, a suitable location and
|
2017-03-06 17:16:41 +01:00
|
|
|
;; generic name is used. Returns an absolute path.
|
2016-08-10 11:54:11 +02:00
|
|
|
(define (mkdtemp . components)
|
2017-03-06 17:16:41 +01:00
|
|
|
(canonical-path (_mkdtemp (if (null? components)
|
2017-03-21 13:15:38 +01:00
|
|
|
(path-join
|
2017-03-21 15:52:47 +01:00
|
|
|
(get-temp-path)
|
2017-03-21 13:15:38 +01:00
|
|
|
(string-append "gpgscm-" (get-isotime) "-"
|
|
|
|
(basename-suffix *scriptname* ".scm")
|
|
|
|
"-XXXXXX"))
|
2017-03-06 17:16:41 +01:00
|
|
|
(apply path-join components)))))
|
2016-08-10 11:54:11 +02:00
|
|
|
|
2017-03-23 10:55:34 +01:00
|
|
|
;; Make a temporary directory and remove it at interpreter shutdown.
|
|
|
|
;; Note that there are macros that limit the lifetime of temporary
|
|
|
|
;; directories and files to a lexical scope. Use those if possible.
|
|
|
|
;; Otherwise this works like mkdtemp.
|
|
|
|
(define (mkdtemp-autoremove . components)
|
|
|
|
(let ((dir (apply mkdtemp components)))
|
|
|
|
(atexit (lambda () (unlink-recursively dir)))
|
|
|
|
dir))
|
|
|
|
|
2016-12-22 14:42:50 +01:00
|
|
|
(define-macro (with-temporary-working-directory . expressions)
|
|
|
|
(let ((tmp-sym (gensym)))
|
|
|
|
`(let* ((,tmp-sym (mkdtemp)))
|
|
|
|
(finally (unlink-recursively ,tmp-sym)
|
|
|
|
(with-working-directory ,tmp-sym
|
|
|
|
,@expressions)))))
|
2016-01-06 11:55:25 +01:00
|
|
|
|
|
|
|
(define (make-temporary-file . args)
|
2016-07-05 16:25:21 +02:00
|
|
|
(canonical-path (path-join
|
2016-08-10 11:54:11 +02:00
|
|
|
(mkdtemp)
|
2016-07-05 16:25:21 +02:00
|
|
|
(if (null? args) "a" (car args)))))
|
2016-01-06 11:55:25 +01:00
|
|
|
|
|
|
|
(define (remove-temporary-file filename)
|
|
|
|
(catch '()
|
|
|
|
(unlink filename))
|
|
|
|
(let ((dirname (substring filename 0 (string-rindex filename #\/))))
|
|
|
|
(catch (echo "removing temporary directory" dirname "failed")
|
|
|
|
(rmdir dirname))))
|
|
|
|
|
|
|
|
;; let-like macro that manages temporary files.
|
|
|
|
;;
|
|
|
|
;; (lettmp <bindings> <body>)
|
|
|
|
;;
|
|
|
|
;; Bind all variables given in <bindings>, initialize each of them to
|
|
|
|
;; a string representing an unique path in the filesystem, and delete
|
|
|
|
;; them after evaluting <body>.
|
2016-12-22 14:42:50 +01:00
|
|
|
(define-macro (lettmp bindings . body)
|
|
|
|
(let bind ((bindings' bindings))
|
|
|
|
(if (null? bindings')
|
|
|
|
`(begin ,@body)
|
|
|
|
(let ((name (car bindings'))
|
|
|
|
(rest (cdr bindings')))
|
|
|
|
`(let ((,name (make-temporary-file ,(symbol->string name))))
|
|
|
|
(finally (remove-temporary-file ,name)
|
|
|
|
,(bind rest)))))))
|
2016-01-06 11:55:25 +01:00
|
|
|
|
|
|
|
(define (check-execution source transformer)
|
|
|
|
(lettmp (sink)
|
|
|
|
(transformer source sink)))
|
|
|
|
|
|
|
|
(define (check-identity source transformer)
|
|
|
|
(lettmp (sink)
|
|
|
|
(transformer source sink)
|
|
|
|
(if (not (file=? source sink))
|
2016-12-06 15:21:30 +01:00
|
|
|
(fail "mismatch"))))
|
2016-01-06 11:55:25 +01:00
|
|
|
|
|
|
|
;;
|
|
|
|
;; Monadic pipe support.
|
|
|
|
;;
|
|
|
|
|
|
|
|
(define pipeM
|
|
|
|
(package
|
|
|
|
(define (new procs source sink producer)
|
|
|
|
(package
|
|
|
|
(define (dump)
|
|
|
|
(write (list procs source sink producer))
|
|
|
|
(newline))
|
|
|
|
(define (add-proc command pid)
|
|
|
|
(new (cons (list command pid) procs) source sink producer))
|
|
|
|
(define (commands)
|
|
|
|
(map car procs))
|
|
|
|
(define (pids)
|
|
|
|
(map cadr procs))
|
|
|
|
(define (set-source source')
|
|
|
|
(new procs source' sink producer))
|
|
|
|
(define (set-sink sink')
|
|
|
|
(new procs source sink' producer))
|
|
|
|
(define (set-producer producer')
|
|
|
|
(if producer
|
|
|
|
(throw "producer already set"))
|
|
|
|
(new procs source sink producer'))))))
|
|
|
|
|
|
|
|
|
|
|
|
(define (pipe:do . commands)
|
|
|
|
(let loop ((M (pipeM::new '() CLOSED_FD CLOSED_FD #f)) (cmds commands))
|
|
|
|
(if (null? cmds)
|
|
|
|
(begin
|
|
|
|
(if M::producer (M::producer))
|
|
|
|
(if (not (null? M::procs))
|
|
|
|
(let* ((retcodes (wait-processes (map stringify (M::commands))
|
|
|
|
(M::pids) #t))
|
|
|
|
(results (map (lambda (p r) (append p (list r)))
|
|
|
|
M::procs retcodes))
|
|
|
|
(failed (filter (lambda (x) (not (= 0 (caddr x))))
|
|
|
|
results)))
|
|
|
|
(if (not (null? failed))
|
|
|
|
(throw failed))))) ; xxx nicer reporting
|
|
|
|
(if (and (= 2 (length cmds)) (number? (cadr cmds)))
|
|
|
|
;; hack: if it's an fd, use it as sink
|
|
|
|
(let ((M' ((car cmds) (M::set-sink (cadr cmds)))))
|
|
|
|
(if (> M::source 2) (close M::source))
|
|
|
|
(if (> (cadr cmds) 2) (close (cadr cmds)))
|
|
|
|
(loop M' '()))
|
|
|
|
(let ((M' ((car cmds) M)))
|
|
|
|
(if (> M::source 2) (close M::source))
|
|
|
|
(loop M' (cdr cmds)))))))
|
|
|
|
|
|
|
|
(define (pipe:open pathname flags)
|
|
|
|
(lambda (M)
|
|
|
|
(M::set-source (open pathname flags))))
|
|
|
|
|
|
|
|
(define (pipe:defer producer)
|
|
|
|
(lambda (M)
|
|
|
|
(let* ((p (outbound-pipe))
|
|
|
|
(M' (M::set-source (:read-end p))))
|
|
|
|
(M'::set-producer (lambda ()
|
|
|
|
(producer (:write-end p))
|
|
|
|
(close (:write-end p)))))))
|
|
|
|
(define (pipe:echo data)
|
|
|
|
(pipe:defer (lambda (sink) (display data (fdopen sink "wb")))))
|
|
|
|
|
|
|
|
(define (pipe:spawn command)
|
|
|
|
(lambda (M)
|
|
|
|
(define (do-spawn M new-source)
|
|
|
|
(let ((pid (spawn-process-fd command M::source M::sink
|
2016-07-26 15:53:50 +02:00
|
|
|
(if (> (*verbose*) 0)
|
2016-01-06 11:55:25 +01:00
|
|
|
STDERR_FILENO CLOSED_FD)))
|
|
|
|
(M' (M::set-source new-source)))
|
|
|
|
(M'::add-proc command pid)))
|
|
|
|
(if (= CLOSED_FD M::sink)
|
|
|
|
(let* ((p (pipe))
|
|
|
|
(M' (do-spawn (M::set-sink (:write-end p)) (:read-end p))))
|
|
|
|
(close (:write-end p))
|
|
|
|
(M'::set-sink CLOSED_FD))
|
|
|
|
(do-spawn M CLOSED_FD))))
|
|
|
|
|
|
|
|
(define (pipe:splice sink)
|
|
|
|
(lambda (M)
|
|
|
|
(splice M::source sink)
|
|
|
|
(M::set-source CLOSED_FD)))
|
|
|
|
|
|
|
|
(define (pipe:write-to pathname flags mode)
|
|
|
|
(open pathname flags mode))
|
|
|
|
|
|
|
|
;;
|
|
|
|
;; Monadic transformer support.
|
|
|
|
;;
|
|
|
|
|
|
|
|
(define (tr:do . commands)
|
|
|
|
(let loop ((tmpfiles '()) (source #f) (cmds commands))
|
|
|
|
(if (null? cmds)
|
|
|
|
(for-each remove-temporary-file tmpfiles)
|
2016-06-23 17:18:13 +02:00
|
|
|
(let* ((v ((car cmds) tmpfiles source))
|
|
|
|
(tmpfiles' (car v))
|
|
|
|
(sink (cadr v))
|
|
|
|
(error (caddr v)))
|
|
|
|
(if error
|
|
|
|
(begin
|
|
|
|
(for-each remove-temporary-file tmpfiles')
|
2016-09-19 17:19:00 +02:00
|
|
|
(apply throw error)))
|
2016-06-23 17:18:13 +02:00
|
|
|
(loop tmpfiles' sink (cdr cmds))))))
|
2016-01-06 11:55:25 +01:00
|
|
|
|
|
|
|
(define (tr:open pathname)
|
|
|
|
(lambda (tmpfiles source)
|
2016-06-23 17:18:13 +02:00
|
|
|
(list tmpfiles pathname #f)))
|
2016-01-06 11:55:25 +01:00
|
|
|
|
|
|
|
(define (tr:spawn input command)
|
|
|
|
(lambda (tmpfiles source)
|
2016-06-21 12:21:10 +02:00
|
|
|
(if (and (member '**in** command) (not source))
|
2016-12-06 15:21:30 +01:00
|
|
|
(fail (string-append (stringify cmd) " needs an input")))
|
2016-01-06 11:55:25 +01:00
|
|
|
(let* ((t (make-temporary-file))
|
|
|
|
(cmd (map (lambda (x)
|
|
|
|
(cond
|
|
|
|
((equal? '**in** x) source)
|
|
|
|
((equal? '**out** x) t)
|
|
|
|
(else x))) command)))
|
2016-06-23 17:18:13 +02:00
|
|
|
(catch (list (cons t tmpfiles) t *error*)
|
|
|
|
(call-popen cmd input)
|
|
|
|
(if (and (member '**out** command) (not (file-exists? t)))
|
2016-12-06 15:21:30 +01:00
|
|
|
(fail (string-append (stringify cmd)
|
2016-06-23 17:18:13 +02:00
|
|
|
" did not produce '" t "'.")))
|
|
|
|
(list (cons t tmpfiles) t #f)))))
|
2016-01-06 11:55:25 +01:00
|
|
|
|
|
|
|
(define (tr:write-to pathname)
|
|
|
|
(lambda (tmpfiles source)
|
|
|
|
(rename source pathname)
|
2016-06-23 17:18:13 +02:00
|
|
|
(list tmpfiles pathname #f)))
|
2016-01-06 11:55:25 +01:00
|
|
|
|
|
|
|
(define (tr:pipe-do . commands)
|
|
|
|
(lambda (tmpfiles source)
|
|
|
|
(let ((t (make-temporary-file)))
|
|
|
|
(apply pipe:do
|
|
|
|
`(,@(if source `(,(pipe:open source (logior O_RDONLY O_BINARY))) '())
|
|
|
|
,@commands
|
|
|
|
,(pipe:write-to t (logior O_WRONLY O_BINARY O_CREAT) #o600)))
|
2016-06-23 17:18:13 +02:00
|
|
|
(list (cons t tmpfiles) t #f))))
|
2016-01-06 11:55:25 +01:00
|
|
|
|
|
|
|
(define (tr:assert-identity reference)
|
|
|
|
(lambda (tmpfiles source)
|
|
|
|
(if (not (file=? source reference))
|
2016-12-06 15:21:30 +01:00
|
|
|
(fail "mismatch"))
|
2016-06-23 17:18:13 +02:00
|
|
|
(list tmpfiles source #f)))
|
2016-01-06 11:55:25 +01:00
|
|
|
|
|
|
|
(define (tr:assert-weak-identity reference)
|
|
|
|
(lambda (tmpfiles source)
|
|
|
|
(if (not (text-file=? source reference))
|
2016-12-06 15:21:30 +01:00
|
|
|
(fail "mismatch"))
|
2016-06-23 17:18:13 +02:00
|
|
|
(list tmpfiles source #f)))
|
2016-01-06 11:55:25 +01:00
|
|
|
|
2016-06-21 12:21:10 +02:00
|
|
|
(define (tr:call-with-content function . args)
|
2016-01-06 11:55:25 +01:00
|
|
|
(lambda (tmpfiles source)
|
2016-06-23 17:18:13 +02:00
|
|
|
(catch (list tmpfiles source *error*)
|
|
|
|
(apply function `(,(call-with-input-file source read-all) ,@args)))
|
|
|
|
(list tmpfiles source #f)))
|
2016-11-03 14:37:15 +01:00
|
|
|
|
|
|
|
;;
|
|
|
|
;; Developing and debugging tests.
|
|
|
|
;;
|
|
|
|
|
|
|
|
;; Spawn an os shell.
|
|
|
|
(define (interactive-shell)
|
2016-12-06 12:13:22 +01:00
|
|
|
(call-with-fds `(,(getenv "SHELL") -i) 0 1 2))
|
2016-11-16 12:32:17 +01:00
|
|
|
|
|
|
|
;;
|
|
|
|
;; The main test framework.
|
|
|
|
;;
|
|
|
|
|
|
|
|
;; A pool of tests.
|
|
|
|
(define test-pool
|
|
|
|
(package
|
|
|
|
(define (new procs)
|
|
|
|
(package
|
|
|
|
(define (add test)
|
|
|
|
(new (cons test procs)))
|
|
|
|
(define (wait)
|
|
|
|
(let ((unfinished (filter (lambda (t) (not t::retcode)) procs)))
|
|
|
|
(if (null? unfinished)
|
|
|
|
(package)
|
|
|
|
(let* ((names (map (lambda (t) t::name) unfinished))
|
|
|
|
(pids (map (lambda (t) t::pid) unfinished))
|
|
|
|
(results
|
|
|
|
(map (lambda (pid retcode) (list pid retcode))
|
|
|
|
pids
|
|
|
|
(wait-processes (map stringify names) pids #t))))
|
|
|
|
(new
|
|
|
|
(map (lambda (t)
|
|
|
|
(if t::retcode
|
|
|
|
t
|
|
|
|
(t::set-retcode (cadr (assoc t::pid results)))))
|
|
|
|
procs))))))
|
|
|
|
(define (passed)
|
|
|
|
(filter (lambda (p) (= 0 p::retcode)) procs))
|
|
|
|
(define (skipped)
|
|
|
|
(filter (lambda (p) (= 77 p::retcode)) procs))
|
|
|
|
(define (hard-errored)
|
|
|
|
(filter (lambda (p) (= 99 p::retcode)) procs))
|
|
|
|
(define (failed)
|
|
|
|
(filter (lambda (p)
|
|
|
|
(not (or (= 0 p::retcode) (= 77 p::retcode)
|
|
|
|
(= 99 p::retcode))))
|
|
|
|
procs))
|
|
|
|
(define (report)
|
2016-11-17 13:12:38 +01:00
|
|
|
(define (print-tests tests message)
|
|
|
|
(unless (null? tests)
|
|
|
|
(apply echo (cons message
|
|
|
|
(map (lambda (t) t::name) tests)))))
|
|
|
|
|
|
|
|
(let ((failed' (failed)) (skipped' (skipped)))
|
|
|
|
(echo (length procs) "tests run,"
|
|
|
|
(length (passed)) "succeeded,"
|
|
|
|
(length failed') "failed,"
|
|
|
|
(length skipped') "skipped.")
|
|
|
|
(print-tests failed' "Failed tests:")
|
|
|
|
(print-tests skipped' "Skipped tests:")
|
|
|
|
(length failed')))))))
|
2016-11-16 12:32:17 +01:00
|
|
|
|
|
|
|
(define (verbosity n)
|
|
|
|
(if (= 0 n) '() (cons '--verbose (verbosity (- n 1)))))
|
|
|
|
|
|
|
|
(define (locate-test path)
|
|
|
|
(if (absolute-path? path) path (in-srcdir path)))
|
|
|
|
|
|
|
|
;; A single test.
|
|
|
|
(define test
|
|
|
|
(package
|
2017-03-09 13:26:06 +01:00
|
|
|
(define (scm setup name path . args)
|
2016-11-16 12:32:17 +01:00
|
|
|
;; Start the process.
|
2016-11-17 11:06:42 +01:00
|
|
|
(define (spawn-scm args' in out err)
|
2016-11-16 12:32:17 +01:00
|
|
|
(spawn-process-fd `(,*argv0* ,@(verbosity (*verbose*))
|
2016-11-17 11:06:42 +01:00
|
|
|
,(locate-test path)
|
2017-03-09 13:26:06 +01:00
|
|
|
,@(if setup (force setup) '())
|
2016-11-17 11:06:42 +01:00
|
|
|
,@args' ,@args) in out err))
|
|
|
|
(new name #f spawn-scm #f #f CLOSED_FD))
|
2016-11-16 12:32:17 +01:00
|
|
|
|
2017-03-09 13:26:06 +01:00
|
|
|
(define (binary setup name path . args)
|
2016-11-16 12:32:17 +01:00
|
|
|
;; Start the process.
|
2016-11-17 11:06:42 +01:00
|
|
|
(define (spawn-binary args' in out err)
|
2017-03-09 13:26:06 +01:00
|
|
|
(spawn-process-fd `(,path ,@(if setup (force setup) '()) ,@args' ,@args)
|
|
|
|
in out err))
|
2016-11-17 11:06:42 +01:00
|
|
|
(new name #f spawn-binary #f #f CLOSED_FD))
|
2016-11-16 12:32:17 +01:00
|
|
|
|
|
|
|
(define (new name directory spawn pid retcode logfd)
|
|
|
|
(package
|
|
|
|
(define (set-directory x)
|
|
|
|
(new name x spawn pid retcode logfd))
|
|
|
|
(define (set-retcode x)
|
|
|
|
(new name directory spawn pid x logfd))
|
|
|
|
(define (set-pid x)
|
|
|
|
(new name directory spawn x retcode logfd))
|
|
|
|
(define (set-logfd x)
|
|
|
|
(new name directory spawn pid retcode x))
|
|
|
|
(define (open-log-file)
|
|
|
|
(let ((filename (string-append (basename name) ".log")))
|
|
|
|
(catch '() (unlink filename))
|
|
|
|
(open filename (logior O_RDWR O_BINARY O_CREAT) #o600)))
|
|
|
|
(define (run-sync . args)
|
|
|
|
(letfd ((log (open-log-file)))
|
|
|
|
(with-working-directory directory
|
|
|
|
(let* ((p (inbound-pipe))
|
|
|
|
(pid (spawn args 0 (:write-end p) (:write-end p))))
|
|
|
|
(close (:write-end p))
|
|
|
|
(splice (:read-end p) STDERR_FILENO log)
|
|
|
|
(close (:read-end p))
|
|
|
|
(let ((t' (set-retcode (wait-process name pid #t))))
|
|
|
|
(t'::report)
|
|
|
|
t')))))
|
|
|
|
(define (run-sync-quiet . args)
|
|
|
|
(with-working-directory directory
|
|
|
|
(set-retcode
|
|
|
|
(wait-process
|
|
|
|
name (spawn args CLOSED_FD CLOSED_FD CLOSED_FD) #t))))
|
|
|
|
(define (run-async . args)
|
|
|
|
(let ((log (open-log-file)))
|
|
|
|
(with-working-directory directory
|
|
|
|
(new name directory spawn
|
|
|
|
(spawn args CLOSED_FD log log)
|
|
|
|
retcode log))))
|
|
|
|
(define (status)
|
|
|
|
(let ((t (assoc retcode '((0 "PASS") (77 "SKIP") (99 "ERROR")))))
|
|
|
|
(if (not t) "FAIL" (cadr t))))
|
|
|
|
(define (report)
|
|
|
|
(unless (= logfd CLOSED_FD)
|
|
|
|
(seek logfd 0 SEEK_SET)
|
|
|
|
(splice logfd STDERR_FILENO)
|
|
|
|
(close logfd))
|
2016-12-22 15:48:07 +01:00
|
|
|
(echo (string-append (status) ":") name))))))
|
2016-11-16 12:32:17 +01:00
|
|
|
|
|
|
|
;; Run the setup target to create an environment, then run all given
|
|
|
|
;; tests in parallel.
|
2017-03-09 13:26:06 +01:00
|
|
|
(define (run-tests-parallel tests)
|
|
|
|
(let loop ((pool (test-pool::new '())) (tests' tests))
|
|
|
|
(if (null? tests')
|
|
|
|
(let ((results (pool::wait)))
|
2017-03-23 10:55:34 +01:00
|
|
|
(for-each (lambda (t) (t::report)) (reverse results::procs))
|
2017-03-09 13:26:06 +01:00
|
|
|
(exit (results::report)))
|
2017-03-23 10:55:34 +01:00
|
|
|
(let* ((wd (mkdtemp-autoremove))
|
2017-03-09 13:26:06 +01:00
|
|
|
(test (car tests'))
|
|
|
|
(test' (test::set-directory wd)))
|
|
|
|
(loop (pool::add (test'::run-async))
|
|
|
|
(cdr tests'))))))
|
2016-11-16 12:32:17 +01:00
|
|
|
|
|
|
|
;; Run the setup target to create an environment, then run all given
|
|
|
|
;; tests in sequence.
|
2017-03-09 13:26:06 +01:00
|
|
|
(define (run-tests-sequential tests)
|
|
|
|
(let loop ((pool (test-pool::new '())) (tests' tests))
|
|
|
|
(if (null? tests')
|
|
|
|
(let ((results (pool::wait)))
|
|
|
|
(exit (results::report)))
|
2017-03-23 10:55:34 +01:00
|
|
|
(let* ((wd (mkdtemp-autoremove))
|
2017-03-09 13:26:06 +01:00
|
|
|
(test (car tests'))
|
|
|
|
(test' (test::set-directory wd)))
|
|
|
|
(loop (pool::add (test'::run-sync))
|
|
|
|
(cdr tests'))))))
|
|
|
|
|
|
|
|
;; Helper to create environment caches from test functions. SETUP
|
|
|
|
;; must be a test implementing the producer side cache protocol.
|
|
|
|
;; Returns a promise containing the arguments that must be passed to a
|
|
|
|
;; test implementing the consumer side of the cache protocol.
|
|
|
|
(define (make-environment-cache setup)
|
2017-03-23 10:55:34 +01:00
|
|
|
(delay (with-temporary-working-directory
|
|
|
|
(let ((tarball (make-temporary-file "environment-cache")))
|
|
|
|
(atexit (lambda () (remove-temporary-file tarball)))
|
|
|
|
(setup::run-sync '--create-tarball tarball)
|
|
|
|
`(--unpack-tarball ,tarball)))))
|
2016-12-20 14:01:35 +01:00
|
|
|
|
|
|
|
;; Command line flag handling. Returns the elements following KEY in
|
|
|
|
;; ARGUMENTS up to the next argument, or #f if KEY is not in
|
|
|
|
;; ARGUMENTS.
|
|
|
|
(define (flag key arguments)
|
|
|
|
(cond
|
|
|
|
((null? arguments)
|
|
|
|
#f)
|
|
|
|
((string=? key (car arguments))
|
|
|
|
(let loop ((acc '())
|
|
|
|
(args (cdr arguments)))
|
|
|
|
(if (or (null? args) (string-prefix? (car args) "--"))
|
|
|
|
(reverse acc)
|
|
|
|
(loop (cons (car args) acc) (cdr args)))))
|
|
|
|
((string=? "--" (car arguments))
|
|
|
|
#f)
|
|
|
|
(else
|
|
|
|
(flag key (cdr arguments)))))
|
|
|
|
(assert (equal? (flag "--xxx" '("--yyy")) #f))
|
|
|
|
(assert (equal? (flag "--xxx" '("--xxx")) '()))
|
|
|
|
(assert (equal? (flag "--xxx" '("--xxx" "yyy")) '("yyy")))
|
|
|
|
(assert (equal? (flag "--xxx" '("--xxx" "yyy" "zzz")) '("yyy" "zzz")))
|
|
|
|
(assert (equal? (flag "--xxx" '("--xxx" "yyy" "zzz" "--")) '("yyy" "zzz")))
|
|
|
|
(assert (equal? (flag "--xxx" '("--xxx" "yyy" "--" "zzz")) '("yyy")))
|
|
|
|
(assert (equal? (flag "--" '("--" "xxx" "yyy" "--" "zzz")) '("xxx" "yyy")))
|