#!/usr/bin/env bb ;; Merges the unit and integration LCOV files into one line-coverage report. ;; ;; bb scripts/coverage_report.clj [--out target/coverage/merged] ;; ;; Reads target/coverage/{unit,integration}/lcov.info (either may be missing), prints a ;; per-file table (unit / integration / merged) and writes the merged result to ;; /lcov.info and /index.html. A line counts as covered in the merged report ;; if either suite hit it. (ns coverage-report (:require [babashka.cli :as cli] [clojure.java.io :as io] [clojure.string :as str])) (def suites [[:unit "target/coverage/unit/lcov.info"] [:integration "target/coverage/integration/lcov.info"]]) (defn parse-lcov "lcov.info text -> {file {line hits}}" [text] (loop [[l & more] (str/split-lines text), file nil, acc {}] (cond (nil? l) acc (str/starts-with? l "SF:") (recur more (subs l 3) (update acc (subs l 3) #(or % {}))) (str/starts-with? l "DA:") (let [[ln hits] (map parse-long (str/split (subs l 3) #","))] (recur more file (update-in acc [file ln] (fnil + 0) hits))) :else (recur more file acc)))) (defn stats [lines] (let [total (count lines) hit (count (filter pos? (vals lines)))] {:hit hit :total total :pct (if (zero? total) 100.0 (* 100.0 (/ hit total)))})) (defn fmt [{:keys [hit total pct]}] (format "%3d/%-3d %6.1f%%" hit total pct)) (defn read-source [file] ;; Cloverage's SF: paths are usually project-relative already (src/vibe/core.clj); fall ;; back to the source roots in case they are classpath-relative. (some (fn [dir] (let [f (io/file dir file)] (when (.isFile f) (slurp f)))) [nil "src" "test" "integration-tests"])) (defn esc [s] (-> s (str/replace "&" "&") (str/replace "<" "<") (str/replace ">" ">"))) (defn html-report [per-suite merged] (let [pct-class #(cond (>= % 80) "hi" (>= % 50) "mid" :else "lo") row (fn [label lines] (let [s (stats lines)] (format "%.1f%% (%d/%d)" (pct-class (:pct s)) (:pct s) (:hit s) (:total s)))) files (sort (keys merged))] (str "vibe line coverage

vibe line coverage

" (apply str (for [k (keys per-suite)] (str ""))) "" (apply str (for [f (concat files [nil])] (str "" (apply str (for [k (keys per-suite)] (row k (if f (get-in per-suite [k f] {}) (into {} (for [[ff ls] (get per-suite k) [l h] ls] [[ff l] h])))))) (row :merged (if f (merged f) (into {} (for [[ff ls] merged [l h] ls] [[ff l] h])))) ""))) "
File" (name k) "merged
" (if f (esc f) "Total") "

Sources

" (apply str (for [f files :let [lines (merged f) src (read-source f)] :when src] (str "
" (esc f) "
"
                       (apply str
                              (map-indexed
                               (fn [i text]
                                 (let [n (inc i) h (get lines n)]
                                   (format "%4d  %s%s"
                                           (cond (nil? h) "" (pos? h) "c" :else "u")
                                           n
                                           (if h
                                             (str/join " " (for [k (keys per-suite)]
                                                             (str (subs (name k) 0 1) ":" (get-in per-suite [k f n] 0))))
                                             "")
                                           (esc text))))
                               (str/split-lines src)))
                       "
")))))) (defn -main [& args] (let [{:keys [out]} (cli/parse-opts args {:coerce {:out :string}}) out (or out "target/coverage/merged") per-suite (into {} (for [[k path] suites :when (.exists (io/file path))] [k (parse-lcov (slurp path))]))] (when (empty? per-suite) (println "No lcov.info found; run `bb unit-coverage` and/or `bb integration-coverage` first.") (System/exit 1)) (let [merged (apply merge-with (fn [a b] (merge-with + a b)) (vals per-suite)) cols (keys per-suite) files (sort (keys merged)) width (apply max 5 (map count files)) hdr (str (format (str "%-" width "s") "File") (apply str (for [k (concat cols [:merged])] (format " %-16s" (name k)))))] (println hdr) (println (apply str (repeat (count hdr) "-"))) (doseq [f files] (println (str (format (str "%-" width "s") f) (apply str (for [k cols] (str " " (fmt (stats (get-in per-suite [k f] {})))))) " " (fmt (stats (merged f)))))) (println (apply str (repeat (count hdr) "-"))) (let [flat (fn [m] (into {} (for [[f ls] m [l h] ls] [[f l] h])))] (println (str (format (str "%-" width "s") "Total") (apply str (for [k cols] (str " " (fmt (stats (flat (per-suite k))))))) " " (fmt (stats (flat merged)))))) (.mkdirs (io/file out)) (spit (io/file out "lcov.info") (apply str (for [f files] (str "TN:\nSF:" f "\n" (apply str (for [[l h] (sort (merged f))] (str "DA:" l "," h "\n"))) "LF:" (count (merged f)) "\nLH:" (:hit (stats (merged f))) "\nend_of_record\n")))) (spit (io/file out "index.html") (html-report per-suite merged)) (println (str "\nMerged report: " out "/index.html (+ lcov.info)"))))) (apply -main *command-line-args*)