我发现很难推断宏观扩张,并且想知道测试它们的最佳实践是什么。
因此,如果我有一个宏,我可以通过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表达式之间差异的库?
答案 0 :(得分:7)
通常测试宏不是Lisp和Common Lisp的强大部分之一。 Common Lisp(和Lisp方言一般)使用过程宏。宏可以依赖于运行时上下文,编译时上下文,实现等。它们也可能有副作用(比如在编译时环境中注册事物,在开发环境中注册事物等)。
所以有人可能想测试一下:
loop
,defstruct
,...宏。loop
和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-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)
我通常只测试功能,而不是扩展的形状。
是的,有各种各样的环境和环境可能影响发生的事情,但如果你依赖这些事情,那么为你的测试设置它们应该没问题。
一些常见情况: