用 diff-cover 在 CI 中只对变更行做覆盖率门禁

举报
yd_229231784 发表于 2026/11/13 10:00:09 2026/11/13
【摘要】 针对历史覆盖率低、整体门禁频繁误伤新 PR 的问题,本文用 pytest-cov 生成 coverage.xml,再用 diff-cover 对比 origin/main,只检查变更行并设置 --fail-under=80,同时通过 coverage omit 与 diff-cover 排除迁移和生成文件。

用 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% 的覆盖率要求,门禁更聚焦,也更容易长期执行。

【版权声明】本文为华为云社区用户原创内容,未经允许不得转载,如需转载请自行联系原作者进行授权。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。