前置き
Goal
fluentd でメールから redmine を更新するプラグインを TDD で作る
シナリオ
- gem スケルトン作成
- テスト書く
- プラグイン作る
- gem をビルドする
- td-agent にインストールする
作る
gem スケルトン作成
以下のコマンドですぐ作れる。gem install bundler は事前に実行しておく。
$ bundle gem --test=minitest fluent-plugin-out_redmine
$ cd fluent-plugin-out_redmine
$ git commit -m "initial commit"
version を消して、rake (test) が通るようにする。fluentd は version があるとエラーになるので消さないといけない。他に fluentd 特有のポイントとして、プラグインの名前は out_foo などだが、プラグインのクラス名は FooOutput と逆になる。ネームスペースも Fluent::FooOutput になる。Fluent::Plugin::FooOutput とはならない。ここはエラーになるか試してないのでお作法程度のルールなのかどうか不明。
ついでに minitest がカラーで表示できるようにする。
追記:fluentd が gemspec に入ってないので追記が必要。spec.add_development_dependency "fluentd"
diff --git a/fluent-plugin-out_redmine.gemspec b/fluent-plugin-out_redmine.gemspec
index c22284c..219b4a8 100644
--- a/fluent-plugin-out_redmine.gemspec
+++ b/fluent-plugin-out_redmine.gemspec
@@ -1,11 +1,10 @@
# coding: utf-8
lib = File.expand_path('../lib', __FILE__)
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
-require 'fluent/plugin/out_redmine/version'
Gem::Specification.new do |spec|
spec.name = "fluent-plugin-out_redmine"
- spec.version = Fluent::Plugin::OutRedmine::VERSION
+ spec.version = '0.0.1'
spec.authors = ["Tooru Nakai"]
@@ -29,4 +28,5 @@ Gem::Specification.new do |spec|
spec.add_development_dependency "bundler", "~> 1.11"
spec.add_development_dependency "rake", "~> 10.0"
spec.add_development_dependency "minitest", "~> 5.0"
+ spec.add_development_dependency "minitest-reporters"
end
diff --git a/lib/fluent/plugin/out_redmine.rb b/lib/fluent/plugin/out_redmine.rb
index a50ffd5..e0c8c03 100644
--- a/lib/fluent/plugin/out_redmine.rb
+++ b/lib/fluent/plugin/out_redmine.rb
@@ -1,5 +1,3 @@
-require "fluent/plugin/out_redmine/version"
-
module Fluent
module Plugin
module OutRedmine
diff --git a/lib/fluent/plugin/out_redmine/version.rb b/lib/fluent/plugin/out_redmine/version.rb
deleted file mode 100644
index 0046d6f..b7cd973 100644
--- a/test/fluent/plugin/out_redmine_test.rb
+++ b/test/fluent/plugin/out_redmine_test.rb
@@ -1,11 +1,7 @@
require 'test_helper'
class Fluent::Plugin::OutRedmineTest < Minitest::Test
- def test_that_it_has_a_version_number
- refute_nil ::Fluent::Plugin::OutRedmine::VERSION
- end
-
def test_it_does_something_useful
- assert false
+ assert true
end
end
diff --git a/test/test_helper.rb b/test/test_helper.rb
index 04156fa..7f395ee 100644
--- a/test/test_helper.rb
+++ b/test/test_helper.rb
@@ -2,3 +2,8 @@ $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
require 'fluent/plugin/out_redmine'
require 'minitest/autorun'
+
+# http://chriskottom.com/blog/2014/06/dress-up-your-minitest-output/
+require 'minitest/reporters'
+reporter_options = { color: true }
+Minitest::Reporters.use! [Minitest::Reporters::DefaultReporter.new(reporter_options)]
テストを書く
必須
def setup
Fluent::Test.setup
end
来週続きを書くであろう自分のためにシナリオを書いておく。
# Feature:
# - user configurable parameter: site="URL of redmine", key="Redmine API Key"
# - params from input: path="path to raw mail message file"
# - invoke rdm-mailhandler.rb
Fluent::Test::BufferedOutputTestDriver.new
に自作プラグインのクラスを渡してインスタンスを作ると、自作プラグインにアクセスできるようになる。Input とかは InputTestDriver とかになるので、迷ったら本体のテストを読む。どのプラグインも同じことをやっているのですぐわかる。例えば https://github.com/fluent/fluentd/blob/master/test/plugin/test_out_file.rb
まずは config を取るところか始める。config は要するに td-agent.conf で設定されるプラグインに渡すパラメータのこと。プラグインの中ではそのままインスタンス変数となる。ということで全景はこちら。
注:ここでクラスの名前もライブラリ側と合わせるために Fluent::Plugin::OutRedmineTest
から Fluent::RedmineOutputTest
に書き換える。
require 'test_helper'
# Feature:
# - user configurable parameter: site="URL of redmine", key="Redmine API Key"
# - params from input: path="path to raw mail message file"
# - invoke rdm-mailhandler.rb
class Fluent::OutRedmineTest < Minitest::Test
def setup
Fluent::Test.setup
end
CONFIG = %[
site http://redmine.rm-example.com
key a-api-key
]
def create_driver(conf=CONFIG)
Fluent::Test::BufferedOutputTestDriver.new(Fluent::RedmineOutput).configure(conf)
end
def test_configure
driver = create_driver
assert_equal 'http://redmine.rm-example.com', driver.instance.site
assert_equal 'a-api-key', driver.instance.key
end
end
訂正:tag を config に書いていたが、test 環境では @tag="test"
になるので削除。
ということでテスト実行。rake
Error:
Fluent::Plugin::OutRedmineTest#test_configure:
NameError: uninitialized constant Fluent::Test
1 tests, 0 assertions, 0 failures, 1 errors, 0 skips
fluentd を test 側で require していないので追加。require する順番に注意。
diff --git a/test/test_helper.rb b/test/test_helper.rb
index 7f395ee..408eaa6 100644
--- a/test/test_helper.rb
+++ b/test/test_helper.rb
@@ -1,7 +1,11 @@
$LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
-require 'fluent/plugin/out_redmine'
require 'minitest/autorun'
+require 'fluent/log'
+require 'fluent/test'
+
+# my plugin must be placed after loading 'fluent'
+require 'fluent/plugin/out_redmine'
# http://chriskottom.com/blog/2014/06/dress-up-your-minitest-output/
require 'minitest/reporters'
テスト。
fluent-plugin-out_redmine/lib/fluent/plugin/out_redmine.rb:2:in `<module:Fluent>': Plugin is not a module (TypeError)
ようやくライブラリに手をいれることになる。bundler が作ったスケルトンには Plugin があったり クラス名が OutRedmine だったりと名前が想定と違う。名前を書き換えて本家からテンプレをコピー。
http://docs.fluentd.org/articles/plugin-development
Fluent::Plugin.register_output にて名前登録もやっておく。ここで 'out_redmine' ではなく 'redmine' にする。
require 'fluentd'
module Fluent
class RedineOutput < BufferedOutput
# First, register the plugin. NAME is the name of this plugin
# and identifies the plugin in the configuration file.
Fluent::Plugin.register_output('redmine', self)
# config_param defines a parameter. You can refer a parameter via @path instance variable
# Without :default, a parameter is required.
config_param :path, :string
# This method is called before starting.
# 'conf' is a Hash that includes configuration parameters.
# If the configuration is invalid, raise Fluent::ConfigError.
def configure(conf)
super
# You can also refer raw parameter via conf[name].
@path = conf['path']
end
# This method is called when starting.
# Open sockets or files here.
def start
super
end
# This method is called when shutting down.
# Shutdown the thread and close sockets or files here.
def shutdown
super
end
# This method is called when an event reaches to Fluentd.
# Convert the event to a raw string.
def format(tag, time, record)
[tag, time, record].to_json + "\n"
## Alternatively, use msgpack to serialize the object.
# [tag, time, record].to_msgpack
end
# This method is called every flush interval. Write the buffer chunk
# to files or databases here.
# 'chunk' is a buffer chunk that includes multiple formatted
# events. You can use 'data = chunk.read' to get all events and
# 'chunk.open {|io| ... }' to get IO objects.
#
# NOTE! This method is called by internal thread, not Fluentd's main thread. So IO wait doesn't affect other plugins.
def write(chunk)
data = chunk.read
print data
end
## Optionally, you can use chunk.msgpack_each to deserialize objects.
#def write(chunk)
# chunk.msgpack_each {|(tag,time,record)|
# }
#end
end
end
ではそれっぽくなったところで、rake
Error:
Fluent::RedmineOutputTest#test_configure:
Fluent::ConfigError: 'path' parameter is required
この path はテンプレのコードに書かれていたもの。削除して必要な config パラメータを設定。
# config_param defines a parameter. You can refer a parameter via @path instance variable
# Without :default, a parameter is required.
config_param :site, :string
config_param :key, :string
ここまで書いて気づいたが、minitest ではなく unit test を使わないといけなかったのでは・・・
でもまあ動いているので...
次は本丸。Input からデータを受け取り、それを処理する Output プラグインとしてのコードを書く。Fluentd の動きを簡単にまとめると、Input プラグインでは、router.emit()
にハッシュを渡すコードを書くことになっている。emit されたデータが Fluentd にわたって、必要に応じて Output プラグインに渡される。この流れをテストの中に書けば良い。
テストの書き方はおおまかに、driver.emit で Input 側でやる入力をシミュレートし、driver.run することで Output プラグインの write をコールする。driver.run は write の実行結果を返すのでそれを評価すれば Output プラグインをテストできることになる。詳細はこちら。https://github.com/fluent/fluentd/blob/master/lib/fluent/test/output_test.rb
ここでそもそも emit されたデータが Output プラグインに渡っているかが気になるところ。これは driver.expect_format というのが用意されているので、それを使う。
def test_format
driver = create_driver
time = Time.parse("2011-01-02 13:14:15 UTC").to_i
input_data = {"path"=>"/path/to/mail"}
# simulate router.emit from Input Plugin
driver.emit(input_data , time)
driver.expect_format ["test", time.to_i,{"path":"/path/to/mail"}].to_msgpack
driver.run
end
rake
Error:
Fluent::RedmineOutputTest#test_format:
Test::Unit::AssertionFailedError: <"\x93\xA4test\xCEM z'\x81\xA4path\xAD/path/to/mail">("ASCII-8BIT") expected but was
<"[\"test\",1293974055,{\"path\":\"/path/to/mail\"}]\n">("UTF-8").
最初のテンプレでは to_json を使っているので、msgpack に変更する。
def format(tag, time, record)
# [tag, time, record].to_json + "\n"
## Alternatively, use msgpack to serialize the object.
[tag, time, record].to_msgpack
end
これで test_format 通過。続いて rdm-mailhandler.rb を使って redmine を更新する。といっても今のところは system でこのスクリプトをコールするだけの実装。
どうやってテストするか。
out_stdout とか out_file とか標準プラグインは write を実行したらリアルなアウトプット(標準出力とかファイルへの出力)がある。つまり driver.run してから結果をチェックすることができる。でも今回の実装は system("/path/to/rdm-mailhandler.rb") するだけで、結果をエミュレートするのはかなり大変。ということで、こいつをコールする redmine_submit メソッドを作り、そいつに想定したメールファイルのパスが渡されていれば、OKとする。
minitest の stub を使ってみた。こちらを参考に。http://blog.willnet.in/entry/2012/12/05/004010
def test_redmine_submit
driver = create_driver
mail_path = "/path/to/mail"
input_data = {"path"=>mail_path}
# MiniTest::Mock.new.expect(method_name, retval, args in array)
mock_method = MiniTest::Mock.new.expect(:call, true, [mail_path])
driver.instance.stub(:redmine_submit, mock_method) do
driver.emit(input_data)
driver.run
end
# Mock.verify returns true if the method was called
assert_equal true, mock_method.verify
end
この stub の書き方だと本体にもきちんと redmine_submit が宣言されている必要がある。なのでテストを実行すると
Error:
Fluent::RedmineOutputTest#test_redmine_submit:
NameError: undefined method `redmine_submit' for class `Fluent::RedmineOutput'
となる。stub するので空のメソッドを追加しておく。
def redmine_submit(mail_path)
end
で、テストするとまだ呼び出してないので・・・
Error:
Fluent::RedmineOutputTest#test_redmine_submit:
MockExpectationError: expected call("/path/to/mail") => true
コールされてないよーとのこと。ここでようやく Fluentd output プラグインに手を入れる。
def write(chunk)
chunk.msgpack_each {|(tag,time,record)|
redmine_submit(record["path"])
}
end
rake
Finished tests in 1.043659s, 2.8745 tests/s, 2.8745 assertions/s.
redmine_submit の実装はさくっと。
def redmine_submit(mail_path)
handler="/path/to/rdm-mailhandler.rb"
system("#{handler} --url #{@site} --key #{@key}")
end
gem をビルドする
$ rake build
で pkg ディレクトリの下にファイルができる。
td-agent にインストールする
/usr/lib64/fluent/ruby/bin/fluent-gem install /path/to/file.gem