Back to Repositories

Validating Configuration Element Handling in Fluentd

This test suite validates the configuration element functionality in Fluentd, focusing on element manipulation, attribute handling, and worker configuration. The tests ensure proper management of configuration hierarchies and settings in the Fluentd logging framework.

Test Coverage Overview

The test suite provides comprehensive coverage of Fluentd’s configuration element features, including element creation, attribute management, and nested configuration structures.

  • Element initialization and manipulation
  • Attribute handling and validation
  • Worker configuration and targeting
  • Configuration serialization and masking

Implementation Analysis

The testing approach uses Minitest’s Test::Unit framework to systematically verify configuration element behavior. Tests are organized using sub_test_case blocks for logical grouping, with detailed assertions validating element properties and transformations.

  • Hierarchical test organization
  • Comprehensive attribute validation
  • Element comparison and merging tests

Technical Details

The test implementation leverages:

  • Test::Unit framework for assertions and test organization
  • Fluent::Config::Element class for configuration management
  • ConfigureProxy for parameter type handling
  • Custom element creation helper methods

Best Practices Demonstrated

The test suite exemplifies strong testing practices through modular test organization, thorough edge case coverage, and comprehensive validation of configuration behaviors.

  • Systematic test case organization
  • Thorough parameter validation
  • Comprehensive secret parameter handling
  • Worker configuration validation

fluent/fluentd

test/config/test_element.rb

            
require_relative '../helper'
require 'fluent/config/element'
require 'fluent/config/configure_proxy'
require 'fluent/configurable'
require 'pp'

class TestConfigElement < ::Test::Unit::TestCase
  def element(name = 'ROOT', arg = '', attrs = {}, elements = [], unused = nil)
    Fluent::Config::Element.new(name, arg, attrs, elements, unused)
  end

  sub_test_case '#elements=' do
    test 'elements can be set by others' do
      e = element()
      assert_equal [], e.elements

      e1 = element('e1')
      e2 = element('e2')
      e.elements = [e1, e2]
      assert_equal [e1, e2], e.elements
    end
  end

  sub_test_case '#elements' do
    setup do
      @c1 = element('source')
      @c2 = element('source', 'yay')
      @c3 = element('match', 'test.**')
      @c4 = element('label', '@mytest', {}, [ element('filter', '**'), element('match', '**') ])
      children = [@c1, @c2, @c3, @c4]
      @e = Fluent::Config::Element.new('ROOT', '', {}, children)
    end

    test 'returns all elements without arguments' do
      assert_equal [@c1, @c2, @c3, @c4], @e.elements
    end

    test 'returns elements with specified names' do
      assert_equal [@c1, @c2, @c3], @e.elements('source', 'match')
      assert_equal [@c3, @c4], @e.elements('match', 'label')
    end

    test 'returns elements with specified name, and arg if specified' do
      assert_equal [@c1, @c2], @e.elements(name: 'source')
      assert_equal [@c2], @e.elements(name: 'source', arg: 'yay')
    end

    test 'keyword argument name/arg and names are exclusive' do
      assert_raise ArgumentError do
        @e.elements('source', name: 'match')
      end
      assert_raise ArgumentError do
        @e.elements('source', 'match', name: 'label', arg: '@mytest')
      end
    end

    test 'specifying only arg without name is invalid' do
      assert_raise ArgumentError do
        @e.elements(arg: '@mytest')
      end
    end
  end

  sub_test_case '#initialize' do
    test 'creates object with blank attrs and elements' do
      e = element('ROOT', '', {}, [])
      assert_equal([], e.elements)
    end

    test 'creates object which contains attrs and elements' do
      e = element('ROOT', '', {"k1" => "v1", "k2" => "v2"}, [
                    element('test', 'mydata', {'k3' => 'v3'}, [])
                  ])
      assert_equal('ROOT', e.name)
      assert_equal('',  e.arg)
      assert_equal('v1', e["k1"])
      assert_equal('v2', e["k2"])
      assert_equal(1, e.elements.size)
      e.each_element('test') do |el|
        assert_equal('test', el.name)
        assert_equal('mydata', el.arg)
        assert_equal('v3', el["k3"])
      end
    end

    test 'creates object which contains attrs, elements and unused' do
      e = element('ROOT', '', {"k1" => "v1", "k2" => "v2", "k4" => "v4"}, [
                    element('test', 'mydata', {'k3' => 'v3'}, [])
                  ], "k3")
      assert_equal("k3", e.unused)
      assert_equal('ROOT', e.name)
      assert_equal('',  e.arg)
      assert_equal('v1', e["k1"])
      assert_equal('v2', e["k2"])
      assert_equal('v4', e["k4"])
      assert_equal(1, e.elements.size)
      e.each_element('test') do |el|
        assert_equal('test', el.name)
        assert_equal('mydata', el.arg)
        assert_equal('v3', el["k3"])
      end
      assert_equal("k3", e.unused)
    end
  end

  sub_test_case "@unused" do
    sub_test_case '#[] has side effect for @unused' do
      test 'without unused argument' do
        e = element('ROOT', '', {"k1" => "v1", "k2" => "v2", "k4" => "v4"}, [
                      element('test', 'mydata', {'k3' => 'v3'}, [])
                    ])
        assert_equal(["k1", "k2", "k4"], e.unused)
        assert_equal('v1', e["k1"])
        assert_equal(["k2", "k4"], e.unused)
        assert_equal('v2', e["k2"])
        assert_equal(["k4"], e.unused)
        assert_equal('v4', e["k4"])
        assert_equal([], e.unused)
      end

      test 'with unused argument' do
        e = element('ROOT', '', {"k1" => "v1", "k2" => "v2", "k4" => "v4"}, [
                      element('test', 'mydata', {'k3' => 'v3'}, [])
                    ], ["k4"])
        assert_equal(["k4"], e.unused)
        assert_equal('v1', e["k1"])
        assert_equal(["k4"], e.unused)
        assert_equal('v2', e["k2"])
        assert_equal(["k4"], e.unused)
        assert_equal('v4', e["k4"])
        # only consume for "k4"
        assert_equal([], e.unused)
      end
    end
  end

  sub_test_case '#add_element' do
    test 'elements can be set by #add_element' do
      e = element()
      assert_equal [], e.elements

      e.add_element('e1', '')
      e.add_element('e2', '')
      assert_equal [element('e1', ''), element('e2', '')], e.elements
    end
  end

  sub_test_case '#==' do
    sub_test_case 'compare with two element objects' do
      test 'equal' do
        e1 = element('ROOT', '', {}, [])
        e2 = element('ROOT', '', {}, [])
        assert_true(e1 == e2)
      end

      data("differ args" => [Fluent::Config::Element.new('ROOT', '', {}, []),
                             Fluent::Config::Element.new('ROOT', 'mydata', {}, [])],
           "differ keys" => [Fluent::Config::Element.new('ROOT', 'mydata', {}, []),
                             Fluent::Config::Element.new('ROOT', 'mydata', {"k1" => "v1"}, [])],
           "differ elements" =>
           [Fluent::Config::Element.new('ROOT', 'mydata', {"k1" => "v1"}, []),
            Fluent::Config::Element.new('ROOT', 'mydata', {"k1" => "v1"}, [
              Fluent::Config::Element.new('test', 'mydata', {'k3' => 'v3'}, [])
            ])])
      test 'not equal' do |data|
        e1, e2 = data
        assert_false(e1 == e2)
      end
    end
  end

  sub_test_case '#+' do
    test 'can merge 2 elements: object side is primary' do
      e1 = element('ROOT', 'mydata', {"k1" => "v1"}, [])
      e2 = element('ROOT', 'mydata2', {"k1" => "ignored", "k2" => "v2"}, [
                     element('test', 'ext', {'k3' => 'v3'}, [])
                   ])
      e = e1 + e2

      assert_equal('ROOT', e.name)
      assert_equal('mydata', e.arg)
      assert_equal('v1', e['k1'])
      assert_equal('v2', e['k2'])
      assert_equal(1, e.elements.size)
      e.each_element('test') do |el|
        assert_equal('test', el.name)
        assert_equal('ext', el.arg)
        assert_equal('v3', el["k3"])
      end
    end
  end

  sub_test_case '#check_not_fetched' do
    sub_test_case 'without unused' do
      test 'can get attribute keys and original Config::Element' do
        e = element('ROOT', 'mydata', {"k1" => "v1"}, [])
        e.check_not_fetched { |key, elem|
          assert_equal("k1", key)
          assert_equal(e, elem)
        }
      end
    end

    sub_test_case 'with unused' do
      test 'can get unused marked attribute keys and original Config::Element' do
        e = element('ROOT', 'mydata', {"k1" => "v1", "k2" => "unused", "k3" => "k3"})
        e.unused = "k2"
        e.check_not_fetched { |key, elem|
          assert_equal("k2", key)
          assert_equal(e, elem)
        }
      end
    end
  end

  sub_test_case '#has_key?' do
    test 'can get boolean with key name' do
      e = element('ROOT', 'mydata', {"k1" => "v1"}, [])
      assert_true(e.has_key?("k1"))
      assert_false(e.has_key?("noexistent"))
    end
  end


  sub_test_case '#to_s' do
    data("without v1_config" => [false, <<-CONF
<ROOT>
  k1 v1
  k2 "stringVal"
  <test ext>
    k2 v2
  </test>
</ROOT>
CONF
                                ],
         "with v1_config" => [true, <<-CONF
<ROOT>
  k1 v1
  k2 "stringVal"
  <test ext>
    k2 v2
  </test>
</ROOT>
CONF
                             ],
        )
    test 'dump config element with #to_s' do |data|
      v1_config, expected = data
      e = element('ROOT', '', {'k1' => 'v1', "k2" =>"\"stringVal\""}, [
                    element('test', 'ext', {'k2' => 'v2'}, [])
                  ])
      e.v1_config = v1_config
      dump = expected
      assert_not_equal(e.inspect, e.to_s)
      assert_equal(dump, e.to_s)
    end

    test 'dump nil and default for v1' do
      expected = <<-CONF
<ROOT>
  str1 
  str2 defstring
</ROOT>
CONF
      e = element('ROOT', '', {'str1' => nil, "str2" => :default}, [])
      type_lookup = ->(type){ Fluent::Configurable.lookup_type(type) }
      p = Fluent::Config::ConfigureProxy.new("test", type_lookup: type_lookup)
      p.config_param :str1, :string
      p.config_param :str2, :string, default: "defstring"
      e.corresponding_proxies << p
      e.v1_config = true
      assert_not_equal(e.inspect, e.to_s)
      assert_equal(expected, e.to_s)
    end
  end

  sub_test_case '#inspect' do
    test 'dump config element with #inspect' do
      e = element('ROOT', '', {'k1' => 'v1'}, [
                   element('test', 'ext', {'k2' => 'v2'}, [])
                  ])
      dump = <<-CONF
name:ROOT, arg:, {\"k1\"=>\"v1\"}, [name:test, arg:ext, {\"k2\"=>\"v2\"}, []]
CONF
      assert_not_equal(e.to_s, e.inspect.gsub(' => ', '=>'))
      assert_equal(dump.chomp, e.inspect.gsub(' => ', '=>'))
    end
  end

  sub_test_case 'for sections which has secret parameter' do
    setup do
      @type_lookup = ->(type){ Fluent::Configurable.lookup_type(type) }
      p1 = Fluent::Config::ConfigureProxy.new(:match, type_lookup: @type_lookup)
      p1.config_param :str1, :string
      p1.config_param :str2, :string, secret: true
      p1.config_param :enum1, :enum, list: [:a, :b, :c]
      p1.config_param :enum2, :enum, list: [:a, :b, :c], secret: true
      p1.config_param :bool1, :bool
      p1.config_param :bool2, :bool, secret: true
      p1.config_param :int1, :integer
      p1.config_param :int2, :integer, secret: true
      p1.config_param :float1, :float
      p1.config_param :float2, :float, secret: true
      p2 = Fluent::Config::ConfigureProxy.new(:match, type_lookup: @type_lookup)
      p2.config_param :size1, :size
      p2.config_param :size2, :size, secret: true
      p2.config_param :time1, :time
      p2.config_param :time2, :time, secret: true
      p2.config_param :array1, :array
      p2.config_param :array2, :array, secret: true
      p2.config_param :hash1, :hash
      p2.config_param :hash2, :hash, secret: true
      p1.config_section :mysection do
        config_param :str1, :string
        config_param :str2, :string, secret: true
        config_param :enum1, :enum, list: [:a, :b, :c]
        config_param :enum2, :enum, list: [:a, :b, :c], secret: true
        config_param :bool1, :bool
        config_param :bool2, :bool, secret: true
        config_param :int1, :integer
        config_param :int2, :integer, secret: true
        config_param :float1, :float
        config_param :float2, :float, secret: true
        config_param :size1, :size
        config_param :size2, :size, secret: true
        config_param :time1, :time
        config_param :time2, :time, secret: true
        config_param :array1, :array
        config_param :array2, :array, secret: true
        config_param :hash1, :hash
        config_param :hash2, :hash, secret: true
      end
      params = {
        'str1' => 'aaa', 'str2' => 'bbb', 'enum1' => 'a', 'enum2' => 'b', 'bool1' => 'true', 'bool2' => 'yes',
        'int1' => '1', 'int2' => '2', 'float1' => '1.0', 'float2' => '0.5', 'size1' => '1k', 'size2' => '1m',
        'time1' => '5m', 'time2' => '3h', 'array1' => 'a,b,c', 'array2' => 'd,e,f',
        'hash1' => 'a:1,b:2', 'hash2' => 'a:2,b:4',
        'unknown1' => 'yay', 'unknown2' => 'boo',
      }
      e2 = Fluent::Config::Element.new('mysection', '', params.dup, [])
      e2.corresponding_proxies << p1.sections.values.first
      @e = Fluent::Config::Element.new('match', '**', params, [e2])
      @e.corresponding_proxies << p1
      @e.corresponding_proxies << p2
    end

    sub_test_case '#to_masked_element' do
      test 'returns a new element object which has masked values for secret parameters and elements' do
        e = @e.to_masked_element
        assert_equal 'aaa',    e['str1']
        assert_equal 'xxxxxx', e['str2']
        assert_equal 'a',      e['enum1']
        assert_equal 'xxxxxx', e['enum2']
        assert_equal 'true',   e['bool1']
        assert_equal 'xxxxxx', e['bool2']
        assert_equal '1',      e['int1']
        assert_equal 'xxxxxx', e['int2']
        assert_equal '1.0',    e['float1']
        assert_equal 'xxxxxx', e['float2']
        assert_equal '1k',     e['size1']
        assert_equal 'xxxxxx', e['size2']
        assert_equal '5m',     e['time1']
        assert_equal 'xxxxxx', e['time2']
        assert_equal 'a,b,c',  e['array1']
        assert_equal 'xxxxxx', e['array2']
        assert_equal 'a:1,b:2', e['hash1']
        assert_equal 'xxxxxx',  e['hash2']
        assert_equal 'yay', e['unknown1']
        assert_equal 'boo', e['unknown2']
        e2 = e.elements.first
        assert_equal 'aaa',    e2['str1']
        assert_equal 'xxxxxx', e2['str2']
        assert_equal 'a',      e2['enum1']
        assert_equal 'xxxxxx', e2['enum2']
        assert_equal 'true',   e2['bool1']
        assert_equal 'xxxxxx', e2['bool2']
        assert_equal '1',      e2['int1']
        assert_equal 'xxxxxx', e2['int2']
        assert_equal '1.0',    e2['float1']
        assert_equal 'xxxxxx', e2['float2']
        assert_equal '1k',     e2['size1']
        assert_equal 'xxxxxx', e2['size2']
        assert_equal '5m',     e2['time1']
        assert_equal 'xxxxxx', e2['time2']
        assert_equal 'a,b,c',  e2['array1']
        assert_equal 'xxxxxx', e2['array2']
        assert_equal 'a:1,b:2', e2['hash1']
        assert_equal 'xxxxxx',  e2['hash2']
        assert_equal 'yay', e2['unknown1']
        assert_equal 'boo', e2['unknown2']
      end
    end

    sub_test_case '#secret_param?' do
      test 'returns boolean which shows values of given key will be masked' do
        assert [email protected]_param?('str1')
        assert @e.secret_param?('str2')
        assert [email protected]_param?('str1')
        assert @e.elements.first.secret_param?('str2')
      end
    end

    sub_test_case '#param_type' do
      test 'returns parameter type which are registered in corresponding proxy' do
        assert_equal :string, @e.param_type('str1')
        assert_equal :string, @e.param_type('str2')
        assert_equal :enum, @e.param_type('enum1')
        assert_equal :enum, @e.param_type('enum2')
        assert_nil @e.param_type('unknown1')
        assert_nil @e.param_type('unknown2')
      end
    end

    # sub_test_case '#dump_value'
    sub_test_case '#dump_value' do
      test 'dumps parameter_name and values with leading indentation' do
        assert_equal "str1 aaa\n", @e.dump_value("str1", @e["str1"], "")
        assert_equal "str2 xxxxxx\n", @e.dump_value("str2", @e["str2"], "")
      end
    end
  end

  sub_test_case '#set_target_worker' do
    test 'set target_worker_id recursively' do
      e = element('label', '@mytest', {}, [ element('filter', '**'), element('match', '**', {}, [ element('store'), element('store') ]) ])
      e.set_target_worker_id(1)
      assert_equal [1], e.target_worker_ids
      assert_equal [1], e.elements[0].target_worker_ids
      assert_equal [1], e.elements[1].target_worker_ids
      assert_equal [1], e.elements[1].elements[0].target_worker_ids
      assert_equal [1], e.elements[1].elements[1].target_worker_ids
    end
  end

  sub_test_case '#for_every_workers?' do
    test 'has target_worker_id' do
      e = element()
      e.set_target_worker_id(1)
      assert_false e.for_every_workers?
    end

    test "doesn't have target_worker_id" do
      e = element()
      assert e.for_every_workers?
    end
  end

  sub_test_case '#for_this_workers?' do
    test 'target_worker_id == current worker_id' do
      e = element()
      e.set_target_worker_id(0)
      assert e.for_this_worker?
    end

    test 'target_worker_ids includes current worker_id' do
      e = element()
      e.set_target_worker_ids([0])
      assert e.for_this_worker?
    end

    test 'target_worker_id != current worker_id' do
      e = element()
      e.set_target_worker_id(1)
      assert_false e.for_this_worker?
    end

    test 'target_worker_ids does not includes current worker_id' do
      e = element()
      e.set_target_worker_ids([1, 2])
      assert_false e.for_this_worker?
    end

    test "doesn't have target_worker_id" do
      e = element()
      assert_false e.for_this_worker?
    end
  end

  sub_test_case '#for_another_worker?' do
    test 'target_worker_id == current worker_id' do
      e = element()
      e.set_target_worker_id(0)
      assert_false e.for_another_worker?
    end

    test 'target_worker_ids contains current worker_id' do
      e = element()
      e.set_target_worker_ids([0, 1])
      assert_false e.for_another_worker?
    end

    test 'target_worker_id != current worker_id' do
      e = element()
      e.set_target_worker_id(1)
      assert e.for_another_worker?
    end

    test 'target_worker_ids does not contains current worker_id' do
      e = element()
      e.set_target_worker_ids([1, 2])
      assert e.for_another_worker?
    end

    test "doesn't have target_worker_id" do
      e = element()
      assert_false e.for_another_worker?
    end
  end

  sub_test_case '#pretty_print' do
    test 'prints inspect to pp object' do
      q = PP.new
      e = element()
      e.pretty_print(q)
      assert_equal e.inspect, q.output
    end
  end
end