lisp-unit.lisp
http://www.cs.northwestern.edu/academics/courses/325/readings/lisp-unit.html
xyzzy-lisp 用の test tool が欲しいな、作ってみようかな。つーか common-lisp 用のならあるんじゃね?と探してみて見つけたのが lisp-unit.lisp
ざっと見た感じ xyzzy-lisp に移植するのは簡単そうなんだけど、とりあえず読んでみて面白いなと思った部分について書いておく。といってもわかりやすく説明できるかまことに不安だが。
大雑把な流れ
(define-test add (assert-equal 3 (add 1 2)) (assert-equal 5 (add 2 3)))
define-test で assert-* する tests を定義しておく。これは *package* に関連付けられて *tests* に保存される。
(run-tests add)
run-tests で指定した名前の tests を実行(というのだろうか?)する。
問題: tests を定義するときに macro を展開してはいけない
test を保存しておくっていうとぱっと思いつくのは
(define-test add (assert-equal 3 (add 1 2)) (assert-equal 5 (add 2 3)))
なら
(lambda () (if (equal 3 (add 1 2)) <pass> <fail>) (if (equal 5 (add 2 3)) <pass> <fail>))
みたいな関数を作っておいて、run-tests されたら funcall するとかいう方法なんだが、使い方として
- 何かに対する tests を書く
- 何かを実装
- 何かに対する tests を実行
- pass するまで 2-3 を繰り返す
というような使い方を考えると、テスト対象の何かが macro だった場合に tests を書いた時点で macro を展開して保存しちゃうと、何かを書き換えても(展開されて保存された)tests のコードは古い macro 定義に従って展開されたままになってしまう。
つまり
(defmacro add (a b) `(* ,a ,b)) ;間違ってるのはわざと
なんて定義だった場合に、関数を作っちゃうとその時点で macro は展開されるので
(lambda () (if (equal 3 (* 1 2)) <pass> <fail>) (if (equal 5 (* 2 3)) <pass> <fail>))
という関数になってしまって、後から add の定義を修正しても test は間違った展開系のままになってしまう。
解決法: sexp を list として保存しておく
lisp-unit.lisp では *tests* に define-test された tests を保存してるのだけど、その時に保存するのは
(add ((assert-equal 3 (add 1 2)) (assert-equal 5 (add 2 3))))
という list で、これを run-test するときに拾ってきて cdr 部分を lambda 式の sexp に埋め込み、function に coerce する、ということをしてた。
;; この式はイメージです (coerce `#'(lambda () ,@(cdr test)) 'function)
そうすると run-tests を呼び出すまでは (assert-equal 3 (add 1 2)) という list の形がキープされて run-tests を呼び出して始めて add を展開するので、add の定義を書き換えるとちゃんと新しい定義で評価される。なるほどねぇ。
おまけ: thunk???
なんか test の code を list として保存しておいて、function に coerce したものを 'thunk' って呼んでる。get-test-thunk とか run-test-thunk とか。
で thunk というのを辞書で引いてみたけど、どうもしっくり来るような意味は無さそうだ。まさか chunk の typo?とか思いつつググったら
thunkって? - higepon blog
へーへーへー。