#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" (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))])