Lisp宏单元测试的惯例和最佳实践

14

我发现很难推理宏展开,并想知道测试它们的最佳方法。

因此,如果我有一个宏,我可以通过 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
我会测试宏的最终效果,而不是中间扩展。虽然问题很好,但我期待答案。 - Jon Chesterfield
3个回答

7
一般来说,测试宏并不是Lisp和Common Lisp的强项。Common Lisp(以及通常的Lisp方言)使用过程宏。这些宏可能依赖于运行时上下文、编译时上下文、实现等等。它们也可能具有副作用(例如在编译时环境中注册事物,在开发环境中注册事物等)。
因此,人们可能想要测试以下内容:
- 生成正确的代码 - 生成的代码实际上执行了正确的操作 - 在代码上下文中实际上可以工作的生成代码 - 在复杂宏的情况下,确保宏参数实际上被正确解析。考虑到像循环、defstruct等宏。 - 宏检测错误形式的参数代码。再次考虑像循环、defstruct这样的宏。 - 副作用
从上面的列表可以推断出,在开发宏时最好尽量减少所有这些问题领域。但是:确实存在非常复杂的宏。真的很可怕。特别是那些用于实现新领域特定语言的宏。
使用类似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表达式差异工具,例如diff-sexp

6

我同意Rainer Joswig的答案。总的来说,这是一个非常难解决的任务,因为宏可以做很多事情。然而,我想指出,在许多情况下,单元测试宏的最简单方法是让宏尽可能少地执行操作。在许多情况下,宏的最简单实现只是对更简单函数的语法糖。例如,在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-frobcleanup-frob相对容易进行单元测试。而call-with-frob则有些困难。它的想法是处理创建frob和确保清理调用发生的样板代码。这有点难以检查,但如果样板只依赖于某些明确定义的接口,那么您可能能够模拟一个frob,以检测它是否已正确清理。最后,with-frob宏非常简单,您可以按照您一直考虑的方式进行测试,即检查其扩展。或者您可能会说,它足够简单,您不需要测试它。
另一方面,如果您正在查看更复杂的宏(例如loop),它实际上是一种自己的编译器,那么您几乎肯定已经将扩展逻辑放在某些单独的函数中。例如,您可能有:
(defmacro loop (&body body)
  (compile-loop body))

如果是这种情况,你真的不需要测试循环(loop),你需要测试编译循环(compile-loop),然后你就回到了通常的单元测试领域。


2

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

是的,可能会有各种上下文和环境影响发生,但如果您依赖这些东西,那么为测试设置它们应该不成问题。

一些常见情况:

  • 绑定宏:测试变量在内部是否按预期绑定,以及任何被外部变量遮蔽的影响
  • 解除保护包装器:从内部引发非局部退出并检查清理工作是否正常进行
  • 定义/注册:测试您是否可以定义/注册所需内容,并在之后使用它们

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