making franz happy
This commit is contained in:
Executable
+9
@@ -0,0 +1,9 @@
|
||||
#!/usr/bin/env bash
|
||||
# Run the full suite (unit + integration) with line coverage and write the joint
|
||||
# report to target/coverage/merged.
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
script/unit-coverage
|
||||
script/integration-coverage
|
||||
script/coverage-report
|
||||
Executable
+7
@@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
# Merge the unit and integration line-coverage results into target/coverage/merged
|
||||
# and print a summary. Requires Babashka.
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
exec bb script/coverage_report.clj "$@"
|
||||
@@ -0,0 +1,136 @@
|
||||
#!/usr/bin/env bb
|
||||
;; Merges the unit and integration LCOV files into one line-coverage report.
|
||||
;;
|
||||
;; bb script/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
|
||||
;; <out>/lcov.info and <out>/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-test"]))
|
||||
|
||||
(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 "<td class=\"%s\">%.1f%% <small>(%d/%d)</small></td>"
|
||||
(pct-class (:pct s)) (:pct s) (:hit s) (:total s))))
|
||||
files (sort (keys merged))]
|
||||
(str "<!doctype html><meta charset=utf-8><title>vibe line coverage</title>
|
||||
<style>
|
||||
body{font:14px system-ui;margin:2em;color:#222}table{border-collapse:collapse}
|
||||
td,th{padding:4px 12px;border-bottom:1px solid #ddd;text-align:left}
|
||||
.hi{color:#0a7d2c}.mid{color:#b36b00}.lo{color:#c0271f}
|
||||
pre{margin:0;font:12px ui-monospace,monospace}pre span{display:block;white-space:pre}
|
||||
.c{background:#e3f6e8}.u{background:#fde3e1}.g{display:inline-block;width:9em;color:#888}
|
||||
details{margin:1em 0}summary{cursor:pointer;font-weight:600}
|
||||
</style>
|
||||
<h1>vibe line coverage</h1>
|
||||
<table><tr><th>File</th>"
|
||||
(apply str (for [k (keys per-suite)] (str "<th>" (name k) "</th>")))
|
||||
"<th>merged</th></tr>"
|
||||
(apply str
|
||||
(for [f (concat files [nil])]
|
||||
(str "<tr><td>" (if f (esc f) "<b>Total</b>") "</td>"
|
||||
(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]))))
|
||||
"</tr>")))
|
||||
"</table><h2>Sources</h2>"
|
||||
(apply str
|
||||
(for [f files
|
||||
:let [lines (merged f)
|
||||
src (read-source f)]
|
||||
:when src]
|
||||
(str "<details><summary>" (esc f) "</summary><pre>"
|
||||
(apply str
|
||||
(map-indexed
|
||||
(fn [i text]
|
||||
(let [n (inc i) h (get lines n)]
|
||||
(format "<span class=\"%s\"><span class=g>%4d %s</span>%s</span>"
|
||||
(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)))
|
||||
"</pre></details>"))))))
|
||||
|
||||
(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 `script/unit-coverage` and/or `script/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*)
|
||||
Executable
+6
@@ -0,0 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
# Run the dockerized integration tests against a Cloverage-instrumented app;
|
||||
# reports go to target/coverage/integration.
|
||||
set -euo pipefail
|
||||
|
||||
COVERAGE=1 exec "$(dirname "$0")/integration-test"
|
||||
Executable
+79
@@ -0,0 +1,79 @@
|
||||
#!/usr/bin/env bash
|
||||
# Run the dockerized integration tests in isolation: builds the app image, starts it on a
|
||||
# random port, runs the suite against it, then tears everything down and exits with the
|
||||
# suite's exit code.
|
||||
#
|
||||
# With COVERAGE=1 (see script/integration-coverage) the app runs under Cloverage
|
||||
# (docker-compose.coverage.yml); after the suite finishes the app is stopped gracefully so
|
||||
# it writes its report, which is copied to target/coverage/integration.
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
root=$PWD
|
||||
|
||||
# Picks a random port in [lo, hi] that's currently free on this host; retries on
|
||||
# collision since another process could grab it first.
|
||||
free_port_in_range() {
|
||||
local lo=$1 hi=$2 port attempt
|
||||
for attempt in $(seq 20); do
|
||||
port=$((lo + RANDOM % (hi - lo + 1)))
|
||||
# A successful connect means something is already listening.
|
||||
if ! (exec 3<>"/dev/tcp/127.0.0.1/$port") 2>/dev/null; then
|
||||
echo "$port"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
echo "no free port found in range $lo-$hi" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
port=$(free_port_in_range 30000 40000)
|
||||
echo "[test] starting vibe-app on random port $port${COVERAGE:+ (under coverage instrumentation)}"
|
||||
|
||||
compose=(docker compose)
|
||||
if [[ -n "${COVERAGE:-}" ]]; then
|
||||
compose+=(-f docker-compose.yml -f docker-compose.coverage.yml)
|
||||
fi
|
||||
|
||||
# Works around an LXC/AppArmor bug in runc (opencontainers/runc#5007) that fails RUN steps
|
||||
# during image build; a docker-container builder with apparmor disabled sidesteps it.
|
||||
builder=vibe-builder
|
||||
if ! docker buildx inspect "$builder" >/dev/null 2>&1; then
|
||||
docker buildx create --name "$builder" \
|
||||
--driver docker-container \
|
||||
--buildkitd-config buildkitd.toml
|
||||
fi
|
||||
docker buildx use "$builder"
|
||||
|
||||
cd integration-test
|
||||
export VIBE_PORT=$port
|
||||
|
||||
teardown() {
|
||||
local status=$?
|
||||
trap - EXIT
|
||||
"${compose[@]}" down -v || true
|
||||
exit "$status"
|
||||
}
|
||||
trap teardown EXIT
|
||||
trap 'exit 130' INT
|
||||
trap 'exit 143' TERM
|
||||
|
||||
# `run` returns the test container's own exit code; `up --abort-on-container-exit` can leak
|
||||
# the app's SIGTERM exit (143) during teardown instead.
|
||||
status=0
|
||||
"${compose[@]}" --ansi never --progress plain run --build --rm integration-test || status=$?
|
||||
|
||||
if [[ -n "${COVERAGE:-}" ]]; then
|
||||
# SIGTERM is what makes Cloverage write its report, so stop (not kill) the app and pull
|
||||
# the report out of the stopped container before `down` removes it.
|
||||
out_dir=$root/target/coverage
|
||||
echo "[test] stopping vibe-app to flush coverage report"
|
||||
"${compose[@]}" stop -t 120 vibe-app || true
|
||||
rm -rf "$out_dir/integration"
|
||||
mkdir -p "$out_dir"
|
||||
if ! "${compose[@]}" cp vibe-app:/app/target/coverage/integration "$out_dir" \
|
||||
|| [[ ! -f $out_dir/integration/lcov.info ]]; then
|
||||
echo "[test] WARNING: no integration coverage report was produced"
|
||||
fi
|
||||
fi
|
||||
|
||||
exit "$status"
|
||||
Executable
+13
@@ -0,0 +1,13 @@
|
||||
#!/usr/bin/env bash
|
||||
# Start an nREPL server with dev dependencies, on the port in .nrepl-port (default 3000).
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
port=3000
|
||||
if [[ -f .nrepl-port ]]; then
|
||||
port=$(tr -d '[:space:]' < .nrepl-port)
|
||||
fi
|
||||
|
||||
exec clojure -A:dev -M -m nrepl.cmdline \
|
||||
--middleware "[cider.nrepl/cider-middleware]" \
|
||||
--port "$port"
|
||||
Executable
+7
@@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
# Run the unit tests, then the integration tests (stops at the first failure).
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
script/unit-test
|
||||
script/integration-test
|
||||
Executable
+8
@@ -0,0 +1,8 @@
|
||||
#!/usr/bin/env bash
|
||||
# Run the unit tests under Cloverage; reports go to target/coverage/unit.
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
exec clojure -M:cov -m cloverage.coverage \
|
||||
--src-ns-path src --test-ns-path test \
|
||||
--lcov --html --output target/coverage/unit
|
||||
Executable
+6
@@ -0,0 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
# Run the Clojure unit tests (test/ directory). Extra args go to the test runner.
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
exec clojure -M:test "$@"
|
||||
Reference in New Issue
Block a user