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]
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
2529THRESHOLD=" ${1:- ${COVERAGE_THRESHOLD:- 80} } "
2630OUTPUT=" ${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).
2934if command -v xcrun > /dev/null 2>&1 ; then
7479 echo " warning: could not locate test binary or profdata; skipping LCOV export" >&2
7580fi
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.
78151echo " ==> Computing coverage for ${SOURCE_FILTER} "
79152python3 - " $CODECOV_JSON " " $SOURCE_FILTER " " $THRESHOLD " << 'PY '
80153import json, sys
0 commit comments