单元测试编译器

Rog*_*son 6 tdd parsing unit-testing compiler-theory abstract-syntax-tree

什么被认为是对复杂单元(如编译器)进行单元测试的最佳方法?

多年来我写了一些编译器和解释器,我发现这种代码很难以一种好的方式进行测试.

如果我们采用类似抽象语法树生成的东西.你会如何使用TDD测试?

小结构可能很容易测试.例如:

string code = @"public class Foo {}";
AST ast = compiler.Parse(code);
Run Code Online (Sandbox Code Playgroud)

因为那不会产生很多ast节点.

但是,如果我真的想测试编译器可以为类似方法生成AST:

[TestMethod]
public void Can_parse_integer_instance_method_in_class ()
{
   string code = @"public class Foo {  public int method(){ return 0;}}";
   AST ast = compiler.Parse(code);
Run Code Online (Sandbox Code Playgroud)

你会断言什么?手动定义代表给定代码的AST,并断言生成的AST符合手动定义的AST看起来非常简洁,甚至可能容易出错.

那么像这样的TDD复杂场景的最佳策略是什么?

小智 6

首先,如果您测试编译器,则无法获得足够的测试!用户真的很依赖编译器生成的输出,就好像它是一个永远的黄金标准,所以要真正注意质量。所以如果可以的话,用你能想到的每一个测试来测试!

其次,使用所有可用的测试方法,并在适当的地方使用它们。事实上,您或许能够在数学上证明某个变换是正确的。如果你能够这样做,你应该这样做。

但是我见过的每个编译器的内部结构都涉及启发式和大量优化的、手工编写的内部代码;因此,辅助证明方法通常不再适用。在这里,测试到位,我的意思是很多!

在收集测试时,请考虑不同的情况:

  1. 积极的标准一致性:您的前端应该接受某些代码模式,并且编译器必须生成正确运行的程序。此类别中的测试需要黄金参考编译器或生成器来生成测试程序的正确输出;或者它涉及手写程序,其中包括对人类推理提供的值进行检查。
  2. 否定测试:每个编译器都必须拒绝错误代码,例如语法错误、类型不匹配等。它必须产生某些类型的错误和警告消息。我不知道有什么方法可以自动生成这样的测试。所以这些也需要人工编写。
  3. 转换测试:每当你在你的编译器(中端)中想出一个奇特的优化时,你可能会想到一些演示优化的示例代码。请注意此类模块之前和之后的转换,它们可能需要您的编译器或仅插入该模块的准系统编译器的特殊选项。还要测试一组合理的大量周围模块组合。我通常在特定转换之前和之后对中间表示进行回归测试,通过与同事的深入推理来定义参考。尝试在转换的两侧编写代码,即您想要转换的代码片段和不应该转换的稍微不同的代码片段。

现在,这听起来像是一项艰巨的工作!是的,它确实如此,但有帮助:世界上有几个用于 (C-) 编译器的商业测试套件以及可能会帮助您应用它们的专家。这是我所知道的一小部分:

  • 这是高尚的高级集成测试,而不是那些深受网络编码潮人喜爱的奇怪的“单元测试”。 (3认同)

yug*_*ugr 1

首先,解析通常是编译器项目的一个微不足道的部分。根据我的经验,它永远不会花费超过 10% 的时间(除非我们正在谈论 C++,但如果您正在设计它,您就不会在这里提出问题),因此您宁愿不要将大量时间投入到解析器测试中。

尽管如此,TDD(或者无论您如何称呼它)在开发中端方面仍然占有一席之地,您经常希望验证您刚刚添加的优化是否确实导致了预期的代码转换。根据我的经验,这样的测试通常是通过为编译器提供特制的测试程序和 grep 输出程序集以获取预期模式来实现的(这个循环是否展开了四次?我们是否设法避免内存写入是这个函数?等等)。Grepping 汇编不如分析结构化表示(S-exprs 或 XML)那么好,但它很便宜并且在大多数情况下工作得很好。不过,随着编译器的增长,支持起来非常困难。