测试编译器

8

我正在开发一种编译器,使用 sablecc 构建。

简而言之,该编译器将输入规范文件(这是我们正在解析的内容)和 .class 文件,并将对 .class 文件的字节码进行处理,以确保在运行 .class 文件时不违反任何规范(这有点像 jml/code contracts!但更加强大)。

我们有几十个系统测试,涵盖了分析阶段的大部分内容(与确保规范有意义以及它们与它们所指定的 .class 文件相符相关)。

我们将它们分为两组:有效测试和无效测试。

  • 有效测试包括源代码文件,当通过我们的编译器编译时,不应出现编译器错误/警告。

  • 无效测试包括源代码文件,当通过我们的编译器编译时,至少应出现一个编译器错误/警告。

这在分析阶段为我们服务得很好。现在的问题是如何测试代码生成阶段。过去,我曾在编译器课程上开发了一个小型编译器,并进行了系统测试。每个测试由几个该语言的源文件和一个 output.txt 组成。运行测试时,我会编译源文件,然后运行其主方法,检查输出结果是否等于 output.txt。当然,所有这些都是自动化的。

现在,处理这个更大的编译器/字节码工具并不容易。要复制我用简单编译器所做的事情并不容易。我想现在的方法是退回到单元测试,而不是系统测试。


任何编译器开发者都知道,编译器由许多访问者组成。我不太确定如何进行单元测试。从我所看到的情况来看,大多数访问者都在调用一个具有与该访问者相关的方法的对应类(我猜想这个想法是为了保持访问者的SRP)。
有几种技术可以用于单元测试编译器:
1. 单独测试每个访问者方法。这似乎是堆栈式访问者的好主意,但对于使用一个或多个堆栈的访问者来说,看起来像是个可怕的主意。然后我也按传统方式单元测试每个其他标准(非访问者)类的方法。 2. 一次性单元测试整个访问者。也就是说,我创建一个树,然后访问它。最后,我验证符号表是否正确更新。我不关心模拟其依赖项。 3. 与2)相同,但现在模拟访问者的依赖项。 4. 还有其他方法吗?
我仍然有一个问题,那就是单元测试将与sabbleCC的AST(实际上非常丑陋)紧密耦合。
我们目前不再进行新的测试,但我希望将火车重新拉回正轨,因为我相信不测试系统就像喂养一个怪物,迟早会在我们最不希望的时候咬我们的屁股;-(
有没有人有编译器测试方面的经验,能给一些非常棒的建议?我有点迷失了!
1个回答

6
我参与了一个项目,其中使用Eclipse编译器将Java AST翻译成另一种语言OpenCL,并且遇到了类似的问题。
我没有什么神奇的解决方案,但我会分享我的经验以帮助你。您用预期输出(带output.txt的)进行测试的技术也是我最开始采用的方法,但对于测试来说这变成了维护的噩梦。当我不得不出于某些原因更改生成器或输出时(这种情况发生了几次),我必须重新编写所有期望的输出文件,而且它们数量巨大。我开始不想改变输出,因为担心打破所有测试(这很糟糕),但最终我放弃了它们,转而对生成的AST进行测试。这意味着我可以'松散地'测试输出。例如,如果我想测试if语句的生成,我只需在生成的类中找到唯一的if语句(我编写了helper方法来完成所有常见的AST操作),验证其关键信息,然后就完成了。这个测试不关心类的名称是否正确,是否有额外的注释等。这种方式最终效果还不错,因为测试更加专注。缺点是测试更紧密地耦合到代码中,因此如果我要摆脱Eclipse编译器/AST库并使用其他东西,我需要重新编写所有测试。最终由于代码生成会随着时间的推移而改变,我愿意支付这个代价。
我还大量依赖集成测试,即对目标语言中生成的代码进行实际编译和运行的测试。我有更多这些类型的测试,纯粹是因为它们似乎更有用,并且可以捕获更多的问题。
至于访问者测试,我同样使用它们进行集成式测试 - 获取一个非常小/特定的Java源文件,在Eclipse编译器中加载它,运行我的访问者之一,并检查结果。没有调用Eclipse编译器的另一种测试方法是模拟出整个AST,但这是不可行的 - 大多数访问者都非常复杂,并需要完全构造/有效的Java AST,因为它们将从主类读取注释。大多数访问者都可以通过这种方式进行测试,因为它们要么生成小的OpenCL代码片段,要么构建数据结构,这些单元测试可以验证。
是的,我的所有测试都与Eclipse编译器非常紧密地耦合在一起。但我们正在编写的实际软件也是如此。使用其他任何东西都意味着我们必须重新编写整个程序,因此这是我们非常乐意承担的代价。我想没有一个通用的解决方案 - 您需要权衡紧密耦合与测试可维护性/简单性的成本。
我们还有相当多的测试实用程序代码,例如使用默认设置设置Eclipse编译器,提取方法树的主体节点的代码等。我们尽可能保持测试的小型化(我知道这可能是常识,但也许值得一提)。
“我也非常依赖于集成测试-实际编译并在目标语言中运行生成的代码的测试。”这些测试实际上是做什么的?它们与output.txt测试有何不同?
与其只生成源代码并将其与预期输出进行比较,现在的集成测试会生成OpenCL代码,编译并运行它。所有生成的代码都会产生输出,然后将该输出进行比较。
例如,我有一个Java类,如果生成器正常工作,则应该生成OpenCL代码,对两个缓冲区中的值求和并将该值放入第三个缓冲区。最初,我会编写一个文本文件,其中包含预期的OpenCL代码,并在我的测试中进行比较。现在,集成测试会生成代码,通过OpenCL编译器运行它,运行它,然后测试会检查值。
“至于访问者测试,我再次使用它们进行更多的集成式测试-获取一个非常小/具体的Java源文件,在Eclipse编译器中加载它,使用其中之一的访问者运行它并检查结果。”你是指运行其中一个访问者,还是运行到你想要测试的访问者?
大多数访问者可以独立于彼此运行。在可能的情况下,我会仅运行正在测试的访问者,或者如果存在其他依赖项,则需要最少的访问者集(通常只需要另一个)。访问者不直接相互通信,而是使用传递的上下文对象。这些可以在测试中人为构造,以将事物置于已知状态。
其他问题,你是否使用mocks-在此项目中使用吗?此外,您是否经常在其他项目中使用mocks?我只是试图了解我正在交谈的人:P
在此项目中,我们在大约5%的测试中使用mocks,可能甚至更少。而且我不会模拟任何Eclipse编译器内容。
关于mocks的问题在于,我需要充分理解我正在模拟的内容,而这并不适用于Eclipse编译器。有很多访问者方法被调用,有时候我不确定应该调用哪一个(例如,对于字符串文字,visit ExtendedStringLiteral还是visit StringLiteral被调用?)如果我模拟出来并假定其中一个,这可能不符合现实,即使测试通过,程序也会失败-这是不希望发生的。我们唯一使用的mock是注释处理器API的几个,几个Eclipse编译器适配器以及我们自己的核心类。
在其他项目中,例如Java EE等项目中,使用了更多的mocks,但我仍然不是它们的狂热用户。API越明确,可预测性越高,我越可能考虑使用mocks。
我们的程序的前几个阶段就像普通编译器一样。我们从源文件中提取信息并填充一个(庞大而复杂的!)符号表。你会如何进行系统测试呢?理论上,我可以创建一个包含有关符号表所有信息的源文件和symbolTable.txt(或.xml或其他文件),但我认为这可能有点复杂。每个集成测试都将是一项复杂的任务!
我会尝试测试符号表的小部分而不是一次性测试整个符号表。如果我正在测试Java树是否正确构建,我会有以下内容:
- 只针对if语句的一个测试: - 源代码包含一个包含一个if语句的方法 - 从此源构建符号表/树 - 仅从主类的方法体中提取语句树(如果存在多个或没有方法体、类、方法体中的顶级语句节点,则测试失败) - 以程序方式比较if语句的节点属性(条件、主体) - 至少为每种其他语句设计一个类似的测试风格的测试。 - 其他测试,也许是针对多个语句等所需的
这种方法是集成式测试,但每个集成测试只测试系统的一小部分。
实质上,我会尽量保持测试尽可能小。用于提取树的部分位于实用方法中的测试代码可以移动,以使测试类保持小巧。
这正是我所采取的方法。但是在我的系统中,事物的顺序变化不大。我有生成器,基本上是响应Java AST节点输出代码,但是生成器可以递归调用自身。例如,响应Java If语句AST节点触发的“if”生成器可以写出“if(”,然后要求其他生成器呈现条件,然后写入“){”,要求其他生成器写出主体,然后写入“}”。

关于访问者测试,我通常会使用更多的集成式测试。我会选取一个非常小且具体的Java源文件,使用Eclipse编译器进行加载,然后运行其中一个访问者并检查结果。你是指只运行其中一个访问者,还是运行所有访问者直到你想要测试的那个为止? - devoured elysium
谢谢。我认为我们工作中最显著的区别是,我的所有访问者必须按顺序调用。只有在其他X-1被运行后,才需要运行访问者X。 - devoured elysium
与其像我最初所做的那样仅生成源代码并将其与预期输出进行比较,集成测试会生成OpenCL代码,编译并运行它。所有生成的代码都会产生输出,然后将该输出进行比较。我仍然不明白。这种方法似乎与我的output.txt方法完全相同。例如:具有内容“printf hello world”的source.src将被编译,然后运行。我将获得一个包含“hello world”的output.txt,并将main函数的输出与output.txt的内容进行比较。 - devoured elysium
@devoured elysium 对不起,我完全误解了你。现在我重新阅读你的问题,意识到我们的方法是一样的。 - prunge
集成测试方法本身应该是简单的,也就是说,我不想创建一个复杂的东西,否则我将开始有两个问题要解决,而不是一个。 - devoured elysium
显示剩余7条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接