fluent-plugin-datacalculator 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/.gitignore ADDED
@@ -0,0 +1,11 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
5
+ # For TextMate, emacs, vim
6
+ *.tmproj
7
+ tmtags
8
+ *~
9
+ \#*
10
+ .\#*
11
+ *.swp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in fluent-plugin-datacounter.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,13 @@
1
+ Copyright (c) 2012- TAGOMORI Satoshi
2
+
3
+ Licensed under the Apache License, Version 2.0 (the "License");
4
+ you may not use this file except in compliance with the License.
5
+ You may obtain a copy of the License at
6
+
7
+ http://www.apache.org/licenses/LICENSE-2.0
8
+
9
+ Unless required by applicable law or agreed to in writing, software
10
+ distributed under the License is distributed on an "AS IS" BASIS,
11
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ See the License for the specific language governing permissions and
13
+ limitations under the License.
data/README.rdoc ADDED
@@ -0,0 +1,52 @@
1
+ = fluent-plugin-datacalculator
2
+
3
+ == Component
4
+
5
+ === DataCalculateOutput
6
+
7
+ Simple Calculate messages and summarize the calculated results.
8
+
9
+ - Summarize calculated results per min/hour/day
10
+ - Summarize calculated results per second (average every min/hour/day)
11
+ - Use finalizer of summarized results (e.g. average)
12
+
13
+ 'input_tag_remove_prefix' option available if you want to remove tag prefix from output field names.
14
+
15
+ == Configuration
16
+
17
+ === DataCalculateOutput
18
+
19
+ <match accesslog.**>
20
+ type datacalculate
21
+ unit minute
22
+ aggregate all
23
+ fomulas sum = amount * price, amounts = amount
24
+ </match>
25
+
26
+ If you use finalizer, like this
27
+
28
+ <match accesslog.**>
29
+ type datacalculate
30
+ unit minute
31
+ aggregate all
32
+ fomulas sum = amount * price, amounts = amount
33
+ finalizer average = amounts > 0 ? 1.0 * sum / amounts : 0
34
+ </match>
35
+
36
+ Finalizer uses the summarized output, so argv in finalizer must exist in left-hand side in fomulas.
37
+
38
+
39
+
40
+ == TODO
41
+
42
+ - multiple finalizer
43
+ - patches welcome!
44
+
45
+ == Thanks
46
+
47
+ tagomoris's fluent-plugin-datacounter is AWESOME plugin! That's nice!
48
+
49
+ == Copyright
50
+
51
+ Copyright:: Copyright (c) 2012- Muddy Dixon
52
+ License:: Apache License, Version 2.0
data/Rakefile ADDED
@@ -0,0 +1,11 @@
1
+ require "bundler/gem_tasks"
2
+
3
+ require 'rake/testtask'
4
+ Rake::TestTask.new(:test) do |test|
5
+ test.libs << 'lib' << 'test'
6
+ test.pattern = 'test/**/test_*.rb'
7
+ test.verbose = true
8
+ end
9
+
10
+ task :default => :test
11
+
data/example.conf ADDED
@@ -0,0 +1,21 @@
1
+ <source>
2
+ type forward
3
+ </source>
4
+ <match payment.**>
5
+ type copy
6
+ <store>
7
+ type stdout
8
+ </store>
9
+ <store>
10
+ type datacalculate
11
+ tag result
12
+ count_interval 5s
13
+ aggregate all
14
+ formulas sum = amount * price, cnt = 1, total = amount
15
+ finalizer ave = cnt > 0 ? 1.00 * sum / cnt : 0
16
+ </store>
17
+ </match>
18
+
19
+ <match result>
20
+ type stdout
21
+ </match>
@@ -0,0 +1,25 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+
4
+ Gem::Specification.new do |s|
5
+ s.name = "fluent-plugin-datacalculator"
6
+ s.version = "0.0.0"
7
+ s.authors = ["Muddy Dixon"]
8
+ s.email = ["muddydixon@gmail.com"]
9
+ s.homepage = "https://github.com/muddydixon/fluent-plugin-datacalculator"
10
+ s.summary = %q{Output filter plugin to calculate messages that matches specified conditions}
11
+ s.description = %q{Output filter plugin to calculate messages that matches specified conditions}
12
+
13
+ s.rubyforge_project = "fluent-plugin-datacalculator"
14
+
15
+ s.files = `git ls-files`.split("\n")
16
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
17
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
18
+ s.require_paths = ["lib"]
19
+
20
+ # specify any dependencies here; for example:
21
+ # s.add_development_dependency "rspec"
22
+ # s.add_runtime_dependency "rest-client"
23
+ s.add_development_dependency "fluentd"
24
+ s.add_runtime_dependency "fluentd"
25
+ end
@@ -0,0 +1,244 @@
1
+ class Fluent::DataCalculatorOutput < Fluent::Output
2
+ Fluent::Plugin.register_output('datacalculate', self)
3
+
4
+ config_param :count_interval, :time, :default => nil
5
+ config_param :unit, :string, :default => 'minute'
6
+ config_param :aggregate, :string, :default => 'tag'
7
+ config_param :tag, :string, :default => 'datacalculate'
8
+ config_param :input_tag_remove_prefix, :string, :default => nil
9
+ config_param :formulas, :string
10
+ config_param :finalizer, :string, :default => nil
11
+ config_param :outcast_unmatched, :bool, :default => false
12
+
13
+ attr_accessor :tick
14
+ attr_accessor :counts
15
+ attr_accessor :last_checked
16
+ attr_accessor :_formulas
17
+ attr_accessor :_finalizer
18
+
19
+ def configure(conf)
20
+ super
21
+
22
+ if @count_interval
23
+ @tick = @count_interval.to_i
24
+ else
25
+ @tick = case @unit
26
+ when 'minute' then 60
27
+ when 'hour' then 3600
28
+ when 'day' then 86400
29
+ else
30
+ raise RuntimeError, "@unit must be one of minute/hour/day"
31
+ end
32
+ end
33
+
34
+ @aggregate = case @aggregate
35
+ when 'tag' then :tag
36
+ when 'all' then :all
37
+ else
38
+ raise Fluent::ConfigError, "flowcounter aggregate allows tag/all"
39
+ end
40
+
41
+ def createFunc (cnt, str)
42
+ str.strip!
43
+ left, right = str.split(/\s*=\s*/, 2)
44
+ rights = right.scan(/[a-zA-Z][\w\d_\.\$]*/).uniq
45
+
46
+ begin
47
+ f = eval('lambda {|'+rights.join(',')+'| '+right + '}')
48
+ rescue SyntaxError
49
+ raise Fluent::ConfigError, "'" + str + "' is not valid"
50
+ end
51
+
52
+ [cnt, left, rights, f]
53
+ end
54
+
55
+ def execFunc (tag, obj, argv, formula)
56
+ if tag != nil
57
+ tag = stripped_tag (tag)
58
+ end
59
+ _argv = []
60
+
61
+ argv.each {|arg|
62
+ if tag != nil and tag != 'all'
63
+ arg = tag + '_' + arg
64
+ end
65
+ _argv.push obj[arg]
66
+ }
67
+ formula.call(*_argv)
68
+ end
69
+
70
+ @_formulas = [[0, 'unmatched', nil, nil]]
71
+ if conf.has_key?('formulas')
72
+ fs = conf['formulas'].split(/\s*,\s*/)
73
+ fs.each_with_index { |str,i |
74
+ @_formulas.push( createFunc(i + 1, str) )
75
+ }
76
+ end
77
+
78
+ if conf.has_key?('finalizer')
79
+ @_finalizer = createFunc(0, conf['finalizer'])
80
+
81
+ # check finalizer field
82
+ cnt = @_finalizer[2].length
83
+ @_finalizer[2].each { |key|
84
+ @_formulas.each { |formula|
85
+ next if formula[2] == nil
86
+ cnt -= 1 if formula[1] == key
87
+ }
88
+ }
89
+ if cnt != 0
90
+ raise Fluent::ConfigError, 'keys in finalizer is not satisfied'
91
+ end
92
+ end
93
+
94
+ if @input_tag_remove_prefix
95
+ @removed_prefix_string = @input_tag_remove_prefix + '.'
96
+ @removed_length = @removed_prefix_string.length
97
+ end
98
+
99
+ @counts = count_initialized
100
+ @mutex = Mutex.new
101
+ end
102
+
103
+ def start
104
+ super
105
+ start_watch
106
+ end
107
+
108
+ def shutdown
109
+ super
110
+ @watcher.terminate
111
+ @watcher.join
112
+ end
113
+
114
+ def count_initialized(keys=nil)
115
+ # counts['tag'][num] = count
116
+ if @aggregate == :all
117
+ {'all' => ([0] * @_formulas.length)}
118
+ elsif keys
119
+ values = Array.new(keys.length) {|i|
120
+ Array.new(@_formulas.length){|j| 0 }
121
+ }
122
+ Hash[[keys, values].transpose]
123
+ else
124
+ {}
125
+ end
126
+ end
127
+
128
+ def countups(tag, counts)
129
+ if @aggregate == :all
130
+ tag = 'all'
131
+ end
132
+
133
+ @mutex.synchronize {
134
+ @counts[tag] ||= [0] * @_formulas.length
135
+ counts.each_with_index do |count, i|
136
+ @counts[tag][i] += count
137
+ end
138
+ }
139
+ end
140
+
141
+ def stripped_tag(tag)
142
+ return tag unless @input_tag_remove_prefix
143
+ return tag[@removed_length..-1] if tag.start_with?(@removed_prefix_string) and tag.length > @removed_length
144
+ return tag[@removed_length..-1] if tag == @input_tag_remove_prefix
145
+ tag
146
+ end
147
+
148
+ def generate_output(counts, step)
149
+ output = {}
150
+ if @aggregate == :all
151
+ # index 0 is unmatched
152
+ sum = if @outcast_unmatched
153
+ counts['all'][1..-1].inject(:+)
154
+ else
155
+ counts['all'].inject(:+)
156
+ end
157
+ counts['all'].each_with_index do |count,i|
158
+ name = @_formulas[i][1]
159
+ output[name] = count
160
+ end
161
+
162
+ if @_finalizer
163
+ output[@_finalizer[1]] = execFunc('all', output, @_finalizer[2], @_finalizer[3])
164
+ end
165
+
166
+ return output
167
+ end
168
+
169
+ counts.keys.each do |tag|
170
+ t = stripped_tag(tag)
171
+ sum = if @outcast_unmatched
172
+ counts[tag][1..-1].inject(:+)
173
+ else
174
+ counts[tag].inject(:+)
175
+ end
176
+ counts[tag].each_with_index do |count,i|
177
+ name = @_formulas[i][1]
178
+ output[t + '_' + name] = count
179
+ end
180
+ if @_finalizer
181
+ output[t + '_' + @_finalizer[1]] = execFunc(tag, output, @_finalizer[2], @_finalizer[3])
182
+ end
183
+ end
184
+ output
185
+ end
186
+
187
+ def flush(step)
188
+ flushed,@counts = @counts,count_initialized(@counts.keys.dup)
189
+ generate_output(flushed, step)
190
+ end
191
+
192
+ def flush_emit(step)
193
+ Fluent::Engine.emit(@tag, Fluent::Engine.now, flush(step))
194
+ end
195
+
196
+ def start_watch
197
+ # for internal, or tests only
198
+ @watcher = Thread.new(&method(:watch))
199
+ end
200
+
201
+ def watch
202
+ # instance variable, and public accessable, for test
203
+ @last_checked = Fluent::Engine.now
204
+ while true
205
+ sleep 0.5
206
+ if Fluent::Engine.now - @last_checked >= @tick
207
+ now = Fluent::Engine.now
208
+ flush_emit(now - @last_checked)
209
+ @last_checked = now
210
+ end
211
+ end
212
+ end
213
+
214
+ def checkArgs (obj, inkeys)
215
+ inkeys.each{ |key|
216
+ if not obj.has_key?(key)
217
+ return false
218
+ end
219
+ }
220
+ return true
221
+ end
222
+
223
+ def emit(tag, es, chain)
224
+ c = [0] * @_formulas.length
225
+
226
+ es.each do |time,record|
227
+ matched = false
228
+ if @_formulas.length > 0
229
+ @_formulas.each do |index, outkey, inkeys, formula|
230
+ next unless formula and checkArgs(record, inkeys)
231
+
232
+ c[index] += execFunc(nil, record, inkeys, formula)
233
+ matched = true
234
+ end
235
+ else
236
+ $log.warn index
237
+ end
238
+ c[0] += 1 unless matched
239
+ end
240
+ countups(tag, c)
241
+
242
+ chain.next
243
+ end
244
+ end
data/test/helper.rb ADDED
@@ -0,0 +1,28 @@
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
+ require 'fluent/test'
15
+ unless ENV.has_key?('VERBOSE')
16
+ nulllogger = Object.new
17
+ nulllogger.instance_eval {|obj|
18
+ def method_missing(method, *args)
19
+ # pass
20
+ end
21
+ }
22
+ $log = nulllogger
23
+ end
24
+
25
+ require 'fluent/plugin/out_datacalculator'
26
+
27
+ class Test::Unit::TestCase
28
+ end
@@ -0,0 +1,172 @@
1
+ # -*- coding: utf-8 -*-
2
+ require 'helper'
3
+
4
+ class DataCalculatorOutputTest < Test::Unit::TestCase
5
+ def setup
6
+ Fluent::Test.setup
7
+ end
8
+
9
+ CONFIG = %[
10
+ unit minute
11
+ aggregate tag
12
+ input_tag_remove_prefix test
13
+ formulas sum = amount * price, amounts = amount, record = 1
14
+ finalizer ave = amounts > 0 ? 1.0 * sum / amounts : 0
15
+ ]
16
+
17
+ def create_driver(conf = CONFIG, tag='test.input')
18
+ Fluent::Test::OutputTestDriver.new(Fluent::DataCalculatorOutput, tag).configure(conf)
19
+ end
20
+
21
+ def test_configure
22
+ assert_raise(Fluent::ConfigError) {
23
+ d = create_driver('')
24
+ }
25
+ # 式がSyntax Error
26
+ assert_raise(Fluent::ConfigError) {
27
+ d = create_driver %[
28
+ formulas sum = 10 ab
29
+ ]
30
+ }
31
+ # finalizerに必要な要素がない
32
+ assert_raise(Fluent::ConfigError) {
33
+ d = create_driver %[
34
+ finalizer ave = cnt > 0 ? sum / cnt : 0
35
+ ]
36
+ }
37
+ # finalizerに必要な要素がない
38
+ assert_raise(Fluent::ConfigError) {
39
+ d = create_driver %[
40
+ formulas sum = 10
41
+ finalizer ave = cnt > 0 ? sum / cnt : 0
42
+ ]
43
+ }
44
+ d = create_driver %[
45
+ formulas sum = amount * price, cnt = amount
46
+ finalizer ave = cnt > 0 ? sum / cnt : 0
47
+ ]
48
+ assert_equal 60, d.instance.tick
49
+ assert_equal :tag, d.instance.aggregate
50
+ assert_equal 'datacalculate', d.instance.tag
51
+ assert_nil d.instance.input_tag_remove_prefix
52
+ assert_equal 'sum = amount * price, cnt = amount', d.instance.formulas
53
+ assert_equal 'ave = cnt > 0 ? sum / cnt : 0', d.instance.finalizer
54
+ assert_equal false, d.instance.outcast_unmatched
55
+
56
+ end
57
+
58
+ def test_count_initialized
59
+ d = create_driver %[
60
+ aggregate all
61
+ formulas sum = amount * price, cnt = amount
62
+ ]
63
+ assert_equal [0,0,0], d.instance.counts['all']
64
+ end
65
+
66
+ def test_create_formula
67
+ d = create_driver %[
68
+ aggregate all
69
+ formulas sum = amount * price, cnt = amount
70
+ ]
71
+ assert_equal 0, d.instance._formulas[0][0]
72
+ assert_equal 'unmatched', d.instance._formulas[0][1]
73
+ assert_equal nil, d.instance._formulas[0][2]
74
+ assert_equal 1, d.instance._formulas[1][0]
75
+ assert_equal 'sum', d.instance._formulas[1][1]
76
+ assert_equal ['amount', 'price'], d.instance._formulas[1][2]
77
+ assert_equal 2, d.instance._formulas[2][0]
78
+ assert_equal 'cnt', d.instance._formulas[2][1]
79
+ assert_equal ['amount'], d.instance._formulas[2][2]
80
+ end
81
+
82
+ def test_countups
83
+ d = create_driver
84
+ assert_nil d.instance.counts['test.input']
85
+ d.instance.countups('test.input', [0, 0, 0, 0])
86
+ assert_equal [0,0,0,0], d.instance.counts['test.input']
87
+ d.instance.countups('test.input', [1, 1, 1, 0])
88
+ assert_equal [1,1,1,0], d.instance.counts['test.input']
89
+ d.instance.countups('test.input', [0, 5, 1, 0])
90
+ assert_equal [1,6,2,0], d.instance.counts['test.input']
91
+ end
92
+
93
+ def test_stripped_tag
94
+ d = create_driver
95
+ assert_equal 'input', d.instance.stripped_tag('test.input')
96
+ assert_equal 'test.input', d.instance.stripped_tag('test.test.input')
97
+ assert_equal 'input', d.instance.stripped_tag('input')
98
+ end
99
+
100
+ def test_generate_output
101
+ d = create_driver
102
+ r1 = d.instance.generate_output({'test.input' => [60,240,120,180], 'test.input2' => [0,600,0,0]}, 60)
103
+
104
+ assert_equal 60, r1['input_unmatched']
105
+ assert_equal 240, r1['input_sum']
106
+ assert_equal 120, r1['input_amounts']
107
+ assert_equal 180, r1['input_record']
108
+ assert_equal 2, r1['input_ave']
109
+
110
+ assert_equal 0, r1['input2_unmatched']
111
+ assert_equal 600, r1['input2_sum']
112
+ assert_equal 0, r1['input2_amounts']
113
+ assert_equal 0, r1['input2_record']
114
+ assert_equal 0, r1['input2_ave']
115
+
116
+
117
+ d = create_driver %[
118
+ aggregate all
119
+ input_tag_remove_prefix test
120
+ formulas sum = amount * price, amounts = amount, record = 1
121
+ finalizer ave = amounts > 0 ? sum / amounts : 0
122
+ ]
123
+
124
+ r2 = d.instance.generate_output({'all' => [60,240,120,180]}, 60)
125
+ assert_equal 60, r2['unmatched']
126
+ assert_equal 240, r2['sum']
127
+ assert_equal 120, r2['amounts']
128
+ assert_equal 180, r2['record']
129
+ assert_equal 2, r2['ave']
130
+ end
131
+
132
+ def test_emit
133
+ d1 = create_driver(CONFIG, 'test.input')
134
+ d1.run do
135
+ 60.times do
136
+ d1.emit({'amount' => 3, 'price' => 100})
137
+ d1.emit({'amount' => 3, 'price' => 200})
138
+ d1.emit({'amount' => 6, 'price' => 50})
139
+ d1.emit({'amount' => 10, 'price' => 100})
140
+ end
141
+ end
142
+ r1 = d1.instance.flush(60)
143
+ assert_equal 0, r1['input_unmatched']
144
+ assert_equal 132000, r1['input_sum']
145
+ assert_equal 1320, r1['input_amounts']
146
+ assert_equal 240, r1['input_record']
147
+ assert_equal 100.0, r1['input_ave']
148
+
149
+ d2 = create_driver(%[
150
+ unit minute
151
+ aggregate all
152
+ input_tag_remove_prefix test
153
+ formulas sum = amount * price, amounts = amount, record = 1
154
+ finalizer ave = amounts > 0 ? 1.0 * sum / amounts : 0
155
+ ], 'test.input2')
156
+
157
+ d2.run do
158
+ 60.times do
159
+ d2.emit({'amount' => 3, 'price' => 100})
160
+ d2.emit({'amount' => 3, 'price' => 200})
161
+ d2.emit({'amount' => 6, 'price' => 50})
162
+ d2.emit({'amount' => 10, 'price' => 100})
163
+ end
164
+ end
165
+ r2 = d2.instance.flush(60)
166
+ assert_equal 0, r2['unmatched']
167
+ assert_equal 132000, r2['sum']
168
+ assert_equal 1320, r2['amounts']
169
+ assert_equal 240, r2['record']
170
+ assert_equal 100.0, r2['ave']
171
+ end
172
+ end
metadata ADDED
@@ -0,0 +1,89 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: fluent-plugin-datacalculator
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Muddy Dixon
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-05-17 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: Output filter plugin to calculate messages that matches specified conditions
47
+ email:
48
+ - muddydixon@gmail.com
49
+ executables: []
50
+ extensions: []
51
+ extra_rdoc_files: []
52
+ files:
53
+ - .gitignore
54
+ - Gemfile
55
+ - LICENSE.txt
56
+ - README.rdoc
57
+ - Rakefile
58
+ - example.conf
59
+ - fluent-plugin-datacalculator.gemspec
60
+ - lib/fluent/plugin/out_datacalculator.rb
61
+ - test/helper.rb
62
+ - test/plugin/test_out_datacalculator.rb
63
+ homepage: https://github.com/muddydixon/fluent-plugin-datacalculator
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: fluent-plugin-datacalculator
83
+ rubygems_version: 1.8.21
84
+ signing_key:
85
+ specification_version: 3
86
+ summary: Output filter plugin to calculate messages that matches specified conditions
87
+ test_files:
88
+ - test/helper.rb
89
+ - test/plugin/test_out_datacalculator.rb