Git教程 · 二分法排错
- 1️⃣ 概述
- 2️⃣ 使用要求
- 3️⃣ 执行过程及其实现
- 3.1 用二分法人工排错
- 3.2 用二分法自动排错
- 4️⃣ 替代解决方案
在开发过程中,我们经常会突然遇到一个错误,是之前早期版本在成功通过测试时没有出现过的。这时候,时下较被看好的调试策略是先搜索出我们第一次发现错误时所在的提交。 由于在使用 Git 开展工作时,我们往往会产生许多小型提交,因此可以通过分析其中的变化来快速查找错误的成因。
Git 支持用二分法来搜索引发问题的提交。
二分法是基于二分搜索的一种查找方法。查找的起点是已确认没问题的提交,终点是已明确有错误的提交,两者之间这段提交历史将会被“分半”,位于“中间”的提交会在工作区 中被激活。然后我们会对被激活的提交进行错误检查。接着,再根据是否在被激活提交中找到该错误的情况,再对错误必然会隐藏的那段剩余的提交历史进行重新“分半”,并检查新的 “中间”提交。如此反复,我们最终就会找到第一次出现该错误的提交。在该工作流中,我们会为你演示以下操作。
- 如何有效地用二分法找出引发问题的提交。
- 如何用二分法实现自动化排错。
1️⃣ 概述
在下图中,我们将会看到一段提交历史,其中有一个确认无误的提交和已明确出了问题的提交。虽然提交历史并不非得要线性发展,但在出了问题的提交到没有问题的提交之间必须要有一条路径,以说明它们之间的父系关系。
当二分查找进程被启动之后, Git 就会在相关的提交历史的中间位置选择一个合适的提交。该提交将会被执行某种人工测试或脚本测试,然后根据其结果被标记为“good”或“had”。 接着,该二分查找任务会去挑选另一个提交对象,对其进行测试并标记。这个进程会一直重复该动作,直至找到其直系父提交中没有错误的那次提交。
2️⃣ 使用要求
- 可重现的错误检测:我们必须要证明相关错误行为的一致性。也就是说,我们要能清楚地识别一个版本是正确还是不正确。对于自动化错误,无论它采用的是测试用例还是脚本,它都必须要能检测到错误。
- 误差检测的成本不能太高:误差检测必须即快速又便宜。使用二分法进行多故障检测的成本取决于我们要测验的提交数量。如果其需要的时间过长或成本过高,对错误的成因来一次分析性搜索显然会更有效率。
3️⃣ 执行过程及其实现
在开发过程中,我们经常会遇到之前版本中不曾出现过的错误。二分法可以帮助我们在提交历史中定位那个包含错误的提交。
为了演示接下来的这些操作,我们做了一个小型的示范性项目。在该项目中,我们实现了各种数学函数。其中值得一提的是一个计算阶乘的功能。该功能会以列表的形式返回从1到5所有数的阶乘。
> java FactorialMain
Factorial of 1 = 1
Factorial of 2 = 1
Factorial of 3 = 2
Factorial of 4 = 6
Factorial of 5 = 24
3.1 用二分法人工排错
首先,我们要对二分查找的基本过程有个交代,以说明在该测试中所要人工查找的错误成因。
-
第1步:定义错误标志
一般情况下,错误往往都是由开发者、测试者或者用户发现的。我们的第一步是要对该错误进行分析和理解,找出该错误的某种标志。
下面我们来看几个错误标志的例子。- 当某个动作或函数调用引发某种异常时,该程序就会被取消执行或显示错误消息。
- 某个函数返回了包含错误结果的信息项。
- 某个测试用例执行失败。
具体到我们这个例子中,3的阶乘可以被视为是一个标志,代表它出错了。
如你所见,多数情况下我们用单凭分析就足以发现问题的成因了,无须进行二分查找。 -
第2步:分别找出没问题的和有问题的提交
该二分查找过程需要我们提供一个没问题的提交和一个出了错的提交。 一个不错的选择是我们可以用最新发行版或者最新历程碑来充当那个确认无误的提交。
如果我们发现被选中来充当没有问题的提交中也包含了该错误,那就必须去回溯更久远的历史了。
由于相关错误的信息已经被上报,我们要想找到一个问题提交并不难,但如果想要在一堆没有问题的提交中搜索更多问题提交,我们就务必要找出那个最古老的问题提交了。
下面是上述例子的日志输出,我们来看看它的提交历史。> git log --oneline 202d25d modulo finished e36fead multiply finished 918ed2f sub finished ebe74ld add finished 87ac59e ComputeFactorial finished 39cbdc0 init
分析结果表明,提交
87ac59e ComputerFactorial finished
应该是没有问题的,出错的应该是提交202d25d modulo finished
。 -
第3步:执行二分法排错
现在,既然我们已经将错误局限在了提交历史一个区间内,就可以开始用二分查找来进行实际的错误搜索了。
我们可以通过bisect start
命令来开始二分查找。在这里,我们必须要将问题提交指定为第一个参数,而没有问题的提交则是第二个参数。> git bisect start 202d25d 87ac59e Bisecting: 1 revision left to test after this(roughly 1 step) [918ed2f29a44e468d690fb770aablad2dbaela5a]sub finished
bisect start
命令会将第一个提交标志成 “bad” 提交,第二个则标志为 “good” 提交。 然后接下来,位于这两个提交之间的那个提交(具体到我们的例子中就是提交918ed2f sub finished
) 会被激活。
现在,工作区中包含了来自某个提交的文件,我们还不不能确定它有没有出问题。由于我们之前已经找到了该错误的标志,该版本的状态目前是可以被测试的。> java FactorialMain Factorial of 1 =1 Factorial of 2 =1 Factorial of 3 =2 Factorial of 4 =6 Factorial of 5 =24
但从在工作区中运行 FactorialMain 的结果表明,该错误仍然存在于其中,这意味着当前提交依然是有问题的。
现在,我们用以下命令中的一个对当前提交进行标志。bisect good
: 错误不在其中,该提交确认无误。bisect bad
: 错误就在其中,该提交有问题。bisect skip
: 当前提交无法被测试。 一般是因为没有被编译或缺失了一些文件,这时 候二分查找进程就会去激活另一个提交来测试。
在我们的例子中,由于错误还存在于该提交中,所以我们会将其标志为 “bad” 提交。
>git bisect bad Bisecting: 0 revisions left to test after this(roughly 0 steps) [ebe741de3366a3fc08fbedfdfa408517dd172ca3]add finished
在 Git 的响应报告中,我们看到目前被激活的是提交
ebe741d add finished
。此外, Git 还报告说这是它必须要测试的最后一个提交。
我们对FactorialComputer 的重新测试表明,该提交中是确认无误的,因此被标志为 “good”提交。> git bisect good commit 918ed2f29a44e468d690fb770aablad2dbaela5a Author:Rene Preissel <rp@eToSquare.de> Date: Fri Jun 2408:04:432011 +0200 sub finished :040000 040000 0e5bfb07e859072a564eaca07346le4a12a0ed61 \ 329e7f864bac874c69be4531452c753cf56be794 M src
现在,Git 告诉我们提交
918ed2f sub finished
才是该错误第一出现的地方。我们现在可以用Git 命令来分析该提交做了哪些修改了(例如git show 918ed2f
)。
最后,我们发现这个例子中阶乘计算只能计算到n-1。
请注意,在我们启动排错过程之前,必须要将工作区重新设置到当前分支的HEAD 上。
关于这一点我们将会在下一步骤中做说明。 -
第4步:停止或取消二分查找
在成功分析出错误根源,或者决定取消某个二分查找之后,我们还必须要用bisect reset
命令将工作区中的内容重置回正常的开发版本。> git bisect reset Previous HEAD position was ebe74ld...add finished Switched to branch 'master'
3.2 用二分法自动排错
在之前的操作序列中,我们测试的是某个提交中是否包含了某个错误,用的是人工测试。
如果我们连对的一个很长历史的区间或者人工测试的成本非常高昂,也可以通过一段脚本来进行自动化测试,让二分查找算法自己去完成它的工作。
-
第1步:定义错误标志
错误标志的的方法与人工的二分法排错一样,我们只要确保这些错误标志能被脚本自动检测到即可。 -
第2步:准备好测试脚本
如果我们想要进行自动化的二分法排错,就必须要提供一段 shell 脚本。为了能实现错误标志的自动检测,这段shell 脚就本必须要根据指定错误是否存在的情况返回以下不同的退出码。Exit code 0
: 表示没有找到错误,二分查找进程应该会将该提交标志为“good”。Exit codes 1-124,126,127
: 表示错误被找到,二分查找进程应该会将该提交标志为“bad”。Exit code 125
: 表示测试由于程序可行性的原因没被执行。 一般情况下,是该版本无法被编译。二分查找过程会直接跳过该提交。
在这里,我们的计算器应用是用Java 编写的。下面我们就将其作为一个例子来演示一下 如何在这类环境中对自动化二分查找进程进行调试。对于其他的开发环境,这段独立的脚本通常要做些相应的调整。
事实上,我们这段自动化错误检验是通过 JUnit 测试来执行的 ( 你可以从 http://wwwjunit.org 网站上下载到JUnit) 。 它只负责检测3阶乘是否真的是6。如果返回结果为 false, 即视为测试失败。
public class FactorialBisectTest { @Test public void testFactorial(){ long result = Computer.factorial(3); Assert.assertEquals(6,result); } }
特别提醒:不要忘记该测试要在一个新文件中实现,它不应该被 Git 纳入版本控制。在 二分查找进程中,工作区中会有不同的提交被激活,而且是一个接一个地进行。如果该测试文件也处于 Git 的控制之下,它在旧提交被激活时就不存在了。而且从另一方面来说,非版本文件也应该被抽离在工作区的修改之外。
另外,自动化的二分查找进程需要我们提供一段 shell 脚本。该 shell 脚本首先必须能编 译我们的Java 源文件,然后再启动
test.Ant
, 将其用作本例中的构建系统。在该计算机项目中,我看可以通过一个名为
build.xml
的构建文件来执行一次纯净的构建过程 (ant clean compile
)。另外为了执行二分查找测试,我们还需要另一个名为bisect-build.xml
的构建文件, 它只提供了一个用于启动测试的 target。再次提醒,该文件不能被 Git 纳入版本控制。<target name="test"> <junit> <classpath refid= "build.classpath" /> <test name= "FakultaetsBisectTest" haltonerror="true" haltonfailure="true" /> </junit> </target>
如果我们想访问不同的 Ant target, 就要有一个名为
bisect-test.sh
的 shell 脚本,这个脚本也不能被 Git 纳入版本控制。#!/bin/bash ant clean compile if [ $? -ne 0]; then exit 125; fi ant -f bisect-build.xml if [ $? -ne 0]; then exit 1; else exit 0; fi
该脚本会去调用构建文件中的各种构建 target,并检测 Ant 的退出码。测试失败时 Ant 会返回一个大于0的退出码。我们需要将其转换成二分查找进程所需要返回的代码。
- 如果构建失败,就返回退出码125。
- 如果测试成功,就返回退出码0。
- 如果测试失败,就返回退出码1。
-
第3步:分别找出没问题的和有问题的提交
在对没问题和有问题提交的搜索方面,这里的流程和人工的过程并没有什么不同。但是, 你也可以用 JUnit 测试来检查错误。举例来说,我们选择提交87ac59e FactorialCompute finished
来验证一下它确实是没有问题的。> git checkout 87ac59e > ant -f bisect-build.xml Buildfile:bisect-build.xml test: BUILD SUCCESSFUL Total time:0 seconds
特别提醒: 在完成上述过程之后,请不要忘记将master分支设置成当前活跃分支。
> git checkout master
-
第4步:执行二分法的自动化排错
在使用自动化排错时,第一次二分查找进程也得要用bisect start
命令来启动。另外,我们还需要将有问题的提交指定为第一参数,没问题的提交为第二参数传递给该命令。> git bisect start 202d25d 87ac59e Bisecting:1 revision left to test after this(Roughly 1 step) [918ed2f29a44e468d690fb770aablad2dbaela5a]sub finished
然后在用
bisect run
命令来执行名为bisect-test.sh
的 shell 脚本。> git bisect run ./bisect-test.sh
下面我们将输出截断,只显示
bisect run
命令的最后几行内容。你会很高兴地看到该命令找到了918ed2f sub finished
是第一个出错的提交。.. Buildfile:bisect-build.xml test: BUILD SUCCESSFUL Total time:0 seconds 918ed2f29a44e468d690fb770aablad2dbaela5a is the first bad commit commit 918ed2f29a44e468d690fb770aablad2dbaela5a Author:Rene Preissel <rp@eToSquare.de> Date: Fri Jun 2408:04:432011 +0200 sub finished :040000 040000 Oe5bfb07e859072a564eaca07346le4a12a0ed61 \ 329e7f864bac874c69be4531452c753cf56be794 M src bisect run success
-
第5步:完成二分查找操作
在成功完成排错之后,我们还必须要用bisect reset
命令来结束整个二分查找进程。> git bisect reset Previous HEAD position was ebe741d...add finished Switched to branch 'master!
4️⃣ 替代解决方案
用合并操作将测试脚本添加到旧提交中去
上面这个过程的优势在于 Git 在激活新提交的时候将一些未被版本化的文件留在了工作区中。这样一来,我们在旧提交中也可以执行这些“新”的测试脚本了。
当然,我们也可以采用另一种解决方案,就是将测试脚本纳入到一个新分支中(见下图中的 bisect-test
分支)。
在该二分查找的 shell 脚本中,二分查找进程会在每次测试运行之前将bisect-test
分支合并到当前提交中。然后用--nocommit
选项防止其变成一个永久性的提交。
然后待测试完成之后,再用 reset
命令重置掉合并操作所带来的修改。这个操作序列和示例脚本可以在 bisect 命令的在线文档的 Example一节中找到。
这个使用 bisect-test
分支的解决方案不仅可以在我们拥有一个测试用例和新增一个新的测试脚本时发挥作用。也可以用于测试必须要适应现有代码的,例如可能是因为测试中某种审核需要访问的数据在旧提交是不可见的。
但在大多数情况下,我们之前所描述的非版本化文件的方案已经够用了,而且它实现起来相对要更容易一些。
《【Git教程】(十四)基于特性分支的开发 — 概述及使用要求,执行过程及其实现,替代方案 ~》