Lisp单元测试宏约定和最佳实践

时间:2016-01-02 21:47:46

标签: unit-testing macros lisp common-lisp

我发现很难推断宏观扩张,并且想知道测试它们的最佳实践是什么。

因此,如果我有一个宏,我可以通过macroexpand-1执行一级宏扩展。

(defmacro incf-twice (n)
  `(progn
     (incf ,n)
     (incf ,n)))

例如

(macroexpand-1 '(incf-twice n))

评估为

(PROGN (INCF N) (INCF N))

将它变成对宏的测试似乎很简单。

(equalp (macroexpand-1 '(incf-twice n))
  '(progn (incf n) (incf n)))

是否有用于组织宏测试的既定惯例?另外,是否有用于总结s表达式之间差异的库?

3 个答案:

答案 0 :(得分:7)

通常测试宏不是Lisp和Common Lisp的强大部分之一。 Common Lisp(和Lisp方言一般)使用过程宏。宏可以依赖于运行时上下文,编译时上下文,实现等。它们也可能有副作用(比如在编译时环境中注册事物,在开发环境中注册事物等)。

所以有人可能想测试一下:

  • 生成正确的代码
  • 生成的代码实际上做了正确的事
  • 生成的代码实际上在代码上下文中起作用
  • 在复杂宏的情况下,实际上正确解析了宏参数。想想loopdefstruct,...宏。
  • 宏检测到错误形成的参数代码。再次考虑像loopdefstruct这样的宏。
  • 副作用

从上面的列表可以推断出最好在开发宏时最小化所有这些问题区域。但是:那里真的有非常复杂的宏。真可怕的。特别是那些习惯于实现新域特定语言的人。

使用类似equalp的内容来比较代码仅适用于相对简单的宏。宏通常会引入新的,未加工的和唯一的符号。因此equalp将无法与那些人合作。

示例:(rotatef a b)看起来很简单,但扩展实际上很复杂:

CL-USER 28 > (pprint (macroexpand-1 '(rotatef a b)))

(PROGN
  (LET* ()
    (LET ((#:|Store-Var-1234| A))
      (LET* ()
        (LET ((#:|Store-Var-1233| B))
          (PROGN
            (SETQ A #:|Store-Var-1233|)
            (SETQ B #:|Store-Var-1234|))))))
  NIL)

#:|Store-Var-1233|是一个符号,它是未处理的并由宏新创建的。

另一个具有复杂扩展的简单宏形式是(defstruct s b)

因此,需要一个s表达式模式匹配器来比较扩展。有一些可用,它们在这里很有用。需要在测试模式中确保生成的符号在需要时是相同的。

还有s-expression diff工具。例如diff-sexp

答案 1 :(得分:4)

我同意Rainer Joswig's answer;一般来说,这是一个非常难以解决的任务,因为宏可以做很多事情。但是,我要指出,在许多情况下,对宏进行单元测试的最简单方法是使宏尽可能少地执行。在许多情况下,宏的最简单实现只是围绕一个更简单的函数的语法糖。例如,Common Lisp中存在 with- ... 宏的典型模式(例如, with-open-file ),其中宏只是封装了一些样板代码:

(defun make-frob (frob-args)
  ;; do something and return the resulting frob
  (list 'frob frob-args))

(defun cleanup-frob (frob)
  (declare (ignore frob))
  ;; release the resources associated with the frob
  )

(defun call-with-frob (frob-args function)
  (let ((frob (apply 'make-frob frob-args)))
    (unwind-protect (funcall function frob)
      (cleanup-frob frob))))

(defmacro with-frob ((var &rest frob-args) &body body)
  `(call-with-frob
    (list ,@frob-args)
    (lambda (,var)
      ,@body)))

此处的前两个功能 make-frob cleanup-frob 对于单元测试来说相对简单。 call-with-frob 有点困难。这个想法是它应该处理创建frob的样板代码并确保清理调用发生。这有点难以检查,但如果样板仅依赖于一些定义良好的接口,那么您可能能够创建一个模拟可以检测它是否正确清理的frob。最后, with-frob 宏非常简单,您可以按照您考虑的方式测试它,即检查其扩展。或者你可能会说它很简单,你不需要测试它。

另一方面,如果你正在寻找一个更复杂的宏,例如 loop ,它本身就是一种编译器,你就是这样的。几乎可以肯定,在某些单独的功能中已经有了扩展逻辑。例如,你可能有

(defmacro loop (&body body)
  (compile-loop body))

在这种情况下,你真的不需要测试循环,你需要测试编译循环,然后你又回到了您通常的单元测试领域。

答案 2 :(得分:2)

我通常只测试功能,而不是扩展的形状。

是的,有各种各样的环境和环境可能影响发生的事情,但如果你依赖这些事情,那么为你的测试设置它们应该没问题。

一些常见情况:

  • 绑定宏:测试变量是否按照预期绑定在内,并且任何阴影外部变量都不受影响
  • unwind-protect包装:从内部激发非局部退出并检查清理是否正常工作
  • 定义/注册:测试您可以定义/注册您想要的内容并在之后使用