Back to Repositories

Validating System Configuration Management in Fluentd

This test suite validates the system configuration functionality in Fluentd, focusing on parameter handling, log management, and buffer configurations. It ensures proper initialization and overwriting of system-level settings through comprehensive unit tests.

Test Coverage Overview

The test suite provides extensive coverage of Fluentd’s system configuration capabilities:

  • Default configuration validation
  • Parameter overwriting scenarios
  • Log configuration handling
  • Log rotation mechanisms
  • Source-only buffer parameter validation

Implementation Analysis

The testing approach utilizes a FakeSupervisor class to simulate the system configuration environment. It implements minitest assertions to verify configuration parameters and their interactions, with particular attention to command-line option handling and configuration inheritance.

Technical Details

Key technical components include:

  • Test::Unit framework implementation
  • Dynamic configuration parsing
  • Environment-aware temporary directory handling
  • SI prefix parsing for size configurations
  • Mock supervisor implementation for isolation

Best Practices Demonstrated

The test suite exemplifies several testing best practices:

  • Comprehensive edge case handling
  • Isolated test environments
  • Parametrized testing using data providers
  • Clear test organization with sub-test cases
  • Thorough validation of configuration inheritance

fluent/fluentd

test/config/test_system_config.rb

            
require_relative '../helper'
require 'fluent/configurable'
require 'fluent/config/element'
require 'fluent/config/section'
require 'fluent/system_config'

module Fluent::Config
  class FakeSupervisor
    attr_writer :log_level

    def initialize(**opt)
      @system_config = nil
      @cl_opt = {
        wokers: nil,
        restart_worker_interval: nil,
        root_dir: nil,
        log_level: Fluent::Log::LEVEL_INFO,
        suppress_interval: nil,
        suppress_config_dump: nil,
        suppress_repeated_stacktrace: nil,
        log_event_label: nil,
        log_event_verbose: nil,
        without_source: nil,
        with_source_only: nil,
        enable_input_metrics: nil,
        enable_size_metrics: nil,
        emit_error_log_interval: nil,
        file_permission: nil,
        dir_permission: nil,
      }.merge(opt)
    end

    def for_system_config
      opt = {}
      # this is copy from Supervisor#build_system_config
      Fluent::SystemConfig::SYSTEM_CONFIG_PARAMETERS.each do |param|
        if @cl_opt.key?(param) && !@cl_opt[param].nil?
          if param == :log_level && @cl_opt[:log_level] == Fluent::Log::LEVEL_INFO
            # info level can't be specified via command line option.
            # log_level is info here, it is default value and <system>'s log_level should be applied if exists.
            next
          end

          opt[param] = @cl_opt[param]
        end
      end

      opt
    end
  end

  class TestSystemConfig < ::Test::Unit::TestCase
    TMP_DIR = File.expand_path(File.dirname(__FILE__) + "/tmp/system_config/#{ENV['TEST_ENV_NUMBER']}")

    def parse_text(text)
      basepath = File.expand_path(File.dirname(__FILE__) + '/../../')
      Fluent::Config.parse(text, '(test)', basepath, true).elements.find { |e| e.name == 'system' }
    end

    test 'should not override default configurations when no parameters' do
      conf = parse_text(<<-EOS)
        <system>
        </system>
      EOS
      s = FakeSupervisor.new
      sc = Fluent::SystemConfig.new(conf)
      sc.overwrite_variables(**s.for_system_config)
      assert_equal(1, sc.workers)
      assert_equal(0, sc.restart_worker_interval)
      assert_nil(sc.root_dir)
      assert_equal(Fluent::Log::LEVEL_INFO, sc.log_level)
      assert_nil(sc.suppress_repeated_stacktrace)
      assert_nil(sc.ignore_repeated_log_interval)
      assert_nil(sc.emit_error_log_interval)
      assert_nil(sc.suppress_config_dump)
      assert_nil(sc.without_source)
      assert_nil(sc.with_source_only)
      assert_nil(sc.enable_input_metrics)
      assert_nil(sc.enable_size_metrics)
      assert_nil(sc.enable_msgpack_time_support)
      assert(!sc.enable_jit)
      assert_nil(sc.log.path)
      assert_equal(:text, sc.log.format)
      assert_equal('%Y-%m-%d %H:%M:%S %z', sc.log.time_format)
    end

    data(
      'workers' => ['workers', 3],
      'restart_worker_interval' => ['restart_worker_interval', 60],
      'root_dir' => ['root_dir', File.join(TMP_DIR, 'root')],
      'log_level' => ['log_level', 'error'],
      'suppress_repeated_stacktrace' => ['suppress_repeated_stacktrace', true],
      'ignore_repeated_log_interval' => ['ignore_repeated_log_interval', 10],
      'log_event_verbose' => ['log_event_verbose', true],
      'suppress_config_dump' => ['suppress_config_dump', true],
      'without_source' => ['without_source', true],
      'with_source_only' => ['with_source_only', true],
      'strict_config_value' => ['strict_config_value', true],
      'enable_msgpack_time_support' => ['enable_msgpack_time_support', true],
      'enable_input_metrics' => ['enable_input_metrics', true],
      'enable_size_metrics' => ['enable_size_metrics', true],
      'enable_jit' => ['enable_jit', true],
    )
    test "accepts parameters" do |(k, v)|
      conf = parse_text(<<-EOS)
          <system>
            #{k} #{v}
          </system>
      EOS
      s = FakeSupervisor.new
      sc = Fluent::SystemConfig.new(conf)
      sc.overwrite_variables(**s.for_system_config)
      if k == 'log_level'
        assert_equal(Fluent::Log::LEVEL_ERROR, sc.__send__(k))
      else
        assert_equal(v, sc.__send__(k))
      end
    end

    test "log parameters" do
      conf = parse_text(<<-EOS)
          <system>
            <log>
              path /tmp/fluentd.log
              format json
              time_format %Y
            </log>
          </system>
      EOS
      s = FakeSupervisor.new
      sc = Fluent::SystemConfig.new(conf)
      sc.overwrite_variables(**s.for_system_config)
      assert_equal('/tmp/fluentd.log', sc.log.path)
      assert_equal(:json, sc.log.format)
      assert_equal('%Y', sc.log.time_format)
    end

    # info is removed because info level can't be specified via command line
    data('trace' => Fluent::Log::LEVEL_TRACE,
         'debug' => Fluent::Log::LEVEL_DEBUG,
         'warn' => Fluent::Log::LEVEL_WARN,
         'error' => Fluent::Log::LEVEL_ERROR,
         'fatal' => Fluent::Log::LEVEL_FATAL)
    test 'log_level is ignored when log_level related command line option is passed' do |level|
      conf = parse_text(<<-EOS)
        <system>
          log_level info
        </system>
      EOS
      s = FakeSupervisor.new(log_level: level)
      sc = Fluent::SystemConfig.new(conf)
      sc.overwrite_variables(**s.for_system_config)
      assert_equal(level, sc.log_level)
    end

    sub_test_case "log rotation" do
      data('daily' => "daily",
           'weekly' => 'weekly',
           'monthly' => 'monthly')
      test "strings for rotate_age" do |age|
        conf = parse_text(<<-EOS)
          <system>
            <log>
              rotate_age #{age}
            </log>
          </system>
        EOS
        sc = Fluent::SystemConfig.new(conf)
        assert_equal(age, sc.log.rotate_age)
      end

      test "numeric number for rotate age" do
        conf = parse_text(<<-EOS)
          <system>
            <log>
              rotate_age 3
            </log>
          </system>
        EOS
        sc = Fluent::SystemConfig.new(conf)
        assert_equal(3, sc.log.rotate_age)
      end

      data(h: ['100', 100],
           k: ['1k', 1024],
           m: ['1m', 1024 * 1024],
           g: ['1g', 1024 * 1024 * 1024])
      test "numeric and SI prefix for rotate_size" do |(label, size)|
        conf = parse_text(<<-EOS)
          <system>
            <log>
              rotate_size #{label}
            </log>
          </system>
        EOS
        sc = Fluent::SystemConfig.new(conf)
        assert_equal(size, sc.log.rotate_size)
      end
    end

    test "source-only-buffer parameters" do
      conf = parse_text(<<~EOS)
        <system>
          <source_only_buffer>
            flush_thread_count 4
            overflow_action throw_exception
            path /tmp/source-only-buffer
            flush_interval 1
            chunk_limit_size 100
            total_limit_size 1000
            compress gzip
          </source_only_buffer>
        </system>
      EOS
      s = FakeSupervisor.new
      sc = Fluent::SystemConfig.new(conf)
      sc.overwrite_variables(**s.for_system_config)

      assert_equal(
        [
          4,
          :throw_exception,
          "/tmp/source-only-buffer",
          1,
          100,
          1000,
          :gzip,
        ],
        [
          sc.source_only_buffer.flush_thread_count,
          sc.source_only_buffer.overflow_action,
          sc.source_only_buffer.path,
          sc.source_only_buffer.flush_interval,
          sc.source_only_buffer.chunk_limit_size,
          sc.source_only_buffer.total_limit_size,
          sc.source_only_buffer.compress,
        ]
      )
    end
  end
end