@@ -154,6 +154,88 @@ the string is returned unchanged."
154154 (subseq line end))))
155155
156156
157+ ; ;; Scraping error logs for the real problem
158+
159+ (defstruct peekstream
160+ stream
161+ buffered-line)
162+
163+ (defun next-line (peekstream)
164+ (if (peekstream-buffered-line peekstream)
165+ (shiftf (peekstream-buffered-line peekstream) nil )
166+ (read-line (peekstream-stream peekstream) nil )))
167+
168+ (defun peek-line (peekstream)
169+ (or (peekstream-buffered-line peekstream)
170+ (setf (peekstream-buffered-line peekstream)
171+ (read-line (peekstream-stream peekstream) nil ))))
172+
173+ (defun unread-line (line peekstream)
174+ (setf (peekstream-buffered-line peekstream) line))
175+
176+ (defun =line-matches (pattern)
177+ (let ((scanner (ppcre :create-scanner pattern)))
178+ (lambda (line)
179+ (ppcre :scan scanner line))))
180+
181+ (defun callfun (object)
182+ (lambda (fun)
183+ (funcall fun object)))
184+
185+ (defun =or (&rest funs)
186+ (lambda (object)
187+ (some (callfun object) funs)))
188+
189+ (defun =not (fun)
190+ (complement fun))
191+
192+ (defun skip-text-until (fun stream )
193+ (loop for line = (peek-line stream )
194+ while line
195+ do
196+ (cond ((funcall fun line)
197+ (return line))
198+ (t
199+ (next-line stream )))))
200+
201+ (defun collect-text-while (fun stream )
202+ (with-output-to-string (s)
203+ (loop for line = (peek-line stream )
204+ while (and line (funcall fun line))
205+ do (write-line (next-line stream ) s))))
206+
207+ (defun collect-text-between (start-fun end-fun stream )
208+ (let* ((first-line (skip-text-until start-fun stream ))
209+ (rest (and first-line (next-line stream ) (collect-text-while (=not end-fun) stream ))))
210+ (when first-line
211+ (concatenate ' string
212+ first-line # (#\newline )
213+ rest ))))
214+
215+ (defun extract-warnings-and-errors (file)
216+ " Scrape warnings and errors out of the logfile FILE. Repeated
217+ consecutive strings are coalesced into a single string to avoid
218+ redundant info."
219+ (flet ((coalesce-consecutive-strings (strings)
220+ (let ((out ' ()))
221+ (dolist (string strings (nreverse out))
222+ (unless (equal (first out) string )
223+ (push string out))))))
224+ (coalesce-consecutive-strings
225+ (with-open-file (stream file)
226+ (let ((pstream (make-peekstream :stream stream )))
227+ (let (result)
228+ (loop
229+ (let ((match (collect-text-between (=or (=line-matches " ^; caught (COMMON-LISP:)?(WARNING|ERROR):" )
230+ (=line-matches " ^Unhandled" ))
231+ (=not (=line-matches " ^;" ))
232+ pstream)))
233+ (if match
234+ (push match result)
235+ (return (nreverse result)))))))))))
236+
237+
238+
157239; ;; Posting to S3
158240
159241(defun report-publishing-enabled-p ()
@@ -208,6 +290,8 @@ the string is returned unchanged."
208290(defgeneric write-html-failure-report-content (object stream ))
209291(defgeneric write-html-failure-report-footer (object stream ))
210292
293+ (defgeneric failure-impact (object))
294+
211295(defmethod failure-report-html-file (base object)
212296 (relative-to base (failure-report-url object)))
213297
@@ -225,12 +309,39 @@ the string is returned unchanged."
225309 :reader source)
226310 (failure-log-file
227311 :initarg :failure-log-file
228- :reader failure-log-file)))
312+ :reader failure-log-file)
313+ (warnings-and-errors
314+ :reader warnings-and-errors)
315+ (breaks
316+ :accessor breaks
317+ :initform nil )
318+ (broken-by
319+ :accessor broken-by
320+ :initform nil )))
229321
230322(defmethod print-object ((object failing-system) stream )
231323 (print-unreadable-object (object stream :type t )
232324 (write-string (system-name object) stream )))
233325
326+ (defmethod failure-impact ((object failing-system))
327+ (length (breaks object)))
328+
329+ (defmethod slot-unbound ((class t ) (instance failing-system) (slot (eql ' warnings-and-errors)))
330+ (setf (slot-value instance ' warnings-and-errors)
331+ (extract-warnings-and-errors (failure-log-file instance))))
332+
333+ (defun broken-by-name (failing-system)
334+ (let* ((unhandled-log-line
335+ (find " Unhandled" (warnings-and-errors failing-system)
336+ :test #' search ))
337+ (responsible-system-name
338+ (ppcre :register-groups-bind (system-name)
339+ (" ^Unhandled.*while compiling.*SOURCE-FILE \" (.*?)\" "
340+ unhandled-log-line)
341+ system-name)))
342+ (or responsible-system-name
343+ (system-name failing-system))))
344+
234345(defmethod new-failure-p ((object failing-system))
235346 (let* ((dist (ql-dist :find-dist " quicklisp" ))
236347 (existing-system
@@ -239,22 +350,11 @@ the string is returned unchanged."
239350 (< (days-old (source object)) 30 ))))
240351
241352(defmethod failure-data ((source upstream-source))
242- (let ((result ' ()))
243- (map-source-systems
244- source
245- (lambda (system-file-name system-name)
246- (write-char #\. *trace-output* )
247- (force-output *trace-output* )
248- (let ((file (winfail-file " fail" source system-file-name system-name)))
249- (when (probe-file file)
250- (push (make-instance ' failing-system
251- :system-name system-name
252- :system-file-name
253- system-file-name
254- :source source
255- :failure-log-file file)
256- result)))))
257- result))
353+ (remove (name source)
354+ (failing-systems)
355+ :test-not ' string=
356+ :key (lambda (system)
357+ (name (source system)))))
258358
259359(defmethod name ((object failing-system))
260360 (name (source object)))
@@ -282,6 +382,9 @@ the string is returned unchanged."
282382 (name (source object))
283383 (length (failure-data object)))))
284384
385+ (defmethod failure-impact ((object failing-source))
386+ (reduce #' + (mapcar #' failure-impact (failure-data object))))
387+
285388(defmethod source-link ((source failing-source))
286389 (source-link (source source)))
287390
@@ -347,9 +450,29 @@ source is found that matches the filename, return nil."
347450 (directory fail-wild)))
348451
349452(defun failing-systems ()
350- (remove nil
351- (mapcar #' parse-failure-file-name
352- (failing-source-log-files))))
453+ ; ; This is the best way to get failure info, because it populates
454+ ; ; useful failure cross-reference data.
455+ (let* ((systems (remove nil
456+ (mapcar #' parse-failure-file-name
457+ (failing-source-log-files))))
458+ (table (make-hash-table :test ' equal)))
459+ (dolist (system systems)
460+ (setf (gethash (system-name system) table) system))
461+ (dolist (system systems)
462+ (let ((broken-by (or (gethash (broken-by-name system) table)
463+ system)))
464+ (unless (eq broken-by system)
465+ (setf (broken-by system)
466+ broken-by)
467+ (push system (breaks broken-by)))))
468+ systems))
469+
470+ (defun who-is-broken-by (name)
471+ (remove name (failing-systems)
472+ :test-not #' string=
473+ :key (lambda (failing-system)
474+ (and (broken-by failing-system)
475+ (system-name (broken-by failing-system))))))
353476
354477(defun failure-log-failure-report ()
355478 " Scan the failure log files of all projects to produce a failure report."
@@ -402,6 +525,13 @@ source is found that matches the filename, return nil."
402525 (loop for scanner in *log-lines-that-are-boring*
403526 thereis (ppcre :scan scanner line)))
404527
528+ (defun failure-snippet (object)
529+ (etypecase object
530+ (failing-system
531+ (format nil " ~{ ~A ~^ ...~% ~} ~% "
532+ (extract-warnings-and-errors (failure-log-file object))))
533+ (failing-source
534+ (format nil " ~{ ~A ~} " (mapcar #' failure-snippet (failure-data object))))))
405535
406536(defmethod write-html-failure-report-header (object stream )
407537 (format stream " <html><head><title>~A </title>~
@@ -476,22 +606,45 @@ source is found that matches the filename, return nil."
476606 (new (remove-if-not #' new-failure-p sources))
477607 (old (remove-if #' new-failure-p sources)))
478608 (flet ((show (sources)
479- (dolist (source sources)
609+ (dolist (source ( sort ( copy-seq sources) #' string< :key #' name) )
480610 (let ((link (source-link source)))
481611 (format stream " <li~@[ ~* class='new-failure'~] > ~A :<br>"
482612 (new-failure-p source)
483613 (name source))
614+ (let ((age (source-cache-age-or-nil (source source))))
615+ (when age
616+ (format stream " last modified ~A ago<br>~% " (how-long-ago age))))
484617 (if link
485618 (format stream " <a class='source-link' href='~A '>~A </a>" link link)
486619 (format stream " <span class='source-location'>~A </span>" (location (source source))))
487620 (format stream " </li>~% " )
488621 (format stream " <ul>" )
489- (dolist (system (failure-data source))
490- (format stream " <li~@[ ~* class='new-failure'~] > <a href='~A '>~A </a></li>~% "
491- (new-failure-p system)
492- (failure-report-url system)
493- (system-name system))))
494- (format stream " </ul>~% " ))))
622+ (dolist (system (sort (copy-seq (failure-data source)) #' string< :key #' system-name))
623+ (let ((responsible (broken-by system))
624+ (system-name (system-name system)))
625+ (format stream " <li~@[ ~* class='new-failure'~] > <a name='~A '></a><a href='~A '>~A </a>"
626+ (new-failure-p system)
627+ system-name
628+ (failure-report-url system)
629+ system-name)
630+ (when responsible
631+ (format stream " <i>caused by <a href='#~A '>~A </a></i>~% "
632+ (system-name responsible)
633+ (system-name responsible)))
634+ (when (breaks system)
635+ (format stream " <br>Breaks: " )
636+ (dolist (broken (breaks system))
637+ (format stream " <a href='#~A '>~A </a> "
638+ (system-name broken)
639+ (system-name broken)))
640+ (format stream " <br>" ))
641+ (unless responsible
642+ (format stream " <pre class='snippet'>~A </pre>"
643+ (cl-who :escape-string (failure-snippet system))))
644+ (when responsible
645+ (format stream " <br>" ))
646+ (format stream " </li>~% " ))))
647+ (format stream " </ul>" ))))
495648 (show new)
496649 (format stream " <br><br>" )
497650 (show old)))
@@ -503,6 +656,9 @@ source is found that matches the filename, return nil."
503656 (let ((link (source-link (source object))))
504657 (when link
505658 (format stream " <li> site: <a href='~A '>~A </a>~% " link link)))
659+ (let ((age (source-cache-age-or-nil (source object))))
660+ (when age
661+ (format stream " <li> last updated: ~A ago" (how-long-ago age))))
506662 (format stream " </ul>~% " )
507663 (format stream " <p>~A~% " (versions-and-such)))
508664
0 commit comments