Skip to content

Commit a9647f7

Browse files
committed
[ci] add coverage reporting workflow
1 parent 7f7f20c commit a9647f7

7 files changed

Lines changed: 989 additions & 2 deletions

File tree

.github/workflows/coverage.yml

Lines changed: 272 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,272 @@
1+
name: coverage
2+
permissions:
3+
contents: read
4+
actions: write
5+
on:
6+
push:
7+
branches: [ "main" ]
8+
pull_request:
9+
branches: [ "main" ]
10+
workflow_dispatch:
11+
inputs:
12+
runs-on:
13+
description: "Runner type"
14+
type: choice
15+
options:
16+
- ubuntu-24.04-arm
17+
- ubuntu-latest
18+
- aliyun-ecs-x64
19+
default: ubuntu-latest
20+
rebuildDiskCache:
21+
description: "Rebuild disk cache"
22+
type: boolean
23+
default: false
24+
concurrency:
25+
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}-${{ inputs.runs-on || vars.COVERAGE_RUNS_ON || 'ubuntu-latest' }}
26+
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
27+
jobs:
28+
coverage-github:
29+
outputs:
30+
coverage-artifact-url: ${{ steps.upload_coverage_artifacts.outputs.artifact-url }}
31+
if: ${{ (inputs.runs-on || vars.COVERAGE_RUNS_ON || 'ubuntu-latest') == 'ubuntu-latest' || (inputs.runs-on || vars.COVERAGE_RUNS_ON || 'ubuntu-latest') == 'ubuntu-24.04-arm' }}
32+
name: coverage
33+
runs-on: ${{ inputs.runs-on || vars.COVERAGE_RUNS_ON || 'ubuntu-latest' }}
34+
container:
35+
image: ghcr.io/alibaba/tair-kvcache-kvcm-dev:2026_02_13_12_03_24230b1
36+
volumes:
37+
- /:/host_root/
38+
options: --privileged
39+
steps:
40+
# https://github.com/actions/runner-images/issues/2840
41+
- name: Free up disk space
42+
run: |
43+
echo "Disk space before cleanup:"
44+
df -h
45+
rm -rf /host_root/usr/share/dotnet
46+
rm -rf /host_root/usr/local/lib/android
47+
rm -rf /host_root/opt/ghc
48+
echo "Disk space after cleanup:"
49+
df -h
50+
51+
- &checkout_step
52+
name: Checkout repository
53+
uses: actions/checkout@v4
54+
with:
55+
fetch-depth: 0
56+
57+
- &setup_bazel_step
58+
name: setup_bazel
59+
uses: bazel-contrib/setup-bazel@0.18.0
60+
with:
61+
bazelisk-cache: true
62+
disk-cache: ${{ runner.os }}-${{ github.workflow }}
63+
repository-cache: true
64+
cache-save: ${{ github.event_name == 'push' || (inputs.rebuildDiskCache || false) }}
65+
66+
- &clean_disk_cache_step
67+
name: clean_disk_cache
68+
if: ${{ inputs.rebuildDiskCache || false }}
69+
run: |
70+
rm -rf ~/.cache/bazel-disk
71+
72+
- &install_coverage_tools_step
73+
name: install_coverage_tools
74+
run: |
75+
set -e
76+
set -x
77+
if ! command -v genhtml >/dev/null 2>&1; then
78+
if command -v apt-get >/dev/null 2>&1; then
79+
apt-get update -qq
80+
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq lcov
81+
elif command -v yum >/dev/null 2>&1; then
82+
yum install -y -q lcov
83+
else
84+
echo "No supported package manager found for installing lcov/genhtml" >&2
85+
exit 1
86+
fi
87+
fi
88+
genhtml --version
89+
90+
- &bazel_coverage_step
91+
name: bazel_coverage
92+
env:
93+
COVERAGE_BASE_REF: ${{ github.event.pull_request.base.sha || github.event.before || 'origin/main' }}
94+
COVERAGE_XTRACE: "1"
95+
run: |
96+
set -e
97+
BASE_REF="${COVERAGE_BASE_REF}"
98+
if [ -z "${BASE_REF}" ] || [[ "${BASE_REF}" =~ ^0+$ ]]; then
99+
BASE_REF="origin/main"
100+
fi
101+
tools/coverage/run_coverage.sh \
102+
--base-ref "${BASE_REF}" \
103+
--head-ref HEAD \
104+
--output-dir coverage \
105+
--include-prefix kv_cache_manager/ \
106+
--jobs 8 \
107+
--local-test-jobs 8 \
108+
--test-timeout 900 \
109+
--test-output errors \
110+
--fetch-main \
111+
-- \
112+
//kv_cache_manager/... \
113+
//integration_test/...
114+
115+
- &delete_old_disk_cache_step
116+
name: delete_old_disk_cache
117+
if: ${{ github.event_name == 'push' || (inputs.rebuildDiskCache || false) }}
118+
env:
119+
GH_TOKEN: ${{ github.token }}
120+
run: |
121+
set -x
122+
ARCH=$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/')
123+
curl -sLo /usr/local/bin/jq "https://github.com/jqlang/jq/releases/download/jq-1.8.1/jq-linux-${ARCH}" && chmod +x /usr/local/bin/jq
124+
GH_VER=2.89.0
125+
curl -sL "https://github.com/cli/cli/releases/download/v${GH_VER}/gh_${GH_VER}_linux_${ARCH}.tar.gz" | tar xz --strip-components=1 -C /usr/local
126+
RUNNER_ARCH_LOWER=$(echo "${{ runner.arch }}" | tr '[:upper:]' '[:lower:]')
127+
DISK_CACHE_NAME="disk-${{ runner.os }}-${{ github.workflow }}"
128+
gh cache list --repo ${{ github.repository }} --ref "${{ github.ref }}" --json id,key --limit 100 | \
129+
jq -r --arg arch "$RUNNER_ARCH_LOWER" --arg name "$DISK_CACHE_NAME" \
130+
'.[] | select((.key | contains($arch)) and (.key | contains($name))) | .id' | \
131+
xargs -I {} gh cache delete {} --repo ${{ github.repository }} || true
132+
133+
- &append_coverage_summary_step
134+
name: Append coverage summary
135+
if: ${{ always() && hashFiles('coverage/coverage-summary.md') != '' }}
136+
run: |
137+
cat coverage/coverage-summary.md >> "${GITHUB_STEP_SUMMARY}"
138+
if [ -f coverage/html/index.html ]; then
139+
{
140+
echo ""
141+
echo "HTML coverage report: \`coverage/html/index.html\` in the uploaded artifact."
142+
} >> "${GITHUB_STEP_SUMMARY}"
143+
fi
144+
145+
- &upload_coverage_artifacts_step
146+
name: Upload coverage artifacts
147+
id: upload_coverage_artifacts
148+
uses: actions/upload-artifact@v6
149+
if: always()
150+
with:
151+
name: coverage-report
152+
path: |
153+
coverage/**
154+
bazel-testlogs/**/*.xml
155+
if-no-files-found: warn
156+
overwrite: true
157+
158+
coverage-aliyun:
159+
outputs:
160+
coverage-artifact-url: ${{ steps.upload_coverage_artifacts.outputs.artifact-url }}
161+
if: ${{ (inputs.runs-on || vars.COVERAGE_RUNS_ON || 'ubuntu-latest') == 'aliyun-ecs-x64' }}
162+
name: coverage
163+
runs-on: aliyun-ecs-x64
164+
container:
165+
image: ghcr.io/alibaba/tair-kvcache-kvcm-dev:2026_02_13_12_03_24230b1
166+
options: --privileged
167+
steps:
168+
- *checkout_step
169+
- *setup_bazel_step
170+
- *clean_disk_cache_step
171+
- *install_coverage_tools_step
172+
- *bazel_coverage_step
173+
- *delete_old_disk_cache_step
174+
- *append_coverage_summary_step
175+
- *upload_coverage_artifacts_step
176+
177+
comment-pr-coverage:
178+
if: ${{ always() && github.event_name == 'pull_request' && (needs.coverage-github.outputs.coverage-artifact-url != '' || needs.coverage-aliyun.outputs.coverage-artifact-url != '') }}
179+
needs:
180+
- coverage-github
181+
- coverage-aliyun
182+
name: comment_pr_coverage
183+
runs-on: ubuntu-latest
184+
permissions:
185+
actions: read
186+
issues: write
187+
steps:
188+
- name: Download coverage artifact
189+
uses: actions/download-artifact@v6
190+
continue-on-error: true
191+
with:
192+
name: coverage-report
193+
path: coverage-artifact
194+
195+
- name: Comment PR coverage
196+
uses: actions/github-script@v8
197+
env:
198+
COVERAGE_ARTIFACT_URL: ${{ needs.coverage-github.outputs.coverage-artifact-url || needs.coverage-aliyun.outputs.coverage-artifact-url }}
199+
with:
200+
script: |
201+
const fs = require('fs');
202+
203+
const marker = '<!-- tair-kvcache-coverage-report -->';
204+
const summaryPath = 'coverage-artifact/coverage/coverage-summary.md';
205+
if (!fs.existsSync(summaryPath)) {
206+
core.warning(`Coverage summary not found at ${summaryPath}; skip coverage comment.`);
207+
return;
208+
}
209+
210+
let summary = fs.readFileSync(summaryPath, 'utf8').trim();
211+
const maxSummaryLength = 60000;
212+
if (summary.length > maxSummaryLength) {
213+
summary = `${summary.slice(0, maxSummaryLength)}\n\n_Comment truncated; see the uploaded artifact for the full report._`;
214+
}
215+
216+
const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
217+
const artifactUrl = process.env.COVERAGE_ARTIFACT_URL || '';
218+
const links = artifactUrl
219+
? `HTML report and raw LCOV: [coverage-report](${artifactUrl})`
220+
: `Workflow run: [${context.runId}](${runUrl})`;
221+
const body = [
222+
marker,
223+
summary,
224+
'',
225+
links,
226+
`Workflow run: [${context.runId}](${runUrl})`,
227+
].join('\n');
228+
229+
const issue_number = context.payload.pull_request?.number;
230+
if (!issue_number) {
231+
core.warning('No pull request number found; skip coverage comment.');
232+
return;
233+
}
234+
235+
try {
236+
const { owner, repo } = context.repo;
237+
const comments = await github.paginate(github.rest.issues.listComments, {
238+
owner,
239+
repo,
240+
issue_number,
241+
per_page: 100,
242+
});
243+
const existing = comments.find((comment) => comment.body?.includes(marker));
244+
245+
if (existing) {
246+
await github.rest.issues.updateComment({
247+
owner,
248+
repo,
249+
comment_id: existing.id,
250+
body,
251+
});
252+
} else {
253+
await github.rest.issues.createComment({
254+
owner,
255+
repo,
256+
issue_number,
257+
body,
258+
});
259+
}
260+
} catch (error) {
261+
core.warning(`Failed to write PR coverage comment: ${error.message}`);
262+
}
263+
264+
validate-runner-selection:
265+
if: ${{ (inputs.runs-on || vars.COVERAGE_RUNS_ON || 'ubuntu-latest') != 'ubuntu-latest' && (inputs.runs-on || vars.COVERAGE_RUNS_ON || 'ubuntu-latest') != 'ubuntu-24.04-arm' && (inputs.runs-on || vars.COVERAGE_RUNS_ON || 'ubuntu-latest') != 'aliyun-ecs-x64' }}
266+
name: validate_runner_selection
267+
runs-on: ubuntu-latest
268+
steps:
269+
- name: fail_invalid_runner_selection
270+
run: |
271+
echo "::error::Invalid runner selection. Set runs-on or COVERAGE_RUNS_ON to one of: ubuntu-latest, ubuntu-24.04-arm, aliyun-ecs-x64."
272+
exit 1

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ bin/*
66
lib/*
77
var/*
88
logs/*
9+
/coverage/
910
kv_cache_manager/optimizer/analysis/result/*
1011
kv_cache_manager/optimizer/logs/*
1112
__pycache__/
@@ -15,4 +16,4 @@ __pycache__/
1516
!.bazeliskrc
1617
!.aoneci
1718
compile_commands.json
18-
stub_source
19+
stub_source

docs/develop/README.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,17 @@ bazelisk info --announce_rc repository_cache
5151
- 需要本地启动一个Redis或Valkey。
5252
- ```bazelisk test //kv_cache_manager/common/test:redis_client_real_service_test //kv_cache_manager/meta/test:meta_redis_backend_real_service_test //kv_cache_manager/meta/test:meta_storage_backend_manager_real_redis_test //kv_cache_manager/meta/test:meta_indexer_redis_test //kv_cache_manager/manager/test:MetaSearcherRedisTest //kv_cache_manager/config/test:registry_manager_redis_backend_test --test_tag_filters=redis```
5353
- 启用ASAN:上述命令后添加 ```--config=debug --config=asan --test_env ASAN_OPTIONS=detect_odr_violation=0```
54+
- 覆盖率:
55+
- ```tools/coverage/run_coverage.sh --base-ref origin/main --head-ref HEAD --output-dir coverage --include-prefix kv_cache_manager/ --jobs 8 --local-test-jobs 8 -- //kv_cache_manager/... //integration_test/...```
56+
- 脚本会执行 Bazel LCOV 采集、归一化负 hit count、生成全量/增量覆盖率摘要,并在安装了 lcov/genhtml 时生成 HTML 报告。
57+
- 输出位于 ```coverage/```,包含 ```lcov.info``````coverage-summary.md``````coverage-summary.json``````html/index.html```
58+
- 如本地未安装 ```genhtml``` 且只需要 LCOV/摘要,可添加 ```--no-html```
59+
- ```gcov_json_isolated.sh``` 用于规避 Bazel 6.4 C++ coverage collector 在并发 gcov json 采集时共享 ```*.gcov.json.gz``` 中间文件导致的竞态。
60+
- Bazel 6.4 默认不向 gcov 传 ```-b```;当前开发镜像的 GCC 10 支持通过 ```COVERAGE_GCOV_OPTIONS=-b``` 生成 ```BRDA/BRF/BRH``` 分支覆盖率记录。
61+
- CI 会上传 ```coverage/lcov.info``````coverage/coverage-summary.md``````coverage/coverage-summary.json``````coverage/html/```
62+
- PR 触发的 coverage 会更新同一条固定评论;评论 job 仅下载 coverage artifact,不检出或执行 PR 代码。
63+
- ```coverage``` workflow 使用 setup-bazel 的 Bazelisk cache、repository cache 和独立 disk cache。PR 只读 cache,main push 或手动 ```rebuildDiskCache``` 会保存并替换旧 disk cache。
64+
- 可通过 workflow_dispatch 的 ```runs-on``` 或仓库变量 ```COVERAGE_RUNS_ON``` 选择 ```ubuntu-latest``````ubuntu-24.04-arm``````aliyun-ecs-x64```
5465
### 测试资源清理
5566

5667
测试结束后会自动清理资源。测试工作目录位于 bazel runfiles 目录中,不会污染源代码目录。如果测试异常退出,可能需要手动清理:
@@ -158,4 +169,4 @@ githooks中已经添加了C++等语言的格式化脚本,请确保开发环境
158169
提交前检查和 commit message 格式见 [Commit 要求](commit_requirements.md)
159170

160171
## CI
161-
可参考```.github/workflows```目录下的配置。```test-opensrc``` 在一个 ```normal_test``` job 中运行普通单元测试和集成测试(包含默认配置下的客户端测试目标),ASAN 测试使用独立 job。
172+
可参考```.github/workflows```目录下的配置。```test-opensrc``` 在一个 ```normal_test``` job 中运行普通单元测试和集成测试(包含默认配置下的客户端测试目标),ASAN 测试使用独立 job。```coverage``` 负责全量 LCOV、增量覆盖率和 HTML 报告。

0 commit comments

Comments
 (0)