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:陽の目は見ないがやる。これが「でも、やるんだよ」ってやつか?

CAP定理について

CAP定理という分散ストレージシステムの設計において非常に重要な定理がある。まだ、以下の元の論文を読んでいないので、正確な理解かどうかは保証できないが、理解している範囲で考えることを記す。

https://www.comp.nus.edu.sg/~gilbert/pubs/BrewersConjecture-SigAct.pdf

CAP定理によると、分散システムでは以下の3つを同時に満たすことはできない:

  • Consistency 一貫性、
  • Availability 可用性、
  • Partition tolerance 分断耐性。

それぞれ、

  • all nodes see the same data at the same time,
  • every request receives a response about whether it succeeded or failed,
  • the system continues to operate despite arbitrary partitioning due to network failures

という意味である。*1

分散ストレージシステムはいろいろあるのだけれど、C・A・Pのいずれかを捨てるという選択を行っている。この選択がいわゆる設計思想というものなので、どの選択をしているかでシステムの特徴が分かる。

システムとして、Availability はほとんどの場合必須なので、

  • Consistency + Availability (CA)、 か
  • Partition tolerance + Availavility (PA)

の組み合わせが選択される。Consistency + Partition tolerance を選択している例は私は知らない。*2CA あるいは PA が選択されているシステムとしては以下が挙げられる:

列挙すると分かるが、CA は一貫性をとっているのだから当然だが、厳密でお固いものが並んでいる。一方で、PA の方は柔らかい印象だ。耐障害性・ロバスト性は PA の方が強い。柔らかいから、逆に壊れにくいという感じでしょうか。

どちらを選択するかは用途によるとしか言いようがないが、個人的には PA が好きだ。主観だが、PA の選択の方が設計思想としては新しい。一貫性を保証するというのも難しいので、初期はいかにして一貫性を担保するかを頑張ってきた。しかし、一貫性の問題が解決されたら、実は完全な一貫性はオーバスペックなのでは?――となってきた。一時的に不整合が起こるかもしれないが、それもほとんどないし、仮に不整合がおきても、時間がたてば辻褄が合うから別にいいじゃん――というある意味で割り切った態度を PA を選択したシステムはとることが多い。これを、結果整合性という。

例えば、SMB(Windowsの共有フォルダのこと)*4はネットワークに繋がっていなければ使えないし、遅い。しかし、現実同じファイルを複数人が同時に編集することはあまりしないので、常に最新である必要もない。その用途だと Windows の共有フォルダは非常に使いにくい。MS Office は整合性を保つために編集中のファイルをロックする機能があるが、あれはイライラする。競合が少ないなら、ちょっとぐらい不整合があってもいいじゃんと、Dropbox は割り切っており、この割り切りは合理的だと思う。個人用のネットワークドライブなんかは、私はセンスねえなと思う。

PA が好きなのは、どこか割り切ったところが入っている点で、言葉として変だが、何を割り切るかは割り切れないものだ。完全に Art の領域だ。どこを割り切ったらいいのか、つまり何を捨てて何を取るかということについて、まだ最もよいバランスというのは合意がとれていない。工夫の余地がまだ残っていて、興味深い。

*1:英語版 Wikipedia から引用。https://en.wikipedia.org/wiki/CAP_theorem。日本語版の記事はどうも間違っている気がする。

*2:もちろん、C・A・Pのうち一つだけしか満たしていないものもある。Consistency だけの場合が多いかな? 例えば、初期の HDFS は Consistency しか満たしていなかった。なお、C・A・P以外にもスケーラビリティという別の話があって、Consistency とスケーラビリティは相反するところがある。HDFS は Consistency を満たしつつ、スケーラビリティも持たせるために、非常に大胆な割り切りをしている。

*3:例えば日米間の光ファイバーがサメに食われても、日本のサイトは覗けるのでインターネット全体として止まるわけではないという意味です。一つのサイトと多数のクライアントという風に見れば、CA です。インターネットの強力な耐障害性は PA を選択しているといった「ゆるさ」が理由だが、このネタだけでいくらでも語れるので踏み込まない。気が向けばいつか書く。

*4:クライアントキャッシュがないことを前提としている。SMBのクライアントキャッシュの取り扱いってどうなっているのかよく知らない。

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 - 超ウィザード級ハッカーのたのしみ

コード型ログ(5) Bashで自分のディレクトリを知る

シェルスクリプトで自分のディレクトリを知りたいことがあります。

. ./functions.sh

例えば、上のように他のシェルスクリプトを読み込みたいときに相対パスを使うのはよくないときがあります。なぜならば、./の場所はシェルスクリプトを実行するカレントディレクトリであって、シェルスクリプトのファイルがある場所ではないからです。シェルスクリプトは、どのディレクトリから実行しても動くようにしたいものです。

シェルスクリプトのファイルが存在するディレクトリを知るには以下のようにします。

MYNAME="${BASH_SOURCE:-$0}"
MYDIR=$(cd -P -- $(dirname -- "${MYNAME}"); pwd)

${MYNAME}には今のファイルの相対パスが入っています。

${BASH_SOURCE:-$0}となっているのは、$0にはBash以外では今のファイルの名が入るのですが、Bashだとsourceもしくは.でファイルを呼んだ実行ファイルの名前が入るからです。Bashには今のファイル名を表す変数BASH_SOURCEというのが定義されているので、BASH_SOURCEが定義されている場合にはそれを使い、BASH_SOURCEが定義されていない、つまりBash以外のシェルの場合は$0を使います。

"${A:-a}"というのは変数Aが定義されているときは${A}を返し、そうでないときはaを返すという意味です。なお、:-のセミコロンは省略して${A-a}でも可能です。*1

${MYDIR}には今のファイルのディレクトリの絶対パスが入っています。

dirname -- "${MYNAME}ディレクトリ名を取得します。--はファイル名が-で始まっているような場合にコマンドラインオプションとしてみなされないようにするためです。慣例として--以降の引数はオプションとしてみなされません。

しかし、このままでは相対パスかもしれません。なので、絶対パスが欲しいときには、$(cd -P -- ${RELATIVE_DIR}; pwd)として、絶対パスに変化します。-P${RELATIVE_DIR}シンボリックリンクが含まれていたときにそれを解くオプションです。

. ${MYDIR}/functions.sh

とすれば、どこから実行ファイルを呼んでもfunctions.shを見つけることができます。


追記

上では、実行ファイルがシンボリックリンクとなっている場合は、実体のあるディレクトリは分からない。その場合には、

MYNAME=$(readlink -f "${BASH_SOURCE:-$0}")

とする。

*1:ややこしいので個人的には"${A:-a}"で統一してほしいです。

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

テストコードってどんどん増えていく。だらだらスクリプトを書くのは簡単だが、すぐに収拾が付かなくなり、経済的に耐えられないレベルで混乱してくる。何のテストをしているのかももちろんわからないが、動かし方も分からないし、どうなったら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: テストスクリプトのディレクトリ構造フレームワーク - 超ウィザード級ハッカーのたのしみ

ncatで遊んでみる

ncatという超便利コマンドを恥ずかしながらいままで知らなかった。HTTPプロトコルを学ぶには最適なおもちゃだ。

www.example.com の 80番ポートに、 / を GET するという HTTP リクエストを投げてみる。

fjk@x240:~$ ncat www.example.com 80 << END
GET / HTTP/1.1
Host: www.example.com

END

すると、以下のような HTTP レスポンスが返ってくる。200番のステータスコードが帰ってきているので、リクエストはうまいこといったようだ。いろいろヘッダーフィールドについているが、こう見ると HTTP プロトコルはテキストを送って返すだけという非常に単純なプロトコルなことが分かる。

HTTP/1.1 200 OK
Accept-Ranges: bytes
Cache-Control: max-age=604800
Content-Type: text/html
Date: Tue, 12 Jul 2016 14:47:58 GMT
Etag: "359670651"
Expires: Tue, 19 Jul 2016 14:47:58 GMT
Last-Modified: Fri, 09 Aug 2013 23:54:35 GMT
Server: ECS (rhv/818F)
Vary: Accept-Encoding
X-Cache: HIT
x-ec-custom-error: 1
Content-Length: 1270

<!doctype html>
<html>
<head>
    <title>Example Domain</title>
以下略

www.google.com に GET を投げると 302 が帰ってきた。

fjk@x240:~$ ncat www.google.com 80 << END
GET / HTTP/1.1
Host: www.google.com

END

HTTP/1.1 302 Found
Cache-Control: private
Content-Type: text/html; charset=UTF-8
Location: http://www.google.co.jp/?gfe_rd=cr&ei=-AKFV4uqMq7U8AeQ67-IBA
Content-Length: 261
Date: Tue, 12 Jul 2016 14:47:20 GMT

<HTML><HEAD><meta http-equiv="content-type" content="text/html;charset=utf-8">
<TITLE>302 Moved</TITLE></HEAD><BODY>
<H1>302 Moved</H1>
The document has moved
<A HREF="http://www.google.co.jp/?gfe_rd=cr&amp;ei=-AKFV4uqMq7U8AeQ67-IBA">here</A>.
</BODY></HTML>

http://www.google.co.jp/?gfe_rd=cr&ei=-AKFV4uqMq7U8AeQ67-IBA に移動しているらしい。この文字列の意味は ncat では分からない。

ncatはソケットにリクエストを投げるだけでなくて、リクエストを待つことができる。

fjk@x240:~$ sudo ncat -l -p 8000 << END
HTTP/1.1 200 OK

<HTML><HEAD></HEAD><BODY><H1>HELLO WORLD</H1></BODY></HTML>
END

としたら、8000番ポートに接続されたときに、 HTTPレスポンスを返す。ブラウザで http://localhost:8000/ にアクセスしたら

f:id:fjkz:20160713000501p:plain

ちゃんとWebサーバーっぽい動きをしている。

リクエスト内容は標準出力に吐かれる。

GET / HTTP/1.1
Host: localhost:8000
Connection: keep-alive
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.106 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Encoding: gzip, deflate, sdch
Accept-Language: ja,en-US;q=0.8,en;q=0.6

Google Chrome は HTTP 1.1 でリクエストを投げて、Chrome のバージョンもリクエストに乗せていることが分かる。

試してみると、HTTP はテキストベースで、すごい単純なプロトコルであることが見える。もっと良いプロトコルがあるに違いないが、現実的にはこういう単純で原始的なプロトコルを組み合わせると、今の巨大なインターネットになると思うと、なんとも感慨深いものがある。

テスト駆動開発で品質が上がる証拠

テスト駆動開発(TDD)で品質が上がる証拠を得た。

Realizing quality improvement through test driven development: result and experiences of four industrial teams*1

性質が異なる4つの製品の開発プロジェクトで TDD を行って、TDD でない類似の開発プロジェクトとの比較をした。4つのプロジェクトはデバイスドライバ・OS・Webアプリケーション・PCアプリケーションと全く異なる種類のものである。

f:id:fjkz:20160708213157p:plain

欠陥密度は40%から90%減少し、一方で実装時間は15%から35%増加するという結果が得られた。データだけ見ると、TDDの導入は投資としてはお得だと言える。