racket/collects/meta/drdr/analyze.rkt
2012-08-14 21:34:01 -06:00

363 lines
14 KiB
Racket

#lang racket
(require racket/file
"diff.rkt"
"scm.rkt"
"list-count.rkt"
"notify.rkt"
"cache.rkt"
"dirstruct.rkt"
"status.rkt"
"metadata.rkt"
"path-utils.rkt"
"rendering.rkt")
(provide (all-from-out "rendering.rkt"))
; Email
(require net/sendmail
"formats.rkt")
(define list@
(match-lambda
[(and c (cons x y))
(if (lc-zero? x)
empty
(list c))]))
(define (list-limit l n)
(for/list ([e (in-list l)]
[i (in-range n)])
e))
(define responsible-ht-id->str
#hasheq([timeout . "Timeout"]
[unclean . "Unclean Exit"]
[stderr . "STDERR Output"]
[changes . "Changes"]))
(define responsible-ht-severity
'(timeout unclean stderr changes))
(define (rev->responsible-ht rev)
(define log-dir (revision-log-dir rev))
(define top-analyze
(parameterize ([cache/file-mode 'no-cache]
[current-rev rev])
(dir-rendering log-dir)))
(rendering->responsible-ht rev top-analyze))
(define (rendering->responsible-ht rev top-analyze)
(match-define
(struct rendering (_ _ _ timeout unclean stderr _ changes))
top-analyze)
(statuses->responsible-ht rev timeout unclean stderr changes))
(define (statuses->responsible-ht rev timeout unclean stderr changes)
(parameterize ([current-rev rev])
(define log-dir (revision-log-dir rev))
(define base-path
(rebase-path log-dir "/"))
(define responsible->problems (make-hash))
(for ([lc (in-list (list timeout unclean stderr changes))]
[id (in-list responsible-ht-severity)])
(for ([pp (in-list (lc->list lc))])
(define p (bytes->string/utf-8 pp))
(define bp (base-path p))
(for ([responsible
(in-list
(rendering-responsibles (log-rendering p)))])
(hash-update!
(hash-ref! responsible->problems responsible make-hasheq)
id
(curry list* bp)
empty))))
responsible->problems))
(define (2hash-copy ht)
(define 2ht (make-hash))
(for ([(r ht) (in-hash ht)])
(hash-set! 2ht r (hash-copy ht)))
2ht)
(define (responsible-ht-difference old new)
(let ([ht (2hash-copy new)])
(for ([(r rht) (in-hash old)])
(define nrht (hash-ref! ht r make-hash))
(for ([(id ps) (in-hash rht)])
(hash-update! nrht id
(curry remove* ps)
empty)
(when (zero? (length (hash-ref nrht id)))
(hash-remove! nrht id)))
(when (zero? (hash-count nrht))
(hash-remove! ht r)))
ht))
(define responsible-ht/c
(hash/c string? (hash/c symbol? (listof path?))))
(define (responsible-ht->status-ht diff)
(for/hash ([id (in-list responsible-ht-severity)])
(define id-l
(for*/list ([(_ ht) (in-hash diff)]
[f (in-list (hash-ref ht id empty))])
f))
(values id (remove-duplicates id-l))))
(provide/contract
[rendering->responsible-ht
(exact-positive-integer? rendering? . -> . responsible-ht/c)]
[statuses->responsible-ht
(exact-positive-integer? list/count list/count list/count list/count . -> . responsible-ht/c)]
[responsible-ht-severity (listof symbol?)]
[responsible-ht-id->str (hash/c symbol? string?)]
[responsible-ht-difference (responsible-ht/c responsible-ht/c . -> . responsible-ht/c)])
(define ERROR-LIMIT 50)
(define (notify cur-rev
start end
duration
timeout unclean stderr changes)
(define abs-dur (- end start))
(define nums
(map lc->number
(list timeout unclean stderr changes)))
(define totals
(apply
format
"(timeout ~a) (unclean ~a) (stderr ~a) (changes ~a)"
(map number->string nums)))
(define (path->url pth)
(format "http://drdr.racket-lang.org/~a~a" cur-rev pth))
(define responsible-ht
(statuses->responsible-ht
cur-rev
timeout
unclean
stderr
changes))
(define responsibles
(for/list ([(responsible ht) (in-hash responsible-ht)]
#:when (ormap (curry hash-has-key? ht)
(take responsible-ht-severity 3)))
responsible))
(define committer
(with-handlers ([exn:fail? (lambda (x) #f)])
(scm-commit-author
(read-cache*
(revision-commit-msg cur-rev)))))
(define diff
(with-handlers ([exn:fail? (lambda (x) #t)])
(define old (rev->responsible-ht (previous-rev)))
(responsible-ht-difference old responsible-ht)))
(define include-committer?
(and ; The committer can be found
committer
; There is a condition
(not (empty? responsibles))
; It is different from before
(hash? diff)
(for*/or ([(r ht) (in-hash diff)]
[(id ps) (in-hash ht)])
(and
(for/or ([p (in-list ps)])
; XXX This squelch should be disabled if the committer changed this file
; XXX But even then it can lead to problems
(not (path-random? (build-path (revision-trunk-dir cur-rev) (substring (path->string* p) 1)))))
(not (symbol=? id 'changes))))))
(define mail-recipients
(remove-duplicates
(append (if include-committer?
(list committer)
empty)
responsibles)))
; Send messages to everyone...
(unless (andmap zero? nums)
(for ([r (in-list mail-recipients)])
(send-mail-message
"drdr@racket-lang.org"
(format "[DrDr] R~a ~a"
cur-rev totals)
(list (format "~a@racket-lang.org" r))
empty empty
(flatten
(list (format "DrDr has finished building push #~a after ~a."
cur-rev
(format-duration-ms abs-dur))
""
(format "http://drdr.racket-lang.org/~a/"
cur-rev)
""
(if (and include-committer? (equal? committer r))
(list
(format "Push #~a (which you did) contained a NEW condition that may need inspecting." cur-rev)
(let ([diff-smash (responsible-ht->status-ht diff)])
(for/list ([(id paths) (in-hash diff-smash)]
#:when (not (symbol=? id 'changes)))
(if (empty? paths)
empty
(list (format " ~a" id)
(for/list ([f (in-list paths)]
[i (in-range ERROR-LIMIT)]
#:when (not (path-random?
(build-path (revision-trunk-dir cur-rev)
(substring (path->string* f) 1)))))
(format " ~a" (path->url f)))
""))))
"")
empty)
(if (hash-has-key? responsible-ht r)
(list* "A file you are responsible for has a condition that may need inspecting."
(for/list ([(id files) (in-hash (hash-ref responsible-ht r))]
#:when (not (symbol=? id 'changes)))
(list (format " ~a:" id)
(for/list ([f (in-list files)]
[i (in-range ERROR-LIMIT)])
(format " ~a" (path->url f)))
""))
"")
empty))))))
; Send message to IRC
(send-mail-message "drdr@racket-lang.org"
(format "http://drdr.racket-lang.org/~a/"
cur-rev)
(list "eli+ircbot@eli.barzilay.org")
empty empty
(list* (format " (abs ~a) (sum ~a) ~a"
(format-duration-ms abs-dur)
(format-duration-ms duration)
totals)
(if (empty? responsibles)
empty
(list (apply string-append (add-between responsibles " ")))))))
; End Email
(define (trunk-path pth)
(define rev (current-rev))
((rebase-path (revision-log-dir rev) (revision-trunk-dir rev))
pth))
(define (analyze-path pth dir?)
(define rev (current-rev))
(define log-dir (revision-log-dir rev))
(define analyze-dir (revision-analyze-dir rev))
(define the-analyze-path
((rebase-path log-dir analyze-dir) pth))
(if dir?
(build-path the-analyze-path "index.analyze")
(path-add-suffix the-analyze-path ".analyze")))
(define (analyze-revision cur-rev)
(cache/file/timestamp
(build-path (revision-dir cur-rev) "analyzed")
(lambda ()
(match (analyze-logs cur-rev)
[(struct rendering (start end duration timeout unclean stderr _ changes))
(notify cur-rev
start end
duration
timeout unclean stderr changes)]
[#f
(void)])
(safely-delete-directory (revision-trunk-dir cur-rev))
(void))))
(define (analyze-logs rev)
(define log-dir (revision-log-dir rev))
(define analyze-dir (revision-analyze-dir rev))
(make-directory* analyze-dir)
(parameterize ([current-rev rev])
(dir-rendering log-dir #:committer? #t)))
(define (log-rendering log-pth)
; XXX
(if (or #t (file-exists? log-pth))
(cache/file
(analyze-path log-pth #f)
(lambda ()
#;(notify! "Analyzing log: ~S" log-pth)
(match (read-cache log-pth)
[(? eof-object?)
#f]
[(and log (struct status (start end command-line output-log)))
(define dur (status-duration log))
(define any-stderr? (ormap stderr? output-log))
(define changed?
(if (previous-rev)
(with-handlers ([exn:fail?
; This #f means that new files are NOT considered changed
(lambda (x) #f)])
(define prev-log-pth ((rebase-path (revision-log-dir (current-rev)) (revision-log-dir (previous-rev))) log-pth))
(log-different? output-log (status-output-log (read-cache prev-log-pth))))
#f))
(define responsible
(or (path-responsible (trunk-path log-pth))
(and (regexp-match #rx"/planet/" (path->string* log-pth))
"jay")
; XXX maybe mflatt, eli, or tewk
(and (regexp-match #rx"/src/" (path->string* log-pth))
"jay")
"nobody"))
(define lc
(list (path->bytes log-pth)))
(make-rendering start end dur
(if (timeout? log) lc empty)
(if (exit? log)
(if (zero? (exit-code log)) empty lc)
empty)
(if any-stderr? lc empty)
responsible
(if changed? lc empty))])))
#f))
(define (dir-rendering dir-pth
#:committer? [committer? #f])
; XXX
(if (or #t (directory-exists? dir-pth))
(cache/file
(analyze-path dir-pth #t)
(lambda ()
(notify! "Analyzing dir: ~S" dir-pth)
(foldl (lambda (sub-pth acc)
(define pth (build-path dir-pth sub-pth))
(define directory? (directory-exists? pth))
(define (next-rendering)
(if directory?
(dir-rendering pth)
(log-rendering pth)))
(match (next-rendering)
[#f
acc]
[(and n (struct rendering (pth-start pth-end pth-dur pth-timeouts pth-unclean-exits pth-stderrs _pth-responsible pth-changed)))
(match acc
[#f n]
[(struct rendering (acc-start acc-end acc-dur acc-timeouts acc-unclean-exits acc-stderrs acc-responsible acc-changed))
(make-rendering (min pth-start acc-start)
(max pth-end acc-end)
(+ pth-dur acc-dur)
(lc+ pth-timeouts acc-timeouts)
(lc+ pth-unclean-exits acc-unclean-exits)
(lc+ pth-stderrs acc-stderrs)
acc-responsible
(lc+ pth-changed acc-changed))])]))
(make-rendering
+inf.0 -inf.0 0
empty empty empty
(or
(and committer?
(with-handlers ([exn:fail? (lambda (x) #f)])
(scm-commit-author (read-cache (revision-commit-msg (current-rev))))))
(or (path-responsible (trunk-path dir-pth))
"nobody"))
empty)
(directory-list* dir-pth))))
#f))
(provide/contract
[analyze-revision (exact-nonnegative-integer? . -> . void)]
[analyze-logs (exact-nonnegative-integer? . -> . void)]
[log-rendering (path-string? . -> . (or/c rendering? false/c))]
[dir-rendering (path-string? . -> . (or/c rendering? false/c))])