Grailsで、tomcatを起動するというだけの目的でファンクショナルテストを設定する方法
Grailsでコントローラの試験を、モックを使わず実際の通信を使って試験したいケースがある。たとえば、Rest APIを開発する場合、Rest APIはブラウザを使用しなくても簡単に呼び出せるものなので、ユニットテストがしたいです。もちろんGrailsにはコントローラをモックで試験する機能があるが、rest APIを呼び出せば済むものを、偽物ですます必然性があまりないと個人的には思う(もちろん試験実施時のtomcat立ち上げ下げのための実行時間を短縮したい、などの関心はありうるが)。
このためにはファンクショナルテストを実行する必要がある(unit/integrationではtomcatはたちあがらない設定になっている)。しかし、Grailsでは、ファンクショナルテストは単にtest/functional配下にテストコードを置くだけでは実行できず、一つなんでもいいからファンクショナルテスト用プラグインを導入しておく必要がある。
Gebの機能は特に使いたくないとしても、たとえばHttpClientなどでRESTの試験だけしたいという場合に、以下の手順でGebを形だけでも導入することで、ファンクショナルテストを行うことができるようになる。
(1) BuildConfig.groovyのdependenciesセクションに以下を追加
dependencies {
:
test "org.gebish:geb-spock:0.10.0"
}
(2) BuildConfig.groovyのpluginsセクションに以下を追加
plugins {
:
test ":geb:0.10.0"
}
(3) BuildConfig.groovyでfolkedモードをfalseに設定する
grails.project.fork = [ : test: false, // ← run: [maxMemory: 768, minMemory: 64, debug: false, maxPerm: 256, forkReserve:false], war: [maxMemory: 768, minMemory: 64, debug: false, maxPerm: 256, forkReserve:false], console: [maxMemory: 768, minMemory: 64, debug: false, maxPerm: 256] ]
(4) テストコードをGRAILS_PROJECT/test/functional配下に作成。
ここでのテストコードは、Gebの機能を使う必要はなく、HttpClientなどを使用した通常のSpockコードで良い(もちろんGeb使っても良い)。
Grails/Groovyでのカバレッジ取得に関してのTIPS
プリミティブ最適化を抑制することでブランチカバレッジをましなものに
- Test Code Coverage Plugin(http://grails.org/plugin/code-coverage)
しかし、上記を使用した場合、分岐網羅(ブランチカバレッジ)は多くの場合、期待する値が取得できない。この理由の1つは、Groovy 1.8以降で導入されたプリミティブ最適化によって、型がプリミティブかどうかによっての条件分岐を行うコードがGroovyのコード生成器によってバイトコード上生成されているためである。Test Code Coverage Pluginはバイトコードレベルでカバレッジ情報を収集するので、ソース上に表われない暗黙の分岐をカバレッジ率の分母に計上してしまう。そしてその値は一般には100%にすることが困難である。
本来Groovyにおいて、プリミティブ最適化を抑止するためのコマンドラインオプション「--disableopt int」が存在する。しかし、GrailsではGroovyをコマンドラインから起動するわけではなくオプションが指定できない。
この問題の対処の一つは、この記事のリンク先にある、「renataogarcia/disableOptimizationsTransformation · GitHub」のコンパイル結果jarである
「DisableOptimizationsTransformation-0.1-SNAPSHOT.jar」をダウンロードし、クラスパスに通すことである。こうすればGrails上でのGroovyの最適化が抑制されるため、ブランチカバレッジが正確に測定できる可能性がたかまる(ただし、ブランチカバレッジが期待する値にならない理由はこれだけが原因とは限らないことに注意*1 )。
なお上記jarをクラスパスに通す方法として以下が考えられる。
どの方法でも良いが、「試験では最適化抑制をし、プロダクトコードでは最適化する」ということはリスクになるため、テスト時に常に抑制するのは怖い気がする(プロダクトコードでも最適化抑制するなら別だが)。なので、テストのとき常に、ではなく、カバレッジ判定のときだけ一時的な指定をするという意味で1でも良いかもしれない。
なお、上記の設定前後でgrails cleanを実行した方がよい。
Dockerを使ってGrails開発
Grails開発でdockerを使用するためのDockerfileを、備忘録兼ねて晒します。開発中に使うものです。運用用は、別のものになるでしょうかね。
onesysadmin/docker-grailsを元にして、Proxy設定を行い、いくつかの工夫をしております(プロキシ設定については、Dockerコンテナに透過的プロキシ設定とかをすれば不要なのかも)。
利点
Grails開発でDockerを使う利点は以下の通り。
- 準備が簡単。Grailsのインストールはもとより、gvmやJDKのインストールすらいらない。grails wrapperなどもいらないわけだ。
- 環境を汚さない。JDKのインストールをしなくてすむ、Postgresなどをコンテナに封入して連携する、などによる。
- 設定含めた開発時実行環境を開発メンバー間でシェアできる
- Dockerはlinuxなので、開発環境windows、実行環境linuxによる差異をなくせる。windowsの場合、boot2dockerなどを使って仮想マシン上でlinuxを動かすことになる(MacOSXではDocker 1.3のboot2dockerでホストのフォルダの共有とVOLUME対応(-v指定による共有)がされているそう]だが、まだ試していない。)。
Dockerfileの準備
まず、onesysadmin/docker-grailsをcloneするなりダウンロードするなりして、使用したいGrailsのバージョンのDockerfileを、Grailsプロジェクトのトップディレクトリにコピーします。
Dockerfileの修正
以下に修正点を示します。修正したDockerfile全体はこちらのgistに。
Proxyの設定
開発環境がファイアウォール背後でproxyの設定が必要になる場合は以下のように設定します。今回の場合、プロキシは以下において必要になります。
ENV PROXY_HOST 10.2.3.4 ENV PROXY_PORT 18080 ENV http_proxy http://${PROXY_HOST}:${PROXY_PORT}/ ENV https_proxy http://${PROXY_HOST}:${PROXY_PORT}/
ENTRYPOINT, EXPOSEの設定
ENTRYPOINT [ "/usr/local/bin/gvm-exec.sh", "grails", "-Dgrails.work.dir=/app/.grails" ] EXPOSE 8080
gvm-exec.shや後で出てくるgvm-wrapper.shは「onesysadmin/docker-grails」の親であるイメージ「onesysadmin/gvm」が入れてくれているスクリプトです。
RUN関係
RUN gvm-wrapper.sh install grails 2.4.4 && \ gvm-wrapper.sh flush archives && \ cd /app && \ gvm-exec.sh grails -Dgrails.work.dir=/app/.grails add-proxy client --host="${PROXY_HOST}" --port="${PROXY_PORT}" && \ gvm-exec.sh grails -Dgrails.work.dir=/app/.grails set-proxy client RUN mv /root/.grails/ProxySettings.groovy /root/.grails/ProxySettings.groovy.bak && \ sed -e "s/'http.nonProxyHosts':''/'http.nonProxyHosts':'localhost'/" /root/.grails/ProxySettings.groovy.bak > /root/.grails/ProxySettings.groovy
proxyの設定がポイント。interactiveにしたときでrun-appしてstop-appして再起動したときにportが所有されつづけてしまう問題(address already in useとなる)の回避のため、nonProxyにlocalhostを設定します。
GRAILS_PROJECT/grails-app/conf/BuildConfigの修正
grails.project.dependency.resolutionの「mavenLocal()」を以下のように修正します。
mavenLocal("/app/.m2")
コンテナのビルド
$ cd /path/to/grails/project $ docker build -t you/project .
DBコンテナとの連携
必要に応じて。以下はPostgresを前提とします。
Postgresのコンテナは、以下を参考に「orchardup/postgresql/」を使用します。
- https://registry.hub.docker.com/u/orchardup/postgresql/
- http://deeeet.com/writing/2014/03/20/docker-link-container/
以降、通常のDB接続の設定(ドライバ追加のためにBuildConfig.groovyのdependenciesに「 runtime 'postgresql:postgresql:9.1-901.jdbc4'」を追加するなど)を行なった上での設定です。
DataSource.groovyの修正
driverClassNameやDB認証などの修正
dataSource { pooled = true // jmxExport = true driverClassName = "org.postgresql.Driver" username = System.getenv("DB_ENV_POSTGRESQL_USER") password = System.getenv("DB_ENV_POSTGRESQL_PASS") }
JDBC URLの設定
必要な環境について、URLを設定します。
url = "jdbc:postgresql://${System.getenv('DB_PORT_5432_TCP_ADDR')}:${System.getenv('DB_PORT_5432_TCP_PORT')}/docker"
「docker」はorchardup/postgresqlが初期的にCREATEしてくれるデータベース名です。別の名前にしたいときは、orchardup/postgresqlの起動時にDB名を指定できるので、それにあわせたものを使えば良いでしょう。
DB連携したGarilsコンテナ実行(例)
$ docker run -d -p 5432:5432 -e POSTGRESQL_USER=docker -e POSTGRESQL_PASS=docker --name pg orchardup/postgresql:latest $ docker run -d -p 8080:8080 -v /path/to/grails/project:/app --link pg:db you/project:latest run-app
おまけ
ENTRYPOINTを指定しているので、bashなどを起動したい場合は、
$ docker run -it -v /path/to/grails/project:/app --link pg:db --entrypoint "bash" you/project:latest
こうかな。
まとめ
上記は、Grailsに限らず、開発時にdockerを使用する場合のパターンの一つになると思うが、ホストのディレクトリのマッピング(-v)を駆使してソース含めほとんどのファイルを外に置くコンセプトなので、単独実行するのと使用感も制約もあまりかわらない。しかし、このパターンだと、リモート利用、つまりdockerクライアントを実行するマシンとdockerデーモンが動作するマシンが別の場合には利用できない。つまりdockerデーモンを動かしているホストのlinuxマシンにログインできるアカウントがないと利用できない。dockerは単体でNFSや高速rsyncみたいなものを提供しないのが残念である。できればもっと開発用PaaSっぽく使えるのに!(ADDじゃ遅そうだし動的更新ができない)*2。
このパターンであれば、Eclipse/GGTSなどをdockerで実行するのもそんなに難しくはない。
*1:なお、grails.project.dependency.resolver = "maven"にしているのにもかかわらず、/root/.grails/ivy-cacheが作成され何かが入っている。理由不明
*2:将来的にはFUSEで対応されるかもしれないんだって。やったー。
PHPのようにGrailsを使う方法
GrailsアプリケーションではGSPのビューは以下の方法で表示できます(他にもあるかも)。
- コントローラーアクションの結果として表示
- renderで指定したビュー
- コントローラ名とアクション名から定まるデフォルトのビュー
- ビューを直接表示する
- URLMappingでURLパターンからビューを指定する
しかし、「ビューだけ」の、GSPの集合だけで機能するサイトは作れません。JSPで言うと、JSPだけのServlet無しのWebサイト、あるいはPHPの埋め込みページだけで構成するようなサイトが作れないのです。
そうなのはおそらく意図的であり、それがWeb開発の黎明期にアンチパターンとしてみなされてきたので排除されているのでしょう。上記のような制約「GSPはコントローラからの画面遷移を経て表示される」があることのメリットは、GSP内で使用できるデータの由来を明確化できること、そして、画面遷移のリンク情報をページの中にばらまくのではなく、コントローラのコードで指定することでしょう。
ただ、そうだとしても、以下のような開発手順をとりたいときがあります。
- 開発時に、まずHTMLでのモックアップがある(デザイナーや企画者が作成するなど)
- それをGSPに一括変換する
- 適宜以下を実施する
- FORMの部分をGSPタグ<g:form>でおきかえる
- レイアウトをSiteMeshでおきかえる
- 静的リソースはアセットにして…
現状のGrailsでは、上記の手順にはなりません。ページの持つ機能をコントローラ・アクションの階層に分割し、そしてコントローラアクションの結果としてのページ群に対応させ…、というトップダウンの機能設計をせざるを得ず、直接画面確認に行けないのです。
本記事では、この問題を解決するために、ページ間遷移について個別のコントローラ無しGSPの集合としてアプリを作る、つまり「PHPのようにGrailsを使う」方法を紹介します。
DispatchController
showPageというアクションを1つだけ持った以下のコントローラDispatchControllerを用意します。
package common class DispatchController { /** grails-app/view/pages配下に配置された、コントローラに所属しない * (コントローラのアクション名と一致していることを期待せず、リンク * をたどることやメニュー選択の結果として表示されることを想定して作 * 成されている)ページを表示する。 */ def showPage() { render view:"/pages/${params.pageName}" } }
URLMappingに以下を追加します。
"/pages/$pageName"(controller: "dispatch", action: "showPage") "/"{ controller = "dispatch" action = "showPage" pageName = "index" }
これで準備は終わり。あとは
配下に、たとえば
- <GRAILS_PROJ>/grails-app/views/pages/index.gsp
- <GRAILS_PROJ>/grails-app/views/pages/a.gsp
- <GRAILS_PROJ>/grails-app/views/pages/c.gsp
というGSP(htmlの拡張子をgspにリネーム)を置けば、
<a href="${request.contextPath}/pages/a">..</a>
というようなリンクで相互にGSPビューを表示できるようになります。views/pages/index.gspがトップページになります。
あもちろん、<g:link>とか使っても良いです。
まとめ
コントローラ・アクションの編成の方法論というのは、Grails初学者にとっては難しいところです。
Scaffoldではドメイン/アクションをベースにしたビューを生成してくれますが、Webサイトというものが一般には「DBテーブルエディタ」ではない以上、この構造は必ずしも一般的ではなく、参考にならないときもあります。
テーブル単位のCRUDに限らない、コントローラ・アクションの階層をどう編成すべきか。この問いに、なんとか解を出さないと、index.gsp以外の1ページを表示することすらままならないかもしれないわけです。デフォルトのindex.gspのようにページごとにURLMapping追加していってもいいんですが、面倒だし不毛ですよね。
多数のページから呼ばれ得る多数のコントローラの関係があるとき、むしろコントローラ/アクションの階層をページ集合とは独立した機能群として設計する方がやりやすいときもあるでしょう。
このPHPぽいアプローチならば、動くサイトを拡張しながら、コントローラを都度考えていくことができます。また、それとは別に、Grailsの初心者にとってのとっつきは、はるかに良くなると思います。
もちろん、この技法と通常のコントローラベースのアプローチを組合せても全く問題ありません。
問題もありえます。この方法だけで、大規模サイトとか業務システムのサイトの全体をつくるべきではないでしょう。単機能の1ページサイトでも不要でしょう。でも、ヘルプページとか、社長のご挨拶ページとか、サイトマップとか、そういうページを含むサイト全体を作るために、この技法を織り交ぜて適用するのが有用なことがあるでしょう。
ということで、選択肢の一つとして心にとめておいておくと便利なのではないかと思い、紹介させてもらいました。
ヒアドキュメントと複数行文字列について
「ヒアドキュメント」をなんで「ヒアドキュメント」っていうかを調べてみた。以下が参考ページ。
- http://programmers.stackexchange.com/questions/143918/why-is-it-called-a-here-document
- http://ja.wikipedia.org/wiki/%E3%83%86%E3%83%AC%E3%82%BF%E3%82%A4%E3%83%97%E7%AB%AF%E6%9C%AB#.22Here_is.22_.E3.82.AD.E3.83.BC
わかったこと
上記の内容が正しいとして、読み取ったことは以下のとおり。
- 「ヒアドキュメント」は「"Here is" document」から来ている。
- "Here is"は何からきているかというと、昔テレタイプ端末にあった「Here is」というキー。
- 「Here is」キーは何かというと、押すとあらかじめ端末ごとに設定できた20文字ぐらいの文字列をホストに送り返すキー。この用途は、例えば、その文字列に端末の識別IDなどを設定しておいて、1キーで「この端末はXXXだよ」とホストに送ること。
- さらに、ホストが送ったENQという制御文字を端末が受けとると、自動的にHere isキー登録文字列をホストに送信するように設定することも可能。ENQの送信は、ホスト側から、ログインしてるあんたの端末どれよ/あんた誰よ(操作者の名前をhere isキー文字列に登録しておいた場合か)、という情報の問合せをするのに用いられた。
ShellスクリプトにおけるHere documentの意味について
上記参考リンクには、それほど詳細には説明されていないので、ここからは推測も交えて、になる。
Shellスクリプトにおけるヒアドキュメントは、Shellが起動する、コマンドのプロセスに対して特定の文字列をパイプ(もしくはシングルタスクOSでは一時ファイル?)を通じて送りつける、という機能である。
cat <<EOT ... EOT
上では、プログラム中に書かれた固定文字列「...」の部分をShellが切り出し、そのデータをパイプ(やひょっとしたら一時ファイル)を通じてcatプロセスに送り込んでいる。これがホストに対してHere isキー登録文字列(=固定文字列)を送付している振舞に似ている。だからこの機能をHere (is) documentと名付けたのではなかろうか。
複数行を含むことができる文字列定数をヒアドキュメントと呼ぶことについて
時はながれて。
これを調べたのは、現代のプログラミング言語のいくつかにおいて、「改行文字を含むことができる」程度の機能をもった文字列定数の表記法を、「ヒアドキュメント」と呼ぶ場合があることにもともと違和感があったからである。
Shellスクリプトにおいては、任意の文字列を処理対象としてコマンドに渡すのにパイプやファイルを経由するしかない場合があるが、その処理を簡易に記述する機能を「ヒアドキュメント」と呼んだのは、当時において合理性があったように思われれる。(まさか、"Here is"キーがその後消滅することなんか、誰にも想像つかないし!)
また、PerlやRubyなどの言語における「ヒアドキュメント」機能は、その背景機構や元々の命名理由とは無関係にShellスクリプトとの表記上の類似性だけでそう呼んでいると推測できる。なぜなら、Shellにあった、起動したプロセスにパイプ繋いで送り込む、という様相が存在しないためである。このことを批判するつもりは別にないが、Shellスクリプトにおいて「ヒアドキュメント」という名称が背後機構をちゃんと説明するものであった、という利点は失うこととなっている。
しかし、PythonやGroovyなどの複数行文字列定数で使用する"""〜"""などには、表記上の類似性すらもないので、ヒアドキュメントと呼ぶ必要が全くないと思う*1。なので自分はそう呼ばないことにしている。なので「プログラミングGroovy」の本にもヒアドキュメントという用語を使うことは意図的に避け、確か複数行文字列定数と呼ぶように徹底したのであるよ。
(2014/12/24)
記憶をたどれば、昔のGroovy(Classic Groovy)には、「本当の(RubyやPerlの意味での)」ヒアドキュメントが実際にあったのですが、JSRに提案される段階で削除されました。ヒアドキュメントの廃止 - どうせ見苦しいですから (smile)。もし使いたい場合はかわりにトリプルクォートを使いましょう
この意味でも、Groovyの複数行文字列定数をヒアドキュメントと呼ぶのはまぎらわしい。
*1:改行を含んでいるとドキュメントっぽいから、ということが理由なら、バックスラッシュ('\')で行末エスケープした通常の文字列定数もヒアドキュメントと呼ぶべき。さらに「ヒア」の意味があるものすべてにヒアをつけるべき。ヒア整数、ヒア引数、ヒア関数…
ペアワイズ法でSpockのテストデータを生成する
(関連記事:http://uehaj.hatenablog.com/entry/2015/10/18/155107 )
JCUnitというすばらしいJUnitの拡張があります。
JCUnitではペアワイズ法もしくはオールペア法という手法でテストデータを生成します。ペアワイズ法というのは、試験対象コードに対するパラメタのバリエーションテストをする際に、より少ないテストデータの件数で、効率良くバグを発見できるというテストデータの選択技法だそうです。(オールペア法の記事)
折角の自動テストならば、テストデータも自動生成してしまおうと。しかし単純に総当たりだと、指数的にケース数が増えて手に負えなくなるので、統計学に基づいて、優れているとされている方法でデータを選択し実用的には十分なようにしよう、ということです。
JCUnitはJUnit4のRunner(@RunWithアノテーション)を使って実現されていますが、本記事では、そのエンジンだけを呼び出すことで、JCUnitをSpockのデータドリブンテストで使う方法を紹介します。
方法
Spockのテストケースで以下のように「genPairwiseTestData」を使用してデータを生成します。genPairwiseTestDataはJCUnitの機能を呼び出すためのラッパーとして機能するstaticメソッドで定義は後述のテストコード全体に含まれています。
@Unroll def "分配法則(a=#a,b=#b,c=#c)"() { expect: c*(a+b) == c*a + c*b where: [a,b,c] << genPairwiseTestData(new Object(){ @FactorField public int a,b,c } ) }
上記中、「[a,b,c] <<は」Spockのデータパイプという機能で、a,b,cというテストコードで使用する変数に対して、3要素のリストの集合を流し込むことで、パラメータのバリエーションに対してテストコードが複数回呼び出されるというものです。@Unrollはさらにそれをテストレポート上で複数のテストメソッドがあるかのように表示するアノテーションです。これらはいずれもSpockの機能に乗っとっていて、データパイプに流し込むデータをJCUnitの機能を使って生成することで、機能連携しています。
@FactorFieldはJCUnitが提供するアノテーションで、JCUnitで指定できる機能を利用できます*1。
上記では、[a,b,c]がすべてintの場合ですが、型がもし違っていれば、
[a,b,c] << genPairwiseTestData(new Object(){ @FactorField public int a @FactorField public String a @FactorField public double c } )
のようにそれぞれ指定します。<<の左辺に表われる変数(上記ではa,b,c)は、右辺の@FactorFieldで同じ順序で一回ずつ指定される必要があります(冗長だが、現状の仕組みでは仕方がない)。
長いテストコード例
以下は単独で実行できるように@Grabで指定したSpockテストコードです。Grails中のSpockテストコードやGradleからの使用では適切な依存関係の指定で置き換えることになるでしょう。
groovy JCUnit.groovy
で実行できます。
@Grab('com.github.dakusui:jcunit:0.4.10') @Grab('org.spockframework:spock-core:0.7-groovy-2.0') import com.github.dakusui.jcunit.core.FactorField; import com.github.dakusui.jcunit.generators.TupleGeneratorFactory; import com.github.dakusui.jcunit.core.tuples.Tuple; import com.github.dakusui.jcunit.generators.ipo2.IPO2; import com.github.dakusui.jcunit.generators.ipo2.optimizers.GreedyIPO2Optimizer; import com.github.dakusui.jcunit.constraint.constraintmanagers.ConstraintManagerBase; import com.github.dakusui.jcunit.constraint.ConstraintManager; import com.github.dakusui.jcunit.exceptions.UndefinedSymbol; import spock.lang.* class HelloSpec extends Specification { static ConstraintManager closureConstraintManager(List<String> names, Closure c) { return new ConstraintManagerBase() { @Override boolean check(Tuple tuple) throws UndefinedSymbol { Map map = [:] for (name in names) { if (!tuple.containsKey(name)) { throw new UndefinedSymbol(); } map[name] = tuple.get(name) } c.delegate = map c.call() } } } static Collection genPairwiseTestData(Object object, Closure constraint = null) { def tg = TupleGeneratorFactory.INSTANCE.createTupleGeneratorForClass(object.class) def names = tg.factors.collect{it.name} def cm if (constraint == null) { cm = tg.constraintManager } else { cm = closureConstraintManager(names, constraint) } IPO2 ipo2 = new IPO2(tg.factors, 2, cm, new GreedyIPO2Optimizer()) ipo2.ipo() return ipo2.result.collect{ testData -> names.collect{ testData[it] } } } @Unroll def "分配法則(a=#a,b=#b,c=#c)"() { expect: c*(a+b) == c*a + c*b where: [a,b,c] << genPairwiseTestData(new Object(){ @FactorField public int a,b,c } ) } @Unroll def test1() { expect: c*(a+b) == c*a + c*b where: [a,b,c] << genPairwiseTestData(new Object(){ @FactorField public int a,b,c }, { a > 0 && b != c }) } @Unroll def test2() { expect: (a+b).size() == a.size() + b.size() where: [a,b] << genPairwiseTestData(new Object(){ @FactorField public String a,b }) } @Unroll def test3() { expect: a+b == b+a where: [a,b] << genPairwiseTestData(new Object(){ @FactorField(intLevels=[1,2,3]) public int a @FactorField public int b }) } @Unroll def test4() { expect: a == b || a != b where: [a,b] << genPairwiseTestData(new Object(){ @FactorField public MyBoolean a @FactorField public MyBoolean b }) } } enum MyBoolean { True, False }
データの生成のされかた
3つのintデータは、デフォルトでは以下のデータがペアワイズ法で生成されます。1, 0, -1, 100, -100, Integer.MAX_VALUE, Integer.MIN_VALUEの7種類の値が、a,b,cのパラメータに流し込まれるので、全組み合せだと7^3=343ケースが実行されるところを、53テストケースに絞り込まれていますね。
[[a:1, b:1, c:0], [a:1, b:0, c:100], [a:1, b:-1, c:-1], [a:1, b:100, c:-100], [a:1, b:-100, c:2147483647], [a:1, b:2147483647, c:-2147483648], [a:1, b:-2147483648, c:1], [a:0, b:1, c:100], [a:0, b:0, c:2147483647], [a:0, b:-1, c:-100], [a:0, b:100, c:0], [a:0, b:-100, c:1], [a:0, b:2147483647, c:-1], [a:0, b:-2147483648, c:-2147483648], [a:-1, b:1, c:2147483647], [a:-1, b:0, c:-2147483648], [a:-1, b:-1, c:1], [a:-1, b:100, c:-1], [a:-1, b:-100, c:100], [a:-1, b:2147483647, c:-100], [a:-1, b:-2147483648, c:0], [a:100, b:1, c:-2147483648], [a:100, b:0, c:-1], [a:100, b:-1, c:0], [a:100, b:100, c:1], [a:100, b:-100, c:-100], [a:100, b:2147483647, c:2147483647], [a:100, b:-2147483648, c:100], [a:-100, b:1, c:1], [a:-100, b:0, c:0], [a:-100, b:-1, c:-2147483648], [a:-100, b:100, c:100], [a:-100, b:-100, c:-1], [a:-100, b:2147483647, c:100], [a:-100, b:-2147483648, c:-100], [a:2147483647, b:1, c:-100], [a:2147483647, b:0, c:1], [a:2147483647, b:-1, c:100], [a:2147483647, b:100, c:2147483647], [a:2147483647, b:-100, c:0], [a:2147483647, b:2147483647, c:-2147483648], [a:2147483647, b:-2147483648, c:-1], [a:-2147483648, b:1, c:-1], [a:-2147483648, b:0, c:-100], [a:-2147483648, b:-1, c:2147483647], [a:-2147483648, b:100, c:-2147483648], [a:-2147483648, b:-100, c:-2147483648], [a:-2147483648, b:2147483647, c:1], [a:-2147483648, b:-2147483648, c:2147483647], [a:-100, b:2147483647, c:2147483647], [a:-2147483648, b:1, c:0], [a:-2147483648, b:-100, c:100], [a:1, b:2147483647, c:0]]
他のデータ型について、デフォルトではどのようなデータの集合(レベル)をつかってデータが生成されるかはこちら。
Enumは勝手に全要素を組合せてくれます。
データ集合の指定
元になるデータの集合(レベル)を指定することもできます。たとえば、intについて1,2,3の3通りのデータからの組合せを生成するには、以下のようにします。
@FactorField(intLevels=[1,2,3]) public int a
制約
データが満たすべき制約條件を与えて、テストケース数をふるいにかけて減らすこともできます。
以下では、変数間の関係がa > 0 && b != cを満たすテストケースに絞り込みます。
[a,b,c] << genPairwiseTestData(new Object(){ @FactorField public int a,b,c }, { a > 0 && b != c })
Groovyなので、クロージャで制約式を与えられるようにしてみました。
おまけ
生成されるテストデータは毎回かわるわけではないので、テストデータをファイルにキャッシュする仕組みがあると良い気もする。
まとめ
JCUnitすばらしい!Spock便利! Groovy超便利!!
追記(2014/10/27)
JCUnitの更新情報として作者の方からコメントを頂いています。記事中では使用していませんが、'levelsFactory'メソッドが0.4.11か0.4.12で'levelsProvider'に改名されていますので注意。
Elmでやってみるシリーズ13:あらためてシダを描く
間が少しあいちゃいましたが、実は続いていたこのシリーズ、「あらためてシダを描く」です。
この図形は「バーンズリー(バーンズレイ)のシダ」と呼ばれる有名な図形で、以前各種の言語で実装するのが少し前に流行ったのが記憶に新しいところです。アルゴリズムなどについては詳しくはこちらをどうぞ。
実は、これについては、前にも「「プログラムでシダを描画する」をelmで描画する - uehaj's blog」で作ったのですが、当時はElmを良く知らずに試行錯誤で作ったものなので、現時点での知見をもとにして再度とりくんでみました。
このデモは計算量が大きく、さらにiframeでブログ内にインライン表示すると、なんらかの理由で非常に負荷が高くなる*1ので、今回はインラインデモは割愛します。全画面表示はこちら。
ソースはこちら。
ドラッグすると、パラメータが変更されて新たなシダ図形が描画さます。
ポイント
- Main.elm(メイン), Leaf.elm(葉の表示)、Gauge.elm(ゲージの表示)の3モジュールに分割しました。Elmアプリケーションをモジュールに分割するにあたっては、Signalや状態更新の記述を独立性高く分割する方法について考えさせられました。
- 結論から言うと、
- モジュールに含まれるのが、純粋関数だけの場合には、思考すべきことはあんまりない。何を公開し何を隠蔽するかを吟味しておけば良い*2
- モジュールが状態を保持する場合(そのコードにfoldpを含む場合)、モジュールごとにぞれぞれ以下のようにパートを分けて、単独実行可能なアプリケーションにする(参考:GameSkeleton.elm)。
- Define constants
- User input
- State
- Update
- Display
- puts it all together and shows it on screen.
- その上で、メインプログラムからモジュールの状態更新(nextStateのような関数)、および現在状態を履歴から計算するfoldpの呼び出し等をどう扱うかについては以下から選択する
- 定時間隔タイマ(fps,..)を使った状態更新が、機能上不要なモジュールであれば、モジュール側がliftしてSignal Elementを返しても良い。つまりイベントハンドラとステート管理をモジュール側で実装しても良い。
- あるいは、定時間隔タイマを使うモジュールであっても、それを使うMain側が定時間隔タイマを使わないのであれば、ステート管理をモジュール側で実装してもよい。
- しかし、モジュール側もMain側も定時間隔タイマを使う、もしくはMain側が定時間隔タイマを使うかどうか限定できない(一般ライブラリにする場合はそうなる)のであれば、両方でステート管理を実装すると、少なくとも現行Elmコンパイラが生成するJSコードにおいては性能劣化が激しい。なので、以下のようにする。
- モジュール側では、
- そのモジュールに関するStateレコードを定義し公開する
- nextState, initStateに相当する処理を公開
- 呼び出す側のMainでは、MainのStateレコードの1つのフィールドとしてモジュールのstateを含める。
- 呼び出す側のMain側では、
- nextStateの処理で、モジュール側のnextStateを呼び出し、MainのStateを更新する。
- foldpを実行するのはMain側のみ。
- モジュール側では、
- その上で、モジュール側は、自分のfoldpを呼び出すmainを定義しても良い(Mainからは呼ばれない)。こうすることで、それぞれのモジュールを単独でも実行可能なElmアプリにできる。これはモジュールのデモや試験に有用。たとえば今回、赤のゲージはゲージ表示モジュール単独で実行できます(→Gauge.html)し、葉の表示モジュールでは、アニメーションや操作に反応しない葉っぱを表示させています(→Leaf.html)。
- リアクティブっつーぐらいで、反応性重要
- このための工夫として、ドラッグ動作中は描画・計算する点の数を減らすようにしました。
- しかし、本来はこれは2モードにならなくても良いはずなのです。しかし残念ながら、現在のElmでは「計算中のマウスカーソル移動イベント」の検出の追随性が問題になります。(後述)
- 前回同様以下の問題がありました。
- 描画する点列の数nは実行を継続すればするほど増えていくのですが、Elmの標準ライブラリのGraphics.CollageおよびGraphics.Elementなどが提供するグラフィックスモデルは、ペイント系というよりドロー系で、描画されるデータが保持されるというものです。Canvasに書き残る、ということがないです(純粋だというわけですね)。
- この結果、nに比例して描画時間が増えていきます。計算結果の点列([(Int,Int)])は、foldpで累積的に追加していけるので、O(1)なのですが、累積的に伸びた結果であるpoints:[Form]の描画時間はO(1)ではなく、O(n)なのです*3。
- この結果、何も考えないと、放置しておくとだんだん重くなってマウスクリックに反応しなくなります。回避策として以下を実施しています。
- 1フレームの描画に要した時間を計測し、しきい値(例1秒)を越えるようなら、点列の追加ペースを落す(半減させる)。最終的に追加ペースは0になるので、そうなったら表示内容は収束し変化しなくなる。
- 他の方針としては、まにあわなくなってきたら解像度とか計算精度落して、とかの戦略もありそうだったが、今回はできなかった。
- 本来なら、Elm開発者Evanczの論文「Elm: Concurrent FRP for Functional GUI」によれば、Elmには「async」というプリミティブがあり、長い計算処理は適当な粒度で非同期イベントとしてイベントキューに再キューイングを行うことで、「ジャイアントUIスレッドロック」のようなことが起きなくなり、レスポンスを損なわずに長い計算ができるはずでした。しかし現状のElm実装にはasyncプリミティブはまだ実装されていません(そのことや理由も上記論文に書いてある*4し、解決されるべき課題として議論されている。)。(追記)asyncの代用として、Time.delay 0が使用可能だそうですが試してない。
- 問題視してみましたが、例えば10000個とかの固定数で計算を打ち切れば良い話です。これをしなかった理由は、高速なCPUをもったマシンでは多数の点列を表示してゴージャスな表示、遅いマシンでは点数が少なくて、ちょっぴりみすぼらしいが表示はされる、というようなことを実現しようとしたかったからです。点数が多いと綺麗な表示になりますからね。
- 諸事情によりElm 0.13のスナップショット版を使用。これはリリース版ではないので、自前でコンパイルしたい方は、googlegroupsのelm-discussionを見て適当にダウンロードしてください。
関連エントリ
「Elmでやってみる」シリーズのまとめエントリ - uehaj's blog
- 作者: Miran Lipovača,田中英行,村主崇行
- 出版社/メーカー: オーム社
- 発売日: 2012/05/23
- メディア: 単行本(ソフトカバー)
- 購入: 25人 クリック: 580回
- この商品を含むブログ (67件) を見る
- 作者: Miran Lipovaca
- 出版社/メーカー: オーム社
- 発売日: 2012/09/21
- メディア: Kindle版
- 購入: 4人 クリック: 9回
- この商品を含むブログを見る
- 作者: Graham Hutton,山本和彦
- 出版社/メーカー: オーム社
- 発売日: 2009/11/11
- メディア: 単行本(ソフトカバー)
- 購入: 14人 クリック: 503回
- この商品を含むブログ (117件) を見る
- 作者: Simon Marlow,山下伸夫,山本和彦,田中英行
- 出版社/メーカー: オライリージャパン
- 発売日: 2014/08/21
- メディア: 大型本
- この商品を含むブログ (2件) を見る
*2:ただし、Elmの現バージョンでは、エラーメッセージが不適切・もしくは全く出ないことがある(mainをexportしてない、拡張レコード型で暗黙に定義されるデータコンストラクタ関数が、個別export( (..)ではなく個々の関数名指定するケースで)未exportになる…)ので注意。
*3:virtual domのように、FormやElementから構成されるツリーの差分を検出し、差分だけをcanvas反映する機構があれば高速化できるかもしれませんね。でもいかにも難しそうですな。
*4:本質的には、JSにスレッドがないせいなんですが、将来的にはHTML5のWebWorkerでスレッドプーリングも併用するか、クロージャ本体を文字列にしてevalしてしかし引数をキャプチャ不要にしてなんとかとか。CPSに書き換えたりしてもできるんじゃないかな。