Changeset 10572

Aug 27, 2008, 12:53:21 AM (11 years ago)

Assorted code coverage extensions.

  • added an :HTML argument to CCL:REPORT-COVERAGE, can be specified as

nil to skip writing html files.

  • New function CCL:COVERAGE-STATISTICS - returns a sequence of

CCL:COVERAGE-STATISTICS objects containing the same information that
is written to the statistics file by report-coverage. See doc (or
source) for the list of accessors.

  • Make coverage snapshots be first class objects. New functions

related to snapshots:

CCL:SAVE-COVERAGE - creates a snapshot
CCL:RESTORE-COVERAGE - restores the coverage from a snapshot
CCL:COMBINE-COVERAGE - combines a set of snapshots into one
CCL:WRITE-COVERAGE-TO-FILE - saves a snapshot in a file
CCL:READ-COVERAGE-FROM-FILE - loads a snapshot from a file

  • Changed the internal representation so that sparse snapshots

(i.e. when most code is not covered) take less space, in anticipation
of having thousands of them around.

  • Made the file written by write-coverage-to-file (and hence

save-coverage-to-file as well) be a lisp source file, which can be
compiled for possibly faster loading. Either the original file or
the compiled file can be handed to read-coverage-from-file or

The documentation in
has been updated with all these changes.

1 edited


  • branches/working-0711/ccl/library/cover.lisp

    r10244 r10572  
    1111          save-coverage-in-file
    1212          restore-coverage-from-file
     14          save-coverage
     15          restore-coverage
     16          combine-coverage
     17          read-coverage-from-file
     18          write-coverage-to-file
     20          coverage-statistics
     21          coverage-source-file
     22          coverage-expressions-total
     23          coverage-expressions-entered
     24          coverage-expressions-covered
     25          coverage-unreached-branches
     26          coverage-code-forms-total
     27          coverage-code-forms-covered
     28          coverage-functions-total
     29          coverage-functions-fully-covered
     30          coverage-functions-partly-covered
     31          coverage-functions-not-entered
    1333          without-compiling-code-coverage))
     45(defstruct (coverage-state (:conc-name "%COVERAGE-STATE-"))
     46  alist)
     48;; Wrapper in case we ever want to do dwim on raw alists
     49(defun coverage-state-alist (coverage)
     50  (etypecase coverage
     51    (coverage-state (%coverage-state-alist coverage))))
    2554(defun file-coverage-file (entry)
    2655  (car entry))
    4372(defun map-function-coverage (lfun fn &optional refs)
    4473  (let ((refs (cons lfun refs)))
     74    (declare (dynamic-extent refs))
    4575    (lfunloop for imm in lfun
    4676              when (code-note-p imm)
    115145    (show note 0 "")))
     147(defun assoc-by-filename (path alist)
     148  (let* ((true-path (probe-file path)))
     149    (find-if #'(lambda (data)
     150                 (or (equalp (car data) path)
     151                     (and true-path (equalp (probe-file (car data)) true-path))))
     152             alist)))
    117154(defun covered-functions-for-file (path)
    118   (let* ((true-path (probe-file path))
    119          (data (find-if #'(lambda (data)
    120                             (or (equalp (car data) path)
    121                                 (and true-path (equalp (probe-file (car data)) true-path))))
    122                         *code-covered-functions*)))
    123     (cdr data)))
     155  (cdr (assoc-by-filename path *code-covered-functions*)))
    125157(defun clear-coverage ()
    130162  (setq *code-covered-functions* nil))
     164(defun reset-function-coverage (lfun)
     165  (map-function-coverage lfun #'(lambda (note)
     166                                  (setf (code-note-code-coverage note) nil))))
    132168(defun reset-coverage ()
    133169  "Reset all coverage data back to the `Not executed` state."
    134   (flet ((reset (note) (setf (code-note-code-coverage note) nil)))
    135     (loop for data in *code-covered-functions*
    136           do (typecase data
    137                (cons ;; (source-file . functions)
     170  (loop for data in *code-covered-functions*
     171        do (typecase data
     172             (cons ;; (source-file . functions)
    138173                (loop for fn across (cdr data)
    139                       do (map-function-coverage fn #'reset)))
    140                (function (map-function-coverage data #'reset))))))
     174                      do (reset-function-coverage fn)))
     175             (function (reset-function-coverage data)))))
    142177;; Name used for consistency checking across file save/restore
     185(defun coverage-mismatch (why &rest args)
     186  ;; Throw to somebody who knows what file we're working on.
     187  (throw 'coverage-mismatch (cons why args)))
     189(defmacro with-coverage-mismatch-catch ((saved-file) &body body)
     190  `(let ((file ,saved-file)
     191         (err (catch 'coverage-mismatch ,@body nil)))
     192     (when err
     193       (error "Mismatched coverage data for ~s, ~?" file (car err) (cdr err)))))
     196;; (name . #(i1 i2 ...)) where in is either an index or (index . subfncoverage).
    150197(defun save-function-coverage (fn &optional (refs ()))
    151   (push fn refs)
    152   (cons (function-covered-name fn)
    153         (lfunloop for imm in fn
    154                   when (code-note-p imm)
    155                   collect (code-note-code-coverage imm)
    156                   when (and (functionp imm) (not (memq imm refs)))
    157                   collect (save-function-coverage imm refs))))
     198  (let ((refs (cons fn refs)))
     199    (declare (dynamic-extent refs))
     200    (cons (function-covered-name fn)
     201          (lfunloop for imm in fn as i upfrom 0
     202                    when (and (code-note-p imm)
     203                              (code-note-code-coverage imm))
     204                    collect i into list
     205                    when (and (functionp imm) (not (memq imm refs)))
     206                    collect (cons i (save-function-coverage imm refs)) into list
     207                    finally (return (and list (coerce list 'vector)))))))
     209(defun copy-function-coverage (fn-data)
     210  (cons (car fn-data)
     211        (and (cdr fn-data)
     212             (map 'vector #'(lambda (imm-data)
     213                              (if (consp imm-data)
     214                                (cons (car imm-data)
     215                                      (copy-function-coverage (cdr imm-data)))
     216                                imm-data))
     217                  (cdr fn-data)))))
     219(defun restore-function-coverage (fn saved-fn-data &optional (refs ()))
     220  (let* ((refs (cons fn refs))
     221         (saved-name (car saved-fn-data))
     222         (saved-imms (cdr saved-fn-data))
     223         (nimms (length saved-imms))
     224         (n 0))
     225    (declare (dynamic-extent refs))
     226    (unless (equalp saved-name (function-covered-name fn))
     227      (coverage-mismatch "had function ~s now have ~s" saved-name fn))
     228    (lfunloop for imm in fn as i upfrom 0
     229              when (code-note-p imm)
     230              do (let* ((next (and (< n nimms) (aref saved-imms n))))
     231                   (when (if (consp next) (<= (car next) i) (and next (< next i)))
     232                     (coverage-mismatch "in ~s" fn))
     233                   (when (setf (code-note-code-coverage imm)
     234                               (and (eql next i) 'restored))
     235                     (incf n)))
     236              when (and (functionp imm) (not (memq imm refs)))
     237              do (let* ((next (and (< n nimms) (aref saved-imms n))))
     238                   (unless (and (consp next) (eql (car next) i))
     239                     (coverage-mismatch "in ~s" fn))
     240                   (restore-function-coverage imm (cdr next) refs)
     241                   (incf n)))))
     244(defun add-function-coverage (fn-data new-fn-data)
     245  (let* ((fn-name (car fn-data))
     246         (imms (cdr fn-data))
     247         (new-fn-name (car new-fn-data))
     248         (new-imms (cdr new-fn-data)))
     249    (flet ((kar (x) (if (consp x) (%car x) x)))
     250      (declare (inline kar))
     251      (unless (equalp fn-name new-fn-name)
     252        (coverage-mismatch "function ~s vs. ~s" fn-name new-fn-name))
     253      (when new-imms
     254        (loop for new across new-imms
     255              as old = (find (kar new) imms :key #'kar)
     256              if (and (null old) (fixnump new))
     257                collect new into extras
     258              else do (unless (eql old new)
     259                        (if (and (consp new) (consp old))
     260                          (add-function-coverage (cdr old) (cdr new))
     261                          (coverage-mismatch "in function ~s" fn-name)))
     262              finally (when extras
     263                        (setf (cdr fn-data)
     264                              (sort (concatenate 'vector imms extras) #'< :key #'kar))))))
     265    fn-data))
    161268(defun save-coverage ()
    162   "Returns an opaque representation of the current code coverage state.
    163 The only operation that may be done on the state is passing it to
    164 RESTORE-COVERAGE. The representation is guaranteed to be readably printable.
    165 A representation that has been printed and read back will work identically
    167   (loop for data in *code-covered-functions*
    168         when (consp data)
    169         collect (cons (car data)
    170                       (map 'vector #'save-function-coverage (cdr data)))))
     269  "Returns a snapshot of the current coverage state"
     270  (make-coverage-state
     271   :alist (loop for data in *code-covered-functions*
     272                when (consp data)
     273                  collect (cons (car data)
     274                                (map 'vector #'save-function-coverage (cdr data))))))
     276(defun combine-coverage (coverage-states)
     277  (let ((result nil))
     278    (map nil
     279         (lambda (coverage-state)
     280           (loop for (saved-file . saved-fns) in (coverage-state-alist coverage-state)
     281                 for result-fns = (cdr (assoc-by-filename saved-file result))
     282                 do (with-coverage-mismatch-catch (saved-file)
     283                      (cond ((null result-fns)
     284                             (push (cons saved-file
     285                                         (map 'vector #'copy-function-coverage saved-fns))
     286                                   result))
     287                            ((not (eql (length result-fns) (length saved-fns)))
     288                             (coverage-mismatch "different function counts"))
     289                            (t
     290                             (loop for result-fn across result-fns
     291                                   for saved-fn across saved-fns
     292                                   do (add-function-coverage result-fn saved-fn)))))))
     293         coverage-states)
     294    (make-coverage-state :alist (nreverse result))))
    172297(defun restore-coverage (coverage-state)
    173   "Restore the code coverage data back to an earlier state produced by
    175   (loop for (saved-file . saved-fns) in coverage-state
    176         as fns = (covered-functions-for-file saved-file)
    177         do (flet ((mismatched (why &rest args)
    178                     (error "Mismatched coverage data for ~s, ~?" saved-file why args)))
    179              (cond ((null fns)
    180                     (warn "Couldn't restore saved coverage for ~s, no matching file present"
    181                           saved-file))
    182                    ((not (eql (length fns) (length saved-fns)))
    183                     (mismatched "was ~s functions, now ~s" (length saved-fns) (length fns)))
    184                    (t
    185                     (loop for fn across fns
    186                           for saved-data across saved-fns
    187                           do (labels
    188                                  ((rec (fn saved-data refs)
    189                                     (push fn refs)
    190                                     (let* ((name (car saved-data))
    191                                            (saved-imms (cdr saved-data)))
    192                                       (unless (equalp name (function-covered-name fn))
    193                                         (mismatched "had ~s now have ~s" name (function-name fn)))
    194                                       (lfunloop for imm in fn
    195                                                 when (code-note-p imm)
    196                                                 do (if (or (null saved-imms) (consp (car saved-imms)))
    197                                                        (mismatched "in ~s" name)
    198                                                        (setf (code-note-code-coverage imm) (pop saved-imms)))
    199                                                 when (and (functionp imm)
    200                                                           (not (memq imm refs)))
    201                                                 do (if (or (null saved-imms) (atom (car saved-imms)))
    202                                                        (mismatched "in ~s" name)
    203                                                        (rec imm (pop saved-imms) refs))))))
    204                                (rec fn saved-data nil))))))))
    206 (defun save-coverage-in-file (pathname)
    207   "Call SAVE-COVERAGE and write the results of that operation into the
    208 file designated by PATHNAME."
     298  "Restore the code coverage data back to an earlier state produced by SAVE-COVERAGE."
     299  (loop for (saved-file . saved-fns) in (coverage-state-alist coverage-state)
     300        for fns = (covered-functions-for-file saved-file)
     301        do (with-coverage-mismatch-catch (saved-file)
     302             (cond ((null fns)
     303                    (warn "Couldn't restore saved coverage for ~s, no matching file present"
     304                          saved-file))
     305                   ((not (eql (length fns) (length saved-fns)))
     306                    (coverage-mismatch "had ~s functions, now have ~s"
     307                                       (length saved-fns) (length fns)))
     308                   (t
     309                    (map nil #'restore-function-coverage fns saved-fns))))))
     311(defvar *loading-coverage*)
     313(defun write-coverage-to-file (coverage pathname)
     314  "Write the coverage state COVERAGE in the file designated by PATHNAME"
    209315  (with-open-file (stream pathname
    210316                          :direction :output
    213319    (with-standard-io-syntax
    214320      (let ((*package* (pkg-arg "CCL")))
    215         (write (save-coverage) :stream stream)))
     321        (write `(setq *loading-coverage* ',(coverage-state-alist coverage))
     322               :stream stream)))
    216323    (values)))
     325(defun read-coverage-from-file (pathname)
     326  " Return the coverage state saved in the file.  Doesn't affect the current coverage state."
     327  (let ((*package* (pkg-arg "CCL"))
     328        (*loading-coverage* :none))
     329    (load pathname)
     330    (when (eq *loading-coverage* :none)
     331      (error "~s doesn't seem to be a saved coverage file" pathname))
     332    (make-coverage-state :alist *loading-coverage*)))
     334(defun save-coverage-in-file (pathname)
     335  "Save the current coverage state in the file designed by PATHNAME"
     336  (write-coverage-to-file (save-coverage) pathname))
    218338(defun restore-coverage-from-file (pathname)
    219   "READ the contents of the file designated by PATHNAME and pass the
    220 result to RESTORE-COVERAGE."
    221   (with-open-file (stream pathname :direction :input)
    222     (with-standard-io-syntax
    223       (let ((*package* (pkg-arg "CCL")))
    224         (restore-coverage (read stream))))
    225     (values)))
     339  "Set the current coverage state from the file designed by PATHNAME"
     340  (restore-coverage (read-coverage-from-file pathname)))
    227342(defun common-coverage-directory ()
    252 (defun report-coverage (output-file &key (external-format :default) (statistics t))
    253   "Print a code coverage report of all instrumented files into DIRECTORY.
    254 If DIRECTORY does not exist, it will be created. The main report will be
    255 printed to the file cover-index.html. The external format of the source
    256 files can be specified with the EXTERNAL-FORMAT parameter.
     367(defstruct (coverage-statistics (:conc-name "COVERAGE-"))
     368  source-file
     369  expressions-total
     370  expressions-entered
     371  expressions-covered
     372  unreached-branches
     373  code-forms-total
     374  code-forms-covered
     375  functions-total
     376  functions-fully-covered
     377  functions-partly-covered
     378  functions-not-entered)
     380(defun coverage-statistics ()
     381  (let* ((*file-coverage* nil)
     382         (*coverage-subnotes* (make-hash-table :test #'eq :shared nil))
     383         (*emitted-code-notes* (make-hash-table :test #'eq :shared nil))
     384         (*entry-code-notes* (make-hash-table :test #'eq :shared nil)))
     385    (get-coverage)
     386    (loop for coverage in *file-coverage*
     387          as stats = (make-coverage-statistics :source-file (file-coverage-file coverage))
     388          do (map nil (lambda (fn)
     389                        (let ((note (function-entry-code-note fn)))
     390                          (when note (precompute-note-coverage note))))
     391                  (file-coverage-toplevel-functions coverage))
     392          do (destructuring-bind (total entered %entered covered %covered)
     393                 (count-covered-sexps coverage)
     394               (declare (ignore %entered %covered))
     395               (setf (coverage-expressions-total stats) total)
     396               (setf (coverage-expressions-entered stats) entered)
     397               (setf (coverage-expressions-covered stats) covered))
     398          do (let ((count (count-unreached-branches coverage)))
     399               (setf (coverage-unreached-branches stats) count))
     400          do (destructuring-bind (total covered %covered) (count-covered-aexps coverage)
     401               (declare (ignore %covered))
     402               (setf (coverage-code-forms-total stats) total)
     403               (setf (coverage-code-forms-covered stats) covered))
     404          do (destructuring-bind (total fully %fully partly %partly never %never)
     405                 (count-covered-entry-notes coverage)
     406               (declare (ignore %fully %partly %never))
     407               (setf (coverage-functions-total stats) total)
     408               (setf (coverage-functions-fully-covered stats) fully)
     409               (setf (coverage-functions-partly-covered stats) partly)
     410               (setf (coverage-functions-not-entered stats) never))
     411          collect stats)))
     414(defun report-coverage (output-file &key (external-format :default) (statistics t) (html t))
     415  "If :HTML is non-nil, generate an HTML report, consisting of an index file in OUTPUT-FILE
     416and, in the same directory, one html file for each instrumented source file that has been
     417loaded in the current session.
     418The external format of the source files can be specified with the EXTERNAL-FORMAT parameter.
    257419If :STATISTICS is non-nil, a CSV file is generated with a table.  If
    258420:STATISTICS is a filename, that file is used, else 'statistics.csv' is
    266428         (*emitted-code-notes* (make-hash-table :test #'eq :shared nil))
    267429         (*entry-code-notes* (make-hash-table :test #'eq :shared nil))
    268          (index-file (merge-pathnames output-file "index.html"))
     430         (index-file (and html (merge-pathnames output-file "index.html")))
    269431         (stats-file (and statistics (merge-pathnames (if (or (stringp statistics)
    270432                                                              (pathnamep statistics))
    281443           (let* ((src-name (enough-namestring file coverage-dir))
    282444                  (html-name (substitute #\_ #\: (substitute #\_ #\. (substitute #\_ #\/ src-name)))))
    283              (with-open-file (stream (make-pathname :name html-name :type "html" :defaults directory)
    284                                      :direction :output
    285                                      :if-exists :supersede
    286                                      :if-does-not-exist :create)
    287                (report-file-coverage index-file coverage stream external-format))
     445             (when html
     446               (with-open-file (stream (make-pathname :name html-name :type "html" :defaults directory)
     447                                       :direction :output
     448                                       :if-exists :supersede
     449                                       :if-does-not-exist :create)
     450                 (report-file-coverage index-file coverage stream external-format)))
    288451             (push (list* src-name html-name coverage) paths))))
    289452    (when (null paths)
    298461                                           (string< (file-namestring f1)
    299462                                                    (file-namestring f2))))))))
    300     (with-open-file (html-stream index-file
    301                                  :direction :output
    302                                  :if-exists :supersede
    303                                  :if-does-not-exist :create)
     463    (if html
     464      (with-open-file (html-stream index-file
     465                                   :direction :output
     466                                   :if-exists :supersede
     467                                   :if-does-not-exist :create)
     468        (if stats-file
     469          (with-open-file (stats-stream stats-file
     470                                        :direction :output
     471                                        :if-exists :supersede
     472                                        :if-does-not-exist :create)
     473            (report-coverage-to-streams paths html-stream stats-stream))
     474          (report-coverage-to-streams paths html-stream nil)))
    304475      (if stats-file
    305476        (with-open-file (stats-stream stats-file
    307478                                      :if-exists :supersede
    308479                                      :if-does-not-exist :create)
    309           (report-coverage-to-streams paths html-stream stats-stream))
    310         (report-coverage-to-streams paths html-stream nil)))
     480          (report-coverage-to-streams paths nil stats-stream))
     481        (error "One of :HTML or :STATISTICS must be non-nil")))
    311482    (values index-file stats-file)))
    313484(defun report-coverage-to-streams (paths html-stream stats-stream)
    314   (write-coverage-styles html-stream)
     485  (when html-stream (write-coverage-styles html-stream))
    315486  (unless paths
    316487    (warn "No coverage data found for any file, producing an empty report. Maybe you forgot to (SETQ CCL::*COMPILE-CODE-COVERAGE* T) before compiling?")
    317     (format html-stream "<h3>No code coverage data found.</h3>~%")
     488    (when html-stream (format html-stream "<h3>No code coverage data found.</h3>~%"))
    318489    (when stats-stream (format stats-stream "No code coverage data found.~%"))
    319490    (return-from report-coverage-to-streams))
    320   (format html-stream "<table class='summary'>")
     491  (when html-stream (format html-stream "<table class='summary'>"))
    321492  (coverage-stats-head html-stream stats-stream)
    322493  (loop for prev = nil then src-name
    327498                                 (pathname-directory (pathname prev)))))
    328499             (let ((dir (namestring (make-pathname :name nil :type nil :defaults src-name))))
    329                (format html-stream "<tr class='subheading'><td colspan='17'>~A</td></tr>~%" dir)
     500               (when html-stream (format html-stream "<tr class='subheading'><td colspan='17'>~A</td></tr>~%" dir))
    330501               (when stats-stream (format stats-stream "~a~%" dir))))
    331502        do (coverage-stats-data html-stream stats-stream coverage even report-name src-name))
    332   (format html-stream "</table>"))
     503  (when html-stream (format html-stream "</table>")))
    334505(defun precompute-note-coverage (note &optional refs)
    508679(defun coverage-stats-head (html-stream stats-stream)
    509   (format html-stream "<tr class='head-row'><td></td>")
    510   (format html-stream "<td class='main-head' colspan='5'>Expressions</td>")
    511   (format html-stream "<td class='main-head' colspan='1'>Branches</td>")
    512   (format html-stream "<td class='main-head' colspan='3'>Code Forms</td>")
    513   (format html-stream "<td class='main-head' colspan='7'>Functions</td></tr>")
    514   (format html-stream "<tr class='head-row'>~{<td width='60px'>~A</td>~}</tr>"
     680  (when html-stream
     681    (format html-stream "<tr class='head-row'><td></td>")
     682    (format html-stream "<td class='main-head' colspan='5'>Expressions</td>")
     683    (format html-stream "<td class='main-head' colspan='1'>Branches</td>")
     684    (format html-stream "<td class='main-head' colspan='3'>Code Forms</td>")
     685    (format html-stream "<td class='main-head' colspan='7'>Functions</td></tr>")
     686    (format html-stream "<tr class='head-row'>~{<td width='60px'>~A</td>~}</tr>"
    515687            '("Source file"
    516688              ;; Expressions
    521693              "Total" "Covered" "% covered"
    522694              ;; Functions
    523               "Total" "Fully covered" "% fully covered" "Partly covered" "% partly covered" "Not entered" "% not entered"))
     695              "Total" "Fully covered" "% fully covered" "Partly covered" "% partly covered" "Not entered" "% not entered")))
    524696  (when stats-stream
    525697    (format stats-stream "~{~a~^,~}"
    534706(defun coverage-stats-data (html-stream stats-stream coverage &optional evenp report-name src-name)
    535   (format html-stream "<tr class='~:[odd~;even~]'>" evenp)
    536   (if report-name
    537     (format html-stream "<td class='text-cell'><a href='~a.html'>~a</a></td>" report-name src-name)
    538     (format html-stream "<td class='text-cell'>~a</td>" (file-coverage-file coverage)))
     707  (when html-stream
     708    (format html-stream "<tr class='~:[odd~;even~]'>" evenp)
     709    (if report-name
     710      (format html-stream "<td class='text-cell'><a href='~a.html'>~a</a></td>" report-name src-name)
     711      (format html-stream "<td class='text-cell'>~a</td>" (file-coverage-file coverage))))
    539712  (when stats-stream
    540713    (format stats-stream "~a," (file-coverage-file coverage)))
    542715  (let ((exp-counts (count-covered-sexps coverage)))
    543     (format html-stream "~{<td>~:[-~;~:*~a~]</td><td>~:[-~;~:*~a~]</td><td>~:[-~;~:*~5,1f%~]</td<td>~:[-~;~:*~a~]</td><td>~:[-~;~:*~5,1f%~]</td>~}" exp-counts)
     716    (when html-stream
     717      (format html-stream "~{<td>~:[-~;~:*~a~]</td><td>~:[-~;~:*~a~]</td><td>~:[-~;~:*~5,1f%~]</td<td>~:[-~;~:*~a~]</td><td>~:[-~;~:*~5,1f%~]</td>~}" exp-counts))
    544718    (when stats-stream
    545719      (format stats-stream "~{~:[~;~:*~a~],~:[~;~:*~a~],~:[~;~:*~5,1f%~],~:[~;~:*~a~],~:[~;~:*~5,1f%~],~}" exp-counts)))
    547721  (let ((count (count-unreached-branches coverage)))
    548     (format html-stream "<td>~:[-~;~:*~a~]</td>" count)
     722    (when html-stream
     723      (format html-stream "<td>~:[-~;~:*~a~]</td>" count))
    549724    (when stats-stream
    550725      (format stats-stream "~:[~;~:*~a~]," count)))
    552727  (let ((exp-counts (count-covered-aexps coverage)))
    553     (format html-stream "~{<td>~:[-~;~:*~a~]</td><td>~:[-~;~:*~a~]</td><td>~:[-~;~:*~5,1f%~]</td>~}" exp-counts)
     728    (when html-stream
     729      (format html-stream "~{<td>~:[-~;~:*~a~]</td><td>~:[-~;~:*~a~]</td><td>~:[-~;~:*~5,1f%~]</td>~}" exp-counts))
    554730    (when stats-stream
    555731      (format stats-stream "~{~:[~;~:*~a~],~:[~;~:*~a~],~:[~;~:*~5,1f%~],~}" exp-counts)))
    557733  (destructuring-bind (total . counts) (count-covered-entry-notes coverage)
    558     (format html-stream "<td>~:[-~;~:*~a~]</td>~{<td>~:[-~;~:*~a~]</td><td>~:[-~;~:*~5,1f%~]</td>~}</tr>" total counts)
     734    (when html-stream
     735      (format html-stream "<td>~:[-~;~:*~a~]</td>~{<td>~:[-~;~:*~a~]</td><td>~:[-~;~:*~5,1f%~]</td>~}</tr>" total counts))
    559736    (when stats-stream
    560737      (format stats-stream "~:[~;~:*~a~],~{~:[~;~:*~a~],~:[-~;~:*~5,1f%~]~^,~}~%" total counts))))
Note: See TracChangeset for help on using the changeset viewer.