用 diff-cover 在 CI 中只对变更行做覆盖率门禁
用 diff-cover 在 CI 中只对变更行做覆盖率门禁
问题场景
长期维护的 Python 仓库常见一种矛盾:历史代码覆盖率不高,但新代码希望有测试。若 CI 直接执行整体覆盖率门禁,历史欠账会让每个 PR 都失败;若完全不设门禁,新代码又容易缺少测试。把门禁限定在本次 diff 的可执行行,是更可落地的折中。
本文用 pytest-cov 生成 coverage.xml,再用 diff-cover 与 origin/main 比较,只统计新增或修改行的覆盖率,并设置 --fail-under=80。迁移目录和生成代码通过 coverage 的 omit 与 diff-cover 的 --exclude 排除,避免它们因没有测试而误伤 PR。
最小可运行示例
目录结构:
repo/
app/
__init__.py
calc.py
migrations/
generated/
tests/
test_calc.py
.coveragerc
requirements-dev.txt
requirements-dev.txt:
pytest
pytest-cov
diff-cover
app/calc.py:
def add(a, b):
return a + b
def div(a, b):
if b == 0:
raise ValueError('b must not be zero')
return a / b
tests/test_calc.py:
from app.calc import add, div
def test_add():
assert add(1, 2) == 3
def test_div():
assert div(6, 2) == 3
.coveragerc 用来限定采集范围,并排除迁移和生成文件:
[run]
source = app
branch = True
omit =
app/migrations/*
app/generated/*
[report]
show_missing = True
在仓库根目录运行:
pytest --cov=app --cov-report=xml:coverage.xml
随后与 origin/main 比较,只检查变更行:
diff-cover coverage.xml --compare-branch=origin/main --fail-under=80 --exclude 'app/migrations/*' --exclude 'app/generated/*'
这里有两个排除层:coverage 的 omit 防止迁移和生成文件进入覆盖率统计;diff-cover 的 --exclude 防止这些文件的 diff 被当成未覆盖行。两处模式应保持一致,否则可能出现“XML 中没有该文件、diff 中仍被计入”的误判。不同 diff-cover 版本对 --exclude 的支持可能不同,执行前可用 diff-cover --help 确认;若版本不支持,应升级到支持该参数的版本,或用一个小的包装脚本先过滤 diff 文件列表。
放进 CI
下面以 GitHub Actions 为例,关键是完整拉取 git 历史,并确保目标分支存在:
name: changed-line-coverage
on:
pull_request:
jobs:
coverage:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- run: pip install -r requirements-dev.txt
- run: git fetch origin main:refs/remotes/origin/main
- run: pytest --cov=app --cov-report=xml:coverage.xml
- run: diff-cover coverage.xml --compare-branch=origin/main --fail-under=80 --exclude 'app/migrations/*' --exclude 'app/generated/*'
如果默认分支不是 main,把命令中的 main 和 origin/main 换成实际目标分支。fetch-depth: 0 不能省略,因为 diff-cover 需要找到比较基线;浅克隆下可能找不到 origin/main,或得到不完整的 diff。
边界与限制
第一,diff-cover 只适合做“增量门禁”,不能替代整体覆盖率治理。建议保留整体覆盖率报告作为趋势参考,但不要让它阻塞历史欠账较多的 PR。
第二,覆盖率 XML 中的路径要与执行目录匹配。示例默认在仓库根目录运行 pytest;单仓多包或 monorepo 场景需要统一工作目录,必要时使用 --src-roots 或分别生成报告后合并。
第三,--fail-under=80 只表示本次 diff 中可测量的代码行覆盖率阈值,注释、空行等不会计入。它也不是分支覆盖率门禁,不能因为配置了 branch = True 就认为分支路径已被强制约束。
第四,迁移文件和生成文件被排除,不等于它们不需要验证。迁移通常需要独立的数据库迁移测试或人工审查;生成文件应保证可重复生成,并把生成校验放在其他流水线步骤中。
第五,门禁结果依赖 git 比较基线。PR 场景下通常比较目标分支最新提交,若目标分支频繁变动,结果可能随基线变化。团队应固定比较分支命名,并在 CI 中显式 fetch,避免本地与流水线行为不一致。
按以上方式,历史低覆盖不会阻塞新 PR,新增或修改的代码行仍要满足 80% 的覆盖率要求,门禁更聚焦,也更容易长期执行。
- 点赞
- 收藏
- 关注作者
评论(0)