Skip to content

Commit 38153f3

Browse files
authored
task(test): measure and verify v1.0 coverage threshold (#1169)
* ci(coverage): add 70 percent line-coverage threshold gate Enhance the existing Code Coverage workflow to satisfy the v1.0 contract threshold (>=70% line coverage, see issue #1095) and the acceptance criteria of issue #1159: - Compute overall line and branch coverage from lcov summary, exposing hit/total counts via step outputs. - Post a per-PR coverage comment (deduplicated via marker) with a line/branch table and pass-or-below-threshold status. - Add an enforcement step that warns when line coverage is below the configured threshold, with optional hard failure controlled by PACS_COVERAGE_ENFORCE (default false / advisory until v1.0 release). - Add a configurable PACS_COVERAGE_THRESHOLD env (default 70). - Tighten workflow permissions to allow PR comment writes. Closes a portion of #1159; full closure follows on PR merge. * docs(changelog): record coverage threshold gate under Unreleased Document the issue #1159 enhancement to the coverage workflow under the Unreleased > Changed section. # Conflicts: # CHANGELOG.md
1 parent 9157ed9 commit 38153f3

2 files changed

Lines changed: 147 additions & 3 deletions

File tree

.github/workflows/coverage.yml

Lines changed: 146 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
# Code Coverage Workflow for PACS System
2-
# Generates code coverage reports using lcov and uploads to Codecov
2+
# Generates code coverage reports using lcov and uploads to Codecov.
3+
# Computes line/branch coverage, posts a PR comment, and enforces the
4+
# v1.0 contract threshold of >=70% line coverage (advisory until v1.0
5+
# release; configurable via PACS_COVERAGE_THRESHOLD env var below).
36
#
4-
# Based on messaging_system coverage patterns
7+
# Based on messaging_system coverage patterns. See issue #1159.
58

69
name: Code Coverage
710

@@ -17,6 +20,18 @@ concurrency:
1720
group: ${{ github.workflow }}-${{ github.ref }}
1821
cancel-in-progress: true
1922

23+
# v1.0 contract: >= 70% line coverage. Set higher to enforce stricter floor.
24+
env:
25+
PACS_COVERAGE_THRESHOLD: '70'
26+
# When 'true', a coverage value below the threshold fails the workflow.
27+
# When 'false', the workflow only annotates the PR. Default false to give
28+
# the project time to climb to threshold without blocking unrelated PRs.
29+
PACS_COVERAGE_ENFORCE: 'false'
30+
31+
permissions:
32+
contents: read
33+
pull-requests: write
34+
2035
jobs:
2136
coverage:
2237
name: Code Coverage Analysis
@@ -42,7 +57,7 @@ jobs:
4257
sudo apt install -y libsqlite3-dev libssl-dev libfmt-dev
4358
sudo apt install -y libgtest-dev libgmock-dev
4459
sudo apt install -y libjpeg-turbo8-dev
45-
sudo apt install -y lcov
60+
sudo apt install -y lcov bc
4661
4762
- name: Configure CMake with coverage
4863
working-directory: pacs_system
@@ -102,6 +117,134 @@ jobs:
102117
# Display coverage summary
103118
lcov --list coverage.info || echo "No coverage records to display"
104119
120+
- name: Compute coverage percentages
121+
id: cov_summary
122+
working-directory: pacs_system/build
123+
run: |
124+
# Extract overall line and branch coverage from lcov summary.
125+
# lcov --summary output looks like:
126+
# lines......: 72.4% (1234 of 1700 lines)
127+
# branches...: 58.1% (456 of 785 branches)
128+
if [ ! -s coverage.info ]; then
129+
echo "coverage.info missing or empty - skipping"
130+
echo "lines_pct=0" >> "$GITHUB_OUTPUT"
131+
echo "branches_pct=0" >> "$GITHUB_OUTPUT"
132+
echo "lines_hit=0" >> "$GITHUB_OUTPUT"
133+
echo "lines_total=0" >> "$GITHUB_OUTPUT"
134+
echo "branches_hit=0" >> "$GITHUB_OUTPUT"
135+
echo "branches_total=0" >> "$GITHUB_OUTPUT"
136+
echo "available=false" >> "$GITHUB_OUTPUT"
137+
exit 0
138+
fi
139+
140+
SUMMARY=$(lcov --summary coverage.info 2>&1 || true)
141+
echo "$SUMMARY"
142+
143+
LINES_LINE=$(echo "$SUMMARY" | grep -E '^\s*lines' | head -n1 || true)
144+
BRANCH_LINE=$(echo "$SUMMARY" | grep -E '^\s*branches' | head -n1 || true)
145+
146+
# Robust extraction: grab first percentage and the "X of Y" pair.
147+
LINES_PCT=$(echo "$LINES_LINE" | grep -oE '[0-9]+(\.[0-9]+)?%' | head -n1 | tr -d '%' || echo "0")
148+
BRANCH_PCT=$(echo "$BRANCH_LINE" | grep -oE '[0-9]+(\.[0-9]+)?%' | head -n1 | tr -d '%' || echo "0")
149+
LINES_HIT=$(echo "$LINES_LINE" | grep -oE '[0-9]+ of [0-9]+' | head -n1 | awk '{print $1}' || echo "0")
150+
LINES_TOTAL=$(echo "$LINES_LINE" | grep -oE '[0-9]+ of [0-9]+' | head -n1 | awk '{print $3}' || echo "0")
151+
BRANCH_HIT=$(echo "$BRANCH_LINE" | grep -oE '[0-9]+ of [0-9]+' | head -n1 | awk '{print $1}' || echo "0")
152+
BRANCH_TOTAL=$(echo "$BRANCH_LINE" | grep -oE '[0-9]+ of [0-9]+' | head -n1 | awk '{print $3}' || echo "0")
153+
154+
: "${LINES_PCT:=0}"
155+
: "${BRANCH_PCT:=0}"
156+
: "${LINES_HIT:=0}"
157+
: "${LINES_TOTAL:=0}"
158+
: "${BRANCH_HIT:=0}"
159+
: "${BRANCH_TOTAL:=0}"
160+
161+
echo "Line coverage: ${LINES_PCT}% (${LINES_HIT} of ${LINES_TOTAL})"
162+
echo "Branch coverage: ${BRANCH_PCT}% (${BRANCH_HIT} of ${BRANCH_TOTAL})"
163+
164+
echo "lines_pct=${LINES_PCT}" >> "$GITHUB_OUTPUT"
165+
echo "branches_pct=${BRANCH_PCT}" >> "$GITHUB_OUTPUT"
166+
echo "lines_hit=${LINES_HIT}" >> "$GITHUB_OUTPUT"
167+
echo "lines_total=${LINES_TOTAL}" >> "$GITHUB_OUTPUT"
168+
echo "branches_hit=${BRANCH_HIT}" >> "$GITHUB_OUTPUT"
169+
echo "branches_total=${BRANCH_TOTAL}" >> "$GITHUB_OUTPUT"
170+
echo "available=true" >> "$GITHUB_OUTPUT"
171+
172+
- name: Post coverage summary to PR
173+
if: github.event_name == 'pull_request' && steps.cov_summary.outputs.available == 'true'
174+
uses: actions/github-script@v7
175+
env:
176+
LINES_PCT: ${{ steps.cov_summary.outputs.lines_pct }}
177+
BRANCHES_PCT: ${{ steps.cov_summary.outputs.branches_pct }}
178+
LINES_HIT: ${{ steps.cov_summary.outputs.lines_hit }}
179+
LINES_TOTAL: ${{ steps.cov_summary.outputs.lines_total }}
180+
BRANCHES_HIT: ${{ steps.cov_summary.outputs.branches_hit }}
181+
BRANCHES_TOTAL: ${{ steps.cov_summary.outputs.branches_total }}
182+
THRESHOLD: ${{ env.PACS_COVERAGE_THRESHOLD }}
183+
ENFORCE: ${{ env.PACS_COVERAGE_ENFORCE }}
184+
with:
185+
script: |
186+
const lines = process.env.LINES_PCT || '0';
187+
const branches = process.env.BRANCHES_PCT || '0';
188+
const linesHit = process.env.LINES_HIT || '0';
189+
const linesTotal = process.env.LINES_TOTAL || '0';
190+
const branchesHit = process.env.BRANCHES_HIT || '0';
191+
const branchesTotal = process.env.BRANCHES_TOTAL || '0';
192+
const threshold = parseFloat(process.env.THRESHOLD || '70');
193+
const enforce = (process.env.ENFORCE || 'false') === 'true';
194+
const linesNum = parseFloat(lines);
195+
const status = linesNum >= threshold ? 'PASS' : 'BELOW THRESHOLD';
196+
const enforceNote = enforce ? '(enforced)' : '(advisory)';
197+
198+
const body = [
199+
'## Code Coverage Report',
200+
'',
201+
'| Metric | Coverage | Hit / Total |',
202+
'|--------|----------|-------------|',
203+
`| Line coverage | ${lines}% | ${linesHit} / ${linesTotal} |`,
204+
`| Branch coverage | ${branches}% | ${branchesHit} / ${branchesTotal} |`,
205+
'',
206+
`**v1.0 threshold**: line coverage must be >= ${threshold}% ${enforceNote}.`,
207+
`**Status**: ${status}`,
208+
'',
209+
'_Source: lcov summary from `coverage.info`. See workflow artifacts for the full report. (#1159)_',
210+
].join('\n');
211+
212+
const marker = '<!-- pacs-coverage-comment -->';
213+
const finalBody = `${marker}\n${body}`;
214+
215+
const { owner, repo } = context.repo;
216+
const issue_number = context.issue.number;
217+
const existing = await github.paginate(github.rest.issues.listComments, {
218+
owner, repo, issue_number, per_page: 100,
219+
});
220+
const prior = existing.find(c => (c.body || '').startsWith(marker));
221+
if (prior) {
222+
await github.rest.issues.updateComment({ owner, repo, comment_id: prior.id, body: finalBody });
223+
} else {
224+
await github.rest.issues.createComment({ owner, repo, issue_number, body: finalBody });
225+
}
226+
227+
- name: Enforce coverage threshold
228+
if: steps.cov_summary.outputs.available == 'true'
229+
working-directory: pacs_system/build
230+
env:
231+
LINES_PCT: ${{ steps.cov_summary.outputs.lines_pct }}
232+
THRESHOLD: ${{ env.PACS_COVERAGE_THRESHOLD }}
233+
ENFORCE: ${{ env.PACS_COVERAGE_ENFORCE }}
234+
run: |
235+
echo "Line coverage: ${LINES_PCT}% (threshold: ${THRESHOLD}%)"
236+
# Use awk to compare floating-point percentages reliably.
237+
BELOW=$(awk -v a="${LINES_PCT}" -v b="${THRESHOLD}" 'BEGIN { print (a+0 < b+0) ? 1 : 0 }')
238+
if [ "${BELOW}" = "1" ]; then
239+
echo "::warning::Line coverage ${LINES_PCT}% is below v1.0 threshold ${THRESHOLD}% (issue #1159)."
240+
if [ "${ENFORCE}" = "true" ]; then
241+
echo "::error::Coverage enforcement is enabled (PACS_COVERAGE_ENFORCE=true). Failing job."
242+
exit 1
243+
fi
244+
else
245+
echo "Line coverage meets v1.0 threshold."
246+
fi
247+
105248
- name: Upload coverage to Codecov
106249
uses: codecov/codecov-action@v6
107250
with:

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2222

2323
- API freeze for v1.0: enumerate the 290-header public surface under `include/kcenon/pacs/` in `docs/v1.0-api-surface.md`; confirm zero `[[deprecated]]` symbols and no experimental staging area; mark `network/detail/accept_worker.h` as the only implementation-detail header outside the v1.0 stability promise ([#1156](https://github.com/kcenon/pacs_system/issues/1156))
2424
- Migrate `decode_rle_segment` (file-scope helper in `src/encoding/compression/rle_codec.cpp`) from `throw std::runtime_error` to `Result<std::vector<uint8_t>>` so malformed RLE segments surface through the public `rle_codec::decode()` `Result<T>` contract instead of escaping as exceptions. Document the remaining 10 throws in `src/integration/` and `src/storage/hsm_storage.cpp` as constructor-invariant or `std::future`-boundary ABI escapes in the new `docs/v1.0-throw-policy.md`. Public headers under `include/kcenon/pacs/` continue to contain zero non-comment `throw` statements ([#1157](https://github.com/kcenon/pacs_system/issues/1157))
25+
- Enhance code coverage workflow with v1.0 70% line-coverage threshold gate, line/branch summary computation, and per-PR coverage comment with hit/total counts; threshold is advisory until v1.0 release (toggle via `PACS_COVERAGE_ENFORCE`) ([#1159](https://github.com/kcenon/pacs_system/issues/1159))
2526
- Remove committed `.key` files from version control; regenerate via `generate_test_certs.sh` in CI and local development ([#1092](https://github.com/kcenon/pacs_system/issues/1092))
2627
- Consolidate directory layout: `samples/` renamed to `examples/` (5 progressive tutorials) and `examples/` renamed to `tools/` (32 CLI utility binaries) so that the role split matches the ecosystem standard. CMake option names (`PACS_BUILD_EXAMPLES`, `PACS_BUILD_SAMPLES`) are preserved for backward compatibility ([#1139](https://github.com/kcenon/pacs_system/issues/1139))
2728
- Align `cmake/*.cmake` modules with the canonical ecosystem template at `common_system/cmake/template`; pacs-specific modules (`pacs_system-config.cmake.in`, `summary.cmake`) and intentional design divergences are documented in `cmake/DEVIATIONS.md`, and the aligned template version is recorded in `cmake/VERSION` ([#1140](https://github.com/kcenon/pacs_system/issues/1140))

0 commit comments

Comments
 (0)