Skip to content
Open
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
79 changes: 79 additions & 0 deletions .github/actions/gcovr/action.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
name: Gcovr
description: Generate code coverage reports with gcovr and llvm-cov

inputs:
build-directory:
description: Build directory containing coverage data (.gcda / .gcno)
required: false
default: build
source-root:
description: Source root for gcovr --root
required: false
default: ${{ github.workspace }}
gcov-executable:
description: gcov executable passed to gcovr (e.g. llvm-cov-16 gcov for Clang)
required: false
default: llvm-cov-16 gcov
gcovr-version:
description: gcovr PyPI version to install
required: false
default: "8.4"
output-directory:
description: Directory for generated coverage reports
required: false
default: build/coverage
fail-under-line:
description: Fail if line coverage is below this percent (0 disables the gate)
required: false
default: "0"
post-pr-comment:
description: Post or update a sticky coverage summary on the pull request
required: false
default: "true"
github-token:
description: Token for posting PR comments
required: false
default: ${{ github.token }}

runs:
using: composite
steps:
- name: Install gcovr
shell: bash
run: |
python3 -m pip install --user "gcovr==${{ inputs.gcovr-version }}"
echo "$HOME/.local/bin" >> "$GITHUB_PATH"

- name: Verify llvm-cov
shell: bash
run: |
llvm_cov="${{ inputs.gcov-executable }}"
llvm_cov_bin="${llvm_cov%% *}"
if ! command -v "${llvm_cov_bin}" >/dev/null 2>&1; then
echo "::error::${llvm_cov_bin} not found; install LLVM or adjust gcov-executable"
exit 1
fi

- name: Generate coverage report
shell: bash
run: |
chmod +x "${{ github.action_path }}/run_gcovr.sh"
SOURCE_ROOT="${{ inputs.source-root }}" \
BUILD_DIR="${{ github.workspace }}/${{ inputs.build-directory }}" \
OUTPUT_DIR="${{ github.workspace }}/${{ inputs.output-directory }}" \
GCOV_EXECUTABLE="${{ inputs.gcov-executable }}" \
FAIL_UNDER_LINE="${{ inputs.fail-under-line }}" \
"${{ github.action_path }}/run_gcovr.sh"

- name: Post coverage PR comment
if: inputs.post-pr-comment == 'true'
shell: bash
env:
GH_TOKEN: ${{ inputs.github-token }}
PR_NUMBER: ${{ github.event.pull_request.number }}
OUTPUT_DIR: ${{ github.workspace }}/${{ inputs.output-directory }}
GITHUB_REPOSITORY: ${{ github.repository }}
GITHUB_RUN_ID: ${{ github.run_id }}
run: |
chmod +x "${{ github.action_path }}/post_pr_comment.sh"
"${{ github.action_path }}/post_pr_comment.sh"
100 changes: 100 additions & 0 deletions .github/actions/gcovr/post_pr_comment.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
#!/usr/bin/env bash
set -euo pipefail

: "${OUTPUT_DIR:?OUTPUT_DIR is required}"
: "${GITHUB_REPOSITORY:?GITHUB_REPOSITORY is required}"
: "${GITHUB_RUN_ID:?GITHUB_RUN_ID is required}"

MARKER='<!-- ydb-cpp-sdk-coverage -->'
SUMMARY_JSON="${OUTPUT_DIR}/summary.json"

if [[ -z "${PR_NUMBER:-}" ]]; then
echo "PR_NUMBER is not set; skipping coverage PR comment"
exit 0
fi

if [[ ! -f "${SUMMARY_JSON}" ]]; then
echo "::warning::${SUMMARY_JSON} not found; skipping coverage PR comment"
exit 0
fi

if ! command -v gh >/dev/null 2>&1; then
echo "::error::gh CLI is required to post PR comments"
exit 1
fi

BODY="$(MARKER="${MARKER}" SUMMARY_JSON="${SUMMARY_JSON}" GITHUB_REPOSITORY="${GITHUB_REPOSITORY}" GITHUB_RUN_ID="${GITHUB_RUN_ID}" python3 <<'PY'
import json
import os
from pathlib import Path

marker = os.environ["MARKER"]
summary = json.loads(Path(os.environ["SUMMARY_JSON"]).read_text())
repo = os.environ["GITHUB_REPOSITORY"]
run_id = os.environ["GITHUB_RUN_ID"]

line = summary["line"]
branch = summary["branch"]
func = summary["function"]

def bar(percent: float) -> str:
filled = max(0, min(10, int(round(percent / 10))))
return "█" * filled + "░" * (10 - filled)

files = [
f for f in summary.get("files", [])
if f.get("line_total", 0) >= 20
]
files.sort(key=lambda f: f.get("line_percent") or 0)
lowest = files[:8]

run_url = f"https://github.com/{repo}/actions/runs/{run_id}"
lines = [
marker,
"## Code coverage",
"",
f"Workflow run: [Coverage job #{run_id}]({run_url}) · download the **coverage-report** artifact for the HTML report.",
"",
"| Metric | Coverage | Covered / total |",
"| --- | --- | --- |",
f"| Line | **{line['percent']:.1f}%** {bar(line['percent'])} | {line['covered']} / {line['num']} |",
f"| Branch | **{branch['percent']:.1f}%** {bar(branch['percent'])} | {branch['covered']} / {branch['num']} |",
f"| Function | **{func['percent']:.1f}%** {bar(func['percent'])} | {func['covered']} / {func['num']} |",
"",
"Scope: `src/`, `include/ydb-cpp-sdk/`, `plugins/` (contrib, tests, and `_deps` excluded).",
]

if lowest:
lines.extend([
"",
"<details>",
"<summary>Lowest line coverage (≥ 20 lines)</summary>",
"",
"| File | Line % | Covered / total |",
"| --- | ---: | --- |",
])
for f in lowest:
pct = f.get("line_percent") or 0
lines.append(
f"| `{f['filename']}` | {pct:.1f}% | {f['line_covered']} / {f['line_total']} |"
)
lines.extend(["", "</details>"])

print("\n".join(lines))
PY
)"

COMMENT_ID="$(gh api "repos/${GITHUB_REPOSITORY}/issues/${PR_NUMBER}/comments" \
--paginate \
--jq ".[] | select(.body | contains(\"${MARKER}\")) | .id" \
| head -n1)"

if [[ -n "${COMMENT_ID}" ]]; then
gh api -X PATCH "repos/${GITHUB_REPOSITORY}/issues/comments/${COMMENT_ID}" \
-f body="${BODY}" >/dev/null
echo "Updated coverage comment ${COMMENT_ID} on PR #${PR_NUMBER}"
else
gh api "repos/${GITHUB_REPOSITORY}/issues/${PR_NUMBER}/comments" \
-f body="${BODY}" >/dev/null
echo "Posted coverage comment on PR #${PR_NUMBER}"
fi
49 changes: 49 additions & 0 deletions .github/actions/gcovr/run_gcovr.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
#!/usr/bin/env bash
set -euo pipefail

: "${SOURCE_ROOT:?SOURCE_ROOT is required}"
: "${BUILD_DIR:?BUILD_DIR is required}"
: "${OUTPUT_DIR:?OUTPUT_DIR is required}"
: "${GCOV_EXECUTABLE:=llvm-cov-16 gcov}"

mkdir -p "${OUTPUT_DIR}"

gcovr_args=(
--root "${SOURCE_ROOT}"
--object-directory "${BUILD_DIR}"
--gcov-executable "${GCOV_EXECUTABLE}"
--print-summary
--json-summary "${OUTPUT_DIR}/summary.json"
--html --html-details
-o "${OUTPUT_DIR}/index.html"
--cobertura "${OUTPUT_DIR}/cobertura.xml"
--exclude '.*contrib/.*'
--exclude '.*tests/.*'
--exclude '.*/_deps/.*'
--filter 'src/'
--filter 'include/ydb-cpp-sdk/'
--filter 'plugins/'
)

if [[ -n "${FAIL_UNDER_LINE:-}" && "${FAIL_UNDER_LINE}" != "0" ]]; then
gcovr_args+=(--fail-under-line "${FAIL_UNDER_LINE}")
fi

gcovr "${gcovr_args[@]}"

if [[ -n "${GITHUB_STEP_SUMMARY:-}" && -f "${OUTPUT_DIR}/summary.json" ]]; then
{
echo "## Code coverage"
echo ""
echo "| Metric | Covered | Total | Percent |"
echo "| --- | ---: | ---: | ---: |"
jq -r '
.line as $l
| .branch as $b
| .function as $f
| "| Line | \($l.covered) | \($l.num) | \($l.percent // 0)% |",
"| Branch | \($b.covered) | \($b.num) | \($b.percent // 0)% |",
"| Function | \($f.covered) | \($f.num) | \($f.percent // 0)% |"
' "${OUTPUT_DIR}/summary.json"
} >> "${GITHUB_STEP_SUMMARY}"
fi
79 changes: 79 additions & 0 deletions .github/workflows/coverage.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
name: Coverage

on:
push:
branches:
- main
pull_request:
types: [opened, synchronize, reopened, ready_for_review]
branches:
- main

concurrency:
group: coverage-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

jobs:
coverage:
runs-on: ubuntu-22.04
permissions:
contents: read
pull-requests: write
services:
ydb:
image: ydbplatform/local-ydb:24.4
ports:
- 2135:2135
- 2136:2136
- 8765:8765
volumes:
- /tmp/ydb_certs:/ydb_certs
env:
YDB_LOCAL_SURVIVE_RESTART: true
YDB_USE_IN_MEMORY_PDISKS: true
YDB_TABLE_ENABLE_PREPARED_DDL: true
options: '-h localhost'
steps:
- name: Checkout PR
uses: actions/checkout@v4
if: github.event.pull_request.head.sha != ''
with:
submodules: true
ref: ${{ github.event.pull_request.head.sha }}

- name: Checkout
uses: actions/checkout@v4
if: github.event.pull_request.head.sha == ''
with:
submodules: true

- name: Install dependencies
uses: ./.github/actions/prepare_vm

- name: Configure and build with coverage
shell: bash
run: |
mkdir -p build
rm -rf build/*
cmake --preset coverage-test-gcc
cmake --build build -j"$(nproc)"

- name: Test
shell: bash
run: |
ctest -j1 --preset coverage-all --output-on-failure

- name: Generate coverage report
uses: ./.github/actions/gcovr
with:
build-directory: build
source-root: ${{ github.workspace }}
gcov-executable: gcov-13
post-pr-comment: ${{ github.event_name == 'pull_request' }}

- name: Upload coverage reports
uses: actions/upload-artifact@v4
with:
name: coverage-report
path: build/coverage/
retention-days: 14
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ include(CMakePackageConfigHelpers)
list(APPEND CMAKE_MODULE_PATH ${YDB_SDK_SOURCE_DIR}/cmake)

include(cmake/global_flags.cmake)
include(cmake/coverage.cmake)
include(cmake/global_vars.cmake)
include(cmake/install.cmake)
include(cmake/common.cmake)
Expand Down
34 changes: 34 additions & 0 deletions CMakePresets.json
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,21 @@
"name": "debug-test-gcc",
"inherits": ["debug-base", "gcc-toolchain", "test"],
"displayName": "Default Debug Test Config (GCC)"
},
{
"name": "coverage-base",
"inherits": ["release-base", "test"],
"hidden": true,
"cacheVariables": {
"YDB_SDK_COVERAGE": "ON",
"ARCADIA_ROOT": "${sourceDir}",
"ARCADIA_BUILD_ROOT": "${sourceDir}/build"
}
},
{
"name": "coverage-test-gcc",
"inherits": ["coverage-base", "gcc-toolchain"],
"displayName": "Coverage Test Config (GCC)"
}
],
"buildPresets": [
Expand Down Expand Up @@ -154,6 +169,25 @@
"YDB_ENDPOINT": "localhost:2136",
"YDB_DATABASE": "/local"
}
},
{
"name": "coverage-all",
"inherits": "all",
"displayName": "Run All Tests (coverage CI)",
"configurePreset": "coverage-test-gcc",
"execution": {
"timeout": 1800
},
"filter": {
"exclude": {
"name": "(ManyMessages|DiscoveryHang|DescribeHang)"
}
},
"environment": {
"YDB_ENDPOINT": "localhost:2136",
"YDB_DATABASE": "/local",
"YDB_VERSION": "24.4"
}
}
]
}
4 changes: 3 additions & 1 deletion cmake/ccache.cmake
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
find_program(CCACHE_PATH ccache)
if (NOT CCACHE_PATH)
if (YDB_SDK_COVERAGE)
message(STATUS "YDB_SDK_COVERAGE is ON: ccache compiler launchers disabled")
elseif (NOT CCACHE_PATH)
message(AUTHOR_WARNING
"Ccache is not found, that will increase the re-compilation time; "
"pass -DCCACHE_PATH=path/to/bin to specify the path to the `ccache` binary file."
Expand Down
1 change: 1 addition & 0 deletions cmake/common.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@ function(_ydb_sdk_add_library Tgt)
target_compile_definitions(${Tgt} ${includeMode}
YDB_SDK_OSS
)
_ydb_sdk_apply_coverage(${Tgt})
endfunction()

function(_ydb_sdk_validate_public_headers)
Expand Down
Loading
Loading