Back to Repositories

Testing Parser Plugin Helper Implementation in Fluentd

This test suite validates the Parser Helper functionality in Fluentd, focusing on parser plugin initialization, configuration, and lifecycle management. The tests ensure proper handling of multiple parser instances and their configuration states.

Test Coverage Overview

The test suite provides comprehensive coverage of the Parser Helper functionality in Fluentd:

  • Parser initialization and configuration validation
  • Multiple parser instance management
  • Default configuration handling
  • Parser lifecycle state transitions
  • Error handling for invalid configurations

Implementation Analysis

The testing approach uses Minitest framework with Ruby to validate parser plugin behavior:

  • Mock parser implementations (ExampleParser and Example2Parser)
  • Configuration validation through config_element structures
  • Lifecycle method verification across different parser states
  • Systematic validation of parser creation and management

Technical Details

Testing infrastructure includes:

  • Fluent::Plugin::TestBase for base testing functionality
  • Custom parser implementations for testing
  • Configuration helpers for parser setup
  • Lifecycle management methods (start, stop, shutdown, etc.)
  • Test setup and teardown procedures

Best Practices Demonstrated

The test suite exemplifies several testing best practices:

  • Proper test isolation through setup/teardown
  • Comprehensive lifecycle testing
  • Edge case handling for configuration scenarios
  • Clear test organization and naming
  • Thorough validation of plugin states

fluent/fluentd

test/plugin_helper/test_parser.rb

            
require_relative '../helper'
require 'fluent/plugin_helper/parser'
require 'fluent/plugin/base'
require 'fluent/time'

class ParserHelperTest < Test::Unit::TestCase
  class ExampleParser < Fluent::Plugin::Parser
    Fluent::Plugin.register_parser('example', self)
    def parse(text)
      ary = text.split(/\s*,\s*/)
      r = {}
      ary.each_with_index do |v, i|
        r[i.to_s] = v
      end
      yield Fluent::EventTime.now, r
    end
  end
  class Example2Parser < Fluent::Plugin::Parser
    Fluent::Plugin.register_parser('example2', self)
    def parse(text)
      ary = text.split(/\s*,\s*/)
      r = {}
      ary.each_with_index do |v, i|
        r[i.to_s] = v
      end
      yield Fluent::EventTime.now, r
    end
  end
  class Dummy < Fluent::Plugin::TestBase
    helpers :parser
    config_section :parse do
      config_set_default :@type, 'example'
    end
  end

  class Dummy2 < Fluent::Plugin::TestBase
    helpers :parser
    config_section :parse do
      config_set_default :@type, 'example2'
    end
  end

  setup do
    @d = nil
  end

  teardown do
    if @d
      @d.stop unless @d.stopped?
      @d.shutdown unless @d.shutdown?
      @d.close unless @d.closed?
      @d.terminate unless @d.terminated?
    end
  end

  test 'can be initialized without any parsers at first' do
    d = Dummy.new
    assert_equal 0, d._parsers.size
  end

  test 'can override default configuration parameters, but not overwrite whole definition' do
    d = Dummy.new
    assert_equal [], d.parser_configs

    d = Dummy2.new
    d.configure(config_element('ROOT', '', {}, [config_element('parse', '', {}, [])]))
    assert_raise NoMethodError do
      d.parse
    end
    assert_equal 1, d.parser_configs.size
    assert_equal 'example2', d.parser_configs.first[:@type]
  end

  test 'creates instance of type specified by conf, or default_type if @type is missing in conf' do
    d = Dummy2.new
    d.configure(config_element())
    i = d.parser_create(conf: config_element('parse', '', {'@type' => 'example'}), default_type: 'example2')
    assert{ i.is_a?(ExampleParser) }

    d = Dummy2.new
    d.configure(config_element())
    i = d.parser_create(conf: nil, default_type: 'example2')
    assert{ i.is_a?(Example2Parser) }
  end

  test 'raises config error if config section is specified, but @type is not specified' do
    d = Dummy2.new
    d.configure(config_element())
    assert_raise Fluent::ConfigError.new("@type is required in <parse>") do
      d.parser_create(conf: config_element('parse', '', {}), default_type: 'example2')
    end
  end

  test 'can be configured with default type without parse sections' do
    d = Dummy.new
    d.configure(config_element())
    assert_equal 1, d._parsers.size
  end

  test 'can be configured with a parse section' do
    d = Dummy.new
    conf = config_element('ROOT', '', {}, [
        config_element('parse', '', {'@type' => 'example'})
      ])
    assert_nothing_raised do
      d.configure(conf)
    end
    assert_equal 1, d._parsers.size
    assert{ d._parsers.values.all?{ |parser| !parser.started? } }
  end

  test 'can be configured with 2 or more parse sections with different usages with each other' do
    d = Dummy.new
    conf = config_element('ROOT', '', {}, [
        config_element('parse', 'default', {'@type' => 'example'}),
        config_element('parse', 'extra', {'@type' => 'example2'}),
      ])
    assert_nothing_raised do
      d.configure(conf)
    end
    assert_equal 2, d._parsers.size
    assert{ d._parsers.values.all?{ |parser| !parser.started? } }
  end

  test 'cannot be configured with 2 parse sections with same usage' do
    d = Dummy.new
    conf = config_element('ROOT', '', {}, [
        config_element('parse', 'default', {'@type' => 'example'}),
        config_element('parse', 'extra', {'@type' => 'example2'}),
        config_element('parse', 'extra', {'@type' => 'example2'}),
      ])
    assert_raises Fluent::ConfigError do
      d.configure(conf)
    end
  end

  test 'creates a parse plugin instance which is already configured without usage' do
    @d = d = Dummy.new
    conf = config_element('ROOT', '', {}, [
        config_element('parse', '', {'@type' => 'example'})
      ])
    d.configure(conf)
    d.start

    parser = d.parser_create
    assert{ parser.is_a? ExampleParser }
    assert parser.started?
  end

  test 'creates a parser plugin instance which is already configured with usage' do
    @d = d = Dummy.new
    conf = config_element('ROOT', '', {}, [
        config_element('parse', 'mydata', {'@type' => 'example'})
      ])
    d.configure(conf)
    d.start

    parser = d.parser_create(usage: 'mydata')
    assert{ parser.is_a? ExampleParser }
    assert parser.started?
  end

  test 'creates a parser plugin without configurations' do
    @d = d = Dummy.new
    d.configure(config_element())
    d.start

    parser = d.parser_create(usage: 'mydata', type: 'example', conf: config_element('parse', 'mydata'))
    assert{ parser.is_a? ExampleParser }
    assert parser.started?
  end

  test 'creates 2 or more parser plugin instances' do
    @d = d = Dummy.new
    conf = config_element('ROOT', '', {}, [
        config_element('parse', 'mydata', {'@type' => 'example'}),
        config_element('parse', 'secret', {'@type' => 'example2'})
      ])
    d.configure(conf)
    d.start

    p1 = d.parser_create(usage: 'mydata')
    p2 = d.parser_create(usage: 'secret')
    assert{ p1.is_a? ExampleParser }
    assert p1.started?
    assert{ p2.is_a? Example2Parser }
    assert p2.started?
  end

  test 'calls lifecycle methods for all plugin instances via owner plugin' do
    @d = d = Dummy.new
    conf = config_element('ROOT', '', {}, [ config_element('parse', '', {'@type' => 'example'}), config_element('parse', 'e2', {'@type' => 'example'}) ])
    d.configure(conf)
    d.start

    i1 = d.parser_create(usage: '')
    i2 = d.parser_create(usage: 'e2')
    i3 = d.parser_create(usage: 'e3', type: 'example2')

    assert i1.started?
    assert i2.started?
    assert i3.started?

    assert !i1.stopped?
    assert !i2.stopped?
    assert !i3.stopped?

    d.stop

    assert i1.stopped?
    assert i2.stopped?
    assert i3.stopped?

    assert !i1.before_shutdown?
    assert !i2.before_shutdown?
    assert !i3.before_shutdown?

    d.before_shutdown

    assert i1.before_shutdown?
    assert i2.before_shutdown?
    assert i3.before_shutdown?

    assert !i1.shutdown?
    assert !i2.shutdown?
    assert !i3.shutdown?

    d.shutdown

    assert i1.shutdown?
    assert i2.shutdown?
    assert i3.shutdown?

    assert !i1.after_shutdown?
    assert !i2.after_shutdown?
    assert !i3.after_shutdown?

    d.after_shutdown

    assert i1.after_shutdown?
    assert i2.after_shutdown?
    assert i3.after_shutdown?

    assert !i1.closed?
    assert !i2.closed?
    assert !i3.closed?

    d.close

    assert i1.closed?
    assert i2.closed?
    assert i3.closed?

    assert !i1.terminated?
    assert !i2.terminated?
    assert !i3.terminated?

    d.terminate

    assert i1.terminated?
    assert i2.terminated?
    assert i3.terminated?
  end
end