Back to Repositories

Validating Configuration Proxy Implementation in Fluentd

This test suite verifies the configuration proxy functionality in Fluentd, focusing on parameter handling and section configuration. It ensures proper initialization, merging, and validation of configuration settings in the Fluentd logging framework.

Test Coverage Overview

The test suite provides comprehensive coverage of the ConfigureProxy class functionality:
  • Parameter initialization and default value handling
  • Configuration section merging and inheritance
  • Parameter type validation and constraints
  • Configuration option validation for different parameter types
  • Configuration description and documentation handling

Implementation Analysis

The testing approach uses MiniTest to systematically verify configuration behavior:
  • Modular test organization using sub_test_case blocks
  • Extensive use of assertions for parameter validation
  • Mock type lookup functionality for parameter type checking
  • Structured testing of configuration proxy features

Technical Details

Key technical components include:
  • Test::Unit framework for test execution
  • Fluent::Config::ConfigureProxy class under test
  • Configuration parameter type system
  • Ruby symbol and string handling for parameter names
  • Hash-based configuration storage

Best Practices Demonstrated

The test suite demonstrates several testing best practices:
  • Thorough edge case testing for configuration options
  • Consistent setup and teardown patterns
  • Clear test naming conventions
  • Comprehensive validation of error conditions
  • Modular test organization for maintainability

fluent/fluentd

test/config/test_configure_proxy.rb

            
require_relative '../helper'
require 'fluent/config/configure_proxy'

module Fluent::Config
  class TestConfigureProxy < ::Test::Unit::TestCase
    setup do
      @type_lookup = ->(type) { Fluent::Configurable.lookup_type(type) }
    end

    sub_test_case 'to generate a instance' do
      sub_test_case '#initialize' do
        test 'has default values' do
          proxy = Fluent::Config::ConfigureProxy.new('section', type_lookup: @type_lookup)
          assert_equal(:section, proxy.name)

          proxy = Fluent::Config::ConfigureProxy.new(:section, type_lookup: @type_lookup)
          assert_equal(:section, proxy.name)
          assert_nil(proxy.param_name)
          assert_equal(:section, proxy.variable_name)
          assert_false(proxy.root?)
          assert_nil(proxy.init)
          assert_nil(proxy.required)
          assert_false(proxy.required?)
          assert_nil(proxy.multi)
          assert_true(proxy.multi?)
        end

        test 'can specify param_name/required/multi with optional arguments' do
          proxy = Fluent::Config::ConfigureProxy.new(:section, param_name: 'sections', init: true, required: false, multi: true, type_lookup: @type_lookup)
          assert_equal(:section, proxy.name)
          assert_equal(:sections, proxy.param_name)
          assert_equal(:sections, proxy.variable_name)
          assert_false(proxy.required)
          assert_false(proxy.required?)
          assert_true(proxy.multi)
          assert_true(proxy.multi?)

          proxy = Fluent::Config::ConfigureProxy.new(:section, param_name: :sections, init: false, required: true, multi: false, type_lookup: @type_lookup)
          assert_equal(:section, proxy.name)
          assert_equal(:sections, proxy.param_name)
          assert_equal(:sections, proxy.variable_name)
          assert_true(proxy.required)
          assert_true(proxy.required?)
          assert_false(proxy.multi)
          assert_false(proxy.multi?)
        end
        test 'raise error if both of init and required are true' do
          assert_raise RuntimeError.new("init and required are exclusive") do
            Fluent::Config::ConfigureProxy.new(:section, init: true, required: true, type_lookup: @type_lookup)
          end
        end
      end

      sub_test_case '#merge' do
        test 'generate a new instance which values are overwritten by the argument object' do
          proxy = p1 = Fluent::Config::ConfigureProxy.new(:section, type_lookup: @type_lookup)
          assert_equal(:section, proxy.name)
          assert_nil(proxy.param_name)
          assert_equal(:section, proxy.variable_name)
          assert_nil(proxy.init)
          assert_nil(proxy.required)
          assert_false(proxy.required?)
          assert_nil(proxy.multi)
          assert_true(proxy.multi?)
          assert_nil(proxy.configured_in_section)

          p2 = Fluent::Config::ConfigureProxy.new(:section, init: false, required: true, multi: false, type_lookup: @type_lookup)
          proxy = p1.merge(p2)
          assert_equal(:section, proxy.name)
          assert_nil(proxy.param_name)
          assert_equal(:section, proxy.variable_name)
          assert_false(proxy.init)
          assert_false(proxy.init?)
          assert_true(proxy.required)
          assert_true(proxy.required?)
          assert_false(proxy.multi)
          assert_false(proxy.multi?)
          assert_nil(proxy.configured_in_section)
        end

        test 'does not overwrite with argument object without any specifications of required/multi' do
          p1 = Fluent::Config::ConfigureProxy.new(:section1, param_name: :sections, type_lookup: @type_lookup)
          p1.configured_in_section = :subsection
          p2 = Fluent::Config::ConfigureProxy.new(:section2, init: false, required: true, multi: false, type_lookup: @type_lookup)
          p3 = Fluent::Config::ConfigureProxy.new(:section3, type_lookup: @type_lookup)
          proxy = p1.merge(p2).merge(p3)
          assert_equal(:section1, proxy.name)
          assert_equal(:sections, proxy.param_name)
          assert_equal(:sections, proxy.variable_name)
          assert_false(proxy.init)
          assert_false(proxy.init?)
          assert_true(proxy.required)
          assert_true(proxy.required?)
          assert_false(proxy.multi)
          assert_false(proxy.multi?)
          assert_equal :subsection, proxy.configured_in_section
        end

        test "does overwrite name of proxy for root sections which are used for plugins" do
          # latest plugin class shows actual plugin implementation
          p1 = Fluent::Config::ConfigureProxy.new('Fluent::Plugin::MyP1'.to_sym, root: true, required: true, multi: false, type_lookup: @type_lookup)
          p1.config_param :key1, :integer

          p2 = Fluent::Config::ConfigureProxy.new('Fluent::Plugin::MyP2'.to_sym, root: true, required: true, multi: false, type_lookup: @type_lookup)
          p2.config_param :key2, :string, default: "value2"

          merged = p1.merge(p2)

          assert_equal 'Fluent::Plugin::MyP2'.to_sym, merged.name
          assert_true merged.root?
        end
      end

      sub_test_case '#overwrite_defaults' do
        test 'overwrites only defaults with others defaults' do
          type_lookup = ->(type) { Fluent::Configurable.lookup_type(type) }
          p1 = Fluent::Config::ConfigureProxy.new(:mychild, type_lookup: type_lookup)
          p1.configured_in_section = :child
          p1.config_param(:k1a, :string)
          p1.config_param(:k1b, :string)
          p1.config_param(:k2a, :integer, default: 0)
          p1.config_param(:k2b, :integer, default: 0)
          p1.config_section(:sub1) do
            config_param :k3, :time, default: 30
          end

          p0 = Fluent::Config::ConfigureProxy.new(:myparent, type_lookup: type_lookup)
          p0.config_section(:child) do
            config_set_default :k1a, "v1a"
            config_param :k1b, :string, default: "v1b"
            config_set_default :k2a, 21
            config_param :k2b, :integer, default: 22
            config_section :sub1 do
              config_set_default :k3, 60
            end
          end

          p1.overwrite_defaults(p0.sections[:child])

          assert_equal "v1a", p1.defaults[:k1a]
          assert_equal "v1b", p1.defaults[:k1b]
          assert_equal 21, p1.defaults[:k2a]
          assert_equal 22, p1.defaults[:k2b]
          assert_equal 60, p1.sections[:sub1].defaults[:k3]
        end
      end

      sub_test_case '#configured_in' do
        test 'sets a section name which have configuration parameters of target plugin in owners configuration' do
          proxy = Fluent::Config::ConfigureProxy.new(:section, type_lookup: @type_lookup)
          proxy.configured_in(:mysection)
          assert_equal :mysection, proxy.configured_in_section
        end

        test 'do not permit to be called twice' do
          proxy = Fluent::Config::ConfigureProxy.new(:section, type_lookup: @type_lookup)
          proxy.configured_in(:mysection)
          assert_raise(ArgumentError) { proxy.configured_in(:myothersection) }
        end
      end

      sub_test_case '#config_param / #config_set_default / #config_argument' do
        setup do
          @proxy = Fluent::Config::ConfigureProxy.new(:section, type_lookup: @type_lookup)
        end

        test 'handles configuration parameters without type as string' do
          @proxy.config_argument(:label)
          @proxy.config_param(:name)
          assert_equal :label, @proxy.argument[0]
          assert_equal :string, @proxy.argument[2][:type]
          assert_equal :string, @proxy.params[:name][1][:type]
        end

        data(
          default: [:default, nil],
          alias: [:alias, :alias_name_in_config],
          secret: [:secret, true],
          skip_accessor: [:skip_accessor, true],
          deprecated: [:deprecated, 'it is deprecated'],
          obsoleted: [:obsoleted, 'it is obsoleted'],
          desc: [:desc, "description"],
        )
        test 'always allow options for all types' do |(option, value)|
          opt = {option => value}
          assert_nothing_raised{ @proxy.config_argument(:param0, **opt) }
          assert_nothing_raised{ @proxy.config_param(:p1, :string, **opt) }
          assert_nothing_raised{ @proxy.config_param(:p2, :enum, list: [:a, :b, :c], **opt) }
          assert_nothing_raised{ @proxy.config_param(:p3, :integer, **opt) }
          assert_nothing_raised{ @proxy.config_param(:p4, :float, **opt) }
          assert_nothing_raised{ @proxy.config_param(:p5, :size, **opt) }
          assert_nothing_raised{ @proxy.config_param(:p6, :bool, **opt) }
          assert_nothing_raised{ @proxy.config_param(:p7, :time, **opt) }
          assert_nothing_raised{ @proxy.config_param(:p8, :hash, **opt) }
          assert_nothing_raised{ @proxy.config_param(:p9, :array, **opt) }
          assert_nothing_raised{ @proxy.config_param(:pa, :regexp, **opt) }
        end

        data(string: :string, integer: :integer, float: :float, size: :size, bool: :bool, time: :time, hash: :hash, array: :array, regexp: :regexp)
        test 'deny list for non-enum types' do |type|
          assert_raise ArgumentError.new(":list is valid only for :enum type, but #{type}: arg") do
            @proxy.config_argument(:arg, type, list: [:a, :b])
          end
          assert_raise ArgumentError.new(":list is valid only for :enum type, but #{type}: p1") do
            @proxy.config_param(:p1, type, list: [:a, :b])
          end
        end

        data(string: :string, integer: :integer, float: :float, size: :size, bool: :bool, time: :time, regexp: :regexp)
        test 'deny value_type for non-hash/array types' do |type|
          assert_raise ArgumentError.new(":value_type is valid only for :hash and :array, but #{type}: arg") do
            @proxy.config_argument(:arg, type, value_type: :string)
          end
          assert_raise ArgumentError.new(":value_type is valid only for :hash and :array, but #{type}: p1") do
            @proxy.config_param(:p1, type, value_type: :integer)
          end
        end

        data(string: :string, integer: :integer, float: :float, size: :size, bool: :bool, time: :time, array: :array, regexp: :regexp)
        test 'deny symbolize_keys for non-hash types' do |type|
          assert_raise ArgumentError.new(":symbolize_keys is valid only for :hash, but #{type}: arg") do
            @proxy.config_argument(:arg, type, symbolize_keys: true)
          end
          assert_raise ArgumentError.new(":symbolize_keys is valid only for :hash, but #{type}: p1") do
            @proxy.config_param(:p1, type, symbolize_keys: true)
          end
        end

        data(string: :string, integer: :integer, float: :float, size: :size, bool: :bool, time: :time, hash: :hash, array: :array)
        test 'deny unknown options' do |type|
          assert_raise ArgumentError.new("unknown option 'required' for configuration parameter: arg") do
            @proxy.config_argument(:arg, type, required: true)
          end
          assert_raise ArgumentError.new("unknown option 'param_name' for configuration parameter: p1") do
            @proxy.config_argument(:p1, type, param_name: :yay)
          end
        end

        test 'desc gets string' do
          assert_nothing_raised do
            @proxy.config_param(:name, :string, desc: "it is description")
          end
          assert_raise ArgumentError.new("name1: desc must be a String, but Symbol") do
            @proxy.config_param(:name1, :string, desc: :yaaaaaaaay)
          end
        end

        test 'alias gets symbol' do
          assert_nothing_raised do
            @proxy.config_param(:name, :string, alias: :label)
          end
          assert_raise ArgumentError.new("name1: alias must be a Symbol, but String") do
            @proxy.config_param(:name1, :string, alias: 'label1')
          end
        end

        test 'secret gets true/false' do
          assert_nothing_raised do
            @proxy.config_param(:name1, :string, secret: false)
          end
          assert_nothing_raised do
            @proxy.config_param(:name2, :string, secret: true)
          end
          assert_raise ArgumentError.new("name3: secret must be true or false, but String") do
            @proxy.config_param(:name3, :string, secret: 'yes')
          end
          assert_raise ArgumentError.new("name4: secret must be true or false, but NilClass") do
            @proxy.config_param(:name4, :string, secret: nil)
          end
        end

        test 'symbolize_keys gets true/false' do
          assert_nothing_raised do
            @proxy.config_param(:data1, :hash, symbolize_keys: false)
          end
          assert_nothing_raised do
            @proxy.config_param(:data2, :hash, symbolize_keys: true)
          end
          assert_raise ArgumentError.new("data3: symbolize_keys must be true or false, but NilClass") do
            @proxy.config_param(:data3, :hash, symbolize_keys: nil)
          end
        end

        test 'value_type gets symbol' do
          assert_nothing_raised do
            @proxy.config_param(:data1, :array, value_type: :integer)
          end
          assert_raise ArgumentError.new("data2: value_type must be a Symbol, but Class") do
            @proxy.config_param(:data2, :array, value_type: Integer)
          end
        end

        test 'list gets an array of symbols' do
          assert_nothing_raised do
            @proxy.config_param(:proto1, :enum, list: [:a, :b])
          end
          assert_raise ArgumentError.new("proto2: enum parameter requires :list of Symbols") do
            @proxy.config_param(:proto2, :enum, list: nil)
          end
          assert_raise ArgumentError.new("proto3: enum parameter requires :list of Symbols") do
            @proxy.config_param(:proto3, :enum, list: ['a', 'b'])
          end
          assert_raise ArgumentError.new("proto4: enum parameter requires :list of Symbols") do
            @proxy.config_param(:proto4, :enum, list: [])
          end
        end

        test 'deprecated gets string' do
          assert_nothing_raised do
            @proxy.config_param(:name1, :string, deprecated: "use name2 instead")
          end
          assert_raise ArgumentError.new("name2: deprecated must be a String, but TrueClass") do
            @proxy.config_param(:name2, :string, deprecated: true)
          end
        end

        test 'obsoleted gets string' do
          assert_nothing_raised do
            @proxy.config_param(:name1, :string, obsoleted: "use name2 instead")
          end
          assert_raise ArgumentError.new("name2: obsoleted must be a String, but TrueClass") do
            @proxy.config_param(:name2, :string, obsoleted: true)
          end
        end

        test 'skip_accessor gets true/false' do
          assert_nothing_raised do
            @proxy.config_param(:format1, :string, skip_accessor: false)
          end
          assert_nothing_raised do
            @proxy.config_param(:format2, :string, skip_accessor: true)
          end
          assert_raise ArgumentError.new("format2: skip_accessor must be true or false, but String") do
            @proxy.config_param(:format2, :string, skip_accessor: 'yes')
          end
        end

        test 'list is required for :enum' do
          assert_nothing_raised do
            @proxy.config_param(:proto1, :enum, list: [:a, :b])
          end
          assert_raise ArgumentError.new("proto1: enum parameter requires :list of Symbols") do
            @proxy.config_param(:proto1, :enum, default: :a)
          end
        end

        test 'does not permit config_set_default for param w/ :default option' do
          @proxy.config_param(:name, :string, default: "name1")
          assert_raise(ArgumentError) { @proxy.config_set_default(:name, "name2") }
        end

        test 'does not permit default value specification twice' do
          @proxy.config_param(:name, :string)
          @proxy.config_set_default(:name, "name1")
          assert_raise(ArgumentError) { @proxy.config_set_default(:name, "name2") }
        end

        test 'does not permit default value specification twice, even on config_argument' do
          @proxy.config_param(:name, :string)
          @proxy.config_set_default(:name, "name1")

          @proxy.config_argument(:name)
          assert_raise(ArgumentError) { @proxy.config_argument(:name, default: "name2") }
        end
      end

      sub_test_case '#config_set_desc' do
        setup do
          @proxy = Fluent::Config::ConfigureProxy.new(:section, type_lookup: @type_lookup)
        end

        test 'does not permit description specification twice w/ :desc option' do
          @proxy.config_param(:name, :string, desc: "description")
          assert_raise(ArgumentError) { @proxy.config_set_desc(:name, "description2") }
        end

        test 'does not permit description specification twice' do
          @proxy.config_param(:name, :string)
          @proxy.config_set_desc(:name, "description")
          assert_raise(ArgumentError) { @proxy.config_set_desc(:name, "description2") }
        end
      end

      sub_test_case '#desc' do
        setup do
          @proxy = Fluent::Config::ConfigureProxy.new(:section, type_lookup: @type_lookup)
        end

        test 'permit to specify description twice' do
          @proxy.desc("description1")
          @proxy.desc("description2")
          @proxy.config_param(:name, :string)
          assert_equal("description2", @proxy.descriptions[:name])
        end

        test 'does not permit description specification twice' do
          @proxy.desc("description1")
          assert_raise(ArgumentError) do
            @proxy.config_param(:name, :string, desc: "description2")
          end
        end
      end

      sub_test_case '#dump_config_definition' do
        setup do
          @proxy = Fluent::Config::ConfigureProxy.new(:section, type_lookup: @type_lookup)
        end

        test 'empty proxy' do
          assert_equal({}, @proxy.dump_config_definition)
        end

        test 'plain proxy w/o default value' do
          @proxy.config_param(:name, :string)
          expected = {
            name: { type: :string, required: true }
          }
          assert_equal(expected, @proxy.dump_config_definition)
        end

        test 'plain proxy w/ default value' do
          @proxy.config_param(:name, :string, default: "name1")
          expected = {
            name: { type: :string, default: "name1", required: false }
          }
          assert_equal(expected, @proxy.dump_config_definition)
        end

        test 'plain proxy w/ default value using config_set_default' do
          @proxy.config_param(:name, :string)
          @proxy.config_set_default(:name, "name1")
          expected = {
            name: { type: :string, default: "name1", required: false }
          }
          assert_equal(expected, @proxy.dump_config_definition)
        end

        test 'plain proxy w/ argument' do
          @proxy.instance_eval do
            config_argument(:argname, :string)
            config_param(:name, :string, default: "name1")
          end
          expected = {
            argname: { type: :string, required: true, argument: true },
            name: { type: :string, default: "name1", required: false }
          }
          assert_equal(expected, @proxy.dump_config_definition)
        end

        test 'plain proxy w/ argument default value' do
          @proxy.instance_eval do
            config_argument(:argname, :string, default: "value")
            config_param(:name, :string, default: "name1")
          end
          expected = {
            argname: { type: :string, default: "value", required: false, argument: true },
            name: { type: :string, default: "name1", required: false }
          }
          assert_equal(expected, @proxy.dump_config_definition)
        end

        test 'plain proxy w/ argument overwriting default value' do
          @proxy.instance_eval do
            config_argument(:argname, :string)
            config_param(:name, :string, default: "name1")
            config_set_default(:argname, "value1")
          end
          expected = {
            argname: { type: :string, default: "value1", required: false, argument: true },
            name: { type: :string, default: "name1", required: false }
          }
          assert_equal(expected, @proxy.dump_config_definition)
        end

        test 'single sub proxy' do
          @proxy.config_section(:sub) do
            config_param(:name, :string, default: "name1")
          end
          expected = {
            sub: {
              alias: nil,
              multi: true,
              required: false,
              section: true,
              name: { type: :string, default: "name1", required: false }
            }
          }
          assert_equal(expected, @proxy.dump_config_definition)
        end

        test 'nested sub proxy' do
          @proxy.config_section(:sub) do
            config_param(:name1, :string, default: "name1")
            config_param(:name2, :string, default: "name2")
            config_section(:sub2) do
              config_param(:name3, :string, default: "name3")
              config_param(:name4, :string, default: "name4")
            end
          end
          expected = {
            sub: {
              alias: nil,
              multi: true,
              required: false,
              section: true,
              name1: { type: :string, default: "name1", required: false },
              name2: { type: :string, default: "name2", required: false },
              sub2: {
                alias: nil,
                multi: true,
                required: false,
                section: true,
                name3: { type: :string, default: "name3", required: false },
                name4: { type: :string, default: "name4", required: false },
              }
            }
          }
          assert_equal(expected, @proxy.dump_config_definition)
        end

        sub_test_case 'w/ description' do
          test 'single proxy' do
            @proxy.config_param(:name, :string, desc: "description for name")
            expected = {
              name: { type: :string, desc: "description for name", required: true }
            }
            assert_equal(expected, @proxy.dump_config_definition)
          end

          test 'single proxy using config_set_desc' do
            @proxy.config_param(:name, :string)
            @proxy.config_set_desc(:name, "description for name")
            expected = {
              name: { type: :string, desc: "description for name", required: true }
            }
            assert_equal(expected, @proxy.dump_config_definition)
          end

          test 'sub proxy' do
            @proxy.config_section(:sub) do
              config_param(:name1, :string, default: "name1", desc: "desc1")
              config_param(:name2, :string, default: "name2", desc: "desc2")
              config_section(:sub2) do
                config_param(:name3, :string, default: "name3")
                config_param(:name4, :string, default: "name4", desc: "desc4")
              end
            end
            expected = {
              sub: {
                alias: nil,
                multi: true,
                required: false,
                section: true,
                name1: { type: :string, default: "name1", desc: "desc1", required: false },
                name2: { type: :string, default: "name2", desc: "desc2", required: false },
                sub2: {
                  alias: nil,
                  multi: true,
                  required: false,
                  section: true,
                  name3: { type: :string, default: "name3", required: false },
                  name4: { type: :string, default: "name4", desc: "desc4", required: false },
                }
              }
            }
            assert_equal(expected, @proxy.dump_config_definition)
          end

          test 'sub proxy w/ desc method' do
            @proxy.config_section(:sub) do
              desc("desc1")
              config_param(:name1, :string, default: "name1")
              config_param(:name2, :string, default: "name2", desc: "desc2")
              config_section(:sub2) do
                config_param(:name3, :string, default: "name3")
                desc("desc4")
                config_param(:name4, :string, default: "name4")
              end
            end
            expected = {
              sub: {
                alias: nil,
                multi: true,
                required: false,
                section: true,
                name1: { type: :string, default: "name1", desc: "desc1", required: false },
                name2: { type: :string, default: "name2", desc: "desc2", required: false },
                sub2: {
                  alias: nil,
                  multi: true,
                  required: false,
                  section: true,
                  name3: { type: :string, default: "name3", required: false },
                  name4: { type: :string, default: "name4", desc: "desc4", required: false },
                }
              }
            }
            assert_equal(expected, @proxy.dump_config_definition)
          end
        end
      end
    end
  end
end