Back to Repositories

Testing Stdout Output Plugin Implementation in Fluentd

This test suite validates the stdout output plugin functionality in Fluentd, focusing on both buffered and non-buffered output configurations. It ensures proper formatting, time handling, and data serialization capabilities through comprehensive unit testing.

Test Coverage Overview

The test suite provides extensive coverage of the stdout output plugin’s core functionality:

  • Configuration validation for both buffered and non-buffered modes
  • Output format handling (JSON and Hash formats)
  • Time format configuration and processing
  • Different JSON parser implementations (oj and yajl)
  • Buffer configuration and chunk handling

Implementation Analysis

The testing approach utilizes Test::Unit framework with a structured setup:

The implementation follows a clear pattern of testing configuration, formatting, and output behavior. It leverages Fluent::Test::Driver::Output for simulating plugin behavior and uses sub_test_cases to organize related test scenarios logically.

  • Driver-based testing methodology
  • Isolated test environments
  • Captured output validation

Technical Details

Key technical components include:

  • Fluent::Test::Driver::Output for plugin testing
  • StringIO for output capture
  • Multiple JSON parsers (oj, yajl)
  • Custom time format handling
  • Buffer configuration elements
  • Event time management utilities

Best Practices Demonstrated

The test suite exemplifies several testing best practices:

  • Comprehensive edge case handling (NaN values, invalid configurations)
  • Clear test organization using sub_test_cases
  • Proper test isolation through setup methods
  • Thorough configuration validation
  • Consistent error handling verification
  • Clean test data management

fluent/fluentd

test/plugin/test_out_stdout.rb

            
require_relative '../helper'
require 'fluent/test/driver/output'
require 'fluent/plugin/out_stdout'

class StdoutOutputTest < Test::Unit::TestCase
  def setup
    Fluent::Test.setup
  end

  CONFIG = %[
  ]
  TIME_FORMAT = '%Y-%m-%d %H:%M:%S.%9N %z'

  def create_driver(conf = CONFIG)
    Fluent::Test::Driver::Output.new(Fluent::Plugin::StdoutOutput).configure(conf)
  end

  sub_test_case 'non-buffered' do
    test 'configure' do
      d = create_driver
      assert_equal 1, d.instance.formatter_configs.size # init: true
      assert_kind_of Fluent::Plugin::StdoutFormatter, d.instance.formatter
      assert_equal 'json', d.instance.formatter.output_type
    end

    test 'configure output_type' do
      d = create_driver(CONFIG + "\noutput_type json")
      assert_kind_of Fluent::Plugin::StdoutFormatter, d.instance.formatter
      assert_equal 'json', d.instance.formatter.output_type

      d = create_driver(CONFIG + "\noutput_type hash")
      assert_kind_of Fluent::Plugin::StdoutFormatter, d.instance.formatter
      assert_equal 'hash', d.instance.formatter.output_type

      assert_raise(Fluent::NotFoundPluginError) do
        d = create_driver(CONFIG + "\noutput_type foo")
      end
    end

    test 'configure with time_format' do
      d = create_driver(CONFIG + <<-CONF)
        <format>
          @type stdout
          time_format %Y-%m-%dT%H:%M:%S.%L%z
        </format>
      CONF

      time = event_time
      out = capture_log do
        d.run(default_tag: 'test') do
          d.feed(time, {'test' => 'test'})
        end
      end

      t = Time.at(time).localtime.strftime("%Y-%m-%dT%H:%M:%S.%L%z")
      assert_equal "#{t} test: {\"test\":\"test\"}\n", out
    end

    test 'emit with default configuration' do
      d = create_driver
      time = event_time()
      out = capture_log do
        d.run(default_tag: 'test') do
          d.feed(time, {'test' => 'test1'})
        end
      end
      assert_equal "#{Time.at(time).localtime.strftime(TIME_FORMAT)} test: {\"test\":\"test1\"}\n", out
    end

    data('oj' => 'oj', 'yajl' => 'yajl')
    test 'emit in json format' do |data|
      d = create_driver(CONFIG + "\noutput_type json\njson_parser #{data}")
      time = event_time()
      out = capture_log do
        d.run(default_tag: 'test') do
          d.feed(time, {'test' => 'test1'})
        end
      end
      assert_equal "#{Time.at(time).localtime.strftime(TIME_FORMAT)} test: {\"test\":\"test1\"}\n", out

      if data == 'yajl'
        # NOTE: Float::NAN is not jsonable
        assert_raise(Yajl::EncodeError) { d.feed('test', time, {'test' => Float::NAN}) }
      else
        out = capture_log { d.feed('test', time, {'test' => Float::NAN}) }
        assert_equal "#{Time.at(time).localtime.strftime(TIME_FORMAT)} test: {\"test\":NaN}\n", out
      end
    end

    test 'emit in hash format' do
      d = create_driver(CONFIG + "\noutput_type hash")
      time = event_time()
      out = capture_log do
        d.run(default_tag: 'test') do
          d.feed(time, {'test' => 'test2'})
        end
      end
      assert_equal "#{Time.at(time).localtime.strftime(TIME_FORMAT)} test: {\"test\"=>\"test2\"}\n", out.gsub(' => ', '=>')

      # NOTE: Float::NAN is not jsonable, but hash string can output it.
      out = capture_log { d.feed('test', time, {'test' => Float::NAN}) }
      assert_equal "#{Time.at(time).localtime.strftime(TIME_FORMAT)} test: {\"test\"=>NaN}\n", out.gsub(' => ', '=>')
    end
  end

  sub_test_case 'buffered' do
    test 'configure' do
      d = create_driver(config_element("ROOT", "", {}, [config_element("buffer")]))
      assert_equal 1, d.instance.formatter_configs.size
      assert_kind_of Fluent::Plugin::StdoutFormatter, d.instance.formatter
      assert_equal 'json', d.instance.formatter.output_type
      assert_equal 10 * 1024, d.instance.buffer_config.chunk_limit_size
      assert d.instance.buffer_config.flush_at_shutdown
      assert_equal ['tag'], d.instance.buffer_config.chunk_keys
      assert d.instance.chunk_key_tag
      assert !d.instance.chunk_key_time
      assert_equal [], d.instance.chunk_keys
    end

    test 'configure with output_type' do
      d = create_driver(config_element("ROOT", "", {"output_type" => "json"}, [config_element("buffer")]))
      assert_kind_of Fluent::Plugin::StdoutFormatter, d.instance.formatter
      assert_equal 'json', d.instance.formatter.output_type

      d = create_driver(config_element("ROOT", "", {"output_type" => "hash"}, [config_element("buffer")]))
      assert_kind_of Fluent::Plugin::StdoutFormatter, d.instance.formatter
      assert_equal 'hash', d.instance.formatter.output_type

      assert_raise(Fluent::NotFoundPluginError) do
        create_driver(config_element("ROOT", "", {"output_type" => "foo"}, [config_element("buffer")]))
      end
    end

    sub_test_case "emit with default config" do
      test '#write(synchronous)' do
        d = create_driver(config_element("ROOT", "", {}, [config_element("buffer")]))
        time = event_time()

        out = capture_log do
          d.run(default_tag: 'test', flush: true) do
            d.feed(time, {'test' => 'test'})
          end
        end
        assert_equal "#{Time.at(time).localtime.strftime(TIME_FORMAT)} test: {\"test\":\"test\"}\n", out
      end
    end

    sub_test_case "emit json" do
      data('oj' => 'oj', 'yajl' => 'yajl')
      test '#write(synchronous)' do |data|
        d = create_driver(config_element("ROOT", "", {"output_type" => "json", "json_parser" => data}, [config_element("buffer")]))
        time = event_time()

        out = capture_log do
          d.run(default_tag: 'test', flush: true) do
            d.feed(time, {'test' => 'test'})
          end
        end
        assert_equal "#{Time.at(time).localtime.strftime(TIME_FORMAT)} test: {\"test\":\"test\"}\n", out
      end
    end

    sub_test_case 'emit hash' do
      test '#write(synchronous)' do
        d = create_driver(config_element("ROOT", "", {"output_type" => "hash"}, [config_element("buffer")]))
        time = event_time()

        out = capture_log do
          d.run(default_tag: 'test', flush: true) do
            d.feed(time, {'test' => 'test'})
          end
        end

        assert_equal "#{Time.at(time).localtime.strftime(TIME_FORMAT)} test: {\"test\"=>\"test\"}\n", out.gsub(' => ', '=>')
      end
    end
  end

  data(
    'utc and !localtime' => "utc true\nlocaltime false",
    '!utc and localtime' => "utc false\nlocaltime true")
  test 'success when configure with localtime and utc' do |c|
    assert_nothing_raised do
      create_driver(CONFIG + c)
    end
  end

  data('utc and localtime' => "utc true\nlocaltime true",
       '!utc and !localtime' => "utc false\nlocaltime false")
  test 'raise an error when configure with localtime and utc' do |c|
    assert_raise(Fluent::ConfigError.new('both of utc and localtime are specified, use only one of them')) do
      create_driver(CONFIG + c)
    end
  end

  # Capture the log output of the block given
  def capture_log(&block)
    tmp = $log
    $log = StringIO.new
    yield
    return $log.string
  ensure
    $log = tmp
  end
end