Skip to content

Commit 916f482

Browse files
committed
Generate Cobertura XML coverage report
Emit a Cobertura report (coverage.xml) from the llvm-cov JSON, filtered to the library sources, for GitHub Code Quality's actions/upload-code-coverage.
1 parent c8330f7 commit 916f482

1 file changed

Lines changed: 79 additions & 6 deletions

File tree

‎Scripts/coverage.sh‎

Lines changed: 79 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
#!/usr/bin/env bash
22
#
3-
# coverage.sh — run the test suite with code coverage, export an LCOV report,
4-
# and enforce a minimum line-coverage threshold for the library sources.
3+
# coverage.sh — run the test suite with code coverage, export LCOV and Cobertura
4+
# reports, and enforce a minimum line-coverage threshold for the library sources.
55
#
6-
# Only the CoreModelSQLite target sources are counted toward the threshold;
7-
# dependencies, generated sources and the test target are excluded so the
8-
# number reflects the coverage of this package's own code.
6+
# Only the CoreModelSQLite target sources are counted toward the threshold and
7+
# reports; dependencies, generated sources and the test target are excluded so
8+
# the number reflects the coverage of this package's own code.
9+
#
10+
# The Cobertura XML (coverage.xml) is what CI hands to GitHub Code Quality via
11+
# `actions/upload-code-coverage`, which surfaces the coverage percentage on PRs.
912
#
1013
# Usage:
1114
# Scripts/coverage.sh [threshold]
@@ -14,6 +17,7 @@
1417
# COVERAGE_THRESHOLD Minimum line coverage percentage (default: 80).
1518
# Overridden by the optional [threshold] argument.
1619
# COVERAGE_OUTPUT LCOV output file (default: .build/coverage/coverage.lcov).
20+
# COBERTURA_OUTPUT Cobertura XML output file (default: .build/coverage/coverage.xml).
1721
# SKIP_TEST If set to 1, reuse existing coverage data instead of
1822
# re-running `swift test`.
1923

@@ -24,6 +28,7 @@ SOURCE_FILTER="/Sources/CoreModelSQLite/"
2428

2529
THRESHOLD="${1:-${COVERAGE_THRESHOLD:-80}}"
2630
OUTPUT="${COVERAGE_OUTPUT:-.build/coverage/coverage.lcov}"
31+
COBERTURA_OUTPUT="${COBERTURA_OUTPUT:-.build/coverage/coverage.xml}"
2732

2833
# Resolve the correct llvm-cov / llvm-profdata (xcrun on Apple platforms).
2934
if command -v xcrun >/dev/null 2>&1; then
@@ -74,7 +79,75 @@ else
7479
echo "warning: could not locate test binary or profdata; skipping LCOV export" >&2
7580
fi
7681

77-
# 5. Compute line coverage for the library sources and enforce the threshold.
82+
# 5. Generate a Cobertura XML report (for GitHub Code Quality / upload-code-coverage).
83+
echo "==> Writing Cobertura report to $COBERTURA_OUTPUT"
84+
mkdir -p "$(dirname "$COBERTURA_OUTPUT")"
85+
python3 - "$CODECOV_JSON" "$SOURCE_FILTER" "$PWD" "$COBERTURA_OUTPUT" <<'PY'
86+
import json, os, sys, time
87+
from xml.sax.saxutils import escape, quoteattr
88+
89+
report_path, source_filter, repo_root, output = sys.argv[1:5]
90+
91+
with open(report_path) as f:
92+
report = json.load(f)
93+
94+
files = []
95+
total_covered = total_lines = 0
96+
for file in report["data"][0]["files"]:
97+
name = file["filename"]
98+
if source_filter not in name:
99+
continue
100+
# Per-line hit count: the greatest region count starting on each line (a line
101+
# is covered if any region on it executed, so branch sub-regions don't hide it).
102+
line_hits = {}
103+
for seg in file["segments"]:
104+
line, count, has_count = seg[0], seg[2], seg[3]
105+
if has_count:
106+
line_hits[line] = max(line_hits.get(line, 0), count)
107+
covered = sum(1 for h in line_hits.values() if h > 0)
108+
total_covered += covered
109+
total_lines += len(line_hits)
110+
files.append((os.path.relpath(name, repo_root), line_hits, covered, len(line_hits)))
111+
112+
def rate(c, t):
113+
return c / t if t else 0.0
114+
115+
overall = rate(total_covered, total_lines)
116+
timestamp = int(time.time())
117+
118+
out = [
119+
'<?xml version="1.0" ?>',
120+
'<!DOCTYPE coverage SYSTEM "http://cobertura.sourceforge.net/xml/coverage-04.dtd">',
121+
'<coverage line-rate="%.4f" branch-rate="0" lines-covered="%d" lines-valid="%d" '
122+
'branches-covered="0" branches-valid="0" complexity="0" version="llvm-cov" timestamp="%d">'
123+
% (overall, total_covered, total_lines, timestamp),
124+
" <sources>",
125+
" <source>%s</source>" % escape(repo_root),
126+
" </sources>",
127+
" <packages>",
128+
' <package name="CoreModelSQLite" line-rate="%.4f" branch-rate="0" complexity="0">' % overall,
129+
" <classes>",
130+
]
131+
for rel, line_hits, covered, total in sorted(files):
132+
out.append(
133+
' <class name=%s filename=%s line-rate="%.4f" branch-rate="0" complexity="0">'
134+
% (quoteattr(os.path.basename(rel)), quoteattr(rel), rate(covered, total))
135+
)
136+
out.append(" <methods/>")
137+
out.append(" <lines>")
138+
for line in sorted(line_hits):
139+
out.append(' <line number="%d" hits="%d"/>' % (line, line_hits[line]))
140+
out.append(" </lines>")
141+
out.append(" </class>")
142+
out += [" </classes>", " </package>", " </packages>", "</coverage>"]
143+
144+
with open(output, "w") as f:
145+
f.write("\n".join(out) + "\n")
146+
147+
print(" %d/%d lines (%.2f%%)" % (total_covered, total_lines, 100 * overall))
148+
PY
149+
150+
# 6. Compute line coverage for the library sources and enforce the threshold.
78151
echo "==> Computing coverage for ${SOURCE_FILTER}"
79152
python3 - "$CODECOV_JSON" "$SOURCE_FILTER" "$THRESHOLD" <<'PY'
80153
import json, sys

0 commit comments

Comments
 (0)