research!rsc

关于编程的思考和链接,作者:Russ Cox

RSS

使用差异覆盖率进行调试 (Differential Coverage for Debugging)

发布于 2025 年 4 月 25 日星期五。

最近我一直在调试一些非我编写的代码,这让我回忆起一个技巧。我相信这是一个非常古老的调试技术(比如二分法),但它应该被更广泛地了解。假设你有一个失败的测试用例。你可以通过比较成功测试的覆盖率和失败测试的覆盖率,来了解可能涉及哪些代码。

例如,我在我的开发副本 math/big 中插入了一个 bug:

$ **go test**
--- FAIL: TestAddSub (0.00s)
  int_test.go:2020: addSub(-0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff, 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff) = -0x0, -0x1fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe, want 0x0, -0x1fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe
FAIL
exit status 1
FAIL	math/big	7.528s
$

现在我们收集一个通过和一个失败的 profile:

$ **go test -coverprofile=c1.prof -skip='TestAddSub$'**
PASS
coverage: 85.0% of statements
ok 	math/big	8.373s
% **go test -coverprofile=c2.prof -run='TestAddSub$'**
--- FAIL: TestAddSub (0.00s)
  int_test.go:2020: addSub(-0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff, 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff) = -0x0, -0x1fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe, want 0x0, -0x1fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe
FAIL
coverage: 4.7% of statements
exit status 1
FAIL	math/big	0.789s
$

现在我们可以对它们进行差异比较,生成一个 profile,显示失败测试的独特之处:

$ **(head -1 c1.prof; diff c[12].prof | sed -n 's/^> //p') >c3.prof**
$ **go tool cover -html=c3.prof**

head -1 保留了一行覆盖率 profile 头部。 diff | sed 仅保存失败测试的 profile 中独有的行,而 go tool cover -html 在 Web 浏览器中打开 profile。

在生成的 profile 中,“covered”(绿色)表示它在失败的测试中运行,但未在通过的测试中运行,因此值得仔细研究。查看文件列表,只有 natmul.go 具有非零覆盖率百分比,这意味着它包含失败测试独有的行。 如果我们打开 natmul.go,我们可以看到各种红色(“uncovered”)的行。 这些行在通过的测试中运行,但在失败的测试中没有运行。 它们是被排除的,尽管这些行通常会运行,但在失败的测试中被跳过这一事实可能会引发有关什么逻辑导致它们被跳过的有用问题。 在这种情况下,只是该测试没有执行它们:根本没有调用 nat.mul 方法。

向下滚动,我们找到唯一的绿色部分。 这段代码是我插入 bug 的地方:else 分支缺少 za.neg = false,从而导致测试失败中的 -0x0。 差异覆盖率计算和显示成本低廉,并且在正确的情况下,可以节省大量时间。 在超过 15,000 行代码中,差异覆盖率确定了 10 行,其中包括两个相关的行。

当然,这种技术并非万无一失:如果 bug 依赖于数据,或者测试对代码中的特定错误不敏感,则通过的测试仍然可以执行有 bug 的代码。 但是很多时候,有 bug 的代码只会触发失败。 在这些情况下,差异覆盖率会精确指出值得仔细检查的代码块。

你可以在此处查看完整的 profile

一种更简单但仍然有用的技术是查看单个失败测试的基本覆盖率 profile。 它可以准确地了解测试中运行了哪些代码段,这可以指导你进行调试:未运行的代码不是问题所在。 并且,如果你对特定函数如何返回错误感到困惑,则覆盖率会精确指出错误的行。 在上面的例子中,失败的测试仅覆盖了 4.7% 的代码。

差异覆盖率也适用于通过的测试。 想要查找在 net/http 中实现 SOCK5 代理的代码吗?

$ **go test -short -skip=SOCKS5 -coverprofile=c1.prof net/http**
$ **go test -short -run=SOCKS5 -coverprofile=c2.prof net/http**
$ **(head -1 c1.prof; diff c[12].prof | sed -n 's/^> //p') >c3.prof**
$ **go tool cover -html=c3.prof**

玩得开心!