dev*_*ium 8 java compiler-construction testing unit-testing sablecc
我目前正在研究使用sablecc构建的编译器.
简而言之,编译器将把规范文件(这是我们正在解析的)和.class文件作为输入,并将检测.class文件字节码,以确保在运行.class文件时,任何规范没有被违反(这有点像jml /代码合同!但更强大的方式).
我们有几十个系统测试,涵盖了分析阶段的大部分内容(与确保规范有意义相关,并且它们也与它们应该指定的.class文件一致).
我们将它们分为两组:有效测试和无效测试.
有效的测试由源代码文件组成,当我们的编译器编译时,应该不会弹出编译器错误/警告.
无效测试由源代码文件组成,当我们的编译器编译时,应该弹出至少一个编译器错误/警告.
在我们处于分析阶段时,这对我们很有帮助.现在的问题是如何测试代码生成阶段.在过去,我已经完成了对我在编译器课程上开发的一个小编译器的系统测试.每个测试都包含一些该语言的源文件和一个output.txt.运行测试时,我将编译源文件,然后运行其main方法,检查输出结果是否等于output.txt.当然,所有这些都是自动化的.
现在,处理这个更大的编译器/字节码 - 仪器,事情并不那么容易.复制我用简单编译器完成的工作并不容易.我想要走的路是在这个阶段从系统测试中退缩,并专注于单元测试.
正如任何编译器开发人员所知,编译器包含大量访问者.我不太确定如何进行单元测试.从我所看到的情况来看,大多数访问者都在调用一个与该访问者相关的方法的对应类(我想这个想法是为访问者保留SRP).
我可以采用几种技术对我的编译器进行单元测试:
单独测试每个访问者的方法.这对于无堆栈访问者来说似乎是一个好主意,但对于使用一个(或多个)堆栈的访问者来说,这看起来是个糟糕的主意.然后,我将以传统方式对标准(读取,非访问者)类中的每个其他方法进行单元测试.
一次性测试整个访客.也就是说,我创建了我随后访问的树.最后,我验证符号表是否正确更新.我不关心嘲笑它的依赖关系.
与2)相同,但现在嘲笑访问者的依赖关系.
别的什么?
我仍然有问题,单元测试将与sabbleCC的AST(这真的很难看)非常紧密地结合在一起.
我们目前没有进行任何新的测试,但是我想把火车带回正轨,因为我确信没有测试系统就像喂养怪物一样,迟早会回来咬我们的.当我们最不期望的时候对接;-(
有没有人有任何编译器测试的经验,可以提供一些关于如何继续进行的一些建议?我有点迷失在这里!
我参与了一个项目,其中Java AST被翻译成另一种语言OpenCL,使用Eclipse编译器,并且有类似的问题.
我没有为你提供神奇的解决方案,但我会分享我的经验以防万一.
您使用预期输出(使用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.txt(或.xml或其他)创建一个测试,其中包含有关symbolTable的所有信息,但我认为这样做有点复杂.每个集成测试都是一件复杂的事情!
我试着采用一次性测试符号表的小位而不是整个批次的方法.如果我正在测试Java树是否正确构建,我会有类似的东西:
一个测试只针对if语句:
至少对一种类似风格的其他类型的陈述进行测试.
这种方法是集成式测试,但每次集成测试只测试系统的一小部分.
基本上我会尽量保持测试尽可能小.用于提取树的位的许多测试代码可以移动到实用程序方法中以使测试类保持较小.
我想也许我可以创建一个漂亮的打印机,它将采用符号表并输出相应的源文件(如果一切正常,就像原始源文件一样).问题是原始文件的内容可能与我的漂亮打印机打印的顺序不同.我担心通过这种方法,我可能只是打开另一种蠕虫.我一直在不懈地重构代码的一部分,这些错误开始显露出来.我真的需要一些集成测试来让我保持正轨.
这正是我采取的方法.但是在我的系统中,东西的顺序没有太大变化.我有基本上输出代码以响应Java AST节点的生成器,但是有一点自由,因为生成器可以递归地调用它们自己.例如,响应Java If语句AST节点而被触发的'if'生成器可以写出'if(',然后请求其他生成器呈现条件,然后写'){',请其他生成器写入出身体,然后写'}'.