Common Lispでマクロ展開時エラーをテスト
前回記事の「Parenscriptで少し遊んで見る (5)defstruct編 - eshamster’s diary」ではdefstruc.psマクロ内で展開時のエラー処理を入れたので、テストも一緒に作っています。このときに、マクロ展開時のエラーをテストする方法が意外と見つからなかったのでメモ。探し方が悪いのか、そんなの当たり前でしょということで誰も書いていないのか。
CL-USER> (defmacro throw-error-macro (x) (if (numberp x) `(+ ,x 10) (error 'type-error))) THROW-ERROR-MACRO
特に意味のないマクロですが、数値型を受けとると10を足す式を返し、それ以外の型を受けとると(展開時に)type-error
を投げます。この挙動をテストしたい訳ですが…
CL-USER> (ql:quickload :prove :silent t) (:PROVE) CL-USER> (prove:is-error (throw-error-macro nil) 'type-error) ; ; compilation unit aborted ; caught 1 fatal ERROR condition ; caught 1 ERROR condition ; Evaluation aborted on #<SIMPLE-ERROR "unbound condition slot: ~S" {1005EC7663}>.
というように、prove:is-error
が呼び出される前のコンパイル段階でエラーになるため、prove:is-error
でエラーを捕捉できません。マクロを実行時に評価できれば良いわけですが、実行時評価となればeval
の出番です。
CL-USER> (prove:is-error (eval '(throw-error-macro nil)) 'type-error) ✓ (EVAL '(THROW-ERROR-MACRO NIL)) is expected to raise a condition TYPE-ERROR (got #<TYPE-ERROR {1003C91FE3}>) T #<PASSED-TEST-REPORT RESULT: T, GOT: #<TYPE-ERROR {1003C91FE3}>, EXPECTED: TYPE-ERROR>
できました。一々evalやクォートを書くのが気持ち悪い場合、以下のようにprove-macro-expand-error
マクロを作成します。
CL-USER> (defmacro prove-macro-expand-error (code expected-error) `(prove:is-error (eval ',code) ,expected-error)) PROVE-MACRO-EXPAND-ERROR CL-USER> (prove-macro-expand-error (throw-error-macro nil) 'type-error) ✓ (EVAL '(THROW-ERROR-MACRO NIL)) is expected to raise a condition TYPE-ERROR (got #<TYPE-ERROR {10044945E3}>) T #<PASSED-TEST-REPORT RESULT: T, GOT: #<TYPE-ERROR {10044945E3}>, EXPECTED: TYPE-ERROR>
言われてみれば当たり前な気がしますが、個人的にはeval
を使う機会がなかったので中々出てこない発想でした。
ちなみに、元々は「リードマクロ入門、の10分の1歩ぐらい後か前 - eshamster’s diary」のコメントで教えて頂いたREPL環境でのリードマクロの試用方法から思い至ったため、(format nil "~S" ...)
で文字列化してread-from-string
をしてeval
するという無駄なことをしていました。一方で、リードマクロのエラーをテストしたい場合はread-from-string
を使えば良さそうです。
追記:macroexpand
g000001さんからmacroexpand(-1)
の方が一般的とのコメントを頂きました。eval
は汎用的過ぎるので、可能な限りより用途の限定されたもので代用するのが適切だと思いますが、macroexpand(-1)
はまさにぴったりの用途ですね。Lispを勉強し始めてからは前より人のソースを見ることが増えましたが、まだまだ勉強不足でした。
ということで、macroexpand
版です。見た目としてはeval
がmacroexpand
に置き代わっただけです。
CL-USER> (prove:is-error (macroexpand-1 '(throw-error-macro nil)) 'type-error) ✓ (MACROEXPAND-1 '(THROW-ERROR-MACRO NIL)) is expected to raise a condition TYPE-ERROR (got #<TYPE-ERROR {10060A87D3}>) T #<PASSED-TEST-REPORT RESULT: T, GOT: #<TYPE-ERROR {10060A87D3}>, EXPECTED: TYPE-ERROR> ;; マクロ化 CL-USER> (defmacro prove-macro-expand-error (code expected-error) `(prove:is-error (macroexpand ',code) ,expected-error)) PROVE-MACRO-EXPAND-ERROR CL-USER> (prove-macro-expand-error (throw-error-macro nil) 'type-error) ✓ (MACROEXPAND '(THROW-ERROR-MACRO NIL)) is expected to raise a condition TYPE-ERROR (got #<TYPE-ERROR {1003246EA3}>) T #<PASSED-TEST-REPORT RESULT: T, GOT: #<TYPE-ERROR {1003246EA3}>, EXPECTED: TYPE-ERROR>