fluent-mixin-plaintextformatter 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,21 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ *~
19
+ \#*
20
+ .\#*
21
+ *.swp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in fluent-mixin-plaintextformatter.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 TAGOMORI Satoshi
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,90 @@
1
+ # Fluent::Mixin::PlainTextFormatter
2
+
3
+ Fluent::Mixin::PlainTextFormatter is a mix-in module, that provides '#format' instance method and its configurations to Fluentd BufferedOutput Plugin and TimeSlicedOutput Plugin, to output plain text data (to file, REST storages, KVSs ...).
4
+
5
+ This module provides features to:
6
+
7
+ * format whole data as serialized JSON, single attribute or separated multi attributes
8
+ * include time as line header (formatted by time_format in UTC(default) or localtime), or not
9
+ * include tag as line header (remove_prefix available), or not
10
+ * change field separator (TAB(default), SPACE or COMMA)
11
+ * add new line as termination, or not
12
+
13
+ ## Usage
14
+
15
+ To use this module in your fluentd plugin, include this module like below:
16
+
17
+ class FooOutput < BufferedOutput
18
+ Fluent::Plugin.register_output('foo', self)
19
+
20
+ config_set_default :buffer_type, 'memory'
21
+
22
+ include Fluent::Mixin::PlainTextFormatter
23
+
24
+ config_param :foo_config_x, :string, :default => nil
25
+
26
+ # and other your plugin's configuration parameters...
27
+
28
+ def configure(conf)
29
+ super
30
+
31
+ # ....
32
+ end
33
+
34
+ def start
35
+ # ...
36
+ end
37
+
38
+ def shutdown
39
+ # ....
40
+ end
41
+
42
+ # def format(tag, time, record)
43
+ # # do not define 'format'
44
+ # end
45
+
46
+ def write(chunk)
47
+ # ....
48
+ end
49
+ end
50
+
51
+ And you can overwrite default formatting configurations on your plugin (values below are default of mix-in):
52
+
53
+ class FooOutput < BufferedOutput
54
+ # ...
55
+
56
+ config_set_default :output_include_time, true
57
+ config_set_default :output_include_tag, true
58
+ config_set_default :output_data_type, 'json'
59
+ config_set_default :field_separator, 'TAB'
60
+ config_set_default :add_newline, true
61
+ config_set_default :time_format, nil # nil means ISO8601 '2012-07-13T19:29:49+09:00'
62
+ config_set_default :remove_prefix, nil
63
+ config_set_default :default_tag, nil
64
+
65
+ # ...
66
+ end
67
+
68
+ Provided configurations are below:
69
+
70
+ * output\_include\_time [yes/no]
71
+ * output\_include\_tag [yes/no]
72
+ * output\_data\_type
73
+ * 'json': output by JSON
74
+ * 'attr:key1,key2,key3': values of 'key1' and 'key2' and ..., with separator specified by 'field_separator'
75
+ * field\_separator [TAB/SPACE/COMMA]
76
+ * add_newline [yes/no]
77
+ * time_format
78
+ * format string like '%Y-%m-%d %H:%M:%S' or you want
79
+ * remove_prefix
80
+ * input tag 'test.foo' with 'remove_prefix test', output tag is 'foo'.
81
+ * 'default\_tag' configuration is used when input tag is completely equal to 'remove\_prefix'
82
+
83
+ ## AUTHORS
84
+
85
+ * TAGOMORI Satoshi <tagomoris@gmail.com>
86
+
87
+ ## LICENSE
88
+
89
+ * Copyright: Copyright (c) 2012- tagomoris
90
+ * License: Apache License, Version 2.0
data/Rakefile ADDED
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
3
+
4
+ require 'rake/testtask'
5
+ Rake::TestTask.new(:test) do |test|
6
+ test.libs << 'lib' << 'test'
7
+ test.pattern = 'test/**/test_*.rb'
8
+ test.verbose = true
9
+ end
10
+
11
+ task :default => :test
@@ -0,0 +1,18 @@
1
+ # -*- encoding: utf-8 -*-
2
+ Gem::Specification.new do |gem|
3
+ gem.name = "fluent-mixin-plaintextformatter"
4
+ gem.version = "0.1.0"
5
+ gem.authors = ["TAGOMORI Satoshi"]
6
+ gem.email = ["tagomoris@gmail.com"]
7
+ gem.description = %q{included to format values into json, tsv or csv}
8
+ gem.summary = %q{Text formatter mixin module to create fluentd plugin}
9
+ gem.homepage = "https://github.com/tagomoris/fluent-mixin-plaintextformatter"
10
+
11
+ gem.files = `git ls-files`.split($\)
12
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
13
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
14
+ gem.require_paths = ["lib"]
15
+
16
+ gem.add_development_dependency "fluentd"
17
+ gem.add_runtime_dependency "fluentd"
18
+ end
@@ -0,0 +1,98 @@
1
+ require 'fluent/config'
2
+
3
+ module Fluent
4
+ module Mixin
5
+ module PlainTextFormatter
6
+ attr_accessor :output_include_time, :output_include_tag, :output_data_type
7
+ attr_accessor :add_newline, :field_separator
8
+ attr_accessor :remove_prefix, :default_tag
9
+
10
+ attr_accessor :f_separator
11
+
12
+ def first_value(*args)
13
+ args.reduce{|a,b| (not a.nil?) ? a : b}
14
+ end
15
+
16
+ def configure(conf)
17
+ super
18
+
19
+ @output_include_time = first_value( Fluent::Config.bool_value(conf['output_include_time']), @output_include_time, true )
20
+ @output_include_tag = first_value( Fluent::Config.bool_value(conf['output_include_tag']), @output_include_tag, true )
21
+
22
+ @output_data_type = first_value( conf['output_data_type'], @output_data_type, 'json' )
23
+
24
+ @field_separator = first_value( conf['field_separator'], @field_separator, 'TAB' )
25
+ @f_separator = case @field_separator
26
+ when /SPACE/i then ' '
27
+ when /COMMA/i then ','
28
+ else "\t"
29
+ end
30
+
31
+ @add_newline = first_value( Fluent::Config.bool_value(conf['add_newline']), @add_newline, true )
32
+
33
+ @remove_prefix = conf['remove_prefix']
34
+ if @remove_prefix
35
+ @removed_prefix_string = @remove_prefix + '.'
36
+ @removed_length = @removed_prefix_string.length
37
+ end
38
+ if @output_include_tag and @remove_prefix and @remove_prefix.length > 0
39
+ @default_tag = first_value( conf['default_tag'], @default_tag, nil )
40
+ if @default_tag.nil? or @default_tag.length < 1
41
+ raise Fluent::ConfigError, "Missing 'default_tag' with output_include_tag and remove_prefix."
42
+ end
43
+ end
44
+
45
+ # default timezone: utc
46
+ if not conf.has_key?('localtime') and not conf.has_key?('utc')
47
+ @localtime = false
48
+ elsif conf.has_key?('localtime')
49
+ @localtime = true
50
+ elsif conf.has_key?('utc')
51
+ @localtime = false
52
+ end
53
+ # mix-in default time formatter (or you can overwrite @timef on your own configure)
54
+ @time_format = first_value( conf['time_format'], @time_format, nil )
55
+ @timef = @output_include_time ? Fluent::TimeFormatter.new(@time_format, @localtime) : nil
56
+
57
+ @custom_attributes = if @output_data_type == 'json'
58
+ nil
59
+ elsif @output_data_type =~ /^attr:(.+)$/
60
+ $1.split(',')
61
+ else
62
+ raise Fluent::ConfigError, "invalid output_data_type:'#{@output_data_type}'"
63
+ end
64
+ end
65
+
66
+ def stringify_record(record)
67
+ if @custom_attributes.nil?
68
+ record.to_json
69
+ else
70
+ @custom_attributes.map{|attr|
71
+ (record[attr] || 'NULL').to_s
72
+ }.join(@f_separator)
73
+ end
74
+ end
75
+
76
+ def format(tag, time, record)
77
+ if @remove_prefix
78
+ if tag.start_with?(@removed_prefix_string) and tag.length > @removed_length
79
+ tag = tag[@removed_length..-1]
80
+ elsif tag == @remove_prefix
81
+ tag = @default_tag
82
+ end
83
+ end
84
+ time_str = if @output_include_time
85
+ @timef.format(time) + @f_separator
86
+ else
87
+ ''
88
+ end
89
+ tag_str = if @output_include_tag
90
+ tag + @f_separator
91
+ else
92
+ ''
93
+ end
94
+ time_str + tag_str + stringify_record(record) + (@add_newline ? "\n" : '')
95
+ end
96
+ end
97
+ end
98
+ end
data/test/helper.rb ADDED
@@ -0,0 +1,30 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+ begin
4
+ Bundler.setup(:default, :development)
5
+ rescue Bundler::BundlerError => e
6
+ $stderr.puts e.message
7
+ $stderr.puts "Run `bundle install` to install missing gems"
8
+ exit e.status_code
9
+ end
10
+ require 'test/unit'
11
+
12
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
13
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
14
+
15
+ require 'fluent/test'
16
+ unless ENV.has_key?('VERBOSE')
17
+ nulllogger = Object.new
18
+ nulllogger.instance_eval {|obj|
19
+ def method_missing(method, *args)
20
+ # pass
21
+ end
22
+ }
23
+ $log = nulllogger
24
+ end
25
+
26
+ require 'fluent/mixin/plaintextformatter'
27
+ require_relative 'output'
28
+
29
+ class Test::Unit::TestCase
30
+ end
@@ -0,0 +1,156 @@
1
+ require 'helper'
2
+
3
+ class PlainTextFormatterTest < Test::Unit::TestCase
4
+ def create_plugin_instance(type, conf = CONFIG, tag='test')
5
+ Fluent::Test::BufferedOutputTestDriver.new(type, tag).configure(conf).instance
6
+ end
7
+
8
+ def test_first_value
9
+ p = create_plugin_instance(Fluent::TestAOutput, "type testa\n")
10
+ assert_equal nil, p.first_value()
11
+ assert_equal nil, p.first_value(nil, nil)
12
+ assert_equal 'hoge', p.first_value(nil, nil, 'hoge')
13
+ assert_equal 'bar', p.first_value(nil, 'bar', 'hoge')
14
+ assert_equal 'foo', p.first_value('foo', 'bar', 'hoge')
15
+ assert_equal 'foo', p.first_value('foo', nil, 'hoge')
16
+ assert_equal 'foo', p.first_value('foo', 'bar', nil)
17
+ assert_equal 'foo', p.first_value('foo', nil, nil)
18
+ end
19
+
20
+ def test_default_with_time_tag
21
+ p = create_plugin_instance(Fluent::TestAOutput, "type testa\n")
22
+ r = {'foo' => 'foo foo baz', 'bar' => 10000}
23
+ # stringify
24
+ assert_equal r, JSON.parse(p.stringify_record(r))
25
+
26
+ line = p.format('test.a', 1342163105, r)
27
+ # add_newline true
28
+ assert_equal line[0..-2], line.chomp
29
+ # output_include_time true, output_include_tag true
30
+ assert_equal ['2012-07-13T07:05:05Z', 'test.a'], line.chomp.split(/\t/)[0..1]
31
+ # output_data_type json
32
+ assert_equal r, JSON.parse(line.chomp.split(/\t/)[2])
33
+ end
34
+
35
+ def test_field_separator_newline_json
36
+ p = create_plugin_instance(Fluent::TestBOutput, "type testb\nlocaltime\n")
37
+ r = {'foo' => 'foo foo baz', 'bar' => 10000}
38
+ # stringify
39
+ assert_equal r, JSON.parse(p.stringify_record(r))
40
+
41
+ line = p.format('test.b', 1342163105, r)
42
+ # add_newline false
43
+ assert_equal line[0..-1], line.chomp
44
+ # output_include_time true, output_include_tag true, localtime, separator COMMA
45
+ assert_equal ['2012-07-13T16:05:05+09:00', 'test.b'], line.chomp.split(/,/, 3)[0..1]
46
+ # output_data_type json
47
+ assert_equal r, JSON.parse(line.chomp.split(/,/, 3)[2])
48
+ end
49
+
50
+ def test_time_format
51
+ p = create_plugin_instance(Fluent::TestBOutput, %[
52
+ type testb
53
+ localtime
54
+ time_format %Y/%m/%d %H:%M:%S
55
+ ])
56
+ r = {'foo' => 'foo foo baz', 'bar' => 10000}
57
+ # stringify
58
+ assert_equal r, JSON.parse(p.stringify_record(r))
59
+
60
+ line = p.format('test.b', 1342163105, r)
61
+ # add_newline false
62
+ assert_equal line[0..-1], line.chomp
63
+ # output_include_time true, output_include_tag true, localtime, separator COMMA
64
+ assert_equal ['2012/07/13 16:05:05', 'test.b'], line.chomp.split(/,/, 3)[0..1]
65
+ # output_data_type json
66
+ assert_equal r, JSON.parse(line.chomp.split(/,/, 3)[2])
67
+ end
68
+ def test_separator_space_remove_prefix
69
+ p = create_plugin_instance(Fluent::TestBOutput, %[
70
+ type testb
71
+ localtime
72
+ time_format %Y/%m/%d:%H:%M:%S
73
+ field_separator space
74
+ remove_prefix test
75
+ ])
76
+ r = {'foo' => 'foo foo baz', 'bar' => 10000}
77
+ # stringify
78
+ assert_equal r, JSON.parse(p.stringify_record(r))
79
+
80
+ line = p.format('test.b', 1342163105, r)
81
+ # add_newline false
82
+ assert_equal line[0..-1], line.chomp
83
+ # output_include_time true, output_include_tag true, localtime, separator COMMA
84
+ assert_equal ['2012/07/13:16:05:05', 'b'], line.chomp.split(/ /, 3)[0..1]
85
+ # output_data_type json
86
+ assert_equal r, JSON.parse(line.chomp.split(/ /, 3)[2])
87
+ end
88
+
89
+ def test_default_without_time_tag
90
+ p = create_plugin_instance(Fluent::TestCOutput, "type testc\n")
91
+ r = {'foo' => 'foo foo baz', 'bar' => 10000}
92
+ # stringify
93
+ assert_equal r, JSON.parse(p.stringify_record(r))
94
+
95
+ line = p.format('test.c', 1342163105, r)
96
+ # add_newline
97
+ assert_equal line[0..-2], line.chomp
98
+ # output_include_time false, output_include_tag false
99
+ assert_equal 1, line.chomp.split(/\t/).size
100
+ # output_data_type json
101
+ assert_equal r, JSON.parse(line.chomp.split(/\t/).first)
102
+ end
103
+
104
+ def test_format_single_attribute
105
+ p = create_plugin_instance(Fluent::TestAOutput, %[
106
+ type testa
107
+ output_include_time true
108
+ output_include_tag true
109
+ output_data_type attr:foo
110
+ ])
111
+ r = {'foo' => 'foo foo baz', 'bar' => 10000}
112
+ # stringify
113
+ assert_equal 'foo foo baz', p.stringify_record(r)
114
+ # format
115
+ assert_equal "2012-07-13T07:05:05Z\ttest.a\tfoo foo baz\n", p.format('test.a', 1342163105, r)
116
+
117
+ p = create_plugin_instance(Fluent::TestAOutput, %[
118
+ type testa
119
+ output_include_time false
120
+ output_include_tag false
121
+ output_data_type attr:bar
122
+ ])
123
+ r = {'foo' => 'foo foo baz', 'bar' => 10000}
124
+ # stringify
125
+ assert_equal '10000', p.stringify_record(r)
126
+ # format
127
+ assert_equal "10000\n", p.format('test.a', 1342163105, r)
128
+ end
129
+
130
+ def test_format_multi_attribute
131
+ p = create_plugin_instance(Fluent::TestAOutput, %[
132
+ type testa
133
+ output_include_time true
134
+ output_include_tag true
135
+ output_data_type attr:foo,bar
136
+ ])
137
+ r = {'foo' => 'foo foo baz', 'bar' => 10000}
138
+ # stringify
139
+ assert_equal "foo foo baz\t10000", p.stringify_record(r)
140
+ # format
141
+ assert_equal "2012-07-13T07:05:05Z\ttest.a\tfoo foo baz\t10000\n", p.format('test.a', 1342163105, r)
142
+
143
+ p = create_plugin_instance(Fluent::TestAOutput, %[
144
+ type testa
145
+ output_include_time false
146
+ output_include_tag false
147
+ output_data_type attr:bar,foo
148
+ field_separator comma
149
+ ])
150
+ r = {'foo' => 'foo foo baz', 'bar' => 10000}
151
+ # stringify
152
+ assert_equal "10000,foo foo baz", p.stringify_record(r)
153
+ # format
154
+ assert_equal "10000,foo foo baz\n", p.format('test.a', 1342163105, r)
155
+ end
156
+ end
data/test/output.rb ADDED
@@ -0,0 +1,64 @@
1
+ require 'fluent/mixin/plaintextformatter'
2
+
3
+ module Fluent
4
+ class TestAOutput < Fluent::BufferedOutput
5
+ Fluent::Plugin.register_output('testa', self)
6
+
7
+ config_set_default :buffer_type, 'memory'
8
+
9
+ include Fluent::Mixin::PlainTextFormatter
10
+
11
+ config_set_default :output_include_time, true
12
+ config_set_default :output_include_tag, true
13
+ config_set_default :output_data_type, 'json'
14
+ config_set_default :field_separator, "\t"
15
+ config_set_default :add_newline, true
16
+ config_set_default :remove_prefix, nil
17
+ config_set_default :default_tag, 'tag.blank'
18
+
19
+ def configure(conf)
20
+ super
21
+ end
22
+ end
23
+
24
+ class TestBOutput < Fluent::BufferedOutput
25
+ Fluent::Plugin.register_output('testa', self)
26
+
27
+ config_set_default :buffer_type, 'memory'
28
+
29
+ include Fluent::Mixin::PlainTextFormatter
30
+
31
+ config_set_default :output_include_time, true
32
+ config_set_default :output_include_tag, true
33
+ config_set_default :output_data_type, 'json'
34
+ config_set_default :field_separator, "COMMA"
35
+ config_set_default :add_newline, false
36
+ config_set_default :remove_prefix, nil
37
+ config_set_default :default_tag, 'tag.blank'
38
+
39
+ def configure(conf)
40
+ super
41
+ end
42
+ end
43
+
44
+ class TestCOutput < Fluent::BufferedOutput
45
+ Fluent::Plugin.register_output('testc', self)
46
+
47
+ config_set_default :buffer_type, 'memory'
48
+
49
+ include Fluent::Mixin::PlainTextFormatter
50
+
51
+ config_set_default :output_include_time, false
52
+ config_set_default :output_include_tag, false
53
+ config_set_default :output_data_type, 'json'
54
+ config_set_default :field_separator, "\t"
55
+ config_set_default :add_newline, true
56
+ config_set_default :remove_prefix, nil
57
+ config_set_default :default_tag, 'tag.blank'
58
+
59
+ def configure(conf)
60
+ super
61
+ end
62
+ end
63
+
64
+ end
metadata ADDED
@@ -0,0 +1,90 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: fluent-mixin-plaintextformatter
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - TAGOMORI Satoshi
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-07-13 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: fluentd
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ - !ruby/object:Gem::Dependency
31
+ name: fluentd
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ description: included to format values into json, tsv or csv
47
+ email:
48
+ - tagomoris@gmail.com
49
+ executables: []
50
+ extensions: []
51
+ extra_rdoc_files: []
52
+ files:
53
+ - .gitignore
54
+ - Gemfile
55
+ - LICENSE
56
+ - README.md
57
+ - Rakefile
58
+ - fluent-mixin-plaintextformatter.gemspec
59
+ - lib/fluent/mixin/plaintextformatter.rb
60
+ - test/helper.rb
61
+ - test/mixin/test_plaintextformatter.rb
62
+ - test/output.rb
63
+ homepage: https://github.com/tagomoris/fluent-mixin-plaintextformatter
64
+ licenses: []
65
+ post_install_message:
66
+ rdoc_options: []
67
+ require_paths:
68
+ - lib
69
+ required_ruby_version: !ruby/object:Gem::Requirement
70
+ none: false
71
+ requirements:
72
+ - - ! '>='
73
+ - !ruby/object:Gem::Version
74
+ version: '0'
75
+ required_rubygems_version: !ruby/object:Gem::Requirement
76
+ none: false
77
+ requirements:
78
+ - - ! '>='
79
+ - !ruby/object:Gem::Version
80
+ version: '0'
81
+ requirements: []
82
+ rubyforge_project:
83
+ rubygems_version: 1.8.21
84
+ signing_key:
85
+ specification_version: 3
86
+ summary: Text formatter mixin module to create fluentd plugin
87
+ test_files:
88
+ - test/helper.rb
89
+ - test/mixin/test_plaintextformatter.rb
90
+ - test/output.rb