Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
203 changes: 203 additions & 0 deletions .github/workflows/test_coverage_regression.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
name: Test for unit test coverage regression

on:
pull_request:
branches: [ dev ]
workflow_dispatch:

permissions:
contents: read
pull-requests: write

concurrency:
group: pr-coverage-${{ github.run_id }}
cancel-in-progress: true

jobs:
coverage:
runs-on: ubuntu-24.04

steps:
- uses: actions/checkout@v6

- name: Install build dependencies
run: |
sudo ./INCHI-1-TEST/install_build_dependencies.sh

- name: Generate coverage report
uses: ./.github/actions/generate_coverage_report

- name: Test for unit test regression
run: |
set -euo pipefail
cd CMake_build/coverage_build

BASE_SUMMARY_URL="https://iupac-inchi.github.io/InChI/coverage/summary.txt"
BASE_SUMMARY_FILE="$(mktemp)"
PR_SUMMARY_FILE="coverage_reports/summary.txt"
COVERAGE_REPORT_MD="coverage_reports/coverage_report.md"

mkdir -p coverage_reports
trap 'rm -f "$BASE_SUMMARY_FILE"' EXIT

extract_pct() {
local metric="$1"
local file="$2"

awk -v metric="$metric" '
$0 ~ "^[[:space:]]*" metric "\\.*:" {
if (match($0, /[0-9]+([.][0-9]+)?%/)) {
value = substr($0, RSTART, RLENGTH - 1)
print value
}
exit
}
' "$file"
}

parse_metrics() {
local prefix="$1"
local file="$2"
local parse_error="$3"
local lines branches

lines="$(extract_pct lines "$file")"
branches="$(extract_pct branches "$file")"

if [[ -n "$lines" && -n "$branches" ]]; then
printf -v "${prefix}_LINES" '%s' "$lines"
printf -v "${prefix}_BRANCHES" '%s' "$branches"
printf -v "${prefix}_AVAILABLE" 'true'
else
PROBLEMS+=("$parse_error")
fi
}

pct_diff() { awk -v pr="$1" -v base="$2" 'BEGIN {printf "%+.2f", pr-base}'; }
is_lt() { awk -v pr="$1" -v base="$2" 'BEGIN {exit !(pr < base)}'; }

PROBLEMS=()
PR_AVAILABLE=false
BASE_AVAILABLE=false
PR_LINES=""
PR_BRANCHES=""
BASE_LINES=""
BASE_BRANCHES=""

if lcov --summary coverage.lcov --branch-coverage --ignore-errors inconsistent > "$PR_SUMMARY_FILE"; then
parse_metrics PR "$PR_SUMMARY_FILE" "Unable to parse PR coverage metrics from $PR_SUMMARY_FILE"
else
PROBLEMS+=("Unable to generate PR coverage summary from coverage.lcov")
fi

if curl -fsSL --max-time 10 "$BASE_SUMMARY_URL" -o "$BASE_SUMMARY_FILE"; then
parse_metrics BASE "$BASE_SUMMARY_FILE" "Base summary fetched, but required metrics were incomplete"
else
PROBLEMS+=("Base summary fetch failed from $BASE_SUMMARY_URL")
fi

PR_LINES_DISPLAY="${PR_LINES:-N/A}"
PR_BRANCHES_DISPLAY="${PR_BRANCHES:-N/A}"
BASE_LINES_DISPLAY="${BASE_LINES:-N/A}"
BASE_BRANCHES_DISPLAY="${BASE_BRANCHES:-N/A}"
DIFF_LINES="N/A"
DIFF_BRANCHES="N/A"

if [[ "$PR_AVAILABLE" == true && "$BASE_AVAILABLE" == true ]]; then
DIFF_LINES="$(pct_diff "$PR_LINES" "$BASE_LINES")"
DIFF_BRANCHES="$(pct_diff "$PR_BRANCHES" "$BASE_BRANCHES")"
fi

{
echo "### Coverage Regression Summary"
echo
echo "| Metric | PR (%) | Base (%) | Difference (pp) |"
Comment thread
JanCBrammer marked this conversation as resolved.
echo "| --- | ---: | ---: | ---: |"
echo "| Lines | ${PR_LINES_DISPLAY} | ${BASE_LINES_DISPLAY} | ${DIFF_LINES} |"
echo "| Branches | ${PR_BRANCHES_DISPLAY} | ${BASE_BRANCHES_DISPLAY} | ${DIFF_BRANCHES} |"
if [[ ${#PROBLEMS[@]} -gt 0 ]]; then
echo
echo "### Problems"
echo
printf -- "- %s\n" "${PROBLEMS[@]}"
fi
echo
echo "Find details on the base coverage at https://iupac-inchi.github.io/InChI/coverage/index.html"
} > "$COVERAGE_REPORT_MD"

if [[ "$PR_AVAILABLE" == false || "$BASE_AVAILABLE" == false ]]; then
echo "Coverage regression check skipped due to missing PR or base metrics."
exit 0
fi

FAIL=false
if is_lt "$PR_LINES" "$BASE_LINES"; then
echo "Line coverage regression: ${PR_LINES}% < base ${BASE_LINES}%"
FAIL=true
fi
if is_lt "$PR_BRANCHES" "$BASE_BRANCHES"; then
echo "Branch coverage regression: ${PR_BRANCHES}% < base ${BASE_BRANCHES}%"
FAIL=true
fi
if [[ "$FAIL" == true ]]; then
exit 1
fi

echo "Coverage OK: lines ${PR_LINES}% >= base ${BASE_LINES}%, branches ${PR_BRANCHES}% >= base ${BASE_BRANCHES}%"

- name: Upload coverage reports
uses: actions/upload-artifact@v7
if: always()
with:
name: coverage-reports-${{ github.run_id }}
path: CMake_build/coverage_build/coverage_reports/
retention-days: 30

- name: Post sticky PR comment (same-repo only)
if: always() && github.event.pull_request.head.repo.full_name == github.repository
uses: actions/github-script@v9
with:
script: |
const fs = require('fs');
const report = fs.readFileSync('CMake_build/coverage_build/coverage_reports/coverage_report.md', 'utf8');
Comment thread
JanCBrammer marked this conversation as resolved.

// Build artifact link
const runId = context.runId;
const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${runId}`;
const artifactLink = `Find details on this PR's coverage by downloading [coverage-reports-${runId}](${runUrl}) and opening **html/index.html**`;

const body = `## Unit Test Coverage Report\n\n${report}\n\n${artifactLink}`;
const identifier = '<!-- InChI Unit Test Coverage Report -->';
const bodyWithId = `${identifier}\n${body}`;

// Search for existing comment with the identifier
const comments = await github.paginate(github.rest.issues.listComments, {
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
per_page: 100,
});

const existingComment = comments.find(comment =>
comment.body.includes(identifier)
);

if (existingComment) {
// Update existing comment
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existingComment.id,
body: bodyWithId,
});
console.log('Updated existing comment:', existingComment.id);
} else {
// Create new comment
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: bodyWithId,
});
console.log('Created new comment');
}
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ Please write your comments in the header files where possible.
The Doxygen documentation syntax is quite powerful: you can include formulas, tables, and diagrams.
For details see <https://www.doxygen.nl/manual>.

The documentation is [built on every push](.github/workflows/deploy_docs.yml) to the default branch of this repository and hosted at <https://iupac-inchi.github.io/InChI/>.
The documentation is [built on every push](.github/workflows/deploy_pages.yml) to the default branch of this repository and hosted at <https://iupac-inchi.github.io/InChI/docs/index.html>.
That is, your comments will automatically be rendered to HTML and served as online documentation.

## Code quality
Expand All @@ -103,6 +103,10 @@ InChI is integrated into [Coverity Scan](https://scan.coverity.com/projects/inch

InChI is integrated into [Google OSS-Fuzz](https://github.com/google/oss-fuzz/tree/master/projects/inchi), which runs continuous fuzz testing against the library. Fuzzing-related bugs are tracked at the [OSS-Fuzz issue tracker](https://bugs.chromium.org/p/oss-fuzz/issues/list?q=inchi).

### Code coverage

The unit test coverage is [evaluated on every push](.github/workflows/deploy_pages.yml) to the default branch of this repository and hosted at <https://iupac-inchi.github.io/InChI/coverage/index.html>. Every PR against the default branch is [tested for unit test coverage regressions](.github/workflows/test_coverage_regression.yml).

## Contents of this repository

### INCHI-1-DOC
Expand Down