クラスの依存関係グラフ

Java のクラスの依存関係を調べてみた。

規模が大きいアプリケーションの方が統計が取りやすいので、Apache Hadoop の 3.0.0-alpha バージョンを対象に調べる。テストとアノテーションを除いた、Hadoop 由良のパッケージに含まれるクラスに依存関係のグラフについて調査する。

クラス間の依存関係を調べるのに、JDK8 に含まれる jdeps というコマンドを使った。jdeps に -verbose オプションをつけると 各 jar 内のクラスが依存しているクラスが出力される。-dotoutput オプションで DOT ファイルに結果を保存することができる。下のようなファイルが出力される。

digraph "hadoop-mapreduce-client-common-3.0.0-alpha1.jar" {
    // Path: ./mapreduce/hadoop-mapreduce-client-common-3.0.0-alpha1.jar
   "org.apache.hadoop.mapred.LocalClientProtocolProvider" -> "java.io.IOException";
   "org.apache.hadoop.mapred.LocalClientProtocolProvider" -> "java.lang.Object";
   "org.apache.hadoop.mapred.LocalClientProtocolProvider" -> "java.lang.String";
   "org.apache.hadoop.mapred.LocalClientProtocolProvider" -> "java.net.InetSocketAddress";
   "org.apache.hadoop.mapred.LocalClientProtocolProvider" -> "org.apache.hadoop.classification.InterfaceAudience (not found)";
   "org.apache.hadoop.mapred.LocalClientProtocolProvider" -> "org.apache.hadoop.classification.InterfaceAudience$Private (not found)";
   "org.apache.hadoop.mapred.LocalClientProtocolProvider" -> "org.apache.hadoop.conf.Configuration (hadoop-common-3.0.0-alpha1.jar)";

依存先のクラス名の後ろに( ) 内にパッケージ名が付く。(not found)はパッケージが見つからないという意味です。これがあると→の元と先が同じクラスでも別の名前になってしまうので、sed で取り除く。

今回は、対象の JAR 内のクラスの関係のみを調べたい。そこで、辺が示す先がorg.apache.hadoop以外から始まるパッケージに含まれるクラスとなっている辺は除いた。また、org.apache.hadoop.classification パッケージのクラスも、今回の対象のJAR内のクラスではないので、それらが先にある辺も除いた。

JAR ごとに DOT ファイルが生成されるが、全部をまとめてひとつのグラフとして調べる。

頂点が 7844, 辺が 36076 のサイズの有向グラフとなっている。

graph-tool だと描写が非力だったので、gephi で可視化をする。

f:id:fjkz:20161111223015p:plain

上の画像はグラフを可視化したものである。大きくて色の濃い丸が、in/out を問わない次数の大きい頂点である。辺の色は頂点の色に基づく。少ない頂点のみが次数が大きく、ほとんどの頂点は次数が小さいことが可視化の結果から予想できる。

そこで、次数と、その次数のクラスの数の関係を調べてみる。

f:id:fjkz:20161111203145p:plain

上の図は、次数とその次数を持つクラスの割合を示している。入力次数・出力次数いずれの場合も、次数が10以上100以下のクラスはべき乗則に従っているように見える。どうやらクラスの依存関係のグラフはスケールフリーネットワークの性質を持つ可能性があるようだ。べき係数は 2 程度で、スケールフリーネットワークでは典型的な値となっている。Barabási–Albert モデルのネットワークの成長過程は、ソフトウェアが拡大していく過程と似ていることから、クラスの関係がスケールフリーネットワークとなっていることが説明できるだろう。*1

次数が大きいクラスというのは、ソフトウェアの中で重要なクラスであると考えられる。べき乗則に従うということは、特定のクラスの重要度が飛び抜けて高いということを意味している。

入力次数が大きいクラスは多くのクラスから呼び出される Fan-in なクラスである。何度も再利用されている非常に有用な部品と言える。多くのクラスから利用されていることから、欠陥も少ないと考えられる。一方で、利用者が多いクラスの動きを変える場合は影響範囲が非常に大きくなるので、なかなか変更できあに。そのため、安定度が高いクラスであると言える。

出力次数が大きいクラスは多くのクラスを呼び出している。Fan-out なクラスである。GoFデザインパターンでいうところの Facade となっていて、そのクラスから呼び出される機能が多いということを意味していると思われる。変更の影響を受けやすく、そのため欠陥密度も高いだろう。このクラスを重点的にテストすると効果が高そうだ。

入力次数の大きいところで、べき乗則に従わない特異的に大きい次数を持ったクラスが存在していて、図の右からはみ出してしまっている。具体的には、org.apache.hadoop.conf.Configuration という設定プロパティの値を得るためのクラスが 1368 の入力次数を持っている。このクラスは、非常に多くのクラスから横断的に使用されるクラスである。このようなクラスの存在が、今回調べたソフトウェアに特有のものなのか一般的に存在しうるのかは、他のソフトウェアも調査しないと分からない。もし異常に入力次数が多いクラスが存在するということが一般的であるとしたら、ソフトウェアにスケールフリーネットワークとは異なる特別なネットワーク構造があると言えるかもしれない。

*1:Barabási–Albert モデルのべき係数は 3 で完全な説明はできない。いつかちゃんとまとめたい。

graph-tool で遊ぶ1

複雑な世界のありようを理解するのに、「グラフ」の可視化だとか分析はすごく有用なのではと考え始めたので、グラフについて勉強してみる。

動くおもちゃがないと楽しくないので、いいのかどうか知らないが、検索して出てきた graph-tool というツールで可視化等をしてみる。

graph-tool.skewed.de

インストールは 「Download - graph-tool: Efficent network analysis with python」 に従えばできた。

みんな大好きな Barabási-Albert モデルのネットワークの絵を書いてみる。

import graph_tool.all as gt

# ノード数 1000 の Barabási-Albert モデルのネットワークを作る
g = gt.price_network(1000, directed=False)

# グラフを描写するときのいい感じのノードの配置を計算する。
# どうも斥力と引力が働く粒子の運動を計算して、釣り合いがとれるような場所を計算しているっぽい。
# 試行毎に違う配置になる。
p = gt.sfdp_layout(g)

# グラフを PNG 画像として出力する。
gt.graph_draw(g, pos=p, output="barabasi-albert.png")

f:id:fjkz:20161108195044p:plain

美しい……。

Bash で Power Assert 風のものを作った

GitHub - fjkz/power-assert-bash: Power Assert for Bash

Bash で Power Assert 風のものを作りました。*1

なぜか Bash には assert がありません。なので、 Bash でテストを書くときは、 set -e として、

[ "$actual" == "$expect" ]

とか書きます。しかし、これだと失敗したときに、情報がないので困ります。set -x としてもいいのですが、これ欲しくない表示も出て、ウザイのです。また結果も見づらい。Bats もテストのどこでエラーとなったかは教えてくれるが、変数の中身とかは教えてくれません。

そこで、 Power Assert 風のものを作ってみました。 Power Assert とは、Spockに含まれる assert の結果を美しく表示してくれる機能です。

. power-assert.bash
A="HELLO WORLD"
[[[ "${A}" == "HELL WORLD" ]]]

.source コマンドで power-assert.bash を読み込むと、 [[[ コマンド (実体は関数) が使えるようになります。 シェルスクリプト[ コマンドとほとんど一緒で、条件が否なら 1 のステータスコードを返し、正なら 0 を返します。しかし、[ と異なり、条件が否の場合はエラーメッセージを吐きます。

上記のスクリプトを実行すると、以下のメッセージが表示されます。

assertion error:

[[[ "${A}" == "HELL WORLD" ]]]  ->  false
     |
     "HELLO WORLD"

at function main (sample.sh:3)

まあ、ただこれだけです。 assert_equals みたいな関数を用意するよりは、いいと思っているがどうなのだろう。

*1:○風としているところが、そんなに powerful ではないことを示していますが……。 powerful にしようと思ったら、BashBash のパーサーをつくらんといけない……。まだ綺麗に表示されない条件も多い。

TT-Runnerの記録2:ベータ版

前回:


GitHub - fjkz/ttap: ttap: a testing framework with file system hierarchy

結合テスト用のテストスクリプトを実行するためのツールを作っている。

シェルスクリプトベタ書きのテスト群を整理された状態にまとめられるものが欲しいという、当初の目的は達成できたように思う。1000 LOC にも満たない小さなプログラムだが、欲しいものの仕様にたどり着くのに時間を要してしまった。

英語のドキュメントは下手なので伝わらないだろうから、日本語のドキュメントも書いた。

https://github.com/fjkz/tt-runner/tree/master/doc/jp

しばらく寝かせて、バグ修正だとか異常系の作り込みだとかをしたら、1.0版としたい。


追記

ttap と名前を変えた。

TT-Runner の記録 1

TT-Runner: テストスクリプトのディレクトリ構造フレームワーク - 超ウィザード級ハッカーのたのしみ

GitHub - fjkz/ttap: A Test Scripts Runner

TT-Runner (Test scripTs Runner あるいは Tree Tests Runner) というのをシコシコと作っています。*1

以下、作業記録。

スクリプトのある場所に cd するようにした。

シェルスクリプトはカレントディレクトリ、つまりどこの場所からスクリプトを実行したのか、にシビアである。スクリプトが存在するディレクトリから実行することを期待するのが多いかと思うので、スクリプトを実行する前にそのディレクトリがあるディレクトリに cd するようにした。

カレントディレクトリを変えないオプション (--no-chdir) も念のために用意した。

--stop-on-failure, --skip-all オプションの追加

一つでも operation が失敗したら、残りはすべて skip するオブション --stop-on-failure と、すべての operation を skip するオプション --skip-all を追加した(--dry-run の方がわかりやすいかもしれない)。主にテストスクリプトデバッグ用です。実行にある程度時間がかかるようなテストを想定しているので、こういうオプションは欲しい。

スクリプトの出力をファイルに保存できるようにした

-o オプションで指定されたディレクトリに各スクリプトの結果を保存するようにした。ok/ngだけですべてが分かればいいが、そうはいかないので、ログが残せないと不便だ。result.txt に ok/ng のテスト結果の出力も残す。一応、TAP っぽく出力はしているが、今の出力は本当に TAP として正しいのか。

今後すること

ファイルに結果を保存できるになったら、コンソールへの出力は再考の余地がある。一つ一つのオペレーションが重いことを想定しているので、今のままでは見づらい。何%終わっていて、経過時間はどのくらいかで、失敗があるのかは、コンソールから簡便に確認したい。

JUnit XML も出力できるようにしたい。

テストツールなのにテストがないので、書く必要がある。

ドキュメントは一応書いてみたが、読めないのでまともにしたい。添削してくれる人が欲しい。

可能であれば、TAPを介して、他のテスティングフレームワークと連携できるようにしたいが、それをし始めると大変だろうか。


次回

TT-Runnerの記録2:ベータ版 - 超ウィザード級ハッカーのたのしみ

*1:陽の目は見ないがやる。これが「でも、やるんだよ」ってやつか?

TT-Runner: テストスクリプトのディレクトリ構造フレームワーク

以前書いたテストツールがある程度できたので、公開する。TT-Runner と名付ける。

GitHub - fjkz/ttap: A Test Scripts Runner

結合テスト以降のユニットテストフレームワークがうまいこと使えなくて、スクリプトをだらだらと書いているような場合を想定したツールです。テストスクリプトの中身はシステムによって大きく異なるので、ディレクトリ構造の方を規定してしまおうという考えです。

以下のような、テストスクリプト群があったとする。

sample/test-before-after
├── after1.sh
├── after2.sh
├── before1.sh
├── before2.sh
├── test1.sh
└── test2.sh

これを実行すると、JUnit風に実行してくれるという、単純なツールです。

$ ./bin/tt-runner.py sample/test-before-after
✓ before1.sh.1
✓ before2.sh.1
✓ test1.sh
✓ after2.sh.1
✓ after1.sh.1
✓ before1.sh.2
✓ before2.sh.2
✓ test2.sh
✓ after2.sh.2
✓ after1.sh.2

operations       : 10
succeeded        : 10
failed           : 0
time taken [sec] : 0

SUCCESS

実行ファイルをディレクトリにしてテストをネストさせることもできて、いくつかの既存のテストスイートをまとめて実行したりすることを想定している。

用途が限定的で明確だから、もしかしたら他の人にとっても使いやすくなるかもしれない。少なくともベタ書きシェルスクリプトが増え続けるよりはマシになるだろう。

自分で使ってみて、使いにくいところがあったら、ちょこちょこと直していく予定である。


次回: TT-Runner の記録 1 - 超ウィザード級ハッカーのたのしみ

構造化されたテストスクリプト群の実行ツール

テストコードってどんどん増えていく。だらだらスクリプトを書くのは簡単だが、すぐに収拾が付かなくなり、経済的に耐えられないレベルで混乱してくる。何のテストをしているのかももちろんわからないが、動かし方も分からないし、どうなったらpassなのかも分からないような、テストスイートが乱立して、もう全部すてればと思ってしまうほどになる。

テストの自動化を整備するなら、アーキテクチャが必要だ。ユニットテストは、JUnitやSpockやBatsなどの便利なフレームワークがあるので、散らかりっぷりが抑えられる。フレームワークの良さは、それに従えば勝手に整理されるということだ。

システムテストになってくると、システムによってテストも大きく異なってくるので、テスティングフレームワークと呼べるほどに親切なフレームワークを作るのは難しい。だが、既存のテスティングフレームワークの思想は参考にできることが多い:

  • テストケースで共通化できる処理は、Before / After でまとめる;
  • テストケースは独立である;
  • テストは繰り返し実行可能である。

乱立して増え続けるテスト用のスクリプト群もこれを守るように構造化されれば、経済的なメリットは大きいだろう。

そこで、構造化されたテストスクリプト群を実行するツールを作っている。構造化されていることを期待して実行するので、構造化されていなければテストを実行できず、構造化された状態が保たれるはず――という寸法だ。

test
├── after
│   └── run_destroy.sh
├── before
│   └── run_deploy.sh
├── testsuite1
│   ├── setup_db.sh
│   ├── test1.sh
│   └── test2.sh
└── testsuite2
    ├── post-run.sh
    ├── pre-run.sh
    ├── run1.sh
    └── run2.sh

上のようなディレクトリ構造のテストがあったときに、テストを実行すると以下のようにディレクトリ構造にしたがってスクリプトが実行される。

1..14
ok 1 before/run_deploy.sh # 0.0 sec
ok 2 testsuite1/setup_db.sh # 0.0 sec
ok 3 testsuite1/test1.sh # 0.0 sec
ok 4 testsuite1/setup_db.sh # 0.0 sec
ok 5 testsuite1/test2.sh # 0.0 sec
ok 6 after/run_destroy.sh # 0.0 sec
ok 7 before/run_deploy.sh # 0.0 sec
ok 8 testsuite2/pre-run.sh # 0.0 sec
ok 9 testsuite2/run1.sh # 0.0 sec
ok 10 testsuite2/post-run.sh # 0.0 sec
ok 11 testsuite2/pre-run.sh # 0.0 sec
ok 12 testsuite2/run2.sh # 0.0 sec
ok 13 testsuite2/post-run.sh # 0.0 sec
ok 14 after/run_destroy.sh # 0.0 sec

run, testの前後に同じ階層のbefore/after, pre/post, setup/teardownが実行されていく。

JUnitとちがって、before/afterも1ケースとしている。これはスクリプトファイルを独立して扱いたいという意図である。

それぞれのスクリプトは、シェルスクリプトをベタ書きしても良いし、そこから Ansible や他のテスティングフレームワークなどの別のツールを呼んだりする。

Jenkinsとの組み合わせを意図してTAPで結果を出力するようにしている。おそらく世間では Jenkins のビルドプロジェクトの設定を工夫して、今回したいようなことを実現しているのだろうが、Jenkins と業務が密結合になってしまう。また、Jenkinsを駆使するならば、シェルスクリプトベタ書きと変わらない。間にこういうのをかませたら便利かと思っている。

ある程度できたら公開したい。単純なものは逆に難しかったりする。本当に、JUnit は良く出来ている。


追記 2016-07-24

できた。

TT-Runner: テストスクリプトのディレクトリ構造フレームワーク - 超ウィザード級ハッカーのたのしみ