Back to Repositories

Testing Counter Store Operations in Fluentd

This test suite validates the functionality of Fluentd’s Counter Store component, which manages counter operations and value persistence. The tests cover initialization, value manipulation, and reset behaviors with time-based operations.

Test Coverage Overview

The test suite provides comprehensive coverage of the Counter Store functionality, including:
  • Counter initialization and key management
  • Value retrieval and modification operations
  • Time-based reset mechanisms
  • Type validation for counter values
  • Error handling for invalid operations

Implementation Analysis

The testing approach utilizes Test::Unit framework with structured sub-test cases. Implementation highlights include:
  • Time manipulation using Timecop for predictable testing
  • Instance variable inspection for internal state verification
  • Modular test organization with setup/teardown hooks
  • Data-driven test cases for value validation

Technical Details

Technical implementation specifics:
  • Uses Minitest/Test::Unit framework
  • Timecop for time manipulation
  • Fluent::EventTime integration
  • Custom helper methods for internal state access
  • Platform-specific accommodations for Windows testing

Best Practices Demonstrated

The test suite exemplifies several testing best practices:
  • Isolated test cases with proper setup/teardown
  • Comprehensive edge case coverage
  • Clear test naming conventions
  • Proper error scenario validation
  • Platform-aware testing considerations

fluent/fluentd

test/counter/test_store.rb

            
require_relative '../helper'
require 'fluent/counter/store'
require 'fluent/time'
require 'timecop'

class CounterStoreTest < ::Test::Unit::TestCase
  setup do
    @name = 'key_name'
    @scope = "server\tworker\tplugin"

    # timecop isn't compatible with EventTime
    t = Time.parse('2016-09-22 16:59:59 +0900')
    Timecop.freeze(t)
    @now = Fluent::EventTime.now
  end

  teardown do
    Timecop.return
  end

  def extract_value_from_counter(counter, key)
    store = counter.instance_variable_get(:@storage).instance_variable_get(:@store)
    store[key]
  end

  def travel(sec)
    # Since Timecop.travel() causes test failures on Windows/AppVeyor by inducing
    # rounding errors to Time.now, we need to use Timecop.freeze() instead.
    Timecop.freeze(Time.now + sec)
  end

  sub_test_case 'init' do
    setup do
      @reset_interval = 10
      @store = Fluent::Counter::Store.new
      @data = { 'name' => @name, 'reset_interval' => @reset_interval }
      @key = Fluent::Counter::Store.gen_key(@scope, @name)
    end

    test 'create new value in the counter' do
      v = @store.init(@key, @data)

      assert_equal @name, v['name']
      assert_equal @reset_interval, v['reset_interval']

      v2 = extract_value_from_counter(@store, @key)
      v2 = @store.send(:build_response, v2)
      assert_equal v, v2
    end

    test 'raise an error when a passed key already exists' do
      @store.init(@key, @data)

      assert_raise Fluent::Counter::InvalidParams do
        @store.init(@key, @data)
      end
    end

    test 'return a value when passed key already exists and a ignore option is true' do
      v = @store.init(@key, @data)
      v1 = extract_value_from_counter(@store, @key)
      v1 = @store.send(:build_response, v1)
      v2 = @store.init(@key, @data, ignore: true)
      assert_equal v, v2
      assert_equal v1, v2
    end
  end

  sub_test_case 'get' do
    setup do
      @store = Fluent::Counter::Store.new
      data = { 'name' => @name, 'reset_interval' => 10 }
      @key = Fluent::Counter::Store.gen_key(@scope, @name)
      @store.init(@key, data)
    end

    test 'return a value from the counter' do
      v = extract_value_from_counter(@store, @key)
      expected = @store.send(:build_response, v)
      assert_equal expected, @store.get(@key)
    end

    test 'return a raw value from the counter when raw option is true' do
      v = extract_value_from_counter(@store, @key)
      assert_equal v, @store.get(@key, raw: true)
    end

    test "return nil when a passed key doesn't exist" do
      assert_equal nil, @store.get('unknown_key')
    end

    test "raise a error when a passed key doesn't exist and raise_error option is true" do
      assert_raise Fluent::Counter::UnknownKey do
        @store.get('unknown_key', raise_error: true)
      end
    end
  end

  sub_test_case 'key?' do
    setup do
      @store = Fluent::Counter::Store.new
      data = { 'name' => @name, 'reset_interval' => 10 }
      @key = Fluent::Counter::Store.gen_key(@scope, @name)
      @store.init(@key, data)
    end

    test 'return true when passed key exists' do
      assert_true @store.key?(@key)
    end

    test "return false when passed key doesn't exist" do
      assert_true [email protected]?('unknown_key')
    end
  end

  sub_test_case 'delete' do
    setup do
      @store = Fluent::Counter::Store.new
      data = { 'name' => @name, 'reset_interval' => 10 }
      @key = Fluent::Counter::Store.gen_key(@scope, @name)
      @init_value = @store.init(@key, data)
    end

    test 'delete a value from the counter' do
      v = @store.delete(@key)
      assert_equal @init_value, v
      assert_nil extract_value_from_counter(@store, @key)
    end

    test "raise an error when passed key doesn't exist" do
      assert_raise Fluent::Counter::UnknownKey do
        @store.delete('unknown_key')
      end
    end
  end

  sub_test_case 'inc' do
    setup do
      @store = Fluent::Counter::Store.new
      @init_data = { 'name' => @name, 'reset_interval' => 10 }
      @travel_sec = 10
    end

    data(
      positive: 10,
      negative: -10
    )
    test 'increment or decrement a value in the counter' do |value|
      key = Fluent::Counter::Store.gen_key(@scope, @name)
      @store.init(key, @init_data)
      travel(@travel_sec)
      v = @store.inc(key, { 'value' => value })

      assert_equal value, v['total']
      assert_equal value, v['current']
      assert_equal @now, v['last_reset_at'] # last_reset_at doesn't change

      v1 = extract_value_from_counter(@store, key)
      v1 = @store.send(:build_response, v1)
      assert_equal v, v1
    end

    test "raise an error when passed key doesn't exist" do
      assert_raise Fluent::Counter::UnknownKey do
        @store.inc('unknown_key', { 'value' => 1 })
      end
    end

    test 'raise an error when a type of passed value is incompatible with a stored value' do
      key1 = Fluent::Counter::Store.gen_key(@scope, @name)
      key2 = Fluent::Counter::Store.gen_key(@scope, 'name2')
      key3 = Fluent::Counter::Store.gen_key(@scope, 'name3')
      v1 = @store.init(key1, @init_data.merge('type' => 'integer'))
      v2 = @store.init(key2, @init_data.merge('type' => 'float'))
      v3 = @store.init(key3, @init_data.merge('type' => 'numeric'))
      assert_equal 'integer', v1['type']
      assert_equal 'float', v2['type']
      assert_equal 'numeric', v3['type']

      assert_raise Fluent::Counter::InvalidParams do
        @store.inc(key1, { 'value' => 1.1 })
      end

      assert_raise Fluent::Counter::InvalidParams do
        @store.inc(key2, { 'value' => 1 })
      end

      assert_nothing_raised do
        @store.inc(key3, { 'value' => 1 })
        @store.inc(key3, { 'value' => 1.0 })
      end
    end
  end

  sub_test_case 'reset' do
    setup do
      @store = Fluent::Counter::Store.new
      @travel_sec = 10

      @inc_value = 10
      @key = Fluent::Counter::Store.gen_key(@scope, @name)
      @store.init(@key, { 'name' => @name, 'reset_interval' => 10 })
      @store.inc(@key, { 'value' => 10 })
    end

    test 'reset a value in the counter' do
      travel(@travel_sec)

      v = @store.reset(@key)
      assert_equal @travel_sec, v['elapsed_time']
      assert_true v['success']
      counter = v['counter_data']

      assert_equal @name, counter['name']
      assert_equal @inc_value, counter['total']
      assert_equal @inc_value, counter['current']
      assert_equal 'numeric', counter['type']
      assert_equal @now, counter['last_reset_at']
      assert_equal 10, counter['reset_interval']

      v1 = extract_value_from_counter(@store, @key)
      assert_equal 0, v1['current']
      assert_true v1['current'].is_a?(Integer)
      assert_equal @inc_value, v1['total']
      assert_equal (@now + @travel_sec), Fluent::EventTime.new(*v1['last_reset_at'])
      assert_equal (@now + @travel_sec), Fluent::EventTime.new(*v1['last_modified_at'])
    end

    test 'reset a value after `reset_interval` passed' do
      first_travel_sec = 5
      travel(first_travel_sec) # jump time less than reset_interval
      v = @store.reset(@key)

      assert_equal false, v['success']
      assert_equal first_travel_sec, v['elapsed_time']
      store = extract_value_from_counter(@store, @key)
      assert_equal 10, store['current']
      assert_equal @now, Fluent::EventTime.new(*store['last_reset_at'])

      # time is passed greater than reset_interval
      travel(@travel_sec)
      v = @store.reset(@key)
      assert_true v['success']
      assert_equal @travel_sec + first_travel_sec, v['elapsed_time']

      v1 = extract_value_from_counter(@store, @key)
      assert_equal 0, v1['current']
      assert_equal (@now + @travel_sec + first_travel_sec), Fluent::EventTime.new(*v1['last_reset_at'])
      assert_equal (@now + @travel_sec + first_travel_sec), Fluent::EventTime.new(*v1['last_modified_at'])
    end

    test "raise an error when passed key doesn't exist" do
      assert_raise Fluent::Counter::UnknownKey do
        @store.reset('unknown_key')
      end
    end
  end
end