1.0.6.23: fix a source location tracking problem in sb-cover
[sbcl.git] / contrib / sb-cover / cover.lisp
1 ;;; A frontend for the SBCL code coverage facility. Written by Juho
2 ;;; Snellman, and placed under public domain.
3
4 ;;; This module includes a modified version of the source path parsing
5 ;;; routines from Swank. That code was written by Helmut Eller, and
6 ;;; was placed under Public Domain
7
8 (defpackage #:sb-cover
9   (:use #:cl #:sb-c)
10   (:export #:report
11            #:reset-coverage #:clear-coverage
12            #:store-coverage-data))
13
14 (in-package #:sb-cover)
15
16 (defclass sample-count ()
17   ((mode :accessor mode-of :initarg :mode)
18    (all :accessor all-of :initform 0)
19    (ok :accessor ok-of :initform 0)))
20
21 (defun clear-coverage ()
22   "Clear all files from the coverage database. The files will be re-entered
23 into the database when the FASL files (produced by compiling
24 STORE-COVERAGE-DATA optimization policy set to 3) are loaded again into the
25 image."
26   (sb-c::clear-code-coverage))
27
28 (defun reset-coverage ()
29   "Reset all coverage data back to the `Not executed` state."
30   (sb-c::reset-code-coverage))
31
32 (defun report (directory &key (external-format :default))
33   "Print a code coverage report of all instrumented files into DIRECTORY.
34 If DIRECTORY does not exist, it will be created. The main report will be
35 printed to the file cover-index.html. The external format of the source
36 files can be specified with the EXTERNAL-FORMAT parameter."
37   (let ((paths)
38         (*default-pathname-defaults* (merge-pathnames (pathname directory))))
39     (ensure-directories-exist *default-pathname-defaults*)
40     (maphash (lambda (k v)
41                (declare (ignore v))
42                (let* ((n (substitute #\_ #\. (substitute #\_ #\/ k)))
43                       (path (make-pathname :name n :type "html")))
44                  (when (probe-file k)
45                    (with-open-file (stream path
46                                            :direction :output
47                                            :if-exists :supersede
48                                            :if-does-not-exist :create)
49                      (push (list* k n (report-file k stream external-format))
50                            paths)))))
51              *code-coverage-info*)
52     (let ((report-file (make-pathname :name "cover-index" :type "html")))
53       (with-open-file (stream report-file
54                               :direction :output :if-exists :supersede
55                               :if-does-not-exist :create)
56         (write-styles stream)
57         (unless paths
58           (warn "No coverage data found for any file, producing an empty report. Maybe you~%forgot to (DECLAIM (OPTIMIZE SB-COVER:STORE-COVERAGE-DATA))?")
59           (format stream "<h3>No code coverage data found.</h3>")
60           (close stream)
61           (return-from report))
62         (format stream "<table class='summary'>")
63         (format stream "<tr class='head-row'><td></td><td class='main-head' colspan='3'>Expression</td><td class='main-head' colspan='3'>Branch</td></tr>")
64         (format stream "<tr class='head-row'>~{<td width='80px'>~A</td>~}</tr>"
65                 (list "Source file"
66                       "Covered" "Total" "%"
67                       "Covered" "Total" "%"))
68         (setf paths (sort paths #'string< :key #'car))
69         (loop for prev = nil then source-file
70               for (source-file report-file expression branch) in paths
71               for even = nil then (not even)
72               do (when (or (null prev)
73                            (not (equal (pathname-directory (pathname source-file))
74                                        (pathname-directory (pathname prev)))))
75                    (format stream "<tr class='subheading'><td colspan='7'>~A</td></tr>~%"
76                            (namestring (make-pathname :directory (pathname-directory (pathname source-file))))))
77               do (format stream "<tr class='~:[odd~;even~]'><td class='text-cell'><a href='~a.html'>~a</a></td>~{<td>~:[-~;~:*~a~]</td><td>~:[-~;~:*~a~]</td><td>~:[-~;~:*~5,1f~]</td>~}</tr>"
78                          even
79                          report-file
80                          (enough-namestring (pathname source-file)
81                                             (pathname source-file))
82                          (list (ok-of expression)
83                                (all-of expression)
84                                (percent expression)
85                                (ok-of branch)
86                                (all-of branch)
87                                (percent branch))))
88         (format stream "</table>"))
89       report-file)))
90
91 (defun percent (count)
92   (unless (zerop (all-of count))
93     (* 100
94        (/ (ok-of count) (all-of count)))))
95
96 (defun report-file (file html-stream external-format)
97   "Print a code coverage report of FILE into the stream HTML-STREAM."
98   (format html-stream "<html><head>")
99   (write-styles html-stream)
100   (format html-stream "</head><body>")
101   (let* ((source (detabify (read-file file external-format)))
102          (states (make-array (length source)
103                              :initial-element 0
104                              :element-type '(unsigned-byte 4)))
105          ;; Convert the code coverage records to a more suitable format
106          ;; for this function.
107          (expr-records (convert-records (gethash file *code-coverage-info*)
108                                         :expression))
109          (branch-records (convert-records (gethash file *code-coverage-info*)
110                                           :branch))
111          ;; Cache the source-maps
112          (maps (with-input-from-string (stream source)
113                  (loop with map = nil
114                        with form = nil
115                        with eof = nil
116                        for i from 0
117                        do (setf (values form map)
118                                 (handler-case
119                                     (read-and-record-source-map stream)
120                                   (end-of-file ()
121                                     (setf eof t))
122                                   (error (error)
123                                     (warn "Error when recording source map for toplevel form ~A:~%  ~A" i error)
124                                     (values nil
125                                             (make-hash-table)))))
126                        until eof
127                        when map
128                        collect (cons form map)))))
129     (mapcar (lambda (map)
130               (maphash (lambda (k locations)
131                          (declare (ignore k))
132                          (dolist (location locations)
133                            (destructuring-bind (start end suppress) location
134                              (when suppress
135                                (fill-with-state source states 15 (1- start)
136                                                 end)))))
137                        (cdr map)))
138             maps)
139     ;; Go through all records, find the matching source in the file,
140     ;; and update STATES to contain the state of the record in the
141     ;; indexes matching the source location. We do this in two stages:
142     ;; the first stage records the character ranges, and the second stage
143     ;; does the update, in order from shortest to longest ranges. This
144     ;; ensures that for each index in STATES will reflect the state of
145     ;; the innermost containing form.
146     (let ((counts (list :branch (make-instance 'sample-count :mode :branch)
147                         :expression (make-instance 'sample-count
148                                                    :mode :expression))))
149       (let ((records (append branch-records expr-records))
150             (locations nil))
151         (dolist (record records)
152           (destructuring-bind (mode path state) record
153             (let* ((path (reverse path))
154                    (tlf (car path))
155                    (source-form (car (nth tlf maps)))
156                    (source-map (cdr (nth tlf maps)))
157                    (source-path (cdr path)))
158               (cond ((eql mode :branch)
159                      (let ((count (getf counts :branch)))
160                        ;; For branches mode each record accounts for two paths
161                        (incf (ok-of count)
162                              (ecase state
163                                (5 2)
164                                ((6 9) 1)
165                                (10 0)))
166                        (incf (all-of count) 2)))
167                     (t
168                      (let ((count (getf counts :expression)))
169                        (when (eql state 1)
170                          (incf (ok-of count)))
171                        (incf (all-of count)))))
172               (if source-map
173                   (handler-case
174                       (multiple-value-bind (start end)
175                           (source-path-source-position (cons 0 source-path)
176                                                        source-form
177                                                        source-map)
178                         (push (list start end source state) locations))
179                     (error ()
180                       (warn "Error finding source location for source path ~A in file ~A~%" source-path file)))
181                   (warn "Unable to find a source map for toplevel form ~A in file ~A~%" tlf file)))))
182         ;; Now process the locations, from the shortest range to the longest
183         ;; one.
184         (dolist (location (sort locations #'<
185                                 :key (lambda (location)
186                                        (- (second location)
187                                           (first location)))))
188           (destructuring-bind (start end source state) location
189             (fill-with-state source states state start end))))
190       (print-report html-stream file counts states source)
191       (format html-stream "</body></html>")
192       (list (getf counts :expression)
193             (getf counts :branch)))))
194
195 (defun fill-with-state (source states state start end)
196   (let* ((pos (position #\Newline source
197                         :end start
198                         :from-end t))
199          (start-column (if pos
200                            (- start 1 pos)
201                            0))
202          (end-column 0))
203     (loop for i from start below end
204           for col from start-column
205           for char = (aref source i)
206           do (cond ((eql char #\Newline)
207                     (setf col -1))
208                    ((not (eql char #\Space))
209                     (setf end-column (max end-column col)))))
210     (loop for i from start below end
211           for col from start-column
212           for char = (aref source i)
213           do (if (eql char #\Newline)
214                  (setf col -1)
215                  (when (and (zerop (aref states i))
216                             #+nil (<= col end-column)
217                             (>= col start-column))
218                    (setf (aref states i) state))))))
219
220 ;;; Convert tabs to spaces
221 (defun detabify (source)
222   (with-output-to-string (stream)
223     (loop for char across source
224           for col from 0
225           for i from 0
226           do (if (eql char #\Tab)
227                  (loop repeat (- 8 (mod col 8))
228                        do (write-char #\Space stream)
229                        do (incf col)
230                        finally (decf col))
231                  (progn
232                    (when (eql char #\Newline)
233                      ;; Filter out empty last line
234                      (when (eql i (1- (length source)))
235                        (return))
236                      (setf col -1))
237                    (write-char char stream))))))
238
239 (defvar *counts* nil)
240
241 (defun print-report (html-stream file counts states source)
242   ;; Just used for testing
243   (setf *counts* counts)
244   (let ((*print-case* :downcase))
245     (format html-stream
246             "<h3>Coverage report: ~a <br />~%</h3>~%" file)
247     (when (zerop (all-of (getf counts :expression)))
248       (format html-stream "<b>File has no instrumented forms</b>")
249       (return-from print-report))
250     (format html-stream "<table class='summary'><tr class='head-row'>~{<td width='80px'>~a</td>~}"
251             (list "Kind" "Covered" "All" "%"))
252     (dolist (mode '(:expression :branch))
253       (let ((count (getf counts mode)))
254         (format html-stream "<tr class='~:[odd~;even~]'><td>~A</td><td>~a</td><td>~a</td><td>~5,1F</td></tr>~%"
255                 (eql mode :branch)
256                 mode
257                 (ok-of count)
258                 (all-of count)
259                 (percent count))))
260     (format html-stream "</table>"))
261   (format html-stream "<div class='key'><b>Key</b><br />~%")
262   (format html-stream "<div class='state-0'>Not instrumented</div>")
263   (format html-stream "<div class='state-15'>Conditionalized out</div>")
264   (format html-stream "<div class='state-1'>Executed</div>")
265   (format html-stream "<div class='state-2'>Not executed</div>")
266   (format html-stream "<div>&#160;</div>")
267   (format html-stream "<div class='state-5'>Both branches taken</div>")
268   (format html-stream "<div class='state-6'>One branch taken</div>")
269   (format html-stream "<div class='state-10''>Neither branch taken</div>")
270   (format html-stream "</div>")
271   (format html-stream "<nobr><div><code>~%")
272   (flet ((line (line)
273            (format html-stream "</code></div></nobr>~%<nobr><div class='source'><div class='line-number'><code>~A</code></div><code>&#160;" line)
274            line))
275     (loop for last-state = nil then state
276           with line = (line 1)
277           for col from 1
278           for char across source
279           for state across states
280           do (unless (eq state last-state)
281                (when last-state
282                  (format html-stream "</span>"))
283                (format html-stream "<span class='state-~a'>" state))
284           do (case char
285                ((#\Newline)
286                 (setf state nil)
287                 (setf col 0)
288                 (format html-stream "</span>")
289                 (line (incf line)))
290                ((#\Space)
291                 (format html-stream "&#160;"))
292                ((#\Tab)
293                 (error "tab"))
294                (t
295                 (if (alphanumericp char)
296                     (write-char char html-stream)
297                     (format html-stream "&#~A;" (char-code char))))))
298     (format html-stream "</code></div>")))
299
300 (defun write-styles (html-stream)
301   (format html-stream "<style type='text/css'>
302 *.state-0 { background-color: #eeeeee }
303 *.state-1 { background-color: #aaffaa }
304 *.state-5 { background-color: #44dd44 }
305 *.state-2 { background-color: #ffaaaa }
306 *.state-10 { background-color: #ee6666 }
307 *.state-15 { color: #aaaaaa; background-color: #eeeeee }
308 *.state-9,*.state-6 { background-color: #ffffaa }
309 div.key { margin: 20px; width: 200px }
310 div.source { width: 88ex; background-color: #eeeeee; padding-left: 5px;
311              /* border-style: solid none none none; border-width: 1px;
312              border-color: #dddddd */ }
313
314 *.line-number { color: #666666; float: left; width: 6ex; text-align: right; margin-right: 1ex; }
315
316 table.summary tr.head-row { background-color: #aaaaff }
317 table.summary tr td.text-cell { text-align: left }
318 table.summary tr td.main-head { text-align: center }
319 table.summary tr td { text-align: right }
320 table.summary tr.even { background-color: #eeeeff }
321 table.summary tr.subheading { background-color: #aaaaff}
322 table.summary tr.subheading td { text-align: left; font-weight: bold; padding-left: 5ex; }
323 </style>"))
324
325 (defun convert-records (records mode)
326   (ecase mode
327     (:expression
328      (loop for record in records
329            unless (member (caar record) '(:then :else))
330            collect (list mode
331                          (car record)
332                          (ecase (cdr record)
333                            ((t) 1)
334                            ((nil) 2)))))
335     (:branch
336      (let ((hash (make-hash-table :test 'equal)))
337        (dolist (record records)
338          (let ((path (car record)))
339            (when (member (car path) '(:then :else))
340              (setf (gethash (cdr path) hash)
341                    (logior (gethash (cdr path) hash 0)
342                            (ash (if (cdr record)
343                                     1
344                                     2)
345                                 (if (eql (car path) :then)
346                                     0
347                                     2)))))))
348        (let ((list nil))
349          (maphash (lambda (k v)
350                     (push (list mode k v) list))
351                   hash)
352          list)))))
353
354 ;;;; A mutant version of swank-source-path-parser from Swank/Slime.
355
356 (defun read-file (filename external-format)
357   "Return the entire contents of FILENAME as a string."
358   (with-open-file (s filename :direction :input
359                      :external-format external-format)
360     (let ((string (make-string (file-length s))))
361       (read-sequence string s)
362       string)))
363
364 (defun make-source-recorder (fn source-map)
365   "Return a macro character function that does the same as FN, but
366 additionally stores the result together with the stream positions
367 before and after of calling FN in the hashtable SOURCE-MAP."
368   (declare (type function fn))
369   (lambda (stream char)
370     (declare (optimize debug safety))
371     (let ((start (file-position stream))
372           (values (multiple-value-list (funcall fn stream char)))
373           (end (file-position stream)))
374       (unless (null values)
375         (push (list start end *read-suppress*)
376               (gethash (car values) source-map)))
377       (values-list values))))
378
379 (defun make-source-recording-readtable (readtable source-map)
380   "Return a source position recording copy of READTABLE.
381 The source locations are stored in SOURCE-MAP."
382   (let* ((tab (copy-readtable readtable))
383          (*readtable* tab))
384     (dotimes (code 128)
385       (let ((char (code-char code)))
386         (multiple-value-bind (fn term) (get-macro-character char tab)
387           (when fn
388             (set-macro-character char (make-source-recorder fn source-map)
389                                  term tab)))))
390     (suppress-sharp-dot tab)
391     (set-macro-character #\(
392                          (make-source-recorder
393                           (make-recording-read-list source-map)
394                           source-map))
395     tab))
396
397 ;;; Ripped from SB-IMPL, since location recording on a cons-cell level
398 ;;; can't be done just by simple read-table tricks.
399 (defun make-recording-read-list (source-map)
400   (lambda (stream ignore)
401     (block return
402       (when (eql *package* (find-package :keyword))
403         (return-from return
404           (sb-impl::read-list stream ignore)))
405       (let* ((thelist (list nil))
406              (listtail thelist))
407         (do ((firstchar (sb-impl::flush-whitespace stream)
408                         (sb-impl::flush-whitespace stream)))
409             ((char= firstchar #\) ) (cdr thelist))
410           (when (char= firstchar #\.)
411             (let ((nextchar (read-char stream t)))
412               (cond ((sb-impl::token-delimiterp nextchar)
413                      (cond ((eq listtail thelist)
414                             (unless *read-suppress*
415                               (sb-impl::%reader-error
416                                stream
417                                "Nothing appears before . in list.")))
418                            ((sb-impl::whitespace[2]p nextchar)
419                             (setq nextchar (sb-impl::flush-whitespace stream))))
420                      (rplacd listtail
421                              ;; Return list containing last thing.
422                              (car (sb-impl::read-after-dot stream nextchar)))
423                      (return (cdr thelist)))
424                     ;; Put back NEXTCHAR so that we can read it normally.
425                     (t (unread-char nextchar stream)))))
426           ;; Next thing is not an isolated dot.
427           (let ((start (file-position stream))
428                 (listobj (sb-impl::read-maybe-nothing stream firstchar))
429                 (end (file-position stream)))
430             ;; allows the possibility that a comment was read
431             (when listobj
432              (unless (consp (car listobj))
433                 (setf (car listobj) (gensym))
434                 (push (list start end *read-suppress*)
435                       (gethash (car listobj) source-map)))
436               (rplacd listtail listobj)
437               (setq listtail listobj))))))))
438
439 (defun suppress-sharp-dot (readtable)
440   (when (get-macro-character #\# readtable)
441     (let ((sharp-dot (get-dispatch-macro-character #\# #\. readtable)))
442       (set-dispatch-macro-character #\# #\.
443                                     (lambda (&rest args)
444                                       (let ((*read-suppress* t))
445                                         (apply sharp-dot args)))
446                                     readtable))))
447
448 (defun read-and-record-source-map (stream)
449   "Read the next object from STREAM.
450 Return the object together with a hashtable that maps
451 subexpressions of the object to stream positions."
452   (let* ((source-map (make-hash-table :test #'eq))
453          (*readtable* (make-source-recording-readtable *readtable* source-map))
454          (start (file-position stream))
455          (form (read stream))
456          (end (file-position stream)))
457     ;; ensure that at least FORM is in the source-map
458     (unless (gethash form source-map)
459       (push (list start end nil)
460             (gethash form source-map)))
461     (values form source-map)))
462
463 (defun read-source-form (n stream)
464   "Read the Nth toplevel form number with source location recording.
465 Return the form and the source-map."
466   (let ((*read-suppress* t))
467     (dotimes (i n)
468       (read stream)))
469   (let ((*read-suppress* nil)
470         (*read-eval* nil))
471     (read-and-record-source-map stream)))
472
473 (defun source-path-stream-position (path stream)
474   "Search the source-path PATH in STREAM and return its position."
475   (check-source-path path)
476   (destructuring-bind (tlf-number . path) path
477     (multiple-value-bind (form source-map) (read-source-form tlf-number stream)
478       (source-path-source-position (cons 0 path) form source-map))))
479
480 (defun check-source-path (path)
481   (unless (and (consp path)
482                (every #'integerp path))
483     (error "The source-path ~S is not valid." path)))
484
485 (defun source-path-string-position (path string)
486   (with-input-from-string (s string)
487     (source-path-stream-position path s)))
488
489 (defun source-path-file-position (path filename)
490   (with-open-file (file filename)
491     (source-path-stream-position path file)))
492
493 (defun source-path-source-position (path form source-map)
494   "Return the start position of PATH from FORM and SOURCE-MAP.  All
495 subforms along the path are considered and the start and end position
496 of the deepest (i.e. smallest) possible form is returned."
497   ;; compute all subforms along path
498   (let ((forms (loop for n in path
499                      for m on path
500                      for dummy = (when (eql n :progn)
501                                    (return forms))
502                      for f = form then (nth n f)
503                      unless (null (cdr m))
504                      collect f into forms
505                      finally (return forms))))
506     ;; select the first subform present in source-map
507     (loop for form in (reverse forms)
508           for positions = (gethash form source-map)
509           until (and positions (null (cdr positions)))
510           finally (destructuring-bind ((start end suppress)) positions
511                     (declare (ignore suppress))
512                     (return (values (1- start) end))))))