317 lines
11 KiB
Racket
317 lines
11 KiB
Racket
#lang racket/base
|
|
(provide test-files!
|
|
make-cover-environment clear-coverage!
|
|
get-test-coverage
|
|
current-cover-environment environment?
|
|
environment-compile environment-namespace
|
|
coverage-wrapper-map)
|
|
|
|
#|
|
|
|
|
This module implements code coverage. It works by compiling and running the given modules with in a
|
|
separate namespace errortrace annotations that write coverage information to a hashmap exported from
|
|
in "coverage.rkt". This raw coverage information is converted to a usable form by
|
|
`get-test-coverage`.
|
|
|
|
|#
|
|
|
|
(require (for-syntax racket/base))
|
|
(require racket/dict
|
|
syntax/modcode
|
|
racket/function
|
|
syntax/modread
|
|
syntax/modresolve
|
|
syntax/parse
|
|
unstable/syntax
|
|
racket/bool
|
|
racket/runtime-path
|
|
racket/match
|
|
racket/path
|
|
rackunit
|
|
unstable/error
|
|
racket/list
|
|
racket/port
|
|
"private/shared.rkt"
|
|
"private/file-utils.rkt"
|
|
"private/format-utils.rkt"
|
|
"strace.rkt")
|
|
|
|
;; An environment has:
|
|
;; a `namespace`, which shall always have `coverage.rkt` and ''#%builtin attached
|
|
;; a handler for `current-compile`
|
|
;; a function that will annoate expanded code
|
|
;; a reference to the raw coverage map
|
|
(struct environment (namespace compile ann-top raw-cover))
|
|
;; A special structure used for communicating information about programs that call `exit`
|
|
;; `code` is the exit code that `exit` was called with
|
|
(struct an-exit (code))
|
|
|
|
;;; ---------------------- Running Files ---------------------------------
|
|
|
|
;; Test files and build coverage map
|
|
;; returns true if no tests reported as failed, and no files errored.
|
|
(define (test-files! #:submod [submod-name 'test] #:env [env (current-cover-environment)] . files)
|
|
(parameterize ([current-cover-environment env])
|
|
(define abs
|
|
(for/list ([p (in-list files)])
|
|
(if (list? p)
|
|
(cons (->absolute (car p)) (cdr p))
|
|
(->absolute p))))
|
|
(define abs-names
|
|
(for/list ([p (in-list abs)])
|
|
(match p
|
|
[(cons p _) p]
|
|
[_ p])))
|
|
(define tests-failed
|
|
(parameterize* ([current-load/use-compiled (make-cover-load/use-compiled abs-names)]
|
|
[current-output-port
|
|
(if (verbose) (current-output-port) (open-output-nowhere))]
|
|
[current-namespace (get-namespace)])
|
|
(for ([f (in-list abs-names)])
|
|
(compile-file f))
|
|
(for/fold ([tests-failed #f]) ([f (in-list abs)])
|
|
(define failed? (handle-file f submod-name))
|
|
(and failed? tests-failed))))
|
|
(vprintf "ran ~s\n" files)
|
|
(remove-unneeded-results! abs-names)
|
|
(not tests-failed)))
|
|
|
|
;;; ---------------------- Running Aux ---------------------------------
|
|
|
|
|
|
;; PathString -> Void
|
|
(define (compile-file the-file)
|
|
(dynamic-require (build-file-require the-file) (void)))
|
|
|
|
;; (or PathString (list PathString Vector)) Symbol -> Boolean
|
|
;; returns if any tests failed or errors occured
|
|
(define (handle-file maybe-path submod-name)
|
|
(define tests-failed #f)
|
|
(define old-check (current-check-handler))
|
|
(vprintf "attempting to run ~s\n" maybe-path)
|
|
(define the-file (if (list? maybe-path) (first maybe-path) maybe-path))
|
|
(define argv (if (list? maybe-path) (second maybe-path) #()))
|
|
(with-handlers ([(lambda (x) (or (not (exn? x)) (exn:fail? x)))
|
|
(lambda (x)
|
|
(cond [(an-exit? x)
|
|
(vprintf "file ~s exited code ~s" maybe-path (an-exit-code x))]
|
|
[else
|
|
(set! tests-failed #t)
|
|
(error-display x)]))])
|
|
(parameterize ([current-command-line-arguments argv]
|
|
[exit-handler (lambda (x) (raise (an-exit x)))]
|
|
[current-check-handler ;(get-check-handler-parameter)
|
|
(lambda x
|
|
(set! tests-failed #t)
|
|
(vprintf "file ~s had failed tests\n" maybe-path)
|
|
(apply old-check x))])
|
|
(vprintf "running file: ~s with args: ~s\n" the-file argv)
|
|
(exec-file the-file submod-name)))
|
|
tests-failed)
|
|
|
|
;; PathString Symbol -> Void
|
|
(define (exec-file the-file submod-name)
|
|
(define sfile (build-file-require the-file))
|
|
(define submod `(submod ,sfile ,submod-name))
|
|
(run-mod (if (module-declared? submod #t) submod sfile)))
|
|
|
|
;; ModulePath -> Any
|
|
(define (run-mod to-run)
|
|
(vprintf "running ~s\n" to-run)
|
|
(dynamic-require to-run 0)
|
|
(vprintf "finished running ~s\n" to-run))
|
|
|
|
;; PathString -> ModulePath
|
|
(define (build-file-require the-file)
|
|
`(file ,(if (path? the-file) (path->string the-file) the-file)))
|
|
|
|
;; [Listof Any] -> Void
|
|
;; remove any files not in paths from the raw coverage
|
|
(define (remove-unneeded-results! names)
|
|
(define c (get-raw-coverage))
|
|
(for ([s (in-list (hash-keys c))]
|
|
;; first here is like "srcloc-source", but its in list form...
|
|
#:when (not (member (first s) names)))
|
|
(hash-remove! c s)))
|
|
|
|
;;; ---------------------- Compiling ---------------------------------
|
|
|
|
;; (U [Listof Path] #f) -> Loader Compiler
|
|
;; returns a value that can be set of `current-load/use-compiled`
|
|
;; forces the given files to be recompiled whenever load/use-compiled is called
|
|
(define (make-cover-load/use-compiled paths)
|
|
(define load/use-compiled (current-load/use-compiled))
|
|
(define load (current-load))
|
|
(define cover-compile (get-compile))
|
|
(define cover-use-compiled-file-paths
|
|
(cons (build-path "compiled" "cover")
|
|
(use-compiled-file-paths)))
|
|
(lambda (path sym)
|
|
(define abs (->absolute path))
|
|
(define lst (explode-path abs))
|
|
(define dir-list (take lst (sub1 (length lst))))
|
|
(parameterize ([current-load-relative-directory (apply build-path dir-list)])
|
|
(if (implies paths (member abs paths))
|
|
(parameterize ([current-compile cover-compile]
|
|
[use-compiled-file-paths
|
|
cover-use-compiled-file-paths])
|
|
(load path sym))
|
|
(load/use-compiled path sym)))))
|
|
|
|
;; -> Compiler
|
|
;; makes a value sutable for current-compile, such that compile
|
|
;; annotates the source code with annotate-top. meant to be called
|
|
;; only by initialize-cover-environment
|
|
(define (make-cover-compile ns annotate-top)
|
|
(define compile (current-compile))
|
|
(define reg (namespace-module-registry ns))
|
|
(define phase (namespace-base-phase ns))
|
|
;; define so its named in stack traces
|
|
(define cover-compile
|
|
(lambda (e immediate-eval?)
|
|
(define to-compile
|
|
(cond [(or (compiled-expression? (if (syntax? e) (syntax-e e) e))
|
|
(not (eq? reg (namespace-module-registry (current-namespace))))
|
|
(not (equal? phase (namespace-base-phase (current-namespace)))))
|
|
e]
|
|
[else
|
|
(vprintf "compiling ~s with coverage annotations\n"
|
|
(if (not (syntax? e))
|
|
e
|
|
(or (syntax-source-file-name e)
|
|
(syntax-source e)
|
|
(syntax->datum e))))
|
|
(annotate-top (if (syntax? e) (expand-syntax e) (datum->syntax #f e))
|
|
phase)]))
|
|
(compile to-compile immediate-eval?)))
|
|
cover-compile)
|
|
|
|
;;; ---------------------- Environments ---------------------------------
|
|
|
|
(define (clear-coverage!)
|
|
(current-cover-environment (make-cover-environment)))
|
|
|
|
(define (make-cover-environment [ns (make-empty-namespace)])
|
|
(kernelize-namespace! ns)
|
|
(parameterize ([current-namespace ns])
|
|
(define ann (load-annotate-top))
|
|
(environment
|
|
ns
|
|
(make-cover-compile ns ann)
|
|
ann
|
|
(load-raw-coverage))))
|
|
|
|
(define (kernelize-namespace! ns)
|
|
(define cns (current-namespace))
|
|
(namespace-attach-module cns ''#%builtin ns))
|
|
|
|
(define (get-annotate-top)
|
|
(get-val environment-ann-top))
|
|
(define (load-annotate-top)
|
|
(make-annotate-top (load-raw-coverage) (load-cover-name)))
|
|
|
|
|
|
(define (get-raw-coverage)
|
|
(get-val environment-raw-cover))
|
|
(define (load-raw-coverage)
|
|
(dynamic-require 'cover/coverage 'coverage))
|
|
|
|
(define (load-cover-name)
|
|
(dynamic-require 'cover/coverage 'cover-name))
|
|
|
|
(define (get-namespace)
|
|
(get-val environment-namespace))
|
|
|
|
(define (get-compile)
|
|
(get-val environment-compile))
|
|
|
|
(define (get-val access)
|
|
(access (current-cover-environment)))
|
|
|
|
(struct coverage-wrapper (map function)
|
|
#:property prop:procedure (struct-field-index function))
|
|
|
|
;; -> coverage/c
|
|
;; returns a hash of file to a list, where the first of the list is if
|
|
;; that srcloc was covered or not
|
|
;; based on <pkgs>/drracket/drracket/private/debug.rkt
|
|
(define (get-test-coverage [env (current-cover-environment)])
|
|
(parameterize ([current-cover-environment env])
|
|
(vprintf "generating test coverage\n")
|
|
|
|
;; filtered : (listof (list boolean srcloc))
|
|
(define filtered (hash-map (get-raw-coverage)
|
|
(λ (k v) (list (unbox v) (apply make-srcloc k)))))
|
|
|
|
(define out (make-hash))
|
|
|
|
(for ([v (in-list filtered)])
|
|
(define file (srcloc-source (cadr v)))
|
|
(hash-update! out
|
|
file
|
|
(lambda (l) (cons v l))
|
|
null))
|
|
;; Make the hash map immutable
|
|
(define coverage (for/hash ([(k v) (in-hash out)]) (values k v)))
|
|
(define file-map (make-hash))
|
|
(coverage-wrapper
|
|
coverage
|
|
(lambda (key location)
|
|
(define f
|
|
(hash-ref! file-map key
|
|
(lambda ()
|
|
(make-covered? coverage key))))
|
|
(f location)))))
|
|
|
|
(define current-cover-environment
|
|
(make-parameter (make-cover-environment)))
|
|
|
|
;; here live tests for actually saving compiled files
|
|
(module+ test
|
|
(require rackunit racket/runtime-path compiler/cm compiler/compiler)
|
|
(define-runtime-path prog.rkt "tests/prog.rkt")
|
|
(define-runtime-path-list compiled
|
|
(list
|
|
"tests/compiled/prog_rkt.zo"
|
|
"tests/compiled/prog_rkt.dep"))
|
|
(test-begin
|
|
(parameterize ([current-cover-environment (make-cover-environment)])
|
|
(for-each (lambda (f) (when (file-exists? f) (delete-file f)))
|
|
compiled)
|
|
(check-false (ormap file-exists? compiled))
|
|
(check-not-exn
|
|
(lambda ()
|
|
(parameterize ([current-load/use-compiled
|
|
(make-cover-load/use-compiled (list (->absolute prog.rkt)))]
|
|
[current-namespace (get-namespace)])
|
|
(managed-compile-zo prog.rkt))))
|
|
(check-true (andmap file-exists? compiled)))))
|
|
|
|
;; tests repl like interactions
|
|
(module+ test
|
|
(require rackunit racket/runtime-path racket/file
|
|
racket/format
|
|
racket/lazy-require)
|
|
;; break cyclic dependency in testing
|
|
(define-runtime-path simple-multi/2.rkt "tests/simple-multi/2.rkt")
|
|
(define env (make-cover-environment))
|
|
(define ns (environment-namespace env))
|
|
(parameterize ([current-cover-environment env]
|
|
[current-namespace ns])
|
|
(namespace-require 'racket/base)
|
|
(test-begin
|
|
(define file (path->string simple-multi/2.rkt))
|
|
(define modpath file)
|
|
(parameterize ([current-load/use-compiled
|
|
(make-cover-load/use-compiled (list file))])
|
|
(namespace-require `(file ,modpath)))
|
|
(check-equal? (eval `(two)) 10)
|
|
(define x (get-test-coverage env))
|
|
(define covered? (curry x file))
|
|
(for ([_ (in-string (file->string file))]
|
|
[i (in-naturals 1)])
|
|
(define c (covered? i))
|
|
(check-true (or (eq? c 'covered)
|
|
(eq? c 'irrelevant))
|
|
(~a i))))))
|