#lang racket/base
(require "info-helper.rkt"
drracket/tool
drracket/tool-lib
drracket/private/debug
drracket/private/rep
drracket/private/get-extend
racket/class
racket/gui/base
racket/unit
racket/serialize
racket/list
racket/path
mrlib/switchable-button
framework)
(provide tool@)
(define tool@
(unit
(import drracket:tool^ )
(export drracket:tool-exports^)
(define coverage-button-mixin
(mixin (drracket:unit:frame<%>) ()
(super-new)
(inherit get-button-panel
get-definitions-text
register-toolbar-button
get-tabs
get-current-tab
get-interactions-text)
(define (load-coverage)
(let* ([current-tab (get-current-tab)]
[source-file (send (send current-tab get-defs) get-filename)]
[coverage-file (get-temp-coverage-file source-file)]
[test-coverage-info-ht (get-test-coverage-info-ht current-tab coverage-file)])
(when test-coverage-info-ht
(let* ([coverage-report-list (make-coverage-report test-coverage-info-ht coverage-file)]
[frame-group (group:get-the-frame-group)]
[choice-pair (get-covered-files-from-user
(format "Files covered by ~a" source-file)
(map (λ (item)
(format "~a~a (~a%)"
(first item)
(if (second item) "" "*")
(third item)))
coverage-report-list))]
[choice-open-with (first choice-pair)]
[choice-index-list (last choice-pair)]
)
(when choice-index-list
(map (λ (choice-index)
(let* ([coverage-report-item (list-ref coverage-report-list choice-index)]
[coverage-report-file (string->path (first coverage-report-item))]
[coverage-report-lines (last coverage-report-item)]
[edit-frame (handler:edit-file coverage-report-file)])
(when (and choice-open-with (> (length coverage-report-lines) 0))
(send (uncovered-lines-dialog coverage-report-file coverage-report-lines) show #t))
))
choice-index-list))
(map (λ (report-item)
(let* ([coverage-report-file (string->path (first report-item))]
[located-file-tab (locate-file-tab frame-group coverage-report-file)])
(when located-file-tab
(send located-file-tab show-test-coverage-annotations test-coverage-info-ht #f #f #f))
))
coverage-report-list)
))))
(define (get-test-coverage-info-ht current-tab coverage-file)
(let* ([source-file (send (send current-tab get-defs) get-filename)]
[interactions-text (get-interactions-text)]
[test-coverage-info-drracket (send interactions-text get-test-coverage-info)])
(if test-coverage-info-drracket (begin
(when coverage-file
(save-test-coverage-info test-coverage-info-drracket coverage-file))
(send interactions-text set-test-coverage-info #f) test-coverage-info-drracket)
(if (and coverage-file (file-exists? coverage-file)) (if (and (not (is-file-still-valid? source-file coverage-file)) (not (out-of-date-coverage-message coverage-file))) #f
(load-test-coverage-info coverage-file))
(begin (no-coverage-information-found-message source-file)
#f)
))))
(define load-button (new switchable-button%
(label button-label)
(callback (λ (button)
(load-coverage)))
(parent (get-button-panel))
(bitmap code-coverage-bitmap)
))
(register-toolbar-button load-button)
(send (get-button-panel) change-children
(λ (l)
(cons load-button (remq load-button l))))
))
(define (make-coverage-report test-coverage-info-ht coverage-file)
(let* ([file->lines-ht (make-hash)])
(hash-for-each test-coverage-info-ht
(λ (key value)
(let* ([line (syntax-line key)]
[source (format "~a" (syntax-source key))]
[covered? (mcar value)]
[file->lines-value (hash-ref file->lines-ht
source
(list
(is-file-still-valid? (string->path source) coverage-file)
0 (list) ))])
(hash-set! file->lines-ht source
(list (first file->lines-value)
(max line (second file->lines-value))
(if (or covered? (member line (last file->lines-value)))
(last file->lines-value)
(sort (append (last file->lines-value) (list line)) <)))
))))
(let* ([test-coverage-info-list (sort (map (λ (item) (list (first item)
(second item)
(get-percent (length (last item)) (third item))
(last item)))
(hash->list file->lines-ht))
(λ (a b) (< (third a) (third b))))])
test-coverage-info-list)))
(define (is-file-still-valid? file coverage-file)
(if coverage-file
(let* ([file-modify-valid? (> (file-or-directory-modify-seconds coverage-file)
(file-or-directory-modify-seconds file))]
[located-file-frame (send (group:get-the-frame-group) locate-file file)]
[file-untouched-valid? (if located-file-frame
(not (send (send located-file-frame get-editor) is-modified?))
#t)])
(and file-modify-valid? file-untouched-valid?)
)
#t ))
(define (get-temp-coverage-file source-file)
(if source-file
(let* ([file-base (file-dir-from-path source-file)]
[file-name (file-name-from-path source-file)]
[temp-coverage-file-name (path-replace-suffix file-name coverage-suffix)]
[temp-coverage-dir (build-path file-base "compiled")]
[temp-coverage-file (build-path temp-coverage-dir temp-coverage-file-name)])
(when (not (directory-exists? temp-coverage-dir))
(make-directory temp-coverage-dir))
temp-coverage-file
)
#f))
(define (file-dir-from-path path)
(define-values (file-dir file-name must-be-dir) (split-path path))
file-dir)
(define (save-test-coverage-info test-coverage-info coverage-file)
(with-output-to-file coverage-file
(lambda () (begin
(write (hash-map test-coverage-info
(λ (key value)
(cons (list (serialize (syntax-source key))
(syntax-position key)
(syntax-span key)
(syntax-line key))
value))
))))
#:mode 'text
#:exists 'replace
))
(define (load-test-coverage-info coverage-file)
(make-hasheq (map (lambda (element)
(let* ([key (car element)]
[value (cdr element)])
(cons (datum->syntax #f (void) (list
(deserialize (car key))
(cadddr key)
1
(cadr key)
(caddr key)))
(mcons (car value) (cdr value)))))
(read (open-input-file coverage-file)))))
(define (locate-file-tab frame-group file)
(let* ([located-file-frame (send frame-group locate-file file)]
[located-file-tab (if located-file-frame
(findf (λ (t)
(equal?
(send (send t get-defs) get-filename)
file))
(send located-file-frame get-tabs))
#f)])
located-file-tab))
(define (get-listbox-min-height num-items)
(inexact->exact (min 500 (round (sqrt (* 600 num-items))))))
(define (get-percent num total)
(round (* (- 1 (/ num total)) 100)))
(define (out-of-date-coverage-message file)
(equal?
(message-box/custom tool-name
(string-append (format "The multi-file code coverage information for ~a" file)
" may be out of date, run the program again to update it."
" Do you want to use it anyways?")
"Continue" "Cancel" #f
#f
(list 'caution 'default=2))
1))
(define (no-coverage-information-found-message source-file)
(message-box tool-name
(format (string-append "No multi-file code coverage information found for ~a. "
"Make sure the program has been run and Syntactic Test Suite Coverage "
"is enabled in Language->Choose Language...->Dynamic Properties.")
(if source-file source-file "Untitled"))
#f
(list 'ok 'stop))
)
(define (get-covered-files-from-user message choices)
(define button-pressed (box 'close))
(define (button-callback button)
(λ (b e)
(set-box! button-pressed button)
(send dialog show #f)
))
(define (enable-open-buttons enable?)
(send open-button enable enable?)
(send open-with-button enable enable?))
(define dialog (instantiate dialog% (tool-name)))
(new message% [parent dialog] [label message])
(define list-box (new list-box%
[label ""]
[choices choices]
[parent dialog]
[style '(multiple)]
[min-height (get-listbox-min-height (length choices))]
[callback (λ (c e)
(if (> (length (send list-box get-selections)) 0)
(if (eq? (send e get-event-type) 'list-box-dclick)
((button-callback 'open) null null)
(enable-open-buttons #t))
(enable-open-buttons #f)))
]))
(define panel (new horizontal-panel% [parent dialog]
[alignment '(right bottom)]
[stretchable-height #f]))
(define open-button (new button% [parent panel]
[label "Open"]
[callback (button-callback 'open)]
[enabled #f]
[style '(border)]))
(define open-with-button (new button% [parent panel]
[label open-with-label]
[callback (button-callback 'open-with)]
[enabled #f]))
(new button% [parent panel]
[label "Close"]
[callback (button-callback 'close)])
(send dialog show #t)
(case (unbox button-pressed)
['open (list #f (send list-box get-selections))]
['open-with (list #t (send list-box get-selections))]
[else (list #f #f)])
)
(define (uncovered-lines-dialog file lines)
(let* ([dialog (instantiate frame% (tool-name))])
(new message%
[parent dialog]
[label "Lines containing uncovered code in"])
(new message%
[parent dialog]
[label (format "~a:" file)])
(define text-field (new text-field%
[label ""]
[parent dialog]
[style (list 'multiple)]))
(send text-field set-value (foldl (λ (item text)
(string-append text
(if (equal? text "") "" ", ")
(number->string item)))
"" lines))
(send (send text-field get-editor) lock #t)
(define panel (new horizontal-panel%
[parent dialog]
[stretchable-height #f]
[alignment '(right bottom)]))
(new button%
[parent panel]
[label "Close"]
[style '(border)]
[callback (λ (b e) (send dialog show #f))])
dialog))
(define code-coverage-bitmap
(let* ((bmp (make-bitmap 16 16))
(bdc (make-object bitmap-dc% bmp)))
(send bdc erase)
(send bdc set-smoothing 'smoothed)
(send bdc set-pen "black" 1 'transparent)
(send bdc set-brush "forest green" 'solid)
(send bdc draw-rectangle 2 5 12 9)
(send bdc set-brush "maroon" 'solid)
(send bdc draw-rectangle 11 5 14 9)
(send bdc set-bitmap #f)
bmp))
(define (phase1) (void))
(define (phase2) (void))
(drracket:get/extend:extend-unit-frame coverage-button-mixin)))