すっかり時代に乗り遅れてるけど fluentd が必要になったので test も書きながら output プラグインを書いているその記録

# 前置き

## 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)]
```

## テストを書く

必須

```rb:out_redmine_test.rb
  def setup
    Fluent::Test.setup
  end
```

来週続きを書くであろう自分のためにシナリオを書いておく。

```rb:out_redmine_test.rb
# 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``` に書き換える。

```rb:out_redmine_test.rb
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' にする。

```rb:lib/fluent/plugin/out_redmine.rb
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 パラメータを設定。

```rb:lib/fluent/plugin/out_redmine.rb
    # 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 というのが用意されているので、それを使う。

```rb:test/fluent/plugin/out_redmine_test.rb
  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

```rb
  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 するので空のメソッドを追加しておく。

```rb:lib/fluent/plugin/out_redmine.rb
    def redmine_submit(mail_path)
    end
```

で、テストするとまだ呼び出してないので・・・

```
Error:
Fluent::RedmineOutputTest#test_redmine_submit:
MockExpectationError: expected call("/path/to/mail") => true
```

コールされてないよーとのこと。ここでようやく Fluentd output プラグインに手を入れる。

```rb:lib/fluent/plugin/out_redmine.rb
    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 の実装はさくっと。

```rb:lib/fluent/plugin/out_redmine.rb
    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
```