node-handlersocket 0.0.1 リリース
しました.
0.0.0 からの変更点は次のとおりです.
- Bug
- NULL 値を正しく扱っていなかったのを修正しました.
npm install node-handlersocket
でインストール,または npm update node-handlersocket
でアップデートできます.
HandlerSocket の Node.js クライアント node-handlersocket リリース
先週,本家 (英語の) InfoQ や PublicKey で紹介されて話題となった HandlerSocket.
- InfoQ
- InfoQ: MySQL/HandlerSocket and VoltDB: Contenders to NoSQL
- InfoQ
- InfoQ: NoSQL への挑戦者 - MySQL/HandlerSocket と VoltDB
- Publickey
- NoSQLとしてMySQLを使うDeNAが、memcachedよりも高速な75万クエリ/秒を実現
この HandlerSocket は C++ で書かれたクライアントライブラリと,その Perl バインディングが提供されているのですが,同時にプロトコル仕様
が公開されていることもあって,既に第三者によって Ruby や Python,PHP,Java などのバインディングあるいはネイティブ実装のクライアントが公開されています.
自分が見つけたのはこんな感じ.
- Ruby https://github.com/miyucy/handlersocket
- Python http://pypi.python.org/pypi/handlersocket/
- PHP http://lab.klab.org/young/2010/10/handlersocket-plugin-for-mysqlのphpクライアントを公開しました/
- Java http://ameblo.jp/just-do-neet/entry-10682348973.html
そんなわけで (どんなわけで?),Node.js 向けの Pura JavaScript クライアントライブラリ,node-handlersocket を作ってみました.
JavaScsript 弱者 && できたてほやほやなので品質的には自信ないです.突っ込み歓迎です.
npm からインストールすることもできるはず.
npm install node-handlersocket
使い方はこんな感じ.
var hs = require('node-handlersocket'); var con = hs.connect(); con.on('connect', function() { con.openIndex('test', 'EMPLOYEE', 'PRIMARY', [ 'EMPLOYEE_ID', 'EMPLOYEE_NO', 'EMPLOYEE_NAME' ], function(err, index) { index.find('=', 1, function(err, results) { console.log(results[0]); con.end(); }); }); });
他言語のクライアントは専ら本家の Perl クライアントと同じような API になっているようですが,Node.js は非同期な API にしなくちゃということで,独自の API になってます.
できるだけ Node.js の標準モジュールと同じような API にしたつもり.
以下簡単に API を紹介.
まずは HandlerSocket サーバに接続します.
var con = hs.connect();
デフォルトでは localhost の 9998 番ポートに接続します.変更する場合は
var con = hs.connect({ host : 'hostname', port : 9999 });
のようにします.host
だけ,port
だけの指定も可.
connect()
の戻り値は Connection
オブジェクトで,Node.js の EventEmitter
のインスタンスです.
そんなわけで (どんなわけで?),
con.on('connect', function() { ... });
で接続の完了を待ちます.
次にインデックスをオープンします.
con.openIndex('test', 'EMPLOYEE', 'PRIMARY', [ 'EMPLOYEE_ID', 'EMPLOYEE_NO', 'EMPLOYEE_NAME' ], function(err, index) { ... });
openIndex()
のシグネチャは
openIndex(database, table, index, columns, callback)
となっていて,引数は順にデータベース名,テーブル名,インデックス名 (主キーは 'PRIMARY'
),後で検索で取得したり更新対象となるカラム名の配列,コールバック関数です.
コールバック関数のシグネチャは
function(err, index)
で,インデックスのオープンが成功すると err
は null
となり,index
に Index
オブジェクトが渡されます.
インデックスのオープンが失敗すると,err
には Error
オブジェクトが渡されます.
インデックスを取得すると検索や更新を行うことができます.
index.find('=', 1, function(err, result) { ... });
検索を行う find()
のシグネチャは
find(op, keys, [limit, [offset] ], callback)
となっていて,引数は順にオペレータ,インデックス値の配列,取得する行数の上限 (デフォルトは 1),取得する際に読み飛ばす行数 (デフォルトは 0),コールバック関数です.
オペレータは '='
,'>'
,'>='
,'<'
,'<='
を指定することができます.'='
,'>'
,'>='
を指定した場合の結果は昇順,'<'
,'<='
を指定した場合の結果は降順になります.
コールバック関数のシグネチャは
function(err, results)
で,検索が成功すると err
は null
となり,results
には openIndex()
の columns
で指定したカラム値の配列 (レコード) からなる配列 (レコードの配列) が渡されます.検索結果が 0 件の場合,results
は空の配列となります.
検索が失敗すると,err
には Error
オブジェクトが渡されます.
更新を行う update()
と削除を行う remove()
は検索と似ていて,それぞれのシグネチャは
update(op, keys, [limit, [offset] ], values, callback)
remove(op, keys, [limit, [offset] ], callback)
となっていて,values
以外は find()
と同じです.
values
には openIndex()
の columns
で指定したカラム値の配列を指定します.
コールバック関数のシグネチャは
function(err, rows)
で,検索が成功すると err
は null
となり,rows
には更新または削除した行数が渡されます.
混信または削除が失敗すると,err
には Error
オブジェクトが渡されます.
挿入を行う insert()
のシグネチャは
insert(values, callback)
となっていて,values
には openIndex()
の columns
で指定したカラム値の配列を指定します.
コールバック関数のシグネチャは更新および削除と同じです.
ドキュメントはこれから頑張ります.誰か 翻訳 頼む
詳細は本家 HandlerSocket のドキュメントを参照してください.
他言語クライアントの場合,複数のリクエストをパイプライニングするために execute_multi
メソッドがあったりしますが,node-handlersocket は全部非同期のため,コールバックされる結果を待たずに連続してメソッドを呼び出すだけでパイプライニングされます.
また,レスポンスが全部揃う前に個々のレスポンスごとにコールバックされるので効率面で多少有利かも.
API というかコールバックは標準の fs モジュールなどに合わせてあるので,制御フローを簡素化するライブラリと組み合わせることも簡単じゃないかと思います.
例えば Node.js のパッケージマネージャ npm の作者である Isaac Schlueter さんによる a tiny flow control library 「Slide」
と組み合わせると,
var hs = require('../lib/node-handlersocket'), asyncMap = require('slide/async-map'); var indexDefs = [ ['test', 'EMPLOYEE', 'PRIMARY', [ 'EMPLOYEE_ID', 'EMPLOYEE_NO', 'EMPLOYEE_NAME' ]], ['test', 'DEPARTMENT', 'PRIMARY', ['DEPARTMENT_ID', 'DEPARTMENT_NO', 'DEPARTMENT_NAME']], ['test', 'ADDRESS', 'PRIMARY', ['ADDRESS_ID', 'STREET']] ]; var con = hs.connect(); con.on('connect', function() { asyncMap(indexDefs, function(indexDef, callback) { con.openIndex.apply(con, indexDef.concat(callback)) }, function(err, indices) { ... }); });
という感じで,indexDefs
に定義したインデックスが全部オープンし終わると,(最後の) コールバックに Index
の配列が渡されるみたいな.この場合の openIndex()
もパイプライニングされます.試してないけど. 試しました.
ということで,注目の HandlerSocket と注目の Node.js の組み合わせを楽しんでみてください.
Dolteng 0.41.0 リリース
しました.
■変更点
0.40.0 からの変更点は次のとおりです.
- Bug
- [DOLTENG-118] - Jar ファイルの MANIFEST.MF に記述されている Jar が同じディレクトリに存在しないと Database view で NullPointerException が発生する問題を修正しました.[Seasar-user:19575]
- [DOLTENG-121] - Teeda の HTML テンプレートで class 属性に対するダイナミックプロパティを追加すると getXxxStyleClass() のようにならなければならないのに getXxxClass() となってしまう問題を修正しました.[Seasar-user:19949]
- [DOLTENG-125] - Kuina-Dao の orderby 引数の型に OrderingSpec やその配列を使うとエラーマーカが出る問題を修正しました.[Seasar-user:20187]
- Improvement
- [DOLTENG-119] - S2BlazeDSのバージョンを1.0.2に更新しました.
- [DOLTENG-120] - S2BlazeDSのプロジェクトを作成する際にBlazeDS4との組み合わせを作成できるようにしました。
- [DOLTENG-122] - EclipseからのAntの実行でS2JDBC-Genが出力するログが文字化けしないように、s2jdbc-gen-build.xmlの記述を修正しました。
- [DOLTENG-126] - S2Flex2のプロジェクトを作成する際に、Flex SDKの設定を3.2から3.5に変更しました
- Task
- [DOLTENG-123] - Seasar2 のバージョンを 2.4.43 に更新しました.
- [DOLTENG-124] - Teeda のバージョンを 1.0.13-sp10 に更新しました.
- [DOLTENG-127] - S2Flex2のプロジェクトを作成する際に、Flex SDKのFlex 2.0.1 Hotfix 3で作成するのをサポートから外しました。
■セットアップ手順はこちらからどうぞ.
Node.js 用の非同期処理を簡単にしてくれるライブラリ async.js
Node.js といえば非同期処理です.そして非同期処理と言えばコールバック.
そんなわけで (どんなわけで?),すぐにこんなコードになったりしがちですよね.
fs.writeFile(path, "hello", function (error) { fs.open(path, "a", 0666, function (error, file) { fs.write(file, "\nworld", null, "utf-8", function () { fs.close(file, function (error) { fs.readFile(path, "utf-8", function (error, data) { var lines = data.split("\n"); sys.puts(lines[1]); }); }); }); }); });
奥に向かって突き進むのが気持ちいい,そんな風に思っていた時期が僕にもありました...
でも,そのうちこんな冗談も言いたくなりますよね.
The last few lines of our #nodejs application - });});});});});});});});});}); our #clojure app looks like this - )))))))))))))))))))) #joke
こんな小さなサンプルでこれだと,現実的なアプリではどうなることやら,と心配で夜も眠れないので調べてみたら,いろいろなライブラリが当然のようにあったりするわけですね.
JSDeferred
これは元々ブラウザの中で動く普通の JavaScript 向けに作られたようですが,Node.js でも使えます.
Deferred は先週書いたテスティングフレームワーク Vows に出てきた Promise と似てるというか,その応用という感じです.
Promise/Future は将来確定する値を取得できるようにするためのオブジェクトですが,Deferred はさらに,値が確定した時に実行する処理を登録しておくことができます.処理の実行をその時まで先送りするから Deferred なんですね.
んで,JSDeferred を使うと先のサンプルは次のように書くことができます.
var fs = require('fs'), sys = require('sys'); var Deferred = require('../lib/jsdeferred/jsdeferred').Deferred; var path = "fileio.txt"; Deferred.define(); next(function() { var deferred = new Deferred(); fs.writeFile(path, "hello", function(error) { deferred.call(); }); return deferred; }).next(function() { var deferred = new Deferred(); fs.open(path, "a", 0666, function(error, file) { deferred.call(file); }); return deferred; }).next(function(file) { var deferred = new Deferred(); fs.write(file, "\nworld", null, "utf-8", function() { deferred.call(file); }); return deferred; }).next(function(file) { var deferred = new Deferred(); fs.close(file, function(error) { deferred.call(); }); return deferred; }).next(function() { fs.readFile(path, "utf-8", function(error, data) { var lines = data.split("\n"); sys.puts(lines[1]); }); });
うーみゅ... 奥に向かって突き進むコードが普通に縦に落ちていくコードにはなりました.しかし,少し残念な感じがしないでもない...
これは JSDeferred が悪いわけではなく,ビルディングブロックである Deferred
を直接使ってるからこうなっちゃってるんですね.この決まり切った処理をラップした関数を用意すれば,スッキリしたコードになるはずです.しかし,現時点ではそんなありがたいものはなさそう...
ないなら作ればいいじゃない,と王妃様に言われそうな気がしますが,JSDeferred の作者さんも Node.js を使っているようなので,待ってればいずれ出てくるんじゃないのかなぁと期待しておきます.
promised-io
こちらは promised-based なテスティングフレームワーク patr と同じ作者による promised-based な IO フレームワーク.
Node.js の fs
モジュールや http.Client
のラッパーを用意してくれているようです.
しかし... ドキュメントが...
一応こちらにも情報があるのですが,
なかなか辛いものがあります.
その中に出てくる例.
var myFile = fs.open("my-file.txt", "a"); myFile.write("some data").then(myFile.close);
これだけ見ると,結構いい感じに見えます.
一見同期的なコードに見えますが,実際にはファイルの open()
が完了してから write()
が,それが終わってから close()
が行われます.カッコいい.
のですが,もうちょっと何かやろうとすると行き詰まってしまいました.
例えば最初の例を promised-io でやろうとすると,
var sys = require("sys"); var fs = require("../lib/promised-io/fs"); var path = "fileio.txt"; fs.writeFile(path, "hello").then(function() { var myFile = fs.open(path, "a", 0666); myFile.write("\nworld").then(myFile.close); })
この時点でかっこよくない気がする上に,この先の書き方が分かりません.
promised-io はいくつかのクラスを提供するのですが,ここで出てくるのはそのうちの 3 つ,Promise
,Deferred
,そして File
です.これらは,
Promise <|-- Deferred <|-- File
という継承関係にあります (たぶん,でも自分の JavaScript 力では怪しいのだ).
で,問題なのは File
の write()
が返すのが File
ではなく Deferred
っぽいこと.なので,
myFile.write(...).read(...).close().then(...);
なんてできそうに見えないのですね.チェーンができない.
さらに,Deferred
の then()
が返すのは Deferred
ではなく Promise
で,こいつの then()
は
Promise.prototype.then = function(resolvedCallback, errorCallback, progressCallback){ throw new TypeError("The Promise base class is abstract, this function must be implemented by the Promise implementation"); };
ってなってたりして,使い物にならなかったり.
要するに,JSDeferred でできているようなチェーンができない気がするわけです.
自分の勘違いという可能性も高いのですが (なんせ JavaScript では最下級戦士),ドキュメントもアレなので,これ以上関わるのは難しいと判断して次へ行きます.
async.js
そんなわけで (どんなわけで?),ようやく今日の主題,async.js です.前置き長すぎ.
async.js は先の二つとは随分異なった印象のライブラリで,元々は関数型言語に見られる無限リストや map
などの関数を提供するものみたいです.
こいつを使うと例えば
async.range(1, null, 2)
.print()
.end();
これでコンソールに 1, 3, 5... と奇数が延々と出力されます.
最初の range(start, stop, step)
はジェネレータと呼ばれ,これが後続の関数にデータを渡します.この例では print()
です.後続の関数は自分の処理が終わったら次の関数に処理を渡します.つまりパイプラインを構成しているわけです.それが延々と続いて (この例では print()
しかありませんが),最後に end()
にたどり着くと,ジェネレータに戻って次の値が生成されて... を繰り返します.
おっと,うちの環境だと 4000 を越えたところで例外が.スタックオーバーフローみたいな?
Node.js ってエラーがあった時のメッセージが不親切で状況がまるで分からないことが多い気がします.でもまぁ,これはきっとスタックオーバーフローでしょう.
でも大丈夫です.
async.js には delay()
という関数があって,これを挟むと後続の処理を setTimeout()
に渡してくれます.これで一度 Node.js に制御を戻してあげればスタックオーバーフローにはならないはず.
async.range(1, null, 2)
.print()
.delay(0)
.end();
うん,大丈夫っぽい.
さらに
// 偶数 async.range(0, null, 2) .print() .delay(0) .end(); // 奇数 async.range(1, null, 2) .print() .delay(0) .end();
なんてやると,0, 1, 2, 3, 4, 5... という具合に両方の数列が交互に出力されます.同期的なパイプラインに見えるコードが実は非同期に実行されている... なんかカッコいい!!
で,今の delay()
の挙動が結構重要で,これってコールバックされるまで後続の処理を先送りしてくれるわけです.要するに Deferred なんですね.つまり delay()
は,JSDefferd の next()
や promised-io の then()
みたいなことをやってくれているということ.
ってことは,このパイプラインに I/O を含めることだってできるはず.
そんなわけで (どんなわけで?),async.js は Node.js 標準の fs
モジュールライクなプラグインを用意してくれてます.
ただ,ちょっとだけ残念なことに,open()
の引数の並びが Node.js のとは違っていたり,readFile()/writeFile()
はあるのに read()/write()
はなかったり.
そして凄く残念なことに,ネットワーク関係のプラグインはなかったり.うーみゅ...
とりあえず気を取り直して,async.js の fs-node.js
というプラグインをちょっと修正してみました.
@@ -53,7 +53,7 @@ async.plugin({ }) }, - open: function(flags, mode) { + open: function(mode, flags) { return this.$fileOp(function(file, next) { fs.open(file.path, mode || file.mode || "r", flags || file.flags || 0666, next) }, "fd") @@ -124,17 +124,12 @@ async.plugin({ if (!file.path) return next("not a file sequence!") - if (encoding) - fs.readFile(file.path, encoding, readCallback) - else - fs.readFile(file.path, readCallback) - - function readCallback(err, data) { + fs.readFile(file.path, encoding, function readCallback(err, data) { if (err) return next(err) file.data = data next() - } + }) }) }, @@ -142,6 +137,20 @@ async.plugin({ return this.$fileOp(function(file, next) { fs.writeFile(file.path, data || file.data, next) }) + }, + + write: function(buffer, offset, length, position) { + return this.$fileOp(function(file, next) { + if (!Buffer.isBuffer(buffer)) { + buffer = new Buffer(buffer, length); + length = buffer.length; + position = offset; + offset = 0; + } + fs.write(file.fd, buffer, offset, length, position, function(err, result){ + next(null, file); + }); + }); } }, { files: function(files, root) { @@ -154,6 +163,10 @@ async.plugin({ } })) }, + + file: function(file, root) { + return this.files([file], root); + }, glob: function(pattern) { function fileSort(file1, file2) {
これを使うと,最初の例はこうなります.ジャジャーン.
var sys = require('sys'), fs = require('fs'); var async = require('../lib/async'); var path = 'fileio.txt'; async.file(path) .writeFile('hello') .open('a') .write('\nworld') .close() .readFile('utf-8') .get('data') .each(function(data) { var lines = data.split("\n"); sys.puts(lines[1]); }) .end();
おおぉぉぉ...
これです.まさに,これが望んでいたものそのものですよ!!
async.js△!!!!
この動作を簡単に説明すると,
最初の file()
関数はジェネレータで,ファイルのパスを作って後続に渡します.
次の writeFile()
はそのファイルに文字列を書き込みます.
その次の open()
はファイルをオープンし,ファイル記述子を後続へのデータに追加します.
write()
はそのファイル記述子を使って文字列を書き込みます.
close()
はファイルをクローズしてファイル記述子を削除します.
readFile()
はジェネレータが設定したパスを使ってファイルを読み込み,結果をパイプラインを流れるオブジェクトの data
プロパティに設定します.
get()
はその data
プロパティを取り出して後続に渡します.
each()
はそれ (data
) をコールバックで受け取って処理します.
という感じ.
ポイントは,open()
とか write()
という名前の関数は,実際には open()
や write()
を行う関数を呼び出す関数を準備している (パイプラインを構築する) だけということ.そして,実際の関数は前段の処理が終わった後に非同期に呼び出されるということ.
途中に print()
を入れると,パイプラインを流れるデータを表示してくれて便利です.
対応するソースと混ぜるとこんな感じになります.
//async.file(path) { path: 'fileio.txt', name: 'fileio.txt' } // .writeFile('hello') { path: 'fileio.txt', name: 'fileio.txt' } // .open('a') { path: 'fileio.txt', name: 'fileio.txt', fd: 5 } // .write('\nworld') { path: 'fileio.txt', name: 'fileio.txt', fd: 5 } // .close() { path: 'fileio.txt' , name: 'fileio.txt' , fd: undefined } // .readFile('utf-8') { path: 'fileio.txt' , name: 'fileio.txt' , fd: undefined , data: 'hello\nworld' } // .get('data') hello world
ジェネレータの file()
が作ったオブジェクトに open()
が fd
を追加して,close
がそれを消して,readFile
が data
を追加して,get()
がその data
だけを後続に渡している様子が分かります.
ちなみに,ジェネレータに複数のファイルのパスを受け取る async.js 本来の files()
を使うと,
async.files(['file1.txt', 'file2.txt', 'file3.txt']) .writeFile('hello') .open('a') .write('\nworld') .close() .readFile('utf-8') .get('data') .each(function(data) { var lines = data.split("\n"); sys.puts(lines[1]); }) .end();
なんて風に複数のファイルに対する繰り返しも簡単.
さらに,glob()
とか walkfiles()
なんて便利そうなジェネレータも用意されているので,ファイルに対する操作に関してはそこそこ便利そうです.
ネットワーク関係のプラグインがないとか,ReadableStream
にも対応してなさそうとか,エラーハンドリングはどうするとか,課題も多そうな async.js ですが,それでも Node.js でのプログラミングスタイルについて考えさせてくれるいい材料という感じがします.
http.Client
のラッパーでも作りながらもう少し遊んでみようと思ってます.
Node.js 用のテスティングフレームワーク Vows その 3
昨日のエントリ「その 2」で ReadableStream
からの 'data'
イベントを行単位にコールバックしてくれる read-line モジュールを作成し,EventEmitter
を使って擬似的にイベントを発生させるテストを Vows を使って書きました.
今回はその続きとして,本物のファイルを使ったテストを追加します.
ファイルを扱うには Node.js 標準のファイルシステムモジュールを使います.
今回はファイルをストリームとして読み込むので,「fs.ReadStream」を使います.
この fs.createReadStream(path)
を呼び出すと ReadableStream
が返ってきます.(それは EventEmitter
でもあります).こいつを read-line に渡せばいいだけですね!
でもでも,擬似的なテストで使った自前の EventEmitter
と違い,ReadableStream
に対して emit()
を呼び出すわけにはいきません.実際に ReadableStream
からの読み込みが終了して,行ごとの配列が完成するまでは vow で検証することができないのです.どうしよう?
そこで「その 1」で紹介した「プロミス」を使ってみましょう.
プロミスを使うのは簡単で,トピックで (またしても!) EventEmitter
を作って返せばいいだけです.そして ReadableStream
がクローズしたらプロミス (EventEmitter
のことです) にイベントを生成してあげれば vow が呼び出されて検証が行われます.
一つずつ順番にいきましょう.
まずはテスト用のファイルを用意します (test1.txt
).
abc
ファイルシステムモジュールを使うので,
-var vows = require('vows'), assert = require('assert'), events = require('events'); +var vows = require('vows'), assert = require('assert'), events = require('events'), fs = require('fs'); var read_line = require('./read-line');
が必要です.
「その 2」で作成した,EventEmitter
を偽のストリームとして扱うコンテキストとは別に,本当のファイルを使ったテスト用のコンテキストを用意します.
} } } + }, + '実際に読み込んだファイルが' : { + '1行のファイルの場合' : { + + } } }).export(module);
子のコンテキストのトピックと vow はイメージしやすいので先に作成します.
}, '実際に読み込んだファイルが' : { '1行のファイルの場合' : { - + topic : function(topic) { + return topic('test1.txt'); + }, + '1回だけコールバックされること' : function(topic) { + assert.length(topic, 1); + }, + '行全体がコールバックされること' : function(topic) { + assert.equal(topic[0], 'abc'); + } } } }).export(module);
この子コンテキストのトピックが返すのがプロミスです.つまり,親コンテキストのトピックが返す関数の戻り値がプロミスであって欲しいということになります.
そんなわけで (どんなわけで?),親コンテキストのトピックはこうなるはずです.
} }, '実際に読み込んだファイルが' : { + topic : function() { + return function(fileName) { + var promise = new (events.EventEmitter); + + return promise; + }; + }, '1行のファイルの場合' : { topic : function(topic) { return topic('test1.txt');
そしてこの中で fs.createReadStream(path)
を呼び出して,それを read-line の onDataByLine()
に渡します.
topic : function() { return function(fileName) { var promise = new (events.EventEmitter); + var stream = fs.createReadStream(fileName); + stream.setEncoding('utf-8'); + read_line.onDataByLine(stream, function(line) { + }); return promise; }; },
ちなみに ReadableStream
から文字列を読み込むには setEncoding()
してあげないといけません.
read-line からコールバックされたら文字列を配列に貯め込みます.
'実際に読み込んだファイルが' : { topic : function() { return function(fileName) { + var lines = []; var promise = new (events.EventEmitter); var stream = fs.createReadStream(fileName); stream.setEncoding('utf-8'); read_line.onDataByLine(stream, function(line) { - + lines.push(line); }); return promise; };
ファイルがクローズしたイベントを受け取らなくてはいけません.
read_line.onDataByLine(stream, function(line) { lines.push(line); }); + stream.on('end', function() { + + }); return promise; }; },
ファイルがクローズしたならプロミスに通知して vow が呼び出されるようにします.
lines.push(line); }); stream.on('end', function() { - + promise.emit('success', lines); }); return promise; };
まとめると,親コンテキストのトピックはこうなりました.
topic : function() { return function(fileName) { var lines = []; var promise = new (events.EventEmitter); var stream = fs.createReadStream(fileName); stream.setEncoding('utf-8'); read_line.onDataByLine(stream, function(line) { lines.push(line); }); stream.on('end', function() { promise.emit('success', lines); }); return promise; }; },
案外シンプルですね.非同期イベントを扱うテストって大変そうなイメージでしたが,これなら怖くないかも?
実行します.
$ vows read-line-test.js ······························· ✓ OK » 31 honored (0.007s)
テストデータを増やしてみます.
空のファイル (test0.txt
) と,2〜4 行のデータを持ったファイルを作成します.
test2.txt
(最後に改行なし)
abc def
test3.txt
(最後に改行あり)
abc def
test4.txt
(最後に改行あり)
abc def
Eclipse のエディタなんかだと最後に改行があるとその下にもう一行あるかのように表示されるけど,そういうものなのかな? とりあえずここでは改行を行の終端として扱うことにしているので,その後に行はないものとして扱います.
最終形ということで,テストを増やした read-line-test.js
全体を掲載.
var vows = require('vows'), assert = require('assert'), events = require('events'), fs = require('fs'); var read_line = require('./read-line'); vows.describe('read-line').addBatch( { '偽のEventEmitterを使って' : { topic : function() { return function(data) { var lines = []; var emitter = new (events.EventEmitter); read_line.onDataByLine(emitter, function(line) { lines.push(line); }); if (!Array.isArray(data)) { emitter.emit('data', data); } else { for ( var i = 0; i < data.length; ++i) { emitter.emit('data', data[i]); } } emitter.emit('end'); return lines; }; }, '1回だけデータ受信イベントが発生し' : { 'データがundefinedの場合' : { topic : function(topic) { return topic(undefined); }, '1回もコールバックされないこと' : function(topic) { assert.length(topic, 0); } }, 'データがnullの場合' : { topic : function(topic) { return topic(null); }, '1回もコールバックされないこと' : function(topic) { assert.length(topic, 0); } }, 'データが空文字列の場合' : { topic : function(topic) { return topic(''); }, '1回もコールバックされないこと' : function(topic) { assert.length(topic, 0); } }, 'データが改行を含まない文字列の場合' : { topic : function(topic) { return topic('abcdef'); }, '1回だけコールバックされること' : function(topic) { assert.length(topic, 1); }, 'データ全体を文字列としてコールバックされること' : function(topic) { assert.equal(topic[0], 'abcdef'); } }, 'データが改行で始まる文字列の場合' : { topic : function(topic) { return topic('\nabc'); }, '2回コールバックされること' : function(topic) { assert.length(topic, 2); }, '最初に空行がコールバックされること' : function(topic) { assert.equal(topic[0], ''); }, '次に改行の後の文字列がコールバックされること' : function(topic) { assert.equal(topic[1], 'abc'); } }, 'データが改行で終了する文字列の場合' : { topic : function(topic) { return topic('abcdef\n'); }, '1回だけコールバックされること' : function(topic) { assert.length(topic, 1); }, '改行より前の文字列がコールバックされること' : function(topic) { assert.equal(topic[0], 'abcdef'); } }, 'データが途中に改行を含む文字列の場合' : { topic : function(topic) { return topic('abc\ndef'); }, '2回コールバックされること' : function(topic) { assert.length(topic, 2); }, '最初に改行より前の文字列がコールバックされること' : function(topic) { assert.equal(topic[0], 'abc'); }, '次に改行の後の文字列がコールバックされること' : function(topic) { assert.equal(topic[1], 'def'); } }, 'データが途中に改行を二つ含む文字列の場合' : { topic : function(topic) { return topic('abc\ndef\nghi'); }, '3回コールバックされること' : function(topic) { assert.length(topic, 3); }, '最初に一番目の改行より前の文字列がコールバックされること' : function(topic) { assert.equal(topic[0], 'abc'); }, '次に一番目の改行と二番目の改行の間の文字列がコールバックされること' : function(topic) { assert.equal(topic[1], 'def'); }, '最後に二番目の改行より後の文字列がコールバックされること' : function(topic) { assert.equal(topic[2], 'ghi'); } } }, '2回データ受信イベントが発生し' : { 'どのデータにも改行が含まれない場合' : { topic : function(topic) { return topic( [ 'abc', 'def' ]); }, '1回だけコールバックされること' : function(topic) { assert.length(topic, 1); }, '一番目のデータと二番目のデータを連結した文字列がコールバックされること' : function(topic) { assert.equal(topic[0], 'abcdef'); } }, '最初のイベントにだけ改行が含まれる場合' : { topic : function(topic) { return topic( [ 'abc\ndef', 'ghi' ]); }, '2回コールバックされること' : function(topic) { assert.length(topic, 2); }, '最初に一番目のデータの改行より前の文字列がコールバックされること' : function(topic) { assert.equal(topic[0], 'abc'); }, '次に一番目のデータの改行より後と二番目のデータを連結した文字列がコールバックされること' : function(topic) { assert.equal(topic[1], 'defghi'); } }, '二番目のイベントにだけ改行が含まれる場合' : { topic : function(topic) { return topic( [ 'abc', 'def\nghi' ]); }, '2回コールバックされること' : function(topic) { assert.length(topic, 2); }, '最初に一番目のデータと二番目のデータの改行より前を連結した文字列がコールバックされること' : function(topic) { assert.equal(topic[0], 'abcdef'); }, '次に二番目のデータの改行より後の文字列がコールバックされること' : function(topic) { assert.equal(topic[1], 'ghi'); } }, '一番目と二番目の両方のイベントに改行が含まれる場合' : { topic : function(topic) { return topic( [ 'abc\ndef', 'ghi\njkl' ]); }, '3回コールバックされること' : function(topic) { assert.length(topic, 3); }, '最初に一番目のデータの改行より前の文字列がコールバックされること' : function(topic) { assert.equal(topic[0], 'abc'); }, '次に一番目のデータの改行より後と,二番目のデータの改行より前を連結した文字列がコールバックされること' : function(topic) { assert.equal(topic[1], 'defghi'); }, '最後に二番目のデータの改行より後の文字列がコールバックされること' : function(topic) { assert.equal(topic[2], 'jkl'); } } } }, '実際に読み込んだファイルが' : { topic : function() { return function(fileName) { var lines = []; var promise = new (events.EventEmitter); var stream = fs.createReadStream(fileName); stream.setEncoding('utf-8'); read_line.onDataByLine(stream, function(line) { lines.push(line); }); stream.on('end', function() { promise.emit('success', lines); }); return promise; }; }, '空のファイルの場合' : { topic : function(topic) { return topic('test0.txt'); }, '1回もコールバックされないこと' : function(topic) { assert.length(topic, 0); } }, '1行のファイルの場合' : { topic : function(topic) { return topic('test1.txt'); }, '1回だけコールバックされること' : function(topic) { assert.length(topic, 1); }, '行全体がコールバックされること' : function(topic) { assert.equal(topic[0], 'abc'); } }, '2行で最後の行が改行で終わらないファイルの場合' : { topic : function(topic) { return topic('test2.txt'); }, '2回コールバックされること' : function(topic) { assert.length(topic, 2); }, '最初に1行目の文字列がコールバックされること' : function(topic) { assert.equal(topic[0], 'abc'); }, '次に2行目の文字列がコールバックされること' : function(topic) { assert.equal(topic[1], 'def'); } }, '空行を含めて3行で最後の行が改行で終わるファイルの場合' : { topic : function(topic) { return topic('test3.txt'); }, '3回コールバックされること' : function(topic) { assert.length(topic, 3); }, '最初に1行目の文字列がコールバックされること' : function(topic) { assert.equal(topic[0], 'abc'); }, '次に2行目の文字列がコールバックされること' : function(topic) { assert.equal(topic[1], ''); }, '最後に3行目の文字列がコールバックされること' : function(topic) { assert.equal(topic[2], 'def'); } }, '空行を含めて4行で最後の行が改行のみの空行で終わるファイルの場合' : { topic : function(topic) { return topic('test4.txt'); }, '4回コールバックされること' : function(topic) { assert.length(topic, 4); }, '最初に1行目の文字列がコールバックされること' : function(topic) { assert.equal(topic[0], 'abc'); }, '次に2行目の文字列がコールバックされること' : function(topic) { assert.equal(topic[1], ''); }, '続けて3行目の文字列がコールバックされること' : function(topic) { assert.equal(topic[2], 'def'); }, '最後に4行目の文字列がコールバックされること' : function(topic) { assert.equal(topic[3], ''); } } } }).export(module);
ついでに read-line.js
も掲載.「その 2」の途中から修正してないけど.
exports.onDataByLine = function(stream, callback) { var line = ''; stream.on('data', function(data) { if (data) { line += data; while (line.match(/\r?\n/)) { callback(RegExp.leftContext); line = RegExp.rightContext; } } }); stream.on('end', function() { if (line) { callback(line); } }); };
これっぽっちのテスト対象にあのテストの記述量はどうなのよ? って思わなくもないけど気にしない!! 気にしない!!
実行します.
$ vows read-line-test.js ············································ ✓ OK » 44 honored (0.014s)
--spec
します.
$ vows read-line-test.js --spec ♢ read-line 偽のEventEmitterを使って 1回だけデータ受信イベントが発生し データがundefinedの場合 ✓ 1回もコールバックされないこと 偽のEventEmitterを使って 1回だけデータ受信イベントが発生し データがnullの場合 ✓ 1回もコールバックされないこと 偽のEventEmitterを使って 1回だけデータ受信イベントが発生し データが空文字列の場合 ✓ 1回もコールバックされないこと 偽のEventEmitterを使って 1回だけデータ受信イベントが発生し データが改行を含まない文字列の場合 ✓ 1回だけコールバックされること ✓ データ全体を文字列としてコールバックされること 偽のEventEmitterを使って 1回だけデータ受信イベントが発生し データが改行で始まる文字列の場合 ✓ 2回コールバックされること ✓ 最初に空行がコールバックされること ✓ 次に改行の後の文字列がコールバックされること 偽のEventEmitterを使って 1回だけデータ受信イベントが発生し データが改行で終了する文字列の場合 ✓ 1回だけコールバックされること ✓ 改行より前の文字列がコールバックされること 偽のEventEmitterを使って 1回だけデータ受信イベントが発生し データが途中に改行を含む文字列の場合 ✓ 2回コールバックされること ✓ 最初に改行より前の文字列がコールバックされること ✓ 次に改行の後の文字列がコールバックされること 偽のEventEmitterを使って 1回だけデータ受信イベントが発生し データが途中に改行を二つ含む文字列の場合 ✓ 3回コールバックされること ✓ 最初に一番目の改行より前の文字列がコールバックされること ✓ 次に一番目の改行と二番目の改行の間の文字列がコールバックされること ✓ 最後に二番目の改行より後の文字列がコールバックされること 偽のEventEmitterを使って 2回データ受信イベントが発生し どのデータにも改行が含まれない場合 ✓ 1回だけコールバックされること ✓ 一番目のデータと二番目のデータを連結した文字列がコールバックされること 偽のEventEmitterを使って 2回データ受信イベントが発生し 最初のイベントにだけ改行が含まれる場合 ✓ 2回コールバックされること ✓ 最初に一番目のデータの改行より前の文字列がコールバックされること ✓ 次に一番目のデータの改行より後と二番目のデータを連結した文字列がコールバックされること 偽のEventEmitterを使って 2回データ受信イベントが発生し 二番目のイベントにだけ改行が含まれる場合 ✓ 2回コールバックされること ✓ 最初に一番目のデータと二番目のデータの改行より前を連結した文字列がコールバックされること ✓ 次に二番目のデータの改行より後の文字列がコールバックされること 偽のEventEmitterを使って 2回データ受信イベントが発生し 一番目と二番目の両方のイベントに改行が含まれる場合 ✓ 3回コールバックされること ✓ 最初に一番目のデータの改行より前の文字列がコールバックされること ✓ 次に一番目のデータの改行より後と,二番目のデータの改行より前を連結した文字列がコールバックされること ✓ 最後に二番目のデータの改行より後の文字列がコールバックされること 実際に読み込んだファイルが 空のファイルの場合 ✓ 1回もコールバックされないこと 実際に読み込んだファイルが 1行のファイルの場合 ✓ 1回だけコールバックされること ✓ 行全体がコールバックされること 実際に読み込んだファイルが 2行で最後の行が改行で終わらないファイルの場合 ✓ 2回コールバックされること ✓ 最初に1行目の文字列がコールバックされること ✓ 次に2行目の文字列がコールバックされること 実際に読み込んだファイルが 空行を含めて4行で最後の行が改行のみの空行で終わるファイルの場合 ✓ 4回コールバックされること ✓ 最初に1行目の文字列がコールバックされること ✓ 次に2行目の文字列がコールバックされること ✓ 続けて3行目の文字列がコールバックされること ✓ 最後に4行目の文字列がコールバックされること 実際に読み込んだファイルが 空行を含めて3行で最後の行が改行で終わるファイルの場合 ✓ 3回コールバックされること ✓ 最初に1行目の文字列がコールバックされること ✓ 次に2行目の文字列がコールバックされること ✓ 最後に3行目の文字列がコールバックされること ✓ OK » 44 honored (0.025s)
幸せな気分になりました♪
最後の二つのコンテキスト (ファイルが 3 行と4 行のやつ),表示の順番が記述の順番と異なっていることに注意.しかも,実行を繰り返すごとに異なってたりします.
これは,Vows がテスト (コンテキスト) を非同期に実行している証でしょう.表示順はテストの実行が完了する順番で,それはファイルの読み込みが完了する順番に依存しているわけです.Vows△!
説明を文字列で書く辺りとか記述量が多くなりそうなところで好き嫌い別れそうではありますが,Vows はなかなか素敵なテスティングフレームワークだと思います.
非同期イベントのテストも思ってた以上にあっさりと書けましたが,これはテスト対象であるトピックと,検証コードである vow を分けて記述しているおかげでしょう.そしてトピックが検証可能になった時,Vows が vow を呼び出してくれるわけです.まさにイベント駆動.
Node.js のテスティングフレームワークとしては他にも "Promise-based asynchronous test runner" なんていうカッコよさげな「patr」も興味深いのですが,
しばらくは浮気せずに Vows を足場に Node.js を楽しみたいと思ってます.