8

我目前正在研究一种使用sablecc构建的编译器。

长话短说,编译器会将规范文件(这是我们正在解析的)和 .class 文件作为输入,并将检测 .class 文件的字节码,以确保在运行 .class 文件时,任何规范没有被违反(这有点像 jml/code 合同!但更强大)。

我们有几十个系统测试,涵盖了分析阶段的大部分(与确保规范有意义相关,并且它们也与它们应该指定的 .class 文件一致)。

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

  • 有效的测试由源代码文件组成,当我们的编译器编译时应该不会弹出编译器错误/警告。

  • 无效测试由源代码文件组成,当我们的编译器编译时,它们应该会弹出至少一个编译器错误/警告。

当我们处于分析阶段时,这对我们很有帮助。现在的问题是如何测试代码生成阶段。过去,我已经通过我在编译器课程中开发的一个小编译器完成了系统测试。每个测试将包含该语言的几个源文件和一个output.txt. 运行测试时,我会编译源文件,然后运行它的 main 方法,检查输出结果是否等于output.txt. 当然,所有这些都是自动化的。

现在,处理这个更大的编译器/字节码工具,事情就不是那么容易了。复制我用我的简单编译器所做的工作并非易事。我想要走的路是在这个阶段从系统测试中恢复过来,专注于单元测试。


任何编译器开发人员都知道,编译器由许多访问者组成。我不太确定如何对它们进行单元测试。据我所见,大多数访问者都在调用具有与该访问者相关的方法的对应类(我想这个想法是为访问者保留 SRP)。

我可以采用几种技术对编译器进行单元测试:

  1. 分别对每个访问者的方法进行单元测试。对于无堆栈的访问者来说,这似乎是一个好主意,但对于使用一个(或多个)堆栈的访问者来说,这似乎是一个糟糕的主意。然后,我还以传统方式对标准(读取,非访问者)类中的每个其他方法进行单元测试。

  2. 一口气对整个访问者进行单元测试。也就是说,我创建了然后访问的树。最后,我验证符号表是否正确更新。我不关心嘲笑它的依赖。

  3. 与 2) 相同,但现在模拟访问者的依赖项。

  4. 还有什么?

我仍然有一个问题,即单元测试将与 sabbleCC 的 AST 紧密耦合(这真的很难看)。


我们目前没有进行任何新的测试,但我想让火车重回正轨,因为我确信不测试系统就像喂一只迟早会回来咬我们的怪物一样当我们最不期待的时候对接;-(

有没有人有任何编译器测试的经验,可以就现在如何进行提供一些令人惊叹的建议?我有点迷路了!

4

1 回答 1

7

我参与了一个使用 Eclipse 编译器将 Java AST 翻译成另一种语言 OpenCL 的项目,并且遇到了类似的问题。

我对你没有神奇的解决方案,但如果有帮助,我会分享我的经验。

您使用预期输出(使用 output.txt)进行测试的技术也是我开始的方式,但它成为测试的绝对维护噩梦。当我出于某种原因(发生了几次)不得不更改生成器或输出时,我不得不重写所有预期的输出文件——而且它们的数量很大。我开始根本不想更改输出,因为害怕破坏所有测试(这很糟糕),但最后我放弃了它们,而是对生成的 AST 进行了测试。这意味着我可以“松散地”测试输出。例如,如果我想测试 if 语句的生成,我可以在生成的类中找到唯一的 if 语句(我编写了辅助方法来完成所有这些常见的 AST 工作),验证一些关于它的事情,然后做完了。那个测试不会 不在乎类是如何命名的,或者是否有额外的注释或注释。由于测试更加集中,这最终工作得很好。缺点是测试与代码的耦合更紧密,所以如果我想删除 Eclipse 编译器/AST 库并使用其他东西,我需要重写我的所有测试。最后,因为代码生成会随着时间而改变,我愿意付出这个代价。

我还严重依赖集成测试 - 以目标语言实际编译和运行生成代码的测试。与单元测试相比,我拥有更多这些类型的测试,纯粹是因为它们似乎更有用并能发现更多问题。

至于访问者测试,我再次使用它们进行更多集成式测试 - 获取一个非常小的/特定的 Java 源文件,用 Eclipse 编译器加载它,用它运行我的一个访问者并检查结果。在不调用 Eclipse 编译器的情况下进行测试的唯一另一种方法是模拟整个 AST,这是不可行的——大多数访问者都是不平凡的,并且需要一个完全构造/有效的 Java AST,因为他们会从主类中读取注释. 大多数访问者都可以通过这种方式进行测试,因为他们要么生成小的 OpenCL 代码片段,要么构建了单元测试可以验证的数据结构。

是的,我所有的测试都与 Eclipse 编译器紧密耦合。但我们正在编写的实际软件也是如此。使用其他任何东西都意味着无论如何我们都必须重写整个程序,所以我们很乐意为此付出代价。我想没有一种解决方案——你需要权衡紧耦合的成本与测试的可维护性/简单性。

我们也有相当多的测试实用程序代码,例如使用默认设置设置 Eclipse 编译器,提取方法树的主体节点的代码等。我们尽量保持测试尽可能小(我知道这是可能是常识,但可能值得一提)。


(下面对评论的编辑/添加 - 比评论回复更容易阅读/格式化)

“我还严重依赖集成测试——实际编译和运行目标语言生成代码的测试” 这些测试实际上做了什么?它们与 output.txt 测试有何不同?

(再次编辑:重新阅读问题后,我意识到我们的方法是相同的,所以忽略这个)

集成测试不仅仅是生成源代码并将其与我最初所做的预期输出进行比较,而是生成 OpenCL 代码、编译并运行它。所有生成的代码都会产生输出,然后比较该输出。

例如,我有一个 Java 类,如果生成器正常工作,它应该生成 OpenCL 代码,将两个缓冲区中的值相加,然后将值放入第三个缓冲区。最初,我会用预期的 OpenCL 代码编写一个文本文件,并在我的测试中进行比较。现在,集成测试生成代码,通过 OpenCL 编译器运行它,运行它,然后测试检查值。

“至于访问者测试,我再次与他们进行更多的集成式测试——获取一个非常小的/特定的 Java 源文件,用 Eclipse 编译器加载它,用它运行我的一个访问者并检查结果。”你的意思是运行与您的一位访问者一起,还是将所有访问者运行到您要测试的访问者?

大多数访问者可以彼此独立运行。在可能的情况下,我将只与我正在测试的访问者一起运行,或者如果依赖于其他访问者,则需要最少的访问者集(通常只需要另一个访问者)。访问者不直接相互交谈,而是使用传递的上下文对象。这些可以在测试中人为地构建,以使事物进入已知状态。

另一个问题,你在这个项目中使用模拟吗?此外,您是否经常在其他项目中使用模拟?我只是想清楚地了解我正在与之交谈的人:P

在这个项目中,我们在大约 5% 的测试中使用了模拟,甚至可能更少。而且我不会模拟任何 Eclipse 编译器的东西。

模拟的问题是我需要了解我正在模拟的内容,而 Eclipse 编译器并非如此。有很多访问者方法被调用,有时我不确定应该调用哪一个(例如,访问 ExtendedStringLiteral 或访问 StringLiteral 调用字符串文字?)如果我确实模拟了这一点并假设其中一个,这可能与现实不符,即使测试通过,程序也会失败 - 这是不希望的。我们做的唯一模拟是一对注解处理器 API、一对 Eclipse 编译器适配器和一些我们自己的核心类。

其他项目,例如 Java EE 的东西,使用了更多的模拟,但我仍然不是它们的狂热用户。API 越定义、理解和可预测,我就越有可能考虑使用模拟。

我们程序的第一阶段就像一个普通的编译器。我们从源文件中提取信息并填写一个(大而复杂的!)符号表。您将如何进行系统测试?理论上,我可以使用源文件以及包含有关 symbolTable 的所有信息的 symbolTable.txt(或 .xml 或其他)创建一个测试,但我认为这样做会有点复杂。这些集成测试中的每一个都将是一件复杂的事情!

我会尝试采用测试符号表的一小部分而不是一次性测试全部的方法。如果我正在测试是否正确构建了 Java 树,我会有类似的东西:

  • 一项仅针对 if 语句的测试:

    • 具有包含一个 if 语句的一种方法的源代码
    • 从此源构建符号表/树
    • 从主类中仅从方法体中提取语句树(如果 >1 或没有方法体、找到类、方法体中的顶级语句节点,则测试失败)
    • 以编程方式比较 if 语句的节点属性(条件、正文)
  • 至少以相似的风格测试每种其他类型的陈述。

  • 其他测试,可能用于多个语句等或任何需要的测试

这种方法是集成式测试,但每个集成测试只测试系统的一小部分。

基本上我会尽量保持测试尽可能小。许多用于提取树的部分的测试代码可以移动到实用程序方法中,以保持测试类较小。

我想也许我可以创建一个漂亮的打印机来处理符号表并输出相应的源文件(如果一切正常,就像原始源文件一样)。问题是原始文件的顺序可能与我漂亮的打印机打印的顺序不同。恐怕用这种方法我可能只是打开另一罐蠕虫。我一直在无情地重构部分代码,并且错误开始显露出来。我真的需要一些集成测试来让我走上正轨。

这正是我所采取的方法。但是在我的系统中,东西的顺序并没有太大变化。我有生成器,它们基本上输出代码以响应 Java AST 节点,但是生成器可以递归地调用自己有一点自由。例如,响应 Java If 语句 AST 节点触发的 'if' 生成器可以写出 'if (',然后要求其他生成器呈现条件,然后写 ') {',要求其他生成器编写出正文,然后写'}'。

于 2011-08-01T05:04:14.933 回答