把 200 行和 12 个步骤缩到两行四步
目标
把客户发来的故障包(一个处理程序、200 行的 feed、12 步的复现步骤)精简成最小复现用例。先让机器判定“是否仍然是同一个失败”,只用这个判定器来精简,并且证明剩下的内容只要去掉任何一个,复现就会停止。
为什么重要
客户写给我们的复现步骤,通常就是那个人当天做过的全部事情。带着这一切去找原因,需要查看的组合会爆炸。但如果一上来就精简,风险更大:删掉一行,程序因为另一个错误崩溃了,而那也是错误,于是误以为仍然能复现。这样得到的用例,复现的是与我们追查的缺陷无关的另一个缺陷。 所以有一个顺序:先做判定器,只用这个判定器来精简。判定器要同时看退出码和只在该失败中才会出现的特征字符串。运行时间或临时文件名这类每次运行都会变的内容,不要放进判定。 精简由机器来做。一行一行地删,行数是多少就要调用多少次判定器;而把候选分成块来看的 delta debugging(ddmin),会把这个次数降到接近行数的对数。 评分器不会相信你写下的数字。它把评分器生成的多个候选放进判定器来试,并给缩减程序一份每次把答案藏在不同位置的 256 行输入,同时测量结果和判定器的调用次数。
步骤
- 创建并运行 /root/mre/gen_case.py,展开 /root/mre/app/ingest.py、/root/mre/input.txt(200 行)和 /root/mre/repro/steps.txt(12 步)。
- 创建 /root/mre/oracle.py。接收
--input传入的候选,如果它产生了我们追查的失败,退出码为 0,否则为 1。其他错误不算复现。 - 用判定器精简 feed,生成 /root/mre/min_input.txt。剩下的行是原文件中的行,并保持原文件的顺序。
- 在 /root/mre/minimality.json 中,为剩下的每一行写明去掉它之后是否仍能复现,以证明结果是 1-最小。
- 在 /root/mre/shrink.py 中实现 delta debugging(ddmin)。它接收
--input --oracle --out,并且调用判定器的次数不能与行数成正比。 - 精简 12 步的复现步骤,生成 /root/mre/min_steps.txt。剩下的步骤保持原文件中的行原样,并保持顺序。
- 确认改变取值后是否仍能复现,留下 /root/mre/generalize.json 和三份变形文件。
- 在 /root/mre/mre_report.md 中分四节报告。
参考
- 处理程序运行契约:
python3 /root/mre/app/ingest.py --input <피드> --config <설정 JSON>(占位符依次为 feed 文件、配置 JSON)。正常为 0,无法读取配置为 2,我们追查的缺陷为 3,完全没有 qty 行为 4。配置的形式为{"strict": true}。 - 判定器运行契约:
python3 /root/mre/oracle.py --input <후보 파일>(占位符为候选文件)复现时以 0 结束,否则以 1 结束,并向标准输出打印一行。配置文件由判定器自己创建。 - 缩减程序运行契约:
python3 /root/mre/shrink.py --input <원본> --oracle <오라클 경로> --out <결과 파일>(占位符依次为原始文件、判定器路径、结果文件)把精简后的结果写入 out,并向标准输出打印一行。判定器的调用形式为python3 <오라클> --input <후보>(占位符依次为判定器、候选)。 - 步骤试验:
repro/steps.txt中的各行是 shell 命令,把环境变量 MRE_WORK 所指向的尚不存在的目录用作工作位置。在同一个 shell 中依次运行选出的步骤,如果标准错误中出现特征字符串,就算复现。每次试验都要重新设定 MRE_WORK。 - minimality.json:
{"minimal": true|false, "lines": [{"line": 원본 줄, "removed_reproduces": true|false}]}(占位符为原始行)。lines 保持 min_input.txt 中的顺序。 - generalize.json:
{"variants": [{"file": 절대 경로, "change": 무엇을 바꿨는지, "reproduces": true|false}]}(占位符依次为绝对路径、所改动的内容)。变形有三个,其中能复现的有一个。 - 常见错误:把退出码不是 0 就当作复现;在留有前一次运行所产生文件的位置试验步骤;缩减程序没有先确认原始输入能复现。
- 1-最小的意思是“剩下的内容中去掉任何一个,复现就会停止”,并不是“最小”。也许还存在完全不同的更小的组合。
- 判定器调用上限 160 次(以 256 行输入为准)和三个变形这两个数字是本实验的假设。它们不是算法规定的值,而是为了评分而定的值。
- 参考文档:ddmin 原论文页面和 Debugging Book 中关于 delta debugging 的章节讲解了算法,Python subprocess 文档说明了调用判定器的方法。
展开故障包
创建并运行 /root/mre/gen_case.py,生成 /root/mre/app/ingest.py、/root/mre/input.txt(200 行)和 /root/mre/repro/steps.txt(12 步)。
在现场拿到手的只有程序、数据和步骤这三样。展开之后,先把步骤原样运行一次,亲眼看看失败:把 MRE_WORK 设为一个尚不存在的目录再运行。
让机器判定是否为同一个失败
创建 /root/mre/oracle.py。接收 --input 传入的候选,如果它产生了我们追查的失败,退出码为 0,否则为 1。配置文件由判定器自己创建,并且同时查看退出码和特征字符串。
如果把所有不是 0 的退出码都当作复现,精简时就察觉不到已经转到了完全不同的错误上。这个处理程序在无法读取配置时以 2 结束,遇到我们追查的缺陷时以 3 结束,完全没有 qty 行时以 4 结束。标准错误中的特征字符串也要一并查看。
精简 feed
用判定器精简 feed,生成 /root/mre/min_input.txt。剩下的行必须全部是原始 input.txt 中的行,必须保持原文件的顺序,并且去掉其中任意一行,复现就必须停止。
这一步手工做也可以。最简单的办法是一行一行地删,只保留仍能维持复现的行。大约要调用 200 次判定器,这种慢,正是第 5 步要实现 ddmin 的原因。
证明剩下的行为什么都需要
在 /root/mre/minimality.json 中,为 min_input.txt 的每一行写明去掉该行之后是否仍能复现。所有行一去掉复现就停止时,minimal 为 true。lines 保持 min_input.txt 中的顺序。
1-最小的意思是“剩下的内容中去掉任何一个,复现就会停止”。没有这份证明,就回答不了“为什么需要这一行”这个问题,接收方也只能选择相信。判定交给判定器,你只需要写下结果。
把精简自动化
在 /root/mre/shrink.py 中实现 delta debugging(ddmin)。它接收 --input --oracle --out,先确认原始输入能够复现,再进行精简,并且调用判定器的次数不能与行数成正比。
一行一行地删,行数是多少就要调用多少次判定器。ddmin 把候选分成 n 片,先看只用其中一片能不能复现,不行的话再看去掉一片后的其余部分能不能复现。两种都不行,就切得更细并重复。评分器会在 256 行的输入上运行你的缩减程序,并统计调用了多少次判定器。
同样精简复现步骤
精简 12 步的复现步骤,生成 /root/mre/min_steps.txt。剩下的步骤是 steps.txt 中的行原样,必须保持原文件的顺序,并且去掉其中任意一步,复现就必须停止。
每次试验步骤时,都要把 MRE_WORK 重新设为一个尚不存在的目录。如果前一次运行留下了配置文件,即使去掉“创建配置的步骤”也能复现,于是会得出这一步不需要的错误结论。复现的判定,看标准错误中是否出现特征字符串。
确认是结构而不是取值
基于最小用例,分别创建改变取值的版本和改变结构的版本,并写入 /root/mre/generalize.json。变形有三个,只改取值的 /root/mre/min_input_generic.txt 必须能复现,另外两个必须不能复现。三个文件都要真实留下。
这一步要分清:剩下的两行是出于偶然的取值,还是出于结构。把数字改成别的数字仍能复现,说明原因不在那个数字上。反过来,如果能展示把逗号改成句号或改变区域设置之后复现就停止了,就证明了原因是两个条件的组合。
写明把什么称为复现
在 /root/mre/mre_report.md 中分 ## 무엇을 재현이라고 불렀나 ## 얼마나 줄였나 ## 남은 줄이 왜 다 필요한가 ## 남은 가정 四节书写(韩文标题,依次意为“把什么称为复现”“精简了多少”“剩下的行为什么都需要”“剩余的假设”)。原始用例与最小用例的行数和步骤数,以及判定器所看的退出码和特征字符串,都必须出现。
接收方只看到两行输入,会先问“这为什么是个问题”。先写明把什么视为同一个失败,再写精简了多少,以及为什么不能再精简。最后一节写明改变取值后仍能复现这一事实,以及改变什么会使复现停止。