1 ;; Common definitions for writing tests.
3 ;; Copyright (C) 2016 g10 Code GmbH
5 ;; This file is part of GnuPG.
7 ;; GnuPG is free software; you can redistribute it and/or modify
8 ;; it under the terms of the GNU General Public License as published by
9 ;; the Free Software Foundation; either version 3 of the License, or
10 ;; (at your option) any later version.
12 ;; GnuPG is distributed in the hope that it will be useful,
13 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
14 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 ;; GNU General Public License for more details.
17 ;; You should have received a copy of the GNU General Public License
18 ;; along with this program; if not, see <http://www.gnu.org/licenses/>.
20 ;; Trace displays and returns the given value. A debugging aid.
27 (define (stringify expression)
28 (let ((p (open-output-string)))
30 (get-output-string p)))
34 (for-each (lambda (x) (display x) (display " ")) msg)
53 (define (make-counter)
60 (define *progress-nesting* 0)
62 (define (call-with-progress msg what)
63 (set! *progress-nesting* (+ 1 *progress-nesting*))
64 (if (= 1 *progress-nesting*)
75 (what (lambda (item) (display ".") (flush-stdio)))
78 (set! *progress-nesting* (- *progress-nesting* 1)))
80 (define (for-each-p msg proc lst . lsts)
81 (apply for-each-p' `(,msg ,proc ,(lambda (x . xs) x) ,lst ,@lsts)))
83 (define (for-each-p' msg proc fmt lst . lsts)
89 (progress (apply fmt args))
93 ;; Process management.
95 (define (call-with-fds what infd outfd errfd)
96 (wait-process (stringify what) (spawn-process-fd what infd outfd errfd) #t))
100 (if (< (*verbose*) 0) STDOUT_FILENO CLOSED_FD)
101 (if (< (*verbose*) 0) STDERR_FILENO CLOSED_FD)))
103 ;; Accessor functions for the results of 'spawn-process'.
105 (define :stdout cadr)
106 (define :stderr caddr)
109 (define (call-with-io what in)
110 (let ((h (spawn-process what 0)))
111 (es-write (:stdin h) in)
112 (es-fclose (:stdin h))
113 (let* ((out (es-read-all (:stdout h)))
114 (err (es-read-all (:stderr h)))
115 (result (wait-process (car what) (:pid h) #t)))
116 (es-fclose (:stdout h))
117 (es-fclose (:stderr h))
118 (if (> (*verbose*) 2)
120 (echo (stringify what) "returned:" result)
121 (echo (stringify what) "wrote to stdout:" out)
122 (echo (stringify what) "wrote to stderr:" err)))
123 (list result out err))))
125 ;; Accessor function for the results of 'call-with-io'. ':stdout' and
126 ;; ':stderr' can also be used.
127 (define :retcode car)
129 (define (call-check what)
130 (let ((result (call-with-io what "")))
131 (if (= 0 (:retcode result))
133 (throw (string-append (stringify what) " failed")
136 (define (call-popen command input-string)
137 (let ((result (call-with-io command input-string)))
138 (if (= 0 (:retcode result))
140 (throw (:stderr result)))))
146 (define (es-read-all stream)
151 (loop (string-append acc (es-read stream 4096))))))
156 (define (file-exists? name)
157 (call-with-input-file name (lambda (port) #t)))
162 (define (text-file=? a b)
165 (define (file-copy from to)
166 (catch '() (unlink to))
167 (letfd ((source (open from (logior O_RDONLY O_BINARY)))
168 (sink (open to (logior O_WRONLY O_CREAT O_BINARY) #o600)))
169 (splice source sink)))
171 (define (text-file-copy from to)
172 (catch '() (unlink to))
173 (letfd ((source (open from O_RDONLY))
174 (sink (open to (logior O_WRONLY O_CREAT) #o600)))
175 (splice source sink)))
177 (define (path-join . components)
178 (let loop ((acc #f) (rest (filter (lambda (s)
179 (not (string=? "" s))) components)))
182 (loop (if (string? acc)
183 (string-append acc "/" (car rest))
186 (assert (string=? (path-join "foo" "bar" "baz") "foo/bar/baz"))
187 (assert (string=? (path-join "" "bar" "baz") "bar/baz"))
189 ;; Is PATH an absolute path?
190 (define (absolute-path? path)
191 (or (char=? #\/ (string-ref path 0))
192 (and *win32* (char=? #\\ (string-ref path 0)))
194 (char-alphabetic? (string-ref path 0))
195 (char=? #\: (string-ref path 1))
196 (or (char=? #\/ (string-ref path 2))
197 (char=? #\\ (string-ref path 2))))))
199 ;; Make PATH absolute.
200 (define (canonical-path path)
201 (if (absolute-path? path) path (path-join (getcwd) path)))
203 (define (in-srcdir . names)
204 (canonical-path (apply path-join (cons (getenv "srcdir") names))))
206 ;; Try to find NAME in PATHS. Returns the full path name on success,
207 ;; or raises an error.
208 (define (path-expand name paths)
209 (let loop ((path paths))
211 (throw "Could not find" name "in" paths)
212 (let* ((qualified-name (path-join (car path) name))
213 (file-exists (call-with-input-file qualified-name
217 (loop (cdr path)))))))
219 ;; Expand NAME using the gpgscm load path. Use like this:
220 ;; (load (with-path "library.scm"))
221 (define (with-path name)
223 (path-expand name (string-split (getenv "GPGSCM_PATH") *pathsep*))))
225 (define (basename path)
226 (let ((i (string-index path #\/)))
229 (basename (substring path (+ 1 i) (string-length path))))))
231 (define (basename-suffix path suffix)
233 (if (string-suffix? path suffix)
234 (substring path 0 (- (string-length path) (string-length suffix)))
237 ;; Helper for (pipe).
238 (define :read-end car)
239 (define :write-end cadr)
241 ;; let-like macro that manages file descriptors.
243 ;; (letfd <bindings> <body>)
245 ;; Bind all variables given in <bindings> and initialize each of them
246 ;; to the given initial value, and close them after evaluting <body>.
248 (let ((result-sym (gensym)))
249 `((lambda (,(caaadr form))
251 ,(if (= 1 (length (cadr form)))
252 `(catch (begin (close ,(caaadr form))
255 `(letfd ,(cdadr form) ,@(cddr form)))))
256 (close ,(caaadr form))
257 ,result-sym)) ,@(cdaadr form))))
259 (macro (with-working-directory form)
260 (let ((result-sym (gensym)) (cwd-sym (gensym)))
261 `(let* ((,cwd-sym (getcwd))
262 (_ (if ,(cadr form) (chdir ,(cadr form))))
263 (,result-sym (catch (begin (chdir ,cwd-sym)
269 ;; Make a temporary directory. If arguments are given, they are
270 ;; joined using path-join, and must end in a component ending in
271 ;; "XXXXXX". If no arguments are given, a suitable location and
272 ;; generic name is used.
273 (define (mkdtemp . components)
274 (_mkdtemp (if (null? components)
275 (path-join (getenv "TMP")
276 (string-append "gpgscm-" (get-isotime) "-"
277 (basename-suffix *scriptname* ".scm")
279 (apply path-join components))))
281 (macro (with-temporary-working-directory form)
282 (let ((result-sym (gensym)) (cwd-sym (gensym)) (tmp-sym (gensym)))
283 `(let* ((,cwd-sym (getcwd))
286 (,result-sym (catch (begin (chdir ,cwd-sym)
287 (unlink-recursively ,tmp-sym)
291 (unlink-recursively ,tmp-sym)
294 (define (make-temporary-file . args)
295 (canonical-path (path-join
297 (if (null? args) "a" (car args)))))
299 (define (remove-temporary-file filename)
302 (let ((dirname (substring filename 0 (string-rindex filename #\/))))
303 (catch (echo "removing temporary directory" dirname "failed")
306 ;; let-like macro that manages temporary files.
308 ;; (lettmp <bindings> <body>)
310 ;; Bind all variables given in <bindings>, initialize each of them to
311 ;; a string representing an unique path in the filesystem, and delete
312 ;; them after evaluting <body>.
314 (let ((result-sym (gensym)))
315 `((lambda (,(caadr form))
317 ,(if (= 1 (length (cadr form)))
318 `(catch (begin (remove-temporary-file ,(caadr form))
321 `(lettmp ,(cdadr form) ,@(cddr form)))))
322 (remove-temporary-file ,(caadr form))
323 ,result-sym)) (make-temporary-file ,(symbol->string (caadr form))))))
325 (define (check-execution source transformer)
327 (transformer source sink)))
329 (define (check-identity source transformer)
331 (transformer source sink)
332 (if (not (file=? source sink))
336 ;; Monadic pipe support.
341 (define (new procs source sink producer)
344 (write (list procs source sink producer))
346 (define (add-proc command pid)
347 (new (cons (list command pid) procs) source sink producer))
352 (define (set-source source')
353 (new procs source' sink producer))
354 (define (set-sink sink')
355 (new procs source sink' producer))
356 (define (set-producer producer')
358 (throw "producer already set"))
359 (new procs source sink producer'))))))
362 (define (pipe:do . commands)
363 (let loop ((M (pipeM::new '() CLOSED_FD CLOSED_FD #f)) (cmds commands))
366 (if M::producer (M::producer))
367 (if (not (null? M::procs))
368 (let* ((retcodes (wait-processes (map stringify (M::commands))
370 (results (map (lambda (p r) (append p (list r)))
372 (failed (filter (lambda (x) (not (= 0 (caddr x))))
374 (if (not (null? failed))
375 (throw failed))))) ; xxx nicer reporting
376 (if (and (= 2 (length cmds)) (number? (cadr cmds)))
377 ;; hack: if it's an fd, use it as sink
378 (let ((M' ((car cmds) (M::set-sink (cadr cmds)))))
379 (if (> M::source 2) (close M::source))
380 (if (> (cadr cmds) 2) (close (cadr cmds)))
382 (let ((M' ((car cmds) M)))
383 (if (> M::source 2) (close M::source))
384 (loop M' (cdr cmds)))))))
386 (define (pipe:open pathname flags)
388 (M::set-source (open pathname flags))))
390 (define (pipe:defer producer)
392 (let* ((p (outbound-pipe))
393 (M' (M::set-source (:read-end p))))
394 (M'::set-producer (lambda ()
395 (producer (:write-end p))
396 (close (:write-end p)))))))
397 (define (pipe:echo data)
398 (pipe:defer (lambda (sink) (display data (fdopen sink "wb")))))
400 (define (pipe:spawn command)
402 (define (do-spawn M new-source)
403 (let ((pid (spawn-process-fd command M::source M::sink
404 (if (> (*verbose*) 0)
405 STDERR_FILENO CLOSED_FD)))
406 (M' (M::set-source new-source)))
407 (M'::add-proc command pid)))
408 (if (= CLOSED_FD M::sink)
410 (M' (do-spawn (M::set-sink (:write-end p)) (:read-end p))))
411 (close (:write-end p))
412 (M'::set-sink CLOSED_FD))
413 (do-spawn M CLOSED_FD))))
415 (define (pipe:splice sink)
417 (splice M::source sink)
418 (M::set-source CLOSED_FD)))
420 (define (pipe:write-to pathname flags mode)
421 (open pathname flags mode))
424 ;; Monadic transformer support.
427 (define (tr:do . commands)
428 (let loop ((tmpfiles '()) (source #f) (cmds commands))
430 (for-each remove-temporary-file tmpfiles)
431 (let* ((v ((car cmds) tmpfiles source))
437 (for-each remove-temporary-file tmpfiles')
438 (apply throw error)))
439 (loop tmpfiles' sink (cdr cmds))))))
441 (define (tr:open pathname)
442 (lambda (tmpfiles source)
443 (list tmpfiles pathname #f)))
445 (define (tr:spawn input command)
446 (lambda (tmpfiles source)
447 (if (and (member '**in** command) (not source))
448 (fail (string-append (stringify cmd) " needs an input")))
449 (let* ((t (make-temporary-file))
450 (cmd (map (lambda (x)
452 ((equal? '**in** x) source)
453 ((equal? '**out** x) t)
454 (else x))) command)))
455 (catch (list (cons t tmpfiles) t *error*)
456 (call-popen cmd input)
457 (if (and (member '**out** command) (not (file-exists? t)))
458 (fail (string-append (stringify cmd)
459 " did not produce '" t "'.")))
460 (list (cons t tmpfiles) t #f)))))
462 (define (tr:write-to pathname)
463 (lambda (tmpfiles source)
464 (rename source pathname)
465 (list tmpfiles pathname #f)))
467 (define (tr:pipe-do . commands)
468 (lambda (tmpfiles source)
469 (let ((t (make-temporary-file)))
471 `(,@(if source `(,(pipe:open source (logior O_RDONLY O_BINARY))) '())
473 ,(pipe:write-to t (logior O_WRONLY O_BINARY O_CREAT) #o600)))
474 (list (cons t tmpfiles) t #f))))
476 (define (tr:assert-identity reference)
477 (lambda (tmpfiles source)
478 (if (not (file=? source reference))
480 (list tmpfiles source #f)))
482 (define (tr:assert-weak-identity reference)
483 (lambda (tmpfiles source)
484 (if (not (text-file=? source reference))
486 (list tmpfiles source #f)))
488 (define (tr:call-with-content function . args)
489 (lambda (tmpfiles source)
490 (catch (list tmpfiles source *error*)
491 (apply function `(,(call-with-input-file source read-all) ,@args)))
492 (list tmpfiles source #f)))
495 ;; Developing and debugging tests.
498 ;; Spawn an os shell.
499 (define (interactive-shell)
500 (call-with-fds `(,(getenv "SHELL") -i) 0 1 2))
503 ;; The main test framework.
512 (new (cons test procs)))
514 (let ((unfinished (filter (lambda (t) (not t::retcode)) procs)))
515 (if (null? unfinished)
517 (let* ((names (map (lambda (t) t::name) unfinished))
518 (pids (map (lambda (t) t::pid) unfinished))
520 (map (lambda (pid retcode) (list pid retcode))
522 (wait-processes (map stringify names) pids #t))))
527 (t::set-retcode (cadr (assoc t::pid results)))))
530 (filter (lambda (p) (= 0 p::retcode)) procs))
532 (filter (lambda (p) (= 77 p::retcode)) procs))
533 (define (hard-errored)
534 (filter (lambda (p) (= 99 p::retcode)) procs))
537 (not (or (= 0 p::retcode) (= 77 p::retcode)
541 (define (print-tests tests message)
542 (unless (null? tests)
543 (apply echo (cons message
544 (map (lambda (t) t::name) tests)))))
546 (let ((failed' (failed)) (skipped' (skipped)))
547 (echo (length procs) "tests run,"
548 (length (passed)) "succeeded,"
549 (length failed') "failed,"
550 (length skipped') "skipped.")
551 (print-tests failed' "Failed tests:")
552 (print-tests skipped' "Skipped tests:")
553 (length failed')))))))
555 (define (verbosity n)
556 (if (= 0 n) '() (cons '--verbose (verbosity (- n 1)))))
558 (define (locate-test path)
559 (if (absolute-path? path) path (in-srcdir path)))
564 (define (scm name path . args)
565 ;; Start the process.
566 (define (spawn-scm args' in out err)
567 (spawn-process-fd `(,*argv0* ,@(verbosity (*verbose*))
569 ,@args' ,@args) in out err))
570 (new name #f spawn-scm #f #f CLOSED_FD))
572 (define (binary name path . args)
573 ;; Start the process.
574 (define (spawn-binary args' in out err)
575 (spawn-process-fd `(,path ,@args' ,@args) in out err))
576 (new name #f spawn-binary #f #f CLOSED_FD))
578 (define (new name directory spawn pid retcode logfd)
580 (define (set-directory x)
581 (new name x spawn pid retcode logfd))
582 (define (set-retcode x)
583 (new name directory spawn pid x logfd))
585 (new name directory spawn x retcode logfd))
586 (define (set-logfd x)
587 (new name directory spawn pid retcode x))
588 (define (open-log-file)
589 (let ((filename (string-append (basename name) ".log")))
590 (catch '() (unlink filename))
591 (open filename (logior O_RDWR O_BINARY O_CREAT) #o600)))
592 (define (run-sync . args)
593 (letfd ((log (open-log-file)))
594 (with-working-directory directory
595 (let* ((p (inbound-pipe))
596 (pid (spawn args 0 (:write-end p) (:write-end p))))
597 (close (:write-end p))
598 (splice (:read-end p) STDERR_FILENO log)
599 (close (:read-end p))
600 (let ((t' (set-retcode (wait-process name pid #t))))
603 (define (run-sync-quiet . args)
604 (with-working-directory directory
607 name (spawn args CLOSED_FD CLOSED_FD CLOSED_FD) #t))))
608 (define (run-async . args)
609 (let ((log (open-log-file)))
610 (with-working-directory directory
611 (new name directory spawn
612 (spawn args CLOSED_FD log log)
615 (let ((t (assoc retcode '((0 "PASS") (77 "SKIP") (99 "ERROR")))))
616 (if (not t) "FAIL" (cadr t))))
618 (unless (= logfd CLOSED_FD)
619 (seek logfd 0 SEEK_SET)
620 (splice logfd STDERR_FILENO)
622 (echo (string-append (status retcode) ":") name))))))
624 ;; Run the setup target to create an environment, then run all given
625 ;; tests in parallel.
626 (define (run-tests-parallel setup tests)
627 (lettmp (gpghome-tar)
628 (setup::run-sync '--create-tarball gpghome-tar)
629 (let loop ((pool (test-pool::new '())) (tests' tests))
631 (let ((results (pool::wait)))
632 (for-each (lambda (t)
633 (catch (echo "Removing" t::directory "failed:" *error*)
634 (unlink-recursively t::directory))
635 (t::report)) (reverse results::procs))
636 (exit (results::report)))
637 (let* ((wd (mkdtemp))
639 (test' (test::set-directory wd)))
640 (loop (pool::add (test'::run-async '--unpack-tarball gpghome-tar))
643 ;; Run the setup target to create an environment, then run all given
644 ;; tests in sequence.
645 (define (run-tests-sequential setup tests)
646 (lettmp (gpghome-tar)
647 (setup::run-sync '--create-tarball gpghome-tar)
648 (let loop ((pool (test-pool::new '())) (tests' tests))
650 (let ((results (pool::wait)))
651 (for-each (lambda (t)
652 (catch (echo "Removing" t::directory "failed:" *error*)
653 (unlink-recursively t::directory)))
655 (exit (results::report)))
656 (let* ((wd (mkdtemp))
658 (test' (test::set-directory wd)))
659 (loop (pool::add (test'::run-sync '--unpack-tarball gpghome-tar))