require_relative '../helper' require 'fluent/test/driver/parser' require 'fluent/plugin/parser' require 'json' require 'timecop' class ParserTest < ::Test::Unit::TestCase class ExampleParser < Fluent::Plugin::Parser def parse(data) r = JSON.parse(data) yield convert_values(parse_time(r), r) end end def create_driver(conf={}) Fluent::Test::Driver::Parser.new(Fluent::Plugin::Parser).configure(conf) end def setup Fluent::Test.setup end sub_test_case 'base class works as plugin' do def test_init i = Fluent::Plugin::Parser.new assert_nil i.types assert_nil i.null_value_pattern assert !i.null_empty_string assert i.estimate_current_event assert !i.keep_time_key end def test_configure_against_string_literal d = create_driver('keep_time_key true') assert_true d.instance.keep_time_key end def test_parse d = create_driver assert_raise NotImplementedError do d.instance.parse('') end end end sub_test_case '#string_like_null' do setup do @i = ExampleParser.new end test 'returns false if null_empty_string is false and null_value_regexp is nil' do assert ! @i.string_like_null('a', false, nil) assert ! @i.string_like_null('', false, nil) end test 'returns true if null_empty_string is true and string value is empty' do assert ! @i.string_like_null('a', true, nil) assert @i.string_like_null('', true, nil) end test 'returns true if null_value_regexp has regexp and it matches string value' do assert ! @i.string_like_null('a', false, /null/i) assert @i.string_like_null('NULL', false, /null/i) assert @i.string_like_null('empty', false, /null|empty/i) end end sub_test_case '#build_type_converters converters' do setup do @i = ExampleParser.new types_config = { "s" => "string", "i" => "integer", "f" => "float", "b" => "bool", "t1" => "time", "t2" => "time:%Y-%m-%d %H:%M:%S.%N", "t3" => "time:+0100:%Y-%m-%d %H:%M:%S.%N", "t4" => "time:unixtime", "t5" => "time:float", "a1" => "array", "a2" => "array:|", } @hash = { 'types' => JSON.dump(types_config), } end test 'to do #to_s by "string" type' do @i.configure(config_element('parse', '', @hash)) c = @i.type_converters["s"] assert_equal "", c.call("") assert_equal "a", c.call("a") assert_equal "1", c.call(1) assert_equal "1.01", c.call(1.01) assert_equal "true", c.call(true) assert_equal "false", c.call(false) end test 'to do #to_i by "integer" type' do @i.configure(config_element('parse', '', @hash)) c = @i.type_converters["i"] assert_equal 0, c.call("") assert_equal 0, c.call("0") assert_equal 0, c.call("a") assert_equal(-1000, c.call("-1000")) assert_equal 1, c.call(1) assert_equal 1, c.call(1.01) assert_equal 0, c.call(true) assert_equal 0, c.call(false) end test 'to do #to_f by "float" type' do @i.configure(config_element('parse', '', @hash)) c = @i.type_converters["f"] assert_equal 0.0, c.call("") assert_equal 0.0, c.call("0") assert_equal 0.0, c.call("a") assert_equal(-1000.0, c.call("-1000")) assert_equal 1.0, c.call(1) assert_equal 1.01, c.call(1.01) assert_equal 0.0, c.call(true) assert_equal 0.0, c.call(false) end test 'to return true/false, which returns true only for true/yes/1 (C & perl style), by "bool"' do @i.configure(config_element('parse', '', @hash)) c = @i.type_converters["b"] assert_false c.call("") assert_false c.call("0") assert_false c.call("a") assert_true c.call("1") assert_true c.call("true") assert_true c.call("True") assert_true c.call("YES") assert_true c.call(true) assert_false c.call(false) assert_false c.call("1.0") end test 'to parse time string by ruby default time parser without any options' do # "t1" => "time", with_timezone("UTC+02") do # -0200 @i.configure(config_element('parse', '', @hash)) c = @i.type_converters["t1"] assert_nil c.call("") assert_equal_event_time event_time("2016-10-21 01:54:30 -0200"), c.call("2016-10-21 01:54:30") assert_equal_event_time event_time("2016-10-21 03:54:30 -0200"), c.call("2016-10-21 01:54:30 -0400") assert_equal_event_time event_time("2016-10-21 01:55:24 -0200"), c.call("2016-10-21T01:55:24-02:00") assert_equal_event_time event_time("2016-10-21 01:55:24 -0200"), c.call("2016-10-21T03:55:24Z") end end test 'to parse time string with specified time format' do # "t2" => "time:%Y-%m-%d %H:%M:%S.%N", with_timezone("UTC+02") do # -0200 @i.configure(config_element('parse', '', @hash)) c = @i.type_converters["t2"] assert_nil c.call("") assert_equal_event_time event_time("2016-10-21 01:54:30.123000000 -0200"), c.call("2016-10-21 01:54:30.123") assert_equal_event_time event_time("2016-10-21 01:54:30.012345678 -0200"), c.call("2016-10-21 01:54:30.012345678") assert_nil c.call("2016/10/21 015430") end end test 'to parse time string with specified time format and timezone' do # "t3" => "time:+0100:%Y-%m-%d %H:%M:%S.%N", with_timezone("UTC+02") do # -0200 @i.configure(config_element('parse', '', @hash)) c = @i.type_converters["t3"] assert_nil c.call("") assert_equal_event_time event_time("2016-10-21 01:54:30.123000000 +0100"), c.call("2016-10-21 01:54:30.123") assert_equal_event_time event_time("2016-10-21 01:54:30.012345678 +0100"), c.call("2016-10-21 01:54:30.012345678") end end test 'to parse time string in unix timestamp' do # "t4" => "time:unixtime", with_timezone("UTC+02") do # -0200 @i.configure(config_element('parse', '', @hash)) c = @i.type_converters["t4"] assert_equal_event_time event_time("1970-01-01 00:00:00.0 +0000"), c.call("") assert_equal_event_time event_time("2016-10-21 01:54:30.0 -0200"), c.call("1477022070") assert_equal_event_time event_time("2016-10-21 01:54:30.0 -0200"), c.call("1477022070.01") end end test 'to parse time string in floating poing value' do # "t5" => "time:float", with_timezone("UTC+02") do # -0200 @i.configure(config_element('parse', '', @hash)) c = @i.type_converters["t5"] assert_equal_event_time event_time("1970-01-01 00:00:00.0 +0000"), c.call("") assert_equal_event_time event_time("2016-10-21 01:54:30.012 -0200"), c.call("1477022070.012") assert_equal_event_time event_time("2016-10-21 01:54:30.123456789 -0200"), c.call("1477022070.123456789") end end test 'to return array of string' do @i.configure(config_element('parse', '', @hash)) c = @i.type_converters["a1"] assert_equal [], c.call("") assert_equal ["0"], c.call("0") assert_equal ["0"], c.call(0) assert_equal ["0", "1"], c.call("0,1") assert_equal ["0|1", "2"], c.call("0|1,2") assert_equal ["true"], c.call(true) end test 'to return array of string using specified delimiter' do @i.configure(config_element('parse', '', @hash)) c = @i.type_converters["a2"] assert_equal [], c.call("") assert_equal ["0"], c.call("0") assert_equal ["0"], c.call(0) assert_equal ["0,1"], c.call("0,1") assert_equal ["0", "1,2"], c.call("0|1,2") assert_equal ["true"], c.call(true) end end sub_test_case 'example parser without any configurations' do setup do @current_time = Time.parse("2016-10-21 14:22:01.0 +1000") @current_event_time = Fluent::EventTime.new(@current_time.to_i, 0) # @current_time.to_i #=> 1477023721 Timecop.freeze(@current_time) @i = ExampleParser.new @i.configure(config_element('parse', '', {})) end teardown do Timecop.return end test 'parser returns parsed JSON object, leaving empty/NULL strings, with current time' do json = '{"t1":"1477023720.101","s1":"","s2":"NULL","s3":"null","k1":1,"k2":"13.1","k3":"1","k4":"yes"}' @i.parse(json) do |time, record| assert_equal_event_time @current_event_time, time assert_equal "1477023720.101", record["t1"] assert_equal "", record["s1"] assert_equal "NULL", record["s2"] assert_equal "null", record["s3"] assert_equal 1, record["k1"] assert_equal "13.1", record["k2"] assert_equal "1", record["k3"] assert_equal "yes", record["k4"] end end end sub_test_case 'example parser fully configured' do setup do @current_time = Time.parse("2016-10-21 14:22:01.0 +1000") @current_event_time = Fluent::EventTime.new(@current_time.to_i, 0) # @current_time.to_i #=> 1477023721 Timecop.freeze(@current_time) @i = ExampleParser.new hash = { 'keep_time_key' => "no", 'estimate_current_event' => "yes", 'time_key' => "t1", 'time_type' => "float", 'null_empty_string' => 'yes', 'null_value_pattern' => 'NULL|null', 'types' => "k1:string, k2:integer, k3:float, k4:bool", } @i.configure(config_element('parse', '', hash)) end teardown do Timecop.return end test 'parser returns parsed JSON object, leaving empty/NULL strings, with current time' do json = '{"t1":"1477023720.101","s1":"","s2":"NULL","s3":"null","k1":1,"k2":"13.1","k3":"1","k4":"yes"}' @i.parse(json) do |time, record| assert_equal_event_time Fluent::EventTime.new(1477023720, 101_000_000), time assert !record.has_key?("t1") assert{ record.has_key?("s1") && record["s1"].nil? } assert{ record.has_key?("s2") && record["s2"].nil? } assert{ record.has_key?("s3") && record["s3"].nil? } assert_equal "1", record["k1"] assert_equal 13, record["k2"] assert_equal 1.0, record["k3"] assert_equal true, record["k4"] end end test 'parser returns current time if a field is missing specified by time_key' do json = '{"s1":"","s2":"NULL","s3":"null","k1":1,"k2":"13.1","k3":"1","k4":"yes"}' @i.parse(json) do |time, record| assert_equal_event_time @current_event_time, time assert !record.has_key?("t1") assert{ record.has_key?("s1") && record["s1"].nil? } assert{ record.has_key?("s2") && record["s2"].nil? } assert{ record.has_key?("s3") && record["s3"].nil? } assert_equal "1", record["k1"] assert_equal 13, record["k2"] assert_equal 1.0, record["k3"] assert_equal true, record["k4"] end end end sub_test_case 'example parser configured not to estimate current time, and to keep time key' do setup do @current_time = Time.parse("2016-10-21 14:22:01.0 +1000") @current_event_time = Fluent::EventTime.new(@current_time.to_i, 0) # @current_time.to_i #=> 1477023721 Timecop.freeze(@current_time) @i = ExampleParser.new hash = { 'keep_time_key' => "yes", 'estimate_current_event' => "no", 'time_key' => "t1", 'time_type' => "float", 'null_empty_string' => 'yes', 'null_value_pattern' => 'NULL|null', 'types' => "k1:string, k2:integer, k3:float, k4:bool", } @i.configure(config_element('parse', '', hash)) end teardown do Timecop.return end test 'parser returns parsed time with original field and value if the field of time exists' do json = '{"t1":"1477023720.101","s1":"","s2":"NULL","s3":"null","k1":1,"k2":"13.1","k3":"1","k4":"yes"}' @i.parse(json) do |time, record| assert_equal_event_time Fluent::EventTime.new(1477023720, 101_000_000), time assert_equal "1477023720.101", record["t1"] assert{ record.has_key?("s1") && record["s1"].nil? } assert{ record.has_key?("s2") && record["s2"].nil? } assert{ record.has_key?("s3") && record["s3"].nil? } assert_equal "1", record["k1"] assert_equal 13, record["k2"] assert_equal 1.0, record["k3"] assert_equal true, record["k4"] end end test 'parser returns nil as time if the field of time is missing' do json = '{"s1":"","s2":"NULL","s3":"null","k1":1,"k2":"13.1","k3":"1","k4":"yes"}' @i.parse(json) do |time, record| assert_nil time assert !record.has_key?("t1") assert{ record.has_key?("s1") && record["s1"].nil? } assert{ record.has_key?("s2") && record["s2"].nil? } assert{ record.has_key?("s3") && record["s3"].nil? } assert_equal "1", record["k1"] assert_equal 13, record["k2"] assert_equal 1.0, record["k3"] assert_equal true, record["k4"] end end end sub_test_case 'timeout' do class SleepParser < Fluent::Plugin::Parser attr :test_value def configure(conf) super @test_value = nil end def parse(data) sleep 10 @test_value = :passed yield JSON.parse(data), Fluent::EventTime.now end end setup do @i = SleepParser.new @i.instance_variable_set(:@log, Fluent::Test::TestLogger.new) @i.configure(config_element('parse', '', {'timeout' => '1.0'})) @i.start end teardown do @i.stop end test 'stop longer processing and return nil' do waiting(10) { @i.parse('{"k":"v"}') do |time, record| assert_nil @i.test_value assert_nil time assert_nil record end assert_true @i.log.out.logs.first.include?('parsing timed out') } end end end