首页
/ TiDB 测试 Diff 分诊指南:三步定位"与 PR 无关"的测试行为变化

TiDB 测试 Diff 分诊指南:三步定位"与 PR 无关"的测试行为变化

2026-09-05 14:04:36作者:范靓好Udolf

当 TiDB 仓库中的 planner/executor testdata 在 merge、rebase 之后突然发生变化,或单测与全量测试的输出不一致、且 PR 改动的代码无法解释这种变化时,tidb-test-diff-triage 技能提供了一套标准化的分诊流程:先排除 failpoint 环境配置问题,再通过 git bisect 隔离 merge 引入的影响,最后才允许更新期望输出。读完本文,你将掌握 TiDB 测试体系中"环境性问题 / 上游行为变化 / 本地回归"三分类的完整判定方法,并能直接使用仓库内现成的 failpoint 运行脚本和决策依据完成一次可复现、可追溯的排查。

适用场景:什么时候启动分诊流程

该技能定义于 SKILL.md,并在仓库级技能总览 .agents/skills/README.md 中被登记为"triage unexpected test diffs (failpoint vs upstream vs local regression)"的操作性工作流。文档给出的触发条件有三类:

  • 出现了测试 diff,但 PR 中实际触碰的代码无法解释该行为变化;
  • 单测运行(single test run)与全量套件运行(full-suite run)产生了不同输出;
  • merge/rebase 之后,planner/executor 的 testdata 意外改变。

这三类场景的共同特征是:diff 的成因尚未被证明。文档反复强调的核心立场是——在根因被证实之前,不要动期望输出(testdata),否则一次环境性问题会被永久"固化"进仓库。

规则一:先排除 failpoint 配置问题

这是分诊流程的第一步,也是 TiDB 中最容易踩中的坑。文档原文指出:

In TiDB, many test behaviors rely on failpoint instrumentation. -tags=intest,deadlock does not enable failpoints.

也就是说,-tags=intest,deadlock 只是构建标签,它本身并不启用 failpoint。大量测试行为依赖 failpoint 插桩(代码中的 failpoint.Inject / failpoint.Enable 调用),若运行环境未开启 failpoint 转换,测试会走"未插桩"分支,从而产生与 CI 或他人环境不一致的结果。

仓库中确实大量存在这类插桩。例如 pkg/ddl/backfilling_dist_scheduler_test.go 中的分布式回填测试用 testfailpoint.Enable(t, "...", ...) 注入模拟行为,pkg/session/session.go 中也存在 failpoint.Inject("mockCommitError", ...)failpoint.Inject("preCommitHook", ...) 等注入点。如果这些测试在未启用 failpoint 的状态下运行,行为将与期望输出产生偏离。

决策依据:对受影响的包做 failpoint 检测

技能文档要求对照 docs/agents/testing-flow.mdFailpoint decision for unit tests 小节对受影响的包做检查,该小节给出的标准命令是:

rg -n --fixed-strings -- "failpoint." pkg/<package_name>
rg -n --fixed-strings -- "testfailpoint." pkg/<package_name>
# If BUILD.bazel exists, also check failpoint dependency.
test -f pkg/<package_name>/BUILD.bazel && rg -n --fixed-strings -- "@com_github_pingcap_failpoint//:failpoint" pkg/<package_name>/BUILD.bazel

决策规则很直接:

  • rg 命中(代码或 BUILD.bazel 依赖中出现 failpoint)→ 必须用 failpoint-enabled 方式重跑,并在 go test 命令中加 -count=1 保证可复现(避免 Go test 缓存掩盖真实输出);
  • rg 无命中 → 按普通方式运行,并在最终报告中说明检查证据。

该要求同时被写入根级策略文件 AGENTS.md 的 Quick Decision Matrix:Unit tests in a package that uses failpoints | MUST enable failpoints before tests and disable afterward

用 failpoint-enabled 方式重跑

docs/agents/testing-flow.md 推荐的运行方式是仓库提供的封装脚本:

./tools/check/failpoint-go-test.sh pkg/<package_name> -run <TestName> -count=1

阅读 tools/check/failpoint-go-test.sh 的源码可以确认它的完整行为,这正是"脚本在 cleanup 时一定会禁用 failpoint"承诺的实现:

  • 未显式传 -tags 时,脚本默认使用 -tags=intest,deadlock(见第 54 行 tags="intest,deadlock" 与第 126 行的调用拼装);若测试还需要 nextgen 标签,应显式传 -tags=intest,deadlock,nextgen
  • 脚本先在仓库根目录执行 make failpoint-enable,然后进入目标包目录执行 go test
  • 通过 trap cleanup EXIT INT TERM(第 113 行)保证无论 go test 成功、失败还是被中断,退出前都会执行 make failpoint-disable,避免工作区残留已转换的 failpoint 代码。

对照 Makefile 可知,failpoint-enable / failpoint-disable 目标的实现是调用 tools/bin/failpoint-ctl 完成 gofail 插桩代码的转换与还原(第 343–351 行),注释明确写着 "Converting gofail failpoints..." / "Restoring gofail failpoints..."。

另外文档特别提醒:不要在同一个 worktree 的并行任务中直接调用 tools/bin/failpoint-ctl。因为底层的启用/禁用状态由 tools/check/failpoint-state.sh 序列化——该脚本使用 .git/.failpoint-state/lock 目录锁 + refcount 文件做引用计数(enable 时计数为 0 才真正执行 failpoint-ctl enable 并计数加一,disable 时计数归零才真正还原),并支持 FAILPOINT_LOCK_WAIT_SECONDS(默认 600 秒)等环境变量。绕过它直接跑 failpoint-ctl 会破坏这套互斥与计数机制。

若使用 Bazel 直接运行测试,则应先执行 make bazel-failpoint-enable、测试后执行 make bazel-failpoint-disable;若走 make bazel_test,从 Makefile 第 709 行可见该目标本身已依赖 bazel-failpoint-enable,无需单独启用,但测试结束后仍需执行 make bazel-failpoint-disable

判定规则

如果启用 failpoint 后 diff 消失,则将该问题分类为环境/配置问题(setup issue),而不是逻辑回归——这解释了为什么"同一段代码,我的本地结果和别人/CI 不一致"在 TiDB 里最常见的答案就是 failpoint 状态不同。

规则二:隔离 merge 的影响

若 failpoint 排除后 diff 依然存在,说明变化大概率来自合并进来的上游提交。文档给出的三步是:

  1. 用最小目标复现:-run <TestName> -count=1
  2. 对比 good/bad 两个 commit,对合并区间做二分;
  3. 在更新任何期望输出之前,先确定第一个引入坏行为的 commit(first bad commit)。

文档提供的 bisect 命令:

git bisect start
git bisect bad <bad_commit>
git bisect good <good_commit>

这里的实操要点是"先定位、后改输出"。对 TiDB 这种 planner testdata 高度敏感的仓库,merge 引入的优化器行为微调(如代价模型参数、rule 开关变化)会让大片 testdata/*.json 的 plan 文本漂移;此时如果直接全量同步 testdata,等于把上游改动和本地回归混在一个 diff 里,无法区分。bisect 找到 first bad commit 后,可以精确判断每个 testdata 变化是否与该 commit 的意图一致。

规则三:只有根因被证明后才能更新 testdata

文档给出了同步期望 plan/result 的两个且仅两个条件:

  • 上游合并有意改变了优化器行为(upstream merge intentionally changed optimizer behavior);
  • 现有测试期望本身已过时(stale),但查询语义并未改变。

并明确禁止"在根因被识别之前记录/更新 testdata"。这条规则与前两条规则共同构成一条因果链:症状 → 环境排除 → 提交定位 → 才谈得上改期望。任何一步未走通就动 testdata,都会让测试失去回归检测能力。

对集成测试面,docs/agents/testing-flow.md 还补充了配套约定:tests/integrationtest 的输入在 t/ 目录、期望结果在 r/ 目录,结果文件"通常不需要手工编辑;若必须编辑,保持最小改动并在报告前验证正确性"。

排查记录的输出格式

文档要求排查过程沉淀为结构化笔记,字段固定为六项,便于在 PR 描述或 issue 中直接复用:

字段 含义
Symptom diff 到底改了什么
Scope 是单个测试还是全量套件出现差异
Failpoint check 执行的命令与结果(规则一的证据)
First bad commit bisect 得到的 commit hash 与标题(若做了二分)
Conclusion 三选一:setup issue / upstream behavior change / local regression
Action 三选一:带 failpoint 重跑、同步期望输出、或继续修代码

这种"结论必须归入三类之一 + 行动必须对应其一"的写法,正是该技能面向 Agent 自动化的设计意图:排查结果可以被下游流程机器化消费,而不是停留在一段自由文本里。

小结

回到仓库内的完整引用链:策略层 AGENTS.md 声明"用 failpoint 的包必须先启用 failpoint 再跑测试";操作层 docs/agents/testing-flow.md 给出 rg 检测命令与 ./tools/check/failpoint-go-test.sh 标准运行方式;实现层 tools/check/failpoint-go-test.shtools/check/failpoint-state.sh 用 trap 清理和目录锁/引用计数保证了启用/禁用的原子性与可恢复性。而 SKILL.md 分诊流程将这一切组织成"先排除环境、再二分定位、最后才改期望"的可执行决策路径——这也是在 TiDB 仓库中处理一切"看起来与 PR 无关"的测试 diff 时的标准姿势。

登录后查看全文
热门项目推荐
相关项目推荐