Delta Debugging
原作者:Andreas Zeller
Motivation
土豆明码力强劲一夜改千行,黄老仙觉得他思路清晰逻辑通顺犇得不行!
18000行的小明带飞龙这波,要做这个优化轻而易举啊。
哎呀,奶不死的啊,这怎么奶死嘛?**老子是专业码农好吗?这怎么奶死嘛?专业码农这种局面还看不懂啊?
F5了!F5了!FFFF5了,让你们看看什么叫专业码农好吗?直接骑脸了好吗。什么叫飞龙骑脸。
测试集选得好有什么用嘛?
…吔?…别啊?!哎~!呀~!这解说不下去了,哎呀这!!呃啊!
为什么会这样?别打那么惊险呐!!你别害我呀!
**这个罪名我背不起呀!!我背不住这个罪名啊我凑!!哎呀...遭不住啦...
现在问题来了,黄老仙奶死了土豆明,他得想个办法帮土豆明debug,但他又懒得亲自上手,那他该咋办呢?
传统解决方案的缺陷
Regression Containment使用了线性测试的方法,在很多情形下都是非常有效。但是也有以下几个方面表现并不好
- interference:每一个改动自己的工作都ok,但是合起来就会出错了(合作开发的时候经常有这种事)
- inconsistency:一些改动的组合加进去,没准就没办法生成一个可以用于检测的程序了(比如编译不通过)
- granularity:一处逻辑改动可能影响到几千行代码,但其中的仅仅有几行实际引发了这个错误。仅仅把这一大块指出来并没有什么卵用,要更精细的找到错误位置才行
作者称,他的dd+(这个简写让人浮想联翩)算法能够
- 在线性时间检测出interference
- 在log时间内检测出独立的错误改动
- 高效的处理inconsistency,从而可以支持输入更细致的改动(fine-granular)
基本定义
配置:全改动集合C的子集c被称为一个configuration
基准:若c是一个空的configuration被称c为一个baseline
三种输出的可能:
- Pass ✔
- Fail ✖
- Unresolved ?
测试:一个把configuration映射到三种可能输出的函数被称为test
肇谬集:若对于任意一个C子集c’,只要包含c,那么c’的测试结果均不为✔,那么称c为 f a i l u r e − i n d u c i n g c h a n g e s e t failure-inducing\ change\ set failure−inducing change set
最小肇谬集:对于一个肇谬集B,假如它的任何子集都不再被测试为✖,那么称它为最小肇谬集
- 显然,最小肈谬集就是我们要搜索的目标
最理想情况
Monotony(单调性):假如configuration c的测试结果为✖,那么所有包含c的configuration的测试结果也为✖
- 推论:此时若c的测试结果为✔,那么它的所有子集测试结果不为✖
Unambiguity(无二义性,即本质上引发错误的configuration是唯一的):若 c 1 c_1 c1, c 2 c_2 c2的测试结果均为✖,那么他们交集的测试结果不为✔
- 无二义性不允许两个或多个无交集的configuration分别产生错误,也就是说错误的原因在一定程度上可以说是唯一的
- 这个可以节约很多开销,当你在C的子集c中发现了错误,你就不必再考虑c的补集了(c的补集测试结果一定为✔,否则就推出baseline的测试结果为✖了)
Consistency(一致性):任何configuration的测试结果不为**?**
最理想情况的处置方法
所谓的最理想情况,就是测试集是单调、无二义且一致的。这时,我们基于二分法去发现错误。
- 当test(left)=✖,在左半个集合继续查找
- 当test(right)=✖,在右半个集合继续查找
- 当两个测试都是✔,说明是一些改变合在一起导致了错误(interference)
- 此时先保持一侧全部applied,找到另一侧中引发错误所必须的changes;再保持另一侧全部applied,找到这一侧引发错误所必须的changes
- 将两个必须的部分合并,就找到了检测目标
非理想情况的处理方法
ambiguous
当引发问题的改变有多组,dd会正确返回其中的一个
此时只要在全集中删去这一组,调用dd就可以再获取其他的错误,重复这个过程以获得所有肈谬集
not monotone
假如test(a)=✖,但存在b包含a,test(b)=✔,那严格意义上就不能说a是错的了(configuration a的bug在configuration b中已经被修复了)。但包含b的C仍然是错误的,这说明引发错误的部分理应在C-b中啊(或为interference)。刨除所有被修复了的错误,剩下的错误是可以通过dd找到的。
inconsistency(复杂)
任意选取configuration很容易产生inconsistency的问题,下面给出了几个原因
- 融合失败:一个change不能被应用。有可能这个change他是基于一些更早的change的,但那个change并没有被加入到configuration;有可能change a和change b是冲突的,本来写了解决冲突的change c,但c并没有被加入刀configuration
- 生成失败:尽管所选的change都丢进去了,但可能引发了语法或语义错误,因而没办法生成程序
- 执行错误:缺少某些部分,程序可能没办法正确执行。test的输出是不确定的。
先期验证所有组合是否consistency是不现实的,接下来我们考虑如何应对unresolve的情况。
考虑最糟糕的情形,即我们把C分成子集之后,所有的子集测试结果都是unresolved,那么我们应该考虑什么样的组合来尽快找到有效结果呢?
- 尽量少加改动(靠近yesterday)(分的子集越多,子集size越小,得到consistent结果的可能就越大)
- 尽量多加改动(靠近today)
在进一步考虑dd+算法前,我们定义以下三种情况
- Found:如果 t e s t ( c i ) = ✖ test(c_i)=✖ test(ci)=✖,那么 c i c_i ci就包含了一个肈谬集,这和dd是一样的
- Interference:如果任意一个c和它的补集测试结果都为✔,那么c和c的补集构成了干扰关系,这也和dd一样
- Preference(优先):假如c不确定,c的补集pass了,那么接下来优先搜索 d , d = c ‾ ∪ c ′ d,d=\overline c \cup c' d,d=c∪c′其中 c ′ ⊆ c c'\sube c c′⊆c。相当于以c的补集为baseline,去搜索c的子集(这样可以有效缩小肈谬集的可能范围)
- Try again:如果上述情况都不满足,那么我们将子集个数提升为2n,重新进行一次划分,然后再跑一遍
- 每次跑完,假如ci pass了,那么ci就可以从C中拿掉了,因为他们不可能是引发错误的configuration。
- 类似地,假如c的补集没有通过测试,那么c就可以一直被置于applied的了
Avoid Inconsistency
重新考虑一下inconsistency的问题,假如我们一开始就怀疑一些change彼此相关,那我们就可以早早地把他们视为一个change,或者总是放在一个子集里,这样就可以减少很多unresolved test cases了
先验知识
- 更改时间相近、更改来源相近的change更可能相关
- 对同一个文件或同一个目录进行操作的change更可能相关
- 使用相同的引用或使用类似标识符的change更可能相关
- 影响相同函数(function)、模块(module)等语句实体的change更可能相关
- 语义上产生类似影响的change更可能相关
Predicting Test Outcomes
- 假如我们一开始就知道某些change之间有相互联结的关系,那实际上我们就可以预测一些**?**结果,而没必要实实在在的去测试
- 假如我们的changes是有序且总是要利用前置修改的,那么我们能发现很多configuration没必要去测试
时间复杂度总结
三种好性质都具备
最差:O(n)
错误集合仅有一个改动:O(logn)
Ambiguous(有多个错误)
dd算法仍在O(n)时间内返回其中一个错误
Not Monotone
设a错,a是b的子集,b对,则dd算法在O(n)时间内返回c-b中的一个错误
inconsisitent
作者似乎没有给出分析
Future Work
进一步解决Avoid Inconsistency
利用域知识(by exploiting domain knowledge,我完全不能理解这是啥意思)
使用完备的change档案管理限制系统(不知所云)
主要的可以改进的方向还是利用语义层面的相关性。对于一个程序我们可以容易的维护一张PDG(Program Dependency Graph)来描述代码函数、模块间的关系,这样,当我们将一个change应用到某一个模块时,我们很容易发现这将与哪些部分相关。根据change和节点的相关性,我们可以把整张图分成很多slice。在同一个slice(也就是具有语义相关性)的change将被置于同一个subset
删除灰色代码
一些change部分在运行中并不会被执行,作者想利用code coverage tool来把这些changes给直接排除掉