大型语言模型是小样本测试器:探索基于LLM的通用错误重现

互联不一般哥 2024-05-12 21:12:49

Large Language Models are Few-shot Testers: Exploring LLM-based General Bug Reproduction

Sungmin Kang, Juyeon Yoon, Shin Yoo

School of Computing, KAIST, Daejeon, Republic of Korea

引用

Kang S, Yoon J, Yoo S. Large language models are few-shot testers: Exploring llm-based general bug reproduction[C]//2023 IEEE/ACM 45th International Conference on Software Engineering (ICSE). IEEE, 2023: 2312-2323.

论文:https://ieeexplore.ieee.org/abstract/document/10172763

摘要

为了帮助开发者编写测试,人们开发了许多自动测试生成技术。为了促进完全自动化,大多数现有技术都旨在提高覆盖率或生成探索性输入。然而,现有的测试生成技术再很大程度上无法实现更多的语义目标,例如生成测试以重现给定的错误报告。尽管如此,重现错误仍然非常重要,因为我们的实证研究表明,开源软件库中的预期程序语义转化为测试指标,现有的故障重现技术往往只能处理程序崩溃问题,而这只是所有错误报告中的一小部分。为了从一般的错误报告中自动生成测试,我们提出了LIBRO,这是一个使用大型语言模型(LLM)的框架,它已经被证明能够执行与代码相关的任务。由于LLM本身无法执行目标错误代码,因此我们将重点放在后处理步骤上,这些步骤可帮助我们分辨LLM是否有效,并根据其有效性对生成的测试排序。我们对LIBRO的评估结果表明,在广泛研究的Defects4J基准上,LOBRO可以为33%的研究案例(750个案例中的251个)生成故障重闲测试案例,同时为149个错误提出了排在首位的错误重现测试。为了减少数据污染(即LLM可能只是记住了部分或全部测试代码),我们还在LLM训练数据收集结束后提交的31份错误报告上对LIBRO进行了评估:在所有研究的错误报告中,LIBRO为32%的报告生成了错误重现测试。总之,我们的结果表明,LIBRO有潜力通过从错误报告自动生成测试来显著提高开发者的效率。

1 引言

软件测试是通过在被测软件(SUT)上执行测试来确认软件满足规范标准的实践。由于许多软件项目的重要性和安全关键性质,软件测试是软件开发过程中最重要的实践之一。尽管如此,人们普遍认为软件测试是乏味的,因为需要大量的人力。为了填补这一空白,自动化测试生成技术已经研究了近半个世纪,产生了许多工具,它们使用隐式语言(回归或崩溃检测)来指导自动化过程。它们在添加新特性时非常有用,因为它们可以为焦点类生成具有高覆盖率的新测试。

然而,并不是所有的测试都会立即与焦点类一起添加。事实上,我们发现有相当数量的测试源于错误报告,也就是说,这些测试的创建是为了防止所报告的错误在未来出现回归。这表明,从错误报告中生成错误重现测试,是一种未得到充分重视但却对开发者自动编写测试很有影响的方法。我们的观点是基于对300个使用JUnit的开源项目样本的分析:因错误报告而添加的测试数量中位数为整个测试套件规模的28%。因此,开发者经常会遇到从错误报告到测试的问题,由于许多错误报告处理的是语义问题,所以他们的范围在实践中是有限的。

从报告到测试(report-to-test)的一般问题对软件工程界具有重大意义,因为解决这个问题可以让开发者使用更多的自动调试技术,并配备重现所报告错误的测试用例。Koyuncu等人指出,在广泛使用的Defects4J错误时,可能很难使用最先进的自动调试技术(通常在Defects4J上进行评估),因为这些技术依赖于错误重现测试。相反,如果有了自动生成错误及时测试的技术,那么各种自动调试技术就都可以使用了。

作为解决这一问题的初步尝试,我们建议使用大型语言模型(LLM)来生成测试。我们之所以使用LLM,是因为它们在各种自然语言处理任务和编程任务中表现出色。在这项工作中,我们将探索能否将LLM的功能扩展到从错误报告生成测试用例。更重要的是,我们认为,在研究LLM应用于这一问题时,必须同时研究LLM的性能以及我们何时可以依赖LLM生成的测试的问题。这些问题对于实际的开发者使用是至关重要的:Sarkar等人提供的相关实例表明,开发者很难理解LLM在用于代码生成时何时会执行其任务。为了填补这一知识空白,我们提出了LIBRO(LLM Induced Bug ReprOduction),这是一个框架,可促使OpenAI LLM(Codex)生成测试,处理测试结果,并仅在我们有理由相信错误重现已经成功时才提出解决方案。

我们在Defects4J基准和我们构建的新报告测试数据集上进行了广泛的实证实验,旨在找出能够表明LIBRO成功重现错误的特征。我们发现,对于Defects4J基准,LIBRO可以为251个错误生成至少一个错误重现测试,占错误报告中所有研究错误的33.5%。LIBRO还以71.4%的准确率成功推断出哪些错误重现尝试是成功的,并为149个错误生成了一个实际的错误重现测试作为其第一个建议。为了进一步验证,我们在最近建立的一个错误报告数据集上对LIBRO进行了评估,发现我们也能在这个不同的数据集中重现32.2%的错误,并验证了我们的测试建议启发式方法也能在这个不同的数据集中发挥作用。

综上所述,我们的贡献如下:

我们对开放源代码库进行了分析,以验证从错误报告中生成错误重现测试用例的重要性;我们提出了一个利用LLM重现错误的框架,只有当结果可靠时,才会向开发者建议生成的测试;我们在两个数据集上进行了广泛的实证分析,结果表明,我们发现的模式以及LIBRO 的性能是稳健的。

2 动机

如上一节所述,从报告到测试的问题的重要性基于两点看法。首先,与自动调试技术通常的假设不同,在提交错误报告时,很少有能揭示错误的测试。Koyuncu等人报告说,在他们分析的95%的案例中,基于频谱的故障定位(SBFL)技术无法定位到报告时的错误,因此他们提出了一种完全静态的自动调试技术。然而,正如Le等人所证明的,使用动态信息往往能获得更精确的定位结果。因此,从报告到测试的技术可以提高大部分自动调试文献的实用性和/或性能。

另一个观察是,从报告到测试的问题可能是一个被低估的问题,尽管如此,它仍然是测试的重要和反复出现的部分。现有的对开发者的调查显示,开发者认为从错误报告生成测试是改进自动测试的一种方法。Daka和Fraser调查了225名软件开发者,指出了自动测试生成可以帮助开发者的方法,其中三种方法(决定测试什么、现实性、决定检查什么)可以通过错误报告来解决,因为错误再现是一种相对明确的活动。Kochhar等人明确询问了数百名开发者是否同意“在维护过程中,当一个错误被修复时,最好添加一个涵盖该错误的测试用例”这一说法,结果发现,在5分的李克特量表中,平均同意率为4.4。

为了进一步验证开发者是否定期处理报告到测试的问题,我们通过挖掘数百个开源Java存储库来分析可归因于错误报告的测试添加的数量。我们从Alon等人的Java-med数据集开始,该数据集由来自GitHub的1000个顶级Java项目组成。从每个存储库中的提交列表中,我们检查(i)提交是否添加了一个测试,以及(ii)提交是否链接到一个问题。为了确定提交是否添加了测试,我们检查它的diff是否添加了测试装饰器和测试体。此外,如果(i)提交消息提到“(修复/解决/关闭)#NUM”,或者(ii)提交消息提到拉取请求,这反过来又提到了一个问题,我们将提交链接到错误报告(或GitHub中的问题)。我们将此类与报告相关的提交所添加的测试数量与当前(2022年8月)测试套件的大小进行比较,以估计此类测试的流行程度。由于不同的存储库有不同的问题处理实践,我们过滤掉没有添加测试的问题相关提交的存储库,因为这表明了不同的错误处理实践(例如google/guava)。因此,我们分析了300个存储库,如表1所示。

表1 分析的存储库特征

我们发现,在这300个版本库中,通过问题引用提交添加的测试占当前测试套件规模的比例中位数为28.4%,这表明有相当数量的测试是通过错误报告添加的。需要注意的是,这并不意味着测试套件中28.4%的测试都源于错误报告,因为我们并不跟踪测试添加后的情况。不过,这表明从报告到测试的活动在测试套件的演变过程中发挥了重要作用。基于这一结果,我们得出结论:开源开发者会定期处理从报告到测试的生成问题。推而广之,建议和/或自动提交已确认测试的自动报告到测试技术将有助于开发者的自然工作流程。

尽管这个问题非常重要,但其一般形式仍然是一个难以解决的问题。现有的工作试图通过关注不同的问题来解决该问题的特殊情况。有的将报告中的句子分为观察到的行为或预期行为等类别,有的则只重现崩溃(崩溃重现)。我们发现,要解决这个问题,需要对自然语言和编程语言都有很好的理解,更不用说推理能力了。例如,表2中的错误报告没有明确指定任何代码,但一个精通英语和 Java 的用户能够推断出当如果两个参数都是NaN,则 MathUtils中的“等于”方法应返回false。

一个有希望的解决方案是利用预训练的大型语言模型(LLM)的能力。LLM通常是基于transformer的神经网络,其训练目标是语言建模,即根据前面的上下文预测下一个token。它们的一个主要新颖之处在于,它们可以在没有训练的情况下执行任务:只需通过文本提示要求LLM执行任务,LLM通常就能够实际执行任务。因此,令人好奇的一点是,给定一个报告,LLM可以重现多少错误。另一方面,正如导言中所指出的,能够知道我们何时应该相信并使用LLM结果具有重要的实际意义。为此,我们将重点放在寻找高精度的启发式方法上,尽量减少开发者在使用 LIBRO 时的麻烦。

表2 错误报告示例(Defects4J Math-63)

3 方法

图1是我们的方法概览图。给定一份错误报告,LIBRO首先构建一个提示来查询LLM(图1:(A))。利用该提示,通过多次查询LLM生成一组初始候选测试(图1:(B))。然后,LIBRO对测试进行处理,使其可在目标程序中执行(图1:(C))。随后,LIBRO 会识别和整理可能会重现错误的测试,如果有,则对其进行排序,以尽量减少开发者的检查工作(图1:(D))。本节的其余部分将使用表2中提供的运行示例更详细地解释每个阶段。

图1 LIBRO概述

A. 提示工程

LLM的核心是大型自动完成神经网络:先前的研究发现,要求LLM解决问题的不同方式会导致性能水平的显著差异。找到完成给定任务的最佳查询方式被称为提示工程。

为了让LLM从给定的错误报告中生成测试方法,我们根据错误报告构建了一个 Markdown文档用于提示:请看清单1中的示例,它是根据表2所示的错误报告构建的Markdown文档。LIBRO在Markdown文档中添加了几个独特的部分:命令“提供一个重现此问题的自包含示例”、Markdown中的代码块的开始,(例如 ```),最后是部分代码片段public void test,其作用是诱导LLM编写测试方法。

清单1 没有样例的提示示例

我们对这一基本提示的一系列变化进行了评估。Brown等人报告说,LLM从提示中提供的问答示例中获益匪浅。在我们的例子中,这意味着提供错误报告(问题)和相应的错误重现测试(答案)的示例。考虑到这一点,我们试验了不同数量的示例,以了解增加更多示例,以及来自同一项目或其他项目的示例是否会显著影响性能。

由于对提示格式没有实际限制,我们还尝试为崩溃错误提供堆栈跟踪(以模拟提供堆栈跟踪的情况),或提供故障所在类的构造函数(以模拟报告错误位置的情况)。

我们特定的模板格式使得我们生成的提示不太可能逐字存在于LLM训练数据中。此外,在实践中,大多数报告只是通过引用链与漏洞揭示测试相关联。因此,我们的格式在一定程度上减轻了数据泄漏的担忧,手稿后面还将介绍为限制这种威胁而采取的其他措施。

B. 查询LLM

使用生成的提示符,LIBRO查询LLM以预测将跟随提示符的token。由于提示符的性质,它很可能生成一个测试方法,特别是当我们的提示符以public void test序列结束时。我们接受token直到字符串```第一次出现,确保结果只包含测试方法,表示Markdown代码块的结束。

清单2 表2所述错误报告中的LLM结果示例

众所周知,LLM在进行完全贪婪解码(即严格根据最有可能出现的下一个标记进行解码)时结果较差:它们的表现是在进行加权随机抽样时,测试结果会更好,这种行为受温度参数的调节。根据先前的工作,我们将温度设置为0.7,这样LLM就能根据完全相同的提示生成多个不同的测试。我们采取的方法是生成多个候选重现测试,然后利用它们的特性来确定错误实际被重现的可能性有多大。在清单1的提示下,LLM的输出示例如清单2所示:此时,LLM的输出通常无法单独编译,而需要其他结构(如import语句)。接下来,我们将介绍LIBRO如何将生成的测试整合到现有的测试套件中,使其可执行。

C. 测试后处理

我们首先介绍LIBRO如何将测试方法注入现有套件,然后介绍LIBRO如何解决剩余的未满足的依赖关系。

1) 将测试注入一个合适的测试类:如果开发者在错误报告中发现了一个测试方法,他们很可能会将其插入到一个测试类中,该测试类将为测试方法提供所需的上下文(如所需的依赖关系)。例如针对我们正在运行的示例中的错误 ,开发者在MathUtilsTest类中添加了一个重现测试,其中大部分所需的依赖项都已导入,包括焦点类MathUtils。因此,将LLM生成的测试注入现有的测试类是很自然的,因为这与开发者的工作流程相匹配,同时解决大量最初未满足的依赖关系。

清单3 注入清单2中测试的目标测试类

算法1 测试后处理

为了找到将我们的测试方法注入其中的最佳测试类,我们找到与生成的测试在词法上最相似的测试类(算法1,第1行)。我们的直觉是,如果一个测试方法属于一个测试类,那么该测试方法可能使用类似的方法和类,因此在词法上与该测试类中的其他测试相关。在形式上,我们根据公式1为每个测试类别分配一个匹配分值:

公式1

其中,Tt和Tci分别是生成的测试方法和第i个测试类中的token集。例如,清单3显示了MathUtilsTest类的关键语句。这里,测试类包含与清单2中LLM生成的测试(特别是第4行和第6行)使用的方法调用和常量相似的方法调用和常量。

作为完整性检查,我们从Defects4J基准的数学和语言项目中注入了由开发者添加的真实错误重现测试,并根据算法1检查它们是否正常执行。我们发现在89%的时间里,测试都能正常执行,这表明该算法能合理地找到可执行测试的环境。

1) 解决剩余的依赖关系:尽管通过将测试放在正确的类中解决了许多依赖关系问题,但是测试可能会引入需要导入的新结构。为了处理这些情况,LIBRO会启发式地推断要导入的包。

算法1中的第2行至第10行描述了LIBRO的依赖关系解析过程。首先,LIBRO对生成的测试方法进行解析,并识别变量类型和引用的类名/构造函数/异常。然后,LIBRO通过词法匹配测试类中现有的导入语句,过滤已导入的类名(第3行)。

在这个过程中,我们会发现在测试类中没有解析的类型。LIBRO首先会尝试查找具有该类型标识名称的公共类;如果正好有一个这样的文件,则会导出标识类的类路径(第7行),并添加导入语句(第11行)。但是,可能不存在匹配的类,也可能存在多个匹配的类。在这两种情况下,LIBRO都会查找项目中以目标类名称结尾的导入语句(例如,在搜索MathUtils时,LIBRO查找import.*MathUtils;)。LIBRO会在所有项目中选择最常见的导入语句。此外,我们还添加了一些规则,允许正确导入断言语句,即使项目本身没有适当的导入。

我们的后处理管道并不能保证在所有情况下都能编译,但LIBRO使用的启发式方法能够解决原始测试方法中大部分未处理的依赖关系。在完成后处理步骤后,LIBRO会执行测试,以确定可重现错误的候选测试。

D. 选择和排名

当且仅当测试因报告中指定的错误而失败时,该测试才是错误重现测试(BRT)。由 LIBRO生成的测试成为Bug Reproductive Test的一个必要条件是,该测试在有Bug的程序中编译失败:我们称此类测试为FIB测试。然而,并非所有的FIB测试都是BRT,因此很难判断错误重现是否成功。这是我们与崩溃重现工作不同的一个因素,因为崩溃重现技术可以通过比较崩溃时的堆栈跟踪来确认错误是否已被重现。另一方面,向开发者展示所有生成的FIB测试是不明智的,因为要求开发者重复多个解决方案通常是不可取的。因此,LIBRO尝试使用我们观察到的与成功重现错误相关的几种模式,来决定何时建议进行测试,以及如果建议进行测试,建议进行哪种测试。

算法2概述了LIBRO如何决定是否显示结果,以及如果显示结果,如何对生成的测试进行排序。在第1行至第10行中,LIBRO首先决定是否向开发者显示任何结果(选择)。我们将显示相同故障输出(相同的错误类型和错误信息)的FIB测试分组,并查看同组测试的数量(第8行中的max_output_clus_size)。这样做是基于一种直觉,即如果多个测试显示出类似的故障行为,那么LLM很可能对其预测很有信心,因为其独立预测彼此一致,而且很有可能错误重现已经成功。可以对LIBRO进行配置,使其仅在输出结果存在显著一致性时显示结果(将一致性阈值Thr设置为高),或显示更具探索性的结果(将Thr设置为低)。

算法2 测试选择和排名

一旦决定显示其结果,LIBRO将依靠三种启发式方法对生成的测试进行排序,排序顺序为判别强度递增。首先,如果失败消息和/或测试代码显示了错误报告中观察到和提到的行为(异常或输出值),则测试很可能是错误重现。虽然这种启发式是精确的,但它的判定并不具有很强的鉴别力,因为测试只能分为“包含”和“不包含”两组。接下来,我们通过观察输出集群大小(output_clus_size)来了解生成测试之间的“一致性”,它代表了LLM各代的“共识”。最后,LIBRO根据测试长度(因为较短的测试更容易理解)确定优先级,这是最细粒度的信号。我们首先只留下语法上唯一的测试(第11行),然后使用上述启发式方法对输出集群和集群中的测试进行排序(第16行和第18行)。

由于具有相同故障输出的测试彼此相似,我们预计,如果一个群组中的一个测试不是BRT,那么同一群组中的其他测试也可能不是BRT。因此,LIBRO显示了来自不同群组的测试。在第19行至第22行中的每一次迭代中,每个群组中排名第i的测试将被选中并添加到列表中。

4 研究问题

我们的目标是回答以下研究问题。

A. RQ1:效果

在RQ1中,我们试图使用Defects4J基准来定量地评估LIBRO的性能。

• RQ1-1:LIBRO能生成多少错误重现测试?我们评估了在不同的提示设置下,LIBRO总共重现了多少错误。

• RQ1-2:LIBRO与其他技术相比如何?在缺乏通用的报告到测试技术的情况下,我们与碰撞再现技术EvoCrash进行了比较。我们还与“复制&粘贴”技术进行了比较。直接使用错误报告中的代码片段(使用HTML <pre> 标记或通过 infoZilla)作为测试的基线。对于可解析为Java在编译单元中,我们将代码添加为测试类,并在必要时添加JUnit导入,以便作为测试运行。否则,我们会将代码段封装到一个测试方法中,并在与LIBRO相同的条件下对其进行评估。

B. RQ2:效率

在RQ2中,我们根据使用的资源量来检查LIBRO的效率,以提供在现实环境中部署LIBRO的成本估计。

• RQ2-1:需要多少次Codex查询?我们根据生成的测试库,估算在Defects4J数据集上要达到一定的错误重现率需要多少次查询。

• RQ2-2:LIBRO需要多少时间?我们的技术包括查询LLM、使其可执行以及排序:我们测量每个阶段所需的时间。

• RQ2-3:开发者应该检查多少个测试?我们将评估有多少错误可以在1、3和5次建议内重现,以及开发者需要“浪费多少精力”。

C. RQ3:实用性

最后,在问题3中,我们旨在通过将LIBRO应用于GHRB数据集来研究其通用性如何。

• RQ3-1:LIBRO在实际场景下重现错误的频率如何?为了减少数据泄露问题,我们在 GHRB数据集上对LIBRO进行了评估,检查在该数据集上可以重现多少个错误。

• RQ3-2:LIBRO的选择和排序技术的可靠性如何?我们研究了在Defects4J数据集中选择错误和对测试进行排序时所使用的因素是否仍然适用于GHRB数据集,从而是否可用于其他一般项目。

• RQ3-3:重现的成功和失败是什么样的?为了给我们的研究结果提供定性背景,我们描述了GHRB数据集中错误重现成功和失败的例子。

5 实验结果

A. RQ1:LIBRO的效果如何?

1) RQ1-1:表3显示了哪些提示/信息设置效果最佳,其中n = N表示我们查询了N次 LLM以重现测试。在使用源项目中的示例时,我们使用该项目中可用的最古老的测试;否则,我们在所有项目中都使用两个精心挑选的报告-测试对(Time-24、Lang-1)。我们发现,提供构造函数(类似AthenaTest)并无明显帮助,但添加堆栈跟踪却有助于重现崩溃错误,这表明LIBRO可以通过使用堆栈信息更准确地重现问题。有趣的是,添加项目内示例的性能较差:对这些示例的检查发现,在这种情况下,LIBRO简单地复现了所提供的示例,即使它不应该这样做,从而导致性能降低。我们还发现,示例的数量也有显著差异(从默认设置的n=50结果中抽取了两个示例的n=10值),这证实了现有的结论,即增加示例有助于提高性能。反过来,示例的数量似乎比LLM的查询次数更重要,这一点我们将在RQ2-1中进一步探讨。由于两个示例n=50的设置显示了最佳性能,我们在本文的其余部分将其作为默认设置。

表3 不同提示个数的复现性能

在两个示例n=50的设置下,我们发现LIBRO共重现了251个错误,占所研究的750个 Defects4J错误的33.5%。表4列出了每个项目的性能明细。虽然每个项目都至少有一个错误被重现,但重现错误的比例会有很大差异。例如,LIBRO在Closure项目中复现了少量错误,而众所周知,该项目具有独特的测试结构。另一方面,Lang或Jsoup项目的性能更强,因为这些项目的测试一般都是独立且简单的。此外,我们发现生成的测试体平均长度约为6.5 行(不包括注释和空白),这表明LIBRO能够编写有意义的长测试。

表4 Defects4J中每个项目的错误重现率:x/y表示y个错误中重现了x个错误

RQ1-1的回答:可以自动复现大量(251个)错误,并在不同的项目组中复现错误。此外,提示中的示例数量和生成尝试次数对性能也有很大影响。

2) RQ1-2:我们进一步将LIBRO与最先进的崩溃重现技术EvoCrash和使用错误报告中代码片段的“复制&粘贴基线”进行了比较。比较结果见图2。我们发现,与其他基线相比,LIBRO复现了一大批不同的错误。与EvoCrash相比,LIBRO多复现了91个独特的错误(其中19个是崩溃错误),这表明LIBRO可以复现以前的工作无法处理的非崩溃错误(图2(b))。另一方面,“复制&粘贴”基线表明,虽然BRT有时会包含在错误报告中,但从报告到测试的任务并不轻松。有趣的是,“复制&粘贴”基线重现的八个错误没有被LIBRO重现;我们发现这是由于测试长度超过了LIBRO的生成长度,或者是由于依赖于复杂的辅助函数。

图2 错误复现能力基线比较

RQ1-2的回答:与之前的工作相比,LIBRO能够复现一大批不同的错误。

B. RQ2:LIBRO的效率如何?

1) RQ2-1:在此,我们研究必须生成多少测试才能达到一定的错误再现性能。为此,对于每个Defects4J错误,我们从默认设置下生成的50个测试中随机抽取x个测试,使每个错误的测试数减少。然后,我们检查仅使用这些抽样测试时的错误再现数量y。我们重复这一过程1000次,以接近分布情况。

结果如图3所示。请注意,X轴为对数刻度。有趣的是,我们发现测试生成尝试次数与错误重现性能中位数之间存在对数关系。这表明,通过简单地生成更多测试来复现更多错误变得越来越困难,但仍然是可能的。由于该图没有显示高原迹象,因此使用更多的测试样本进行试验可能会获得更好的错误再现结果。

图3 生成尝试次数与性能 左图描述随着尝试次数的增加而重现的错误,右图描述了随着尝试次数的增加而重现的FIB

RQ2-1的回答:重现的错误数量与生成的测试数量成对数增长,没有性能趋于平稳的迹象。

2) RQ2-2:我们在表5中报告了执行管道每个步骤所需的时间。我们发现应用程序接口查询耗时最长,约需5.85秒。

后处理和测试执行(测试执行时)分别需要1.23秒和4秒。总体而言,LIBRO生成50个测试并对其进行处理平均耗时444秒,完全符合基于搜索的技术通常使用的10分钟搜索预算。

表5 LIBRO管道所需的时间

RQ2-2的回答:我们的时间测量结果表明,使用LIBRO所需的时间并不比其他方法长很多。

1) RQ2-3:在这一研究问题中,我们将衡量LIBRO通过其选择和排序程序对错误重现测试进行优先排序的有效性。由于LIBRO只显示超过一定协议阈值(Thr)的结果,因此我们首先在图4中展示了错误重现总数与精确度(即在LIBRO选择的所有错误中成功重现的比例)之间的权衡。随着阈值的增加,更多的建议(包括BRT)被丢弃,但精确度却越来越高,这表明我们可以通过调整选择阈值来平滑地提高精确度。

图4 错误选择的ROC曲线(左),阈值对错误选择数量和精确度的影响(右)

为了尽可能多地保留重现的错误,我们特别将一致性阈值设定为1,这是一个保守的值。在570个有FIB的错误中,有350个被选中。在这350个错误中,有219个被重现(精确度为0.63,而召回率(即被选中的重现错误在所有重现错误中所占比例)为0.87)。从相反的角度看,选择过程过滤掉了188个未被重现的错误,而只丢弃了几个成功重现的错误。请注意,如果我们将阈值设为10,即一个更激进的值,我们就能以0.42的召回率获得0.84的更高精度。无论如何,如图4所示,我们的选择技术明显优于随机技术,这表明它可以节省开发者的资源。

在选定的错误中,我们评估了LIBRO的测试排名相对于随机基线的有效性。随机方法是对生成测试的语法集群(语法等同的FIB测试组)进行随机排名。我们运行随机基线100次,取平均值。

表6列出了排名评估结果。在Defects4J基准上,LIBRO的排序技术在所有acc@n指标上都比随机基线有所改进,在n = 1、3和5时分别比随机基线多出30、14和7个BRT。关于acc@1,第一列显示,在LIBRO生成的排名靠前的测试中,有43%在第一次尝试时就成功重现了原始错误报告。当n增加到5时,57%的选定错误或80%的重现错误都能找到BRT。这里选择的保守阈值强调的是召回率而不是精确率。但是,如果提高阈值,最高精确度可以提高到0.8(Thr=10,n=5)。

wef@nagg值是通过对所有(350个)被选错误的wef@n值进行加总和平均后得出的。总和wef@n值表示在排名前n位的测试中需要人工检查的非BRT的总数。wef@n值越小,表明某种技术能提供更多的错误重现测试。总体而言,与随机基线相比,LIBRO的排序即使在错误被选中之后,也能节省多达14.5%的精力浪费。基于这些结果,我们得出结论:LIBRO可以减少浪费的检查工作,从而有助于协助开发者。

表6 LIBRO与随机基线的排序性能比较

RQ2-3的回答:LIBRO可以减少错误和必须检查的测试的数量:33%的错误被安全地丢弃,同时保留了87%的成功错误再现。在选定的错误集中,80%的错误重现可在5次检查中找到。

C. RQ2:LIBRO的实际效果如何?

1) RQ3-1:我们探讨了LIBRO在GHRB数据集(包含最近的错误报告)上运行时的性能。我们发现,在我们研究的31份错误报告中,LIBRO可以根据50次测试为10个错误自动生成错误重现测试,成功率为32.2%。这一成功率与RQ1-1中介绍的Defects4J的结果相似,表明LIBRO能够通用于新的错误报告。表7提供了按项目分列的结果。在AssertJ、Jsoup、Gson和sslcontext中,错误被成功重现,而在其他两个项目中,错误未被重现。尽管Checkstyle 项目存在大量Bug,但我们仍无法重现这些Bug;经过检查,我们发现这是因为该项目测试严重依赖外部文件,而LIBRO无法访问这些文件。LIBRO也没有为Jackson项目生成BRT,但Jackson项目的错误数量较少,因此很难从中得出结论。

表7 GHRB 中的错误复现:x/y表示y个错误中复现出x个错误

RQ3-1的回答:即使是最近的数据,LIBRO也能生成错误重现测试,这表明它并不是简单地记住训练时的数据。

1) RQ3-2:LIBRO使用与成功复现错误相关的几个预测因素来选择错误和对测试进行排序。在这一研究问题中,我们将检查基于Defects4J数据集所发现的模式在最近的GHRB数据集中是否仍然有效。

回想一下,我们使用最大输出集群大小来衡量FIB之间的一致性,并以此作为选择标准来确定错误是否已被复现。为了观察该标准是否是预测错误复现成功与否的可靠指标,我们观察了两个数据集(有BRT和没有BRT)之间的最大输出群集大小(max_output_clus_size)的变化趋势。在图5中,我们可以看到没有BRT的错误的max_output_clus_size通常较小,大多在10以下;这一模式在两个数据集中是一致的。

图5 复现和未复现错误的max_output_clus_size值分布情况

表6还列出了GHRB的排序结果。这些结果与Defects4J的结果一致,表明我们的排序策略所使用的特征仍然是成功复现错误的良好指标。

RQ3-2的回答:我们发现,在实际数据中,用于排序和选择LIBRO的因素能够持续预测错误的再现。

1) RQ3-3:我们介绍了LIBRO尝试复现错误的案例研究,这些案例或成功或失败。

表8 错误报告已成功重现:为简洁起见省略了URL(AssertJ-Core问题#2666)

我们首先在表8中列出了一份成功复现的错误报告,即AssertJ-Core项目的#2685问题。错误在于,在 tr_TR中,assertContainsIgnoringCase方法错误地处理了字母I。对于这个错误报告,LIBRO生成了清单4中的错误重现测试。尽管错误报告不包含任何可执行代码,但LIBRO成功生成了一个错误重现测试,使用报告中引用的containsIgnoringCase 方法比较了I和i。该错误的BRT排在第二位,表明开发者很快就能获得重现测试。

清单4 为AssertJ-Core-2666生成的FIB测试

表9 错误报告复现失败:为了清晰起见,稍微编辑(Checkstyle Issue #11365)

现在我们转向一个无法成功复制的错误报告。表9包含Checkstyle项目中的问题#11365。错误在于CheckStyle错误地决定一个类应该被声明为final,并错误地引发一个错误。清单5给出了LIBRO生成的FIB测试,由于它在第5行中引用的Java文件不存在,因此测试失败。这突出了LIBRO的一个弱点,即它无法在源代码之外为生成的测试创建工作环境。然而,如果我们将报告中的test.java的内容放入引用的文件中,测试成功地再现了错误,这表明测试本身是功能性的,并且即使测试最初是不正确的,它也可以减少开发者用于编写再现测试的工作量。

清单5 为Checkstyle-11365生成的FIB测试

转述:叶豪

0 阅读:0

互联不一般哥

简介:感谢大家的关注