Back to Repositories

Validating Configuration Type System in Fluentd

This test suite validates configuration type handling in Fluentd, focusing on data type conversion and validation for configuration parameters. It ensures proper parsing and transformation of various data types including sizes, time values, booleans, and regular expressions.

Test Coverage Overview

The test suite provides comprehensive coverage of Fluentd’s configuration type system, examining size value parsing, time value conversion, boolean validation, and regexp handling. Key test cases include:

  • Size value parsing with different units (k, M, G, T)
  • Time duration conversion with various units (s, m, h, d)
  • Boolean value validation with different inputs
  • Regular expression pattern validation
  • Edge cases and error handling for invalid inputs

Implementation Analysis

The testing approach uses Test::Unit with data-driven test cases through the ‘data’ method for systematic validation. Each configuration type has dedicated sub-test cases that verify both normal and edge case scenarios. The implementation leverages Ruby’s type conversion capabilities and includes strict validation modes for production-grade reliability.

Technical Details

Testing tools and configuration:
  • Test::Unit framework for test organization
  • Fluent::Config module for type definitions
  • Custom type converters for config_param definitions
  • Strict validation mode options
  • Support for UTF-8 encoding in string handling

Best Practices Demonstrated

The test suite exemplifies several testing best practices:

  • Comprehensive type validation including edge cases
  • Systematic organization using sub_test_case blocks
  • Clear separation of normal and error cases
  • Explicit handling of nil values
  • Proper error message validation
  • Support for strict validation modes

fluent/fluentd

test/config/test_types.rb

            
require 'helper'
require 'fluent/config/types'

class TestConfigTypes < ::Test::Unit::TestCase
  include Fluent

  sub_test_case 'Config.size_value' do
    data("2k" => [2048, "2k"],
         "2K" => [2048, "2K"],
         "3m" => [3145728, "3m"],
         "3M" => [3145728, "3M"],
         "4g" => [4294967296, "4g"],
         "4G" => [4294967296, "4G"],
         "5t" => [5497558138880, "5t"],
         "5T" => [5497558138880, "5T"],
         "6"  => [6, "6"])
    test 'normal case' do |(expected, val)|
      assert_equal(expected, Config.size_value(val))
      assert_equal(expected, Config.size_value(val, { strict: true }))
    end

    data("integer" => [6, 6],
         "hoge" => [0, "hoge"],
         "empty" => [0, ""])
    test 'not assumed case' do |(expected, val)|
      assert_equal(expected, Config.size_value(val))
    end

    test 'nil' do
      assert_equal(nil, Config.size_value(nil))
    end

    data("integer" => [6, 6],
         "hoge" => [Fluent::ConfigError.new('name1: invalid value for Integer(): "hoge"'), "hoge"],
         "empty" => [Fluent::ConfigError.new('name1: invalid value for Integer(): ""'), ""])
    test 'not assumed case with strict' do |(expected, val)|
      if expected.kind_of? Exception
        assert_raise(expected) do
          Config.size_value(val, { strict: true }, "name1")
        end
      else
        assert_equal(expected, Config.size_value(val, { strict: true }, "name1"))
      end
    end

    test 'nil with strict' do
      assert_equal(nil, Config.size_value(nil, { strict: true }))
    end
  end

  sub_test_case 'Config.time_value' do
    data("10s" => [10, "10s"],
         "10sec" => [10, "10sec"],
         "2m" => [120, "2m"],
         "3h" => [10800, "3h"],
         "4d" => [345600, "4d"])
    test 'normal case' do |(expected, val)|
      assert_equal(expected, Config.time_value(val))
      assert_equal(expected, Config.time_value(val, { strict: true }))
    end

    data("integer" => [4.0, 4],
         "float" => [0.4, 0.4],
         "hoge" => [0.0, "hoge"],
         "empty" => [0.0, ""])
    test 'not assumed case' do |(expected, val)|
      assert_equal(expected, Config.time_value(val))
    end

    test 'nil' do
      assert_equal(nil, Config.time_value(nil))
    end

    data("integer" => [6, 6],
         "hoge" => [Fluent::ConfigError.new('name1: invalid value for Float(): "hoge"'), "hoge"],
         "empty" => [Fluent::ConfigError.new('name1: invalid value for Float(): ""'), ""])
    test 'not assumed case with strict' do |(expected, val)|
      if expected.kind_of? Exception
        assert_raise(expected) do
          Config.time_value(val, { strict: true }, "name1")
        end
      else
        assert_equal(expected, Config.time_value(val, { strict: true }, "name1"))
      end
    end

    test 'nil with strict' do
      assert_equal(nil, Config.time_value(nil, { strict: true }))
    end
  end

  sub_test_case 'Config.bool_value' do
    data("true" => [true, "true"],
         "yes" => [true, "yes"],
         "empty" => [true, ""],
         "false" => [false, "false"],
         "no" => [false, "no"])
    test 'normal case' do |(expected, val)|
      assert_equal(expected, Config.bool_value(val))
    end

    data("true" =>  [true,  true],
         "false" => [false, false],
         "hoge" => [nil, "hoge"],
         "nil" => [nil, nil],
         "integer" => [nil, 10])
    test 'not assumed case' do |(expected, val)|
      assert_equal(expected, Config.bool_value(val))
    end

    data("true" =>  [true,  true],
         "false" => [false, false],
         "hoge" => [Fluent::ConfigError.new("name1: invalid bool value: hoge"), "hoge"],
         "nil" => [nil, nil],
         "integer" => [Fluent::ConfigError.new("name1: invalid bool value: 10"), 10])
    test 'not assumed case with strict' do |(expected, val)|
      if expected.kind_of? Exception
        assert_raise(expected) do
          Config.bool_value(val, { strict: true }, "name1")
        end
      else
        assert_equal(expected, Config.bool_value(val, { strict: true }, "name1"))
      end
    end
  end

  sub_test_case 'Config.regexp_value' do
    data("empty" => [//, "//"],
         "plain" => [/regexp/, "/regexp/"],
         "zero width" => [/^$/, "/^$/"],
         "character classes" => [/[a-z]/, "/[a-z]/"],
         "meta charactersx" => [/.+.*?\d\w\s\S/, '/.+.*?\d\w\s\S/'])
    test 'normal case' do |(expected, str)|
      assert_equal(expected, Config.regexp_value(str))
    end

    data("empty" => [//, ""],
         "plain" => [/regexp/, "regexp"],
         "zero width" => [/^$/, "^$"],
         "character classes" => [/[a-z]/, "[a-z]"],
         "meta charactersx" => [/.+.*?\d\w\s\S/, '.+.*?\d\w\s\S'])
    test 'w/o slashes' do |(expected, str)|
      assert_equal(expected, Config.regexp_value(str))
    end

    data("missing right slash" => "/regexp",
         "too many options" => "/regexp/imx",)
    test 'invalid regexp' do |(str)|
      assert_raise(Fluent::ConfigError.new("invalid regexp: missing right slash: #{str}")) do
        Config.regexp_value(str)
      end
    end

    test 'nil' do
      assert_equal nil, Config.regexp_value(nil)
    end
  end

  sub_test_case 'type converters for config_param definitions' do
    data("test" => ['test', 'test'],
         "1" =>  ['1',    '1'],
         "spaces" =>  ['   ',  '   '])
    test 'string' do |(expected, val)|
      assert_equal expected, Config::STRING_TYPE.call(val, {})
      assert_equal Encoding::UTF_8, Config::STRING_TYPE.call(val, {}).encoding
    end

    test 'string nil' do
      assert_equal nil, Config::STRING_TYPE.call(nil, {})
    end

    data('latin' => 'Märch',
         'ascii' => 'ascii',
         'space' => '     ',
         'number' => '1',
         'Hiragana' => 'あいうえお')
    test 'string w/ binary' do |str|
      actual = Config::STRING_TYPE.call(str.b, {})
      assert_equal str, actual
      assert_equal Encoding::UTF_8, actual.encoding
    end

    data('starts_with_semicolon' => [:conor, ':conor'],
         'simple_string' => [:conor, 'conor'],
         'empty_string' => [nil, ''])
    test 'symbol' do |(expected, val)|
      assert_equal Config::SYMBOL_TYPE.call(val, {}), expected
    end

    data("val" => [:val, 'val'],
         "v" => [:v, 'v'],
         "value" => [:value, 'value'])
    test 'enum' do |(expected, val)|
      assert_equal expected, Config::ENUM_TYPE.call(val, {list: [:val, :value, :v]})
    end

    test 'enum: pick unknown choice' do
      assert_raises(Fluent::ConfigError.new("valid options are val,value,v but got x")) do
        Config::ENUM_TYPE.call('x', {list: [:val, :value, :v]})
      end
    end

    data("empty list"  => {},
         "string list" => {list: ["val", "value", "v"]})
    test 'enum: invalid choices' do | list |
      assert_raises(RuntimeError.new("Plugin BUG: config type 'enum' requires :list of symbols")) do
        Config::ENUM_TYPE.call('val', list)
      end
    end

    test 'enum: nil' do
      assert_equal nil, Config::ENUM_TYPE.call(nil)
    end

    data("1" => [1, '1'],
         "1.0" => [1, '1.0'],
         "1_000" => [1000, '1_000'],
         "1x" => [1, '1x'])
    test 'integer' do |(expected, val)|
      assert_equal expected, Config::INTEGER_TYPE.call(val, {})
    end

    data("integer" => [6, 6],
         "hoge" => [0, "hoge"],
         "empty" => [0, ""])
    test 'integer: not assumed case' do |(expected, val)|
      assert_equal expected, Config::INTEGER_TYPE.call(val, {})
    end

    test 'integer: nil' do
      assert_equal nil, Config::INTEGER_TYPE.call(nil, {})
    end

    data("integer" => [6, 6],
         "hoge" => [Fluent::ConfigError.new('name1: invalid value for Integer(): "hoge"'), "hoge"],
         "empty" => [Fluent::ConfigError.new('name1: invalid value for Integer(): ""'), ""])
    test 'integer: not assumed case with strict' do |(expected, val)|
      if expected.kind_of? Exception
        assert_raise(expected) do
          Config::INTEGER_TYPE.call(val, { strict: true }, "name1")
        end
      else
        assert_equal expected, Config::INTEGER_TYPE.call(val, { strict: true }, "name1")
      end
    end

    test 'integer: nil with strict' do
      assert_equal nil, Config::INTEGER_TYPE.call(nil, { strict: true })
    end

    data("1" => [1.0, '1'],
         "1.0" => [1.0, '1.0'],
         "1.00" => [1.0, '1.00'],
         "1e0" => [1.0, '1e0'])
    test 'float' do |(expected, val)|
      assert_equal expected, Config::FLOAT_TYPE.call(val, {})
    end

    data("integer" => [6, 6],
         "hoge" => [0, "hoge"],
         "empty" => [0, ""])
    test 'float: not assumed case' do |(expected, val)|
      assert_equal expected, Config::FLOAT_TYPE.call(val, {})
    end

    test 'float: nil' do
      assert_equal nil, Config::FLOAT_TYPE.call(nil, {})
    end

    data("integer" => [6, 6],
         "hoge" => [Fluent::ConfigError.new('name1: invalid value for Float(): "hoge"'), "hoge"],
         "empty" => [Fluent::ConfigError.new('name1: invalid value for Float(): ""'), ""])
    test 'float: not assumed case with strict' do |(expected, val)|
      if expected.kind_of? Exception
        assert_raise(expected) do
          Config::FLOAT_TYPE.call(val, { strict: true }, "name1")
        end
      else
        assert_equal expected, Config::FLOAT_TYPE.call(val, { strict: true }, "name1")
      end
    end

    test 'float: nil with strict' do
      assert_equal nil, Config::FLOAT_TYPE.call(nil, { strict: true })
    end

    data("1000" => [1000, '1000'],
         "1k" => [1024, '1k'],
         "1m" => [1024*1024, '1m'])
    test 'size' do |(expected, val)|
      assert_equal expected, Config::SIZE_TYPE.call(val, {})
    end

    data("true" => [true, 'true'],
         "yes" => [true, 'yes'],
         "no" => [false, 'no'],
         "false" => [false, 'false'],
         "TRUE" => [nil, 'TRUE'],
         "True" => [nil, 'True'],
         "Yes" => [nil, 'Yes'],
         "No" => [nil, 'No'],
         "empty" => [true, ''],
         "unexpected_string" => [nil, 'unexpected_string'])
    test 'bool' do |(expected, val)|
      assert_equal expected, Config::BOOL_TYPE.call(val, {})
    end

    data("0" => [0, '0'],
         "1" => [1.0, '1'],
         "1.01" => [1.01,  '1.01'],
         "1s" => [1, '1s'],
         "1," => [60, '1m'],
         "1h" => [3600,  '1h'],
         "1d" => [86400, '1d'])
    test 'time' do |(expected, val)|
      assert_equal expected, Config::TIME_TYPE.call(val, {})
    end

    data("empty" => [//, "//"],
         "plain" => [/regexp/, "/regexp/"],
         "zero width" => [/^$/, "/^$/"],
         "character classes" => [/[a-z]/, "/[a-z]/"],
         "meta charactersx" => [/.+.*?\d\w\s\S/, '/.+.*?\d\w\s\S/'])
    test 'regexp' do |(expected, str)|
      assert_equal(expected, Config::REGEXP_TYPE.call(str, {}))
    end

    data("string and integer" => [{"x"=>"v","k"=>1}, '{"x":"v","k":1}', {}],
         "strings" => [{"x"=>"v","k"=>"1"}, 'x:v,k:1', {}],
         "w/ space" => [{"x"=>"v","k"=>"1"}, 'x:v, k:1', {}],
         "heading space" => [{"x"=>"v","k"=>"1"}, ' x:v, k:1 ', {}],
         "trailing space" => [{"x"=>"v","k"=>"1"}, 'x:v , k:1 ', {}],
         "multiple colons" => [{"x"=>"v:v","k"=>"1"}, 'x:v:v, k:1', {}],
         "symbolize keys" => [{x: "v", k: 1}, '{"x":"v","k":1}', {symbolize_keys: true}],
         "value_type: :string" => [{x: "v", k: "1"}, 'x:v,k:1', {symbolize_keys: true, value_type: :string}],
         "value_type: :string 2" => [{x: "v", k: "1"}, '{"x":"v","k":1}', {symbolize_keys: true, value_type: :string}],
         "value_type: :integer" => [{x: 0, k: 1}, 'x:0,k:1', {symbolize_keys: true, value_type: :integer}],
         "time 1" => [{"x"=>1,"y"=>60,"z"=>3600}, '{"x":"1s","y":"1m","z":"1h"}', {value_type: :time}],
         "time 2" => [{"x"=>1,"y"=>60,"z"=>3600}, 'x:1s,y:1m,z:1h', {value_type: :time}])
    test 'hash' do |(expected, val, opts)|
      assert_equal(expected, Config::HASH_TYPE.call(val, opts))
    end

    test 'hash w/ unknown type' do
      assert_raise(RuntimeError.new("unknown type in REFORMAT: foo")) do
        Config::HASH_TYPE.call("x:1,y:2", {value_type: :foo})
      end
    end

    test 'hash w/ strict option' do
      assert_raise(Fluent::ConfigError.new('y: invalid value for Integer(): "hoge"')) do
        Config::HASH_TYPE.call("x:1,y:hoge", {value_type: :integer, strict: true})
      end
    end

    data('latin' => ['3:Märch', {"3"=>"Märch"}],
         'ascii' => ['ascii:ascii', {"ascii"=>"ascii"}],
         'number' => ['number:1', {"number"=>"1"}],
         'Hiragana' => ['hiragana:あいうえお', {"hiragana"=>"あいうえお"}])
    test 'hash w/ binary' do |(target, expected)|
      assert_equal(expected, Config::HASH_TYPE.call(target.b, { value_type: :string }))
    end

    test 'hash w/ nil' do
      assert_equal(nil, Config::HASH_TYPE.call(nil))
    end

    data("strings and integer" => [["1","2",1],   '["1","2",1]', {}],
         "number strings" => [["1","2","1"], '1,2,1', {}],
         "alphabets" => [["a","b","c"], '["a","b","c"]', {}],
         "alphabets w/o quote" => [["a","b","c"], 'a,b,c', {}],
         "w/ spaces" => [["a","b","c"], 'a, b, c', {}],
         "w/ space before comma" => [["a","b","c"], 'a , b , c', {}],
         "comma or space w/ qupte" => [["a a","b,b"," c "], '["a a","b,b"," c "]', {}],
         "space in a value w/o qupte" => [["a a","b","c"], 'a a,b,c', {}],
         "integers" => [[1,2,1], '[1,2,1]', {}],
         "value_type: :integer w/ quote" => [[1,2,1], '["1","2","1"]', {value_type: :integer}],
         "value_type: :integer w/o quote" => [[1,2,1], '1,2,1', {value_type: :integer}])
    test 'array' do |(expected, val, opts)|
      assert_equal(expected, Config::ARRAY_TYPE.call(val, opts))
    end

    data('["1","2"]' => [["1","2"], '["1","2"]'],
         '["3"]' => [["3"], '["3"]'])
    test 'array w/ default values' do |(expected, val)|
      array_options = {
        default: [],
      }
      assert_equal(expected, Config::ARRAY_TYPE.call(val, array_options))
    end

    test 'array w/ unknown type' do
      assert_raise(RuntimeError.new("unknown type in REFORMAT: foo")) do
        Config::ARRAY_TYPE.call("1,2", {value_type: :foo})
      end
    end

    test 'array w/ strict option' do
      assert_raise(Fluent::ConfigError.new(': invalid value for Integer(): "hoge"')) do
        Config::ARRAY_TYPE.call("1,hoge", {value_type: :integer, strict: true}, "name1")
      end
    end

    test 'array w/ nil' do
      assert_equal(nil, Config::ARRAY_TYPE.call(nil))
    end
  end
end