落書き、もしくは妄想

昔書く書くと言ってた「テスト用のなにか」。名前が無いと不便なのでとりあえず xl-behave としたんだけど、subversion の repository とか trac とか考えると名前を変更するのってけっこーめんどい予感。

書く書くと言って全然書いてないのは、どういうものを作ればいーのかはっきりイメージできてない、あるいはイメージできてるけどそのイメージでいいのか確信が持てない(「ほんとにこれでいいんか?」と思ってしまう)せいな感じなので、その辺整理するためにぐちゃぐちゃ書いてみる。

使い方

たぶん lisp-unit.lisp のパクり。というか TDD/BDD で言われてることそのまま。

  1. テスト/仕様/説明 を書く
  2. コードを書く
  3. テストする
  4. レッド!グリーン!

3の「テストする」は *after-save-buffer-hook* とかで勝手にやるようにしたい。

テスト/仕様/説明 をどう書くのか

今んトコ固まってるのは

(describe NAME
  (expect EXAMPLE
    EXPECTATION*))

NAME はいいとして、EXAMPLE は、まぁなんでも。その EXAMPLE を評価とかしてなにがどうなるというのを EXPECTATION に書く。ここ(EXPECTATION)がはっきりしてない。
# これ書いててプレビューしたら describe がキーワード色になったので、まさかと思って HyperSpec 見てみたら describe 関数があった。どうしよう。他にてきとーな名前探すか、xyzzy には無いから無視するか・・・。

予定というか構想(というか妄想)としては

  • 戻り値
  • コンディション
  • (マクロの)展開形
  • ストリームへの出力
  • 束縛(変数の値)

の5つは記述できるようにしたくて、EXAMPLE を評価なりする時に調べることもできそう。こいつらを「振る舞い種別」と呼ぶ事にする。

んでこの振る舞い種別を、どう記述して、どう検証するのかってところがはっきりしてない。とりあえず検証は置いとく(とかやってると検証するのが鬼めんどくなったりしそうでいやなんだけど)ことにして。

まず EXPECTATION は複数書けるようにする。マクロの展開形はこーで、このストリームにこーゆーのを出力して、戻り値はこれ、とか。なので EXPECTATIONs は &rest で受けて list になる。その list の各要素が個々の EXPECTATION。このために (expect EXAMPLE EXPECTATION*) の順番になった。(expect (EXPECTATION*) EXAMPLE とかもできるっちゃできるんだが。

各 EXPECTATION は list にして、どの振る舞いについてなのかを指定する symbol(「振る舞い指定子」と呼ぶ事にする)を1つ目に持ってくる。

    (returns VALUE)
    (prints "message" *standard-output*)))

振る舞い指定子はどーすんのか(prints? outputs? write? とか、そいつら export すんの?keyword にしない?とか、そもそも symbol なの?とか)ってのは置いとけば、ここまではおk。

振る舞い種別ごとに、それ以降の引数(上の例だと VALUE とか "message" *standard-output* とか)をどう扱うかってのは変わるから、個別に考えよう。

returns: 戻り値
    (returns VALUE-1 VALUE-2 ...)

まず多値への対応は、引数[0] が 戻り値[0] としておく。(values 1 2 3) なら (returns 1 2 3) でそれぞれ equal ってことで。

次に、単純な場合は VALUE として与えられた値を実際の戻り値と equal なり eq なりで比較すればいい。というかそれで大抵は済む気がするので、(returns 1 2 3) みたいなシンプルな書き方で equal なりなんなりで比較するようにしたい。

だがしかし、単に与えられた VALUE と比較するだけだと、戻り値が不定のときに困る。(random 3) とか。この場合「0 以上、3 未満の number」とかいう指定を記述できる必要がある。

# (random 3) が「0 以上、3 未満の number」を *返す* かどうかは確かめられるけど、「0 以下」とか「3 以上」の number だとか、number 以外だとかを *返さない* かどうかって確かめようが無い予感。むー。
更に、list だとか hash-table だとか structure だとかだと、完全に指定する必要は無いこともある。「hash-table で、key=foo に value="foo" を持ってて、key=bar に value="bar" を持ってること」が必要で、他にどんな key/value を持ってるかはどーでもいい、とかいう場合。
# そーいへば2つの hash-table がまったく同じ key/value を保持してるかって比較をする関数って無いような気がする。すぐ作れそうだけど。あーでも保持されてる値はどう比較するとか考えるとめんどいな。equal とかだとどーなるんだろ?気が向いたら調べてみよう。

signals: コンディション
    (signals CONDITION-TYPE ...)

あんまりコンディション使わないんで、何を指定する必要があるのかよくわからん。CONDITION-TYPE の他にはなんかメッセージのようなものがあるのはわかるが、それだけでいいのかが疑問。というか、コンディションを思いっきり使うとしたら独自の condition 定義してそれに色々詰め込んで投げるだろうし、それだと xl-behave で記述できることを制限しちゃマズい。

たしかコンディションってちょっと変わった structure だったから、値への要求の記述を流用できそうな気がする。

expands: 展開形
    (expands EXPANTION)

ふつーに S-Expression 書けちゃっていいよね。

いくつか気になるところ

  • gensym は #:Gxxxx みたいな symbol になるけど、xxxx はどうなるかわかんない=書けない。
    • なので「ここに gensym'ed symbol が来る」という記述をできるようにしないといかん。
    • 同じ gensym'ed symbol が複数箇所に現れるなら、それらが同一の gensym'ed symbol であることも書けるようにしないといかん。
  • 実際の展開形は macroexpand-1 で capture することになるだろうけど、それって実際やってみないとどこまで展開されるのかがあんまりわかんない気がする。わかんないと "マクロ定義は問題ないけど展開形の expectation が間違ってるので fail する" とかいういらん事になる。
prints: 出力
    (prints (STREAM-1 "expected output")
            (STREAM-2 "another output")...)

STREAM は省略可能にしてデフォで *standard-output* に、とか思ったけど、common-lisp なら *standard-output* が基本だろうけど xyzzy ではむしろあんま使わない気がするのでいらない気がしたので却下。

STRING というが、出力と equal な文字列を指定するのは厳しい場合がある。(format t "~S" (make-hash-table)) とか。どうしよう。regexp?

実際に EXAMPLE を評価する時に出力を取り出すとそれは文字列になるから、「どういう文字列か」を指定できればいい。これは値への要求を流用できそう。あと capture するときの都合で stream そのものじゃなくて stream に bind された symbol が必要だった気がする。一旦思考停止。

binds: 束縛
    (binds (SYMBOL VALUE)*)

あんまり悩む部分は無い。SYMBOL は評価しない、VALUE は returns のとこでちょっと触れた「値への要求」を流用する。

値への要求

「こうこうこーゆー値」の記述。

  • この値(値を直接記述できる && その値と eq とか equal とか)
  • string であればいいとか、hash-table であればいいとか、もっと言えば non-nil ならおkとか
  • 入り組んだデータ構造でも、その一部だけ要求を満たせばいい
  • 不確定な値であっても、number なら x 以上 y 未満とか、string なら与えられた regexp に match するとか

とかなんとか色んな場合がありそうなので、全部の要求をなるべくシンプルに記述できるようにしたい。

今考えてるのが

  • 単純に値が書いてあれば eq なり equal なりで比較
  • めんどい要求は (TYPE ...) と書く。...は TYPE によって使えるものが変わる。
    ;; 値だけ置いとく=> equal とかで比較
    (returns 3)

    ;; これは特別扱い
    (returns non-nil)

    ;; 0以上3未満の number
    (returns (number (greater-than 0)
                     (less-than 3)))

    ;; "[Re]ge*p" に match する string
    (returns (string (matches "[Re]ge*p")))

    ;; key=key に VALUE を持つ hash-table
    ;; NOTE: VALUE にはここで説明してる「値への要求」が使える
    (returns (hash-table (key VALUE)))

    ;; slot-1 と slot-2 に VALUE を持つ hoge structure
    (returns (hoge :slot-1 VALUE
                   :slot-2 VALUE))

    ;; quote してあったらその list を単なる値として equal なりで比較
    (return '(1 2 3))

    ;; abc という symbol を含む list
    (return (list (contains 'abc)))

値への要求のなかで値への要求を再帰的に使えるというのは、あって然るべきというかそうじゃないと不自由すぎるんだけど、fail したときに "どこが要求を満たさなかったのか" てのを特定するのがめんどそう。

検証結果のレポート

pass ったのは放っといて fail したのについて

  • EXAMPLE
  • 期待してた挙動
  • 実際の挙動

を表示すりゃいいんだろうけど、期待してた挙動の部分が EXPECTATION をそのまま表示してもわかりにくいかなー、というのがひとつ。期待してたのと実際のと両方表示して、じゃあどこがダメなのよ?ってのはどうすれば見つけやすいのか、てのがひとつ。
とりあえず RSpec の真似してみたらこんな感じ。

(add 1 2 3)
Expected: (returns 6)
  Actual: returned: 6

(add 1 2 3)
Expected: (returns 6)
  Actual: signaled: type-error "あーだこーだ"

でも、展開形とかも同時に fail したらどうしよう。多値のときはどーする?とか色々考えててうがーってなる。ついでにレポートのフォーマット変えようとしたら format がコードのあちこちにあってイライラした。それでこいつを窓から放り投げたのが3月頭の事でした。


少しはすっきりしたのでコーディングに移ろう。よし、明日から本気出す。