はじめに
こんにちは、セコムしてますか?僕はしてません。さて、Erlangでちょこちょこと質問してたら、id:kuenishiから「てめえ、ぐだぐだ言う前にまずOMakeとEUnit使ってひたすらテストしろや」と言われたのでそのセットアップなどをしました。
参照
- shibu.jp: Erlang用の軽量ユニットテストフレームワーク
- 渋神こと@shibukawaの日本語訳のみで十分。どうもありがとうございました。
- 1. ガイド — OMakeマニュアル 日本語訳
- OMakeのことならここを見れば全部載ってます
- szktty/omakebook-jp · GitHub
- OMakeを始めるならこれを読むと幸せになれます。ビルドは"make html"です。
- OMake つかったらC言語でプログラム書く手間がバカみたいに減った - 日記を書く [・w・] はやみずさん
- "omake -P"でご飯三杯いってるときのテンションが伝わるエントリ
設定
まずはコードを書く
テスト以前にそもそものコードを書きますよね。というわけで今回は「決められた値までの素数列を求める関数primesを実装する」ということで書いてみました。
bottom(List)-> case List of [] -> []; [X] -> X; [_|Xs] -> bottom(Xs) end. sift(Divider, Nums)-> lists:filter(fun(X) -> X rem Divider =/= 0 end, Nums). sieve(Nums)-> sieve([], Nums). sieve(Accu, Nums)-> case {Accu, Nums} of {_, []} -> Accu; {[], _} -> sieve([hd(Nums)], sift(hd(Nums), tl(Nums))); {_, [X|Xs]} -> B = bottom(Xs), if X*X > B -> lists:reverse(Accu, Nums); true -> sieve([X|Accu], sift(X, Xs)) end end. primes(Limit)-> sieve(lists:seq(2, Limit)).
テストコードを書く
さて、ちゃんと上のコードが動くのかっつーわけで、「並行して」テストコードを書いていきますよ。並行して、っていうか関数名書いたら実装始める前にテスト書きますよね。期待する結果を書いて。でこんな感じでテスト書きました。
bottom_test_()-> [?_assert( [] =:= bottom([]) ), ?_assert( 1 =:= bottom([1]) ), ?_assert( 10 =:= bottom(lists:seq(1, 10)) ), ?_assert( d =:= bottom([a, b, c, d]) ) ]. sift_test_()-> [?_assert( [] =:= sift(2, []) ), ?_assert( [] =:= sift(1, lists:seq(1, 5)) ), ?_assert( lists:seq(1, 9, 2) =:= sift(2, lists:seq(1, 10)) ) ]. sieve_test_()-> [?_assert( [] =:= sieve([]) ), ?_assert( [1] =:= sieve(lists:seq(1,10)) ), ?_assert( [2,3,5,7] =:= sieve(lists:seq(2,10)) ) ]. primes_test_()-> [?_assert( [2] =:= primes(2) ), ?_assert( [2,3,5,7] =:= primes(10) ) ].
_test_()で終わる関数を書いておくと、EUnitはそれをテストジェネレータとして認識して全部実行してくれます。すごいですねー。
コンパイルと実行
さて、コード書いただけではクソの役にも立たないのでコンパイルして実行します。上の2つを全部まとめたのがこれ。
%%%----------------------------------------------------------------------------- %%% @author Yoshifumi YAMAGUCHI <ymotongpoo AT gmail.com> %%% @copyright (C) 2010, Yoshifumi YAMAGUCHI %%% @doc %%% %%% @end %%% Created : 23 Oct 2010 by Yoshifumi YAMAGUCHI <ymotongpoo AT gmail.com> %%%----------------------------------------------------------------------------- -module(sample). -define(EUNIT, true) -export([bottom/1, sift/2, sieve/1, primes/1]). -compile(export_all). %%%----------------------------------------------------------------------------- bottom(List)-> case List of [] -> []; [X] -> X; [_|Xs] -> bottom(Xs) end. sift(Divider, Nums)-> lists:filter(fun(X) -> X rem Divider =/= 0 end, Nums). sieve(Nums)-> sieve([], Nums). sieve(Accu, Nums)-> case {Accu, Nums} of {_, []} -> Accu; {[], _} -> sieve([hd(Nums)], sift(hd(Nums), tl(Nums))); {_, [X|Xs]} -> B = bottom(Xs), if X*X > B -> lists:reverse(Accu, Nums); true -> sieve([X|Accu], sift(X, Xs)) end end. primes(Limit)-> sieve(lists:seq(2, Limit)). %%%----------------------------------------------------------------------------- -ifdef(EUNIT). -include_lib("eunit/include/eunit.hrl"). bottom_test_()-> [?_assert( [] =:= bottom([]) ), ?_assert( 1 =:= bottom([1]) ), ?_assert( 10 =:= bottom(lists:seq(1, 10)) ), ?_assert( d =:= bottom([a, b, c, d]) ) ]. sift_test_()-> [?_assert( [] =:= sift(2, []) ), ?_assert( [] =:= sift(1, lists:seq(1, 5)) ), ?_assert( lists:seq(1, 9, 2) =:= sift(2, lists:seq(1, 10)) ) ]. sieve_test_()-> [?_assert( [] =:= sieve([]) ), ?_assert( [1] =:= sieve(lists:seq(1,10)) ), ?_assert( [2,3,5,7] =:= sieve(lists:seq(2,10)) ) ]. primes_test_()-> [?_assert( [2] =:= primes(2) ), ?_assert( [2,3,5,7] =:= primes(10) ) ]. -endif.
早速コンパイルして実行ですね。EUnitはmというモジュールだったらm:test()という関数として組み込まれるので、これを実行してあげればよいわけです。
$ erlc sample.erl $ erl -noshell -s sample test -s init stop All 12 tests passed.
おお、ちゃんとテスト通りましたね。じゃあわざとこけるようにテストを書き換えてみます。
bottom_test_()-> [?_assert( [] =:= bottom([]) ), ?_assert( 1 =:= bottom([1]) ), ?_assert( 1 =:= bottom(lists:seq(1, 10)) ), % ここを10じゃなくて1にしてみました ?_assert( d =:= bottom([a, b, c, d]) ) ].
再度コンパイルして実行。
$ erlc sample.erl $ erl -noshell -s sample test -s init stop sample:59: bottom_test_...*failed* ::error:{assertion_failed,[{module,sample}, {line,59}, {expression,"1 =:= bottom ( lists : seq ( 1 , 10 ) )"}, {expected,true}, {value,false}]} in function sample:'-bottom_test_/0-fun-4-'/0 ======================================================= Failed: 1. Skipped: 0. Passed: 11.
ちゃんと?こけてくれてますね。他の11個のテストに関しては通りましたよ、って出てます。ここまでで手動でEUnitを走らせるところまでは来ました。あとはOMakeを使って、ファイルを更新するたびにコンパイルとEUnitの実行をしてもらえるようにすればいいだけです!
OMakefileの設定
OMakeはOCamlで実装されてるビルドツールで、OMake言語という専用言語でGNU Makeよりも柔軟な記述が出来ます。さらにomake -Pというオプションを点けて実行すると、Flymakeのようにファイルを更新するたびにビルドが走ります。早速OMakeを書いてみます。詳しいことは上記リンクに任せるとして、今回は次のようなディレクトリ構成にしました。
unittest/ ├── OMakefile ├── OMakeroot └── src ├── OMakefile └── sample.erl
OMakerootはプロジェクトのルートディレクトリに必ずないといけないファイルで、これを起点としてディレクトリを行ったり来たりできます。OMakefileはプロジェクト内の各ディレクトリに置いておくファイルで、この中でビルドとか書いたりします。
OMakerootは完全にid:kuenishiのやつをパクリましたwのでOMakefileで使ってない設定がたくさんあります。いずれ使います。
- OMakeroot
######################################################################## # The standard OMakeroot file. # You will not normally need to modify this file. # By default, your changes should be placed in the # OMakefile in this directory. # # If you decide to modify this file, note that it uses exactly # the same syntax as the OMakefile. # # # Include the standard installed configuration files. # Any of these can be deleted if you are not using them, # but you probably want to keep the Common file. # open build/C open build/OCaml open build/LaTeX # sth like OTP.om public.ERLC=$(shell which erlc) public.SRCDIR=src public.TESTDIR=test public.EBIN=ebin public.ERLCFLAGS=-DDEBUG +debug_info -Wall public.ERL=erl public.ROOT=$(shell pwd) public.BEAM_EXT=.beam public.ERL_EXT=.erl public.INCLUDES[] = include public.INCLUDES_OPT = -I # # Add the -I option to the includes lazily. # Don't redefine this variable unless you know what you are doing. # public.PREFIXED_INCLUDES = $`(addprefix $(INCLUDES_OPT), $(INCLUDES)) #%$(BEAM_EXT): %$(ERL_EXT) #:scanner: scan-erl-%$(ERL_EXT) # $(ERLC) $(ERLCFLAGS) $(PREFIXED_INCLUDES) $< MakeBeams(names) = erl2beam(name) = private.beam = $(EBIN)/$(removesuffix $(basename $(name)))$(BEAM_EXT) $(beam): $(name) $(ERLC) $(ERLCFLAGS) -pa $(EBIN) $(PREFIXED_INCLUDES) -o $(EBIN) $< # $(ERL) -pa $(EBIN) -noshell -eval '$(removesuffix $(basename $(name))):test().' -s init stop return $(string $(beam)) private.beams=$(names.map $(erl2beam)) return $(beams) erls=$(glob $(string $(SRCDIR)/*$(ERL_EXT))) beams=$(MakeBeams $(erls)) apps= #$(glob $(string $(EBIN)/*.app)) # # The command-line variables are defined *after* the # standard configuration has been loaded. # DefineCommandVars() # # Include the OMakefile in this directory. # .SUBDIRS: .
- OMakefile
######################################################################## # Phony targets are scoped, so you probably want to declare them first. # .PHONY: all clean # eprintln( $(string $(beams)) ) testerls=$(glob $(string $(TESTDIR)/*$(ERL_EXT))) # traverse the subdirs except $(dirs) Subdirs_except(dirs) = # need to export since .SUBDIRS is evaluated in the global scope export VISIT_SUBDIRS sub_omakefiles = $(glob i, */OMakefile) subdirs = $(sub_omakefiles.map $(dirname)) VISIT_SUBDIRS=$(set-diff $(subdirs), $(dirs)) # The rule .SUBDIRS: $(VISIT_SUBDIRS) # traverse all the subdirs Subdirs() = Subdirs_except($(array)) Subdirs()
- src/OMakefile
MODULE = sample FILES = $(MODULE).erl TARGET = $(MODULE).beam .PHONY: all $(TARGET) .DEFAULT: all all: $(TARGET) $(TARGET): $(FILES) $(ERLC) $(FILES) $(ERL) -noshell -s $(MODULE) test -s init stop clean: rm -f *.beam
これで準備完了。プロジェクトルートでomakeを打ってもいいですし、srcディレクトリ内で打ってもいいです。
$ omake *** omake: changing directory to /Users/ymotongpoo/src/erlang/unittest *** omake: reading OMakefiles *** omake: finished reading OMakefiles (0.02 sec) - build src <.DEFAULT> + erl -noshell -s sample test -s init stop All 12 tests passed. *** omake: done (1.43 sec, 0/0 scans, 1/1 rules, 0/73 digests)
ちゃんとEUnitが走りました。
omake -Pで幸せ
さていよいよomake -Pで幸せになりたいと思います。今回はtakeN/2というリストの先頭からN個の要素を取ってきたリストを返す関数を追加したいと思います。まずコマンドラインでおもむろにomake -Pを起動させます。
$ omake -P *** omake: changing directory to /Users/ymotongpoo/src/erlang/omake-eunit-template *** omake: reading OMakefiles *** omake: finished reading OMakefiles (0.02 sec) - build src <sample> + erl -noshell -s sample test -s init stop All 12 tests passed. *** omake: done (1.42 sec, 0/0 scans, 1/1 rules, 0/86 digests) *** omake: polling for filesystem changes
ここで通常のomakeと違い、最後に一行、ポーリングして監視してますよ、って感じのメッセージが出てると思います。ここでsample.erlに以下のような行を追加して保存します。
takeN(N, List)-> % わざと関数名だけ書いて放置します ... takeN_test_()-> % テストを書いておきます [?_assert( [] =:= takeN(0, lists:seq(1,10)) ), ?_assert( [1] =:= takeN(1, lists:seq(1,10)) ), ?_assert( lists:seq(1,5) =:= takeN(5, lists:seq(1,10)) ), ?_assert( lists:seq(1,10) =:= takeN(20, lists:seq(1,10)) ) ].
するとomake -Pを立ち上げているターミナルを見るとこんなメッセージが追記で表示されているのがわかるでしょう。ちゃんと保存したらomakeが走ってますね。
*** omake: file src/sample.erl changed *** omake: rebuilding - build src <sample> + /opt/erlang/R14B/bin/erlc sample.erl ./sample.erl:58: syntax error before: '.' ./sample.erl:93: unbalanced '-endif'========================================================= ] 00021 / 00024 ./sample.erl:86: function takeN/2 undefined ./sample.erl:87: function takeN/2 undefined ./sample.erl:88: function takeN/2 undefined ./sample.erl:89: function takeN/2 undefined
こんな感じでずーっと編集していくと、ターミナルを眺めながらコードを書いてればよい状態になって幸せです。(Emacsとかならshell-modeで立ち上げていると完全にEmacsの中だけで完結します)
テストだけ先に書いてるのであとは保存したときに下記のように「テスト通りました!」って状況になるまでコード書いてればいいだけ!幸せ!
... ./sample.erl:92: unbalanced '-endif' ./sample.erl:51: function takeN/3 undefined *** omake: polling for filesystem changes *** omake: file src/sample.erl changed *** omake: rebuilding - build src <sample> + erl -noshell -s sample test -s init stop All 16 tests passed. *** omake: done (10 min 26.08 sec, 0/0 scans, 6/6 rules, 10/158 digests) *** omake: polling for filesystem changes
追記
eunit.hrlへのパス
上記ドキュメントではEUnitを使う際は
-include_lib("eunit/include/eunit.hrl").
を追加しなさいよ。そしてビルドするときは次のようにeunit.hrlを含むeunitという名前のディレクトリパスを追加しなさいよ、と書いてあります。
erlc -pa "path/to/eunit/ebin" $(ERL_COMPILE_FLAGS) -o$(EBIN) $<
しかしいざOTP R14Bの下のディレクトリを検索してみても似たような名前のものはあっても、eunitという名前のディレクトリはありません。
$ find /opt/erlang/R14B -name eunit.hrl /opt/erlang/R14B/lib/erlang/lib/eunit-2.1.5/include/eunit.hrl
シンボリックリンクでも張ってんのかな?とid:kuenishiにSkypeチャットで聞いてみたところ下記回答。
[10/10/23 9:57:03] kuenishi: include_libは、ertsが適当にパスを読み替えてくれる
分かんねえよ...