Back to Repositories

Testing Thread-Safe Map Operations in concurrent-ruby

This test suite thoroughly validates the functionality of the concurrent Map implementation in concurrent-ruby. It covers thread-safe operations, atomic updates, and proper handling of concurrent access patterns across multiple threads.

Test Coverage Overview

The test suite provides comprehensive coverage of the Map class functionality including:
  • Basic operations like get/set/delete
  • Atomic operations like compute_if_absent and merge_pair
  • Concurrency safety with multiple threads
  • Hash collision handling
  • Default value behaviors
Key edge cases tested include nil values, falsy values, and exception handling during operations.

Implementation Analysis

The testing approach uses RSpec to validate thread-safe map operations. It employs patterns like:
  • Shared examples for collection iteration
  • Helper methods to verify size changes
  • Concurrent access patterns with CountDownLatch
  • Custom collision key classes for hash collision testing
The tests make extensive use of RSpec’s expect syntax and shared context.

Technical Details

Testing tools and configuration:
  • RSpec as the testing framework
  • ThreadSafe::Test helpers for concurrency testing
  • CountDownLatch for thread coordination
  • Custom hash collision classes
  • Thread.abort_on_exception enabled

Best Practices Demonstrated

The test suite demonstrates several testing best practices:
  • Comprehensive coverage of edge cases and error conditions
  • Proper isolation of test cases
  • Careful validation of thread safety
  • Reusable helper methods and shared examples
  • Clear test organization by functionality

ruby-concurrency/concurrent-ruby

spec/concurrent/map_spec.rb

            
require_relative 'collection_each_shared'
require 'concurrent/map'
require 'concurrent/atomic/count_down_latch'

Thread.abort_on_exception = true

module Concurrent
  RSpec.describe Map do
    before(:each) do
      @cache = described_class.new
    end

    it 'default_proc is called with the Concurrent::Map and the key' do
      map = Concurrent::Map.new do |h, k|
        [h, k]
      end
      expect(map[:foo][0]).to be(map)
      expect(map[:foo][1]).to eq(:foo)
    end

    it 'default_proc is called with the Concurrent::Map and the key after #dup' do
      map = Concurrent::Map.new do |h, k|
        [h, k]
      end
      copy = map.dup
      expect(copy[:foo][0]).to be(copy)
      expect(copy[:foo][1]).to eq(:foo)
    end

    it 'concurrency' do
      (1..Concurrent::ThreadSafe::Test::THREADS).map do |i|
        in_thread do
          1000.times do |j|
            key = i * 1000 + j
            @cache[key] = i
            @cache[key]
            @cache.delete(key)
          end
        end
      end.map(&:join)
    end

    it 'retrieval' do
      expect_size_change(1) do
        expect(nil).to eq @cache[:a]
        expect(nil).to eq @cache.get(:a)
        @cache[:a] = 1
        expect(1).to   eq @cache[:a]
        expect(1).to   eq @cache.get(:a)
      end
    end

    it '#put_if_absent' do
      with_or_without_default_proc do
        expect_size_change(1) do
          expect(nil).to eq @cache.put_if_absent(:a, 1)
          expect(1).to   eq @cache.put_if_absent(:a, 1)
          expect(1).to   eq @cache.put_if_absent(:a, 2)
          expect(1).to   eq @cache[:a]
        end
      end
    end

    describe '#compute_if_absent' do
      it 'works in default_proc' do
        map = Concurrent::Map.new do |map, key|
          map.compute_if_absent(key) { key * 2 }
        end
        expect(map[3]).to eq 6
        expect(map.keys).to eq [3]
      end

      it 'common' do
        with_or_without_default_proc do
          expect_size_change(3) do
            expect(1).to eq @cache.compute_if_absent(:a) { 1 }
            expect(1).to eq @cache.compute_if_absent(:a) { 2 }
            expect(1).to eq @cache[:a]

            @cache[:b] = nil
            expect(nil).to  eq @cache.compute_if_absent(:b) { 1 }
            expect(nil).to  eq @cache.compute_if_absent(:c) {}
            expect(nil).to  eq @cache[:c]
            expect(true).to eq @cache.key?(:c)
          end
        end
      end

      it 'with return' do
        with_or_without_default_proc do
          expect_handles_return_lambda(:compute_if_absent, :a)
        end
      end

      it 'exception' do
        with_or_without_default_proc do
          expect_handles_exception(:compute_if_absent, :a)
        end
      end

      it 'atomicity' do
        late_compute_threads_count       = 10
        late_put_if_absent_threads_count = 10
        getter_threads_count             = 5
        compute_started = Concurrent::CountDownLatch.new(1)
        compute_proceed = Concurrent::CountDownLatch.new(
          late_compute_threads_count +
          late_put_if_absent_threads_count +
          getter_threads_count
        )
        block_until_compute_started = lambda do |name|
          # what does it mean?
          if (v = @cache[:a]) != nil
            expect(nil).to v
          end
          compute_proceed.count_down
          compute_started.wait
        end

        expect_size_change 1 do
          late_compute_threads = ::Array.new(late_compute_threads_count) do
            in_thread do
              block_until_compute_started.call('compute_if_absent')
              expect(1).to eq @cache.compute_if_absent(:a) { fail }
            end
          end

          late_put_if_absent_threads = ::Array.new(late_put_if_absent_threads_count) do
            in_thread do
              block_until_compute_started.call('put_if_absent')
              expect(1).to eq @cache.put_if_absent(:a, 2)
            end
          end

          getter_threads = ::Array.new(getter_threads_count) do
            in_thread do
              block_until_compute_started.call('getter')
              repeat_until_success { expect(1).to eq @cache[:a] }
            end
          end

          in_thread do
            @cache.compute_if_absent(:a) do
              compute_started.count_down
              compute_proceed.wait
              sleep(0.2)
              1
            end
          end.join
          (late_compute_threads +
           late_put_if_absent_threads +
           getter_threads).each(&:join)
        end
      end
    end

    describe '#compute_if_present' do
      it 'common' do
        with_or_without_default_proc do
          expect_no_size_change do
            expect(nil).to   eq @cache.compute_if_present(:a) {}
            expect(nil).to   eq @cache.compute_if_present(:a) { 1 }
            expect(nil).to   eq @cache.compute_if_present(:a) { fail }
            expect(false).to eq @cache.key?(:a)
          end

          @cache[:a] = 1
          expect_no_size_change do
            expect(1).to     eq @cache.compute_if_present(:a) { 1 }
            expect(1).to     eq @cache[:a]
            expect(2).to     eq @cache.compute_if_present(:a) { 2 }
            expect(2).to     eq @cache[:a]
            expect(false).to eq @cache.compute_if_present(:a) { false }
            expect(false).to eq @cache[:a]

            @cache[:a] = 1
            yielded    = false
            @cache.compute_if_present(:a) do |old_value|
              yielded = true
              expect(1).to eq old_value
              2
            end
            expect(true).to eq yielded
          end

          expect_size_change(-1) do
            expect(nil).to   eq @cache.compute_if_present(:a) {}
            expect(false).to eq @cache.key?(:a)
            expect(nil).to   eq @cache.compute_if_present(:a) { 1 }
            expect(false).to eq @cache.key?(:a)
          end
        end
      end

      it 'with return' do
        with_or_without_default_proc do
          @cache[:a] = 1
          expect_handles_return_lambda(:compute_if_present, :a)
        end
      end

      it 'exception' do
        with_or_without_default_proc do
          @cache[:a] = 1
          expect_handles_exception(:compute_if_present, :a)
        end
      end
    end

    describe '#compute' do
      it 'common' do
        with_or_without_default_proc do
          expect_no_size_change do
            expect_compute(:a, nil, nil) {}
          end

          expect_size_change(1) do
            expect_compute(:a, nil, 1)   { 1 }
            expect_compute(:a, 1,   2)   { 2 }
            expect_compute(:a, 2, false) { false }
            expect(false).to eq @cache[:a]
          end

          expect_size_change(-1) do
            expect_compute(:a, false, nil) {}
          end
        end
      end

      it 'with return' do
        with_or_without_default_proc do
          expect_handles_return_lambda(:compute, :a)
          @cache[:a] = 1
          expect_handles_return_lambda(:compute, :a)
        end
      end

      it 'exception' do
        with_or_without_default_proc do
          expect_handles_exception(:compute, :a)
          @cache[:a] = 2
          expect_handles_exception(:compute, :a)
        end
      end
    end

    describe '#merge_pair' do
      it 'common' do
        with_or_without_default_proc do
          expect_size_change(1) do
            expect(nil).to  eq @cache.merge_pair(:a, nil) { fail }
            expect(true).to eq @cache.key?(:a)
            expect(nil).to  eq @cache[:a]
          end

          expect_no_size_change do
            expect_merge_pair(:a, nil, nil,   false) { false }
            expect_merge_pair(:a, nil, false, 1)     { 1 }
            expect_merge_pair(:a, nil, 1,     2)     { 2 }
          end

          expect_size_change(-1) do
            expect_merge_pair(:a, nil, 2, nil) {}
            expect(false).to eq @cache.key?(:a)
          end
        end
      end

      it 'with return' do
        with_or_without_default_proc do
          @cache[:a] = 1
          expect_handles_return_lambda(:merge_pair, :a, 2)
        end
      end

      it 'exception' do
        with_or_without_default_proc do
          @cache[:a] = 1
          expect_handles_exception(:merge_pair, :a, 2)
        end
      end
    end

    it 'updates dont block reads' do
      if defined?(Concurrent::Collection::SynchronizedMapBackend) && described_class <= Concurrent::Collection::SynchronizedMapBackend
        skip("Test does not apply to #{Concurrent::Collection::SynchronizedMapBackend}")
      end

      getters_count = 20
      key_klass     = Concurrent::ThreadSafe::Test::HashCollisionKey
      keys          = [key_klass.new(1, 100),
                       key_klass.new(2, 100),
                       key_klass.new(3, 100)] # hash colliding keys
      inserted_keys = []

      keys.each do |key, i|
        compute_started  = Concurrent::CountDownLatch.new(1)
        compute_finished = Concurrent::CountDownLatch.new(1)
        getters_started  = Concurrent::CountDownLatch.new(getters_count)
        getters_finished = Concurrent::CountDownLatch.new(getters_count)

        computer_thread = in_thread do
          getters_started.wait
          @cache.compute_if_absent(key) do
            compute_started.count_down
            getters_finished.wait
            1
          end
          compute_finished.count_down
        end

        getter_threads = getters_count.times.map do
          in_thread do
            getters_started.count_down
            inserted_keys.each do |inserted_key|
              expect(@cache.key?(inserted_key)).to eq true
              expect(@cache[inserted_key]).to eq 1
            end
            expect(false).to eq @cache.key?(key)

            compute_started.wait

            inserted_keys.each do |inserted_key|
              expect(@cache.key?(inserted_key)).to eq true
              expect(@cache[inserted_key]).to eq 1
            end
            expect(@cache.key?(key)).to eq false
            expect(@cache[key]).to eq nil
            getters_finished.count_down

            compute_finished.wait

            expect(@cache.key?(key)).to eq true
            expect(@cache[key]).to eq 1
          end
        end

        join_with getter_threads + [computer_thread]
        inserted_keys << key
      end
    end

    specify 'collision resistance' do
      expect_collision_resistance(
        (0..1000).map { |i| Concurrent::ThreadSafe::Test::HashCollisionKey(i, 1) }
      )
    end

    specify 'collision resistance with arrays' do
      special_array_class = Class.new(::Array) do
        def key # assert_collision_resistance expects to be able to call .key to get the "real" key
          first.key
        end
      end
      # Test collision resistance with a keys that say they responds_to <=>, but then raise exceptions
      # when actually called (ie: an Array filled with non-comparable keys).
      # See https://github.com/headius/thread_safe/issues/19 for more info.
      expect_collision_resistance(
        (0..100).map do |i|
          special_array_class.new(
            [Concurrent::ThreadSafe::Test::HashCollisionKeyNonComparable.new(i, 1)]
          )
        end
      )
    end

    it '#replace_pair' do
      with_or_without_default_proc do
        expect_no_size_change do
          expect(false).to eq @cache.replace_pair(:a, 1, 2)
          expect(false).to eq @cache.replace_pair(:a, nil, nil)
          expect(false).to eq @cache.key?(:a)
        end
      end

      @cache[:a] = 1
      expect_no_size_change do
        expect(true).to  eq @cache.replace_pair(:a, 1, 2)
        expect(false).to eq @cache.replace_pair(:a, 1, 2)
        expect(2).to     eq @cache[:a]
        expect(true).to  eq @cache.replace_pair(:a, 2, 2)
        expect(2).to     eq @cache[:a]
        expect(true).to  eq @cache.replace_pair(:a, 2, nil)
        expect(false).to eq @cache.replace_pair(:a, 2, nil)
        expect(nil).to   eq @cache[:a]
        expect(true).to  eq @cache.key?(:a)
        expect(true).to  eq @cache.replace_pair(:a, nil, nil)
        expect(true).to  eq @cache.key?(:a)
        expect(true).to  eq @cache.replace_pair(:a, nil, 1)
        expect(1).to     eq @cache[:a]
      end
    end

    it '#replace_if_exists' do
      with_or_without_default_proc do
        expect_no_size_change do
          expect(nil).to   eq @cache.replace_if_exists(:a, 1)
          expect(false).to eq @cache.key?(:a)
        end

        @cache[:a] = 1
        expect_no_size_change do
          expect(1).to    eq  @cache.replace_if_exists(:a, 2)
          expect(2).to    eq  @cache[:a]
          expect(2).to    eq  @cache.replace_if_exists(:a, nil)
          expect(nil).to  eq  @cache[:a]
          expect(true).to eq  @cache.key?(:a)
          expect(nil).to  eq  @cache.replace_if_exists(:a, 1)
          expect(1).to    eq  @cache[:a]
        end
      end
    end

    it '#get_and_set' do
      with_or_without_default_proc do
        expect(nil).to  eq  @cache.get_and_set(:a, 1)
        expect(true).to eq  @cache.key?(:a)
        expect(1).to    eq  @cache[:a]
        expect(1).to    eq  @cache.get_and_set(:a, 2)
        expect(2).to    eq  @cache.get_and_set(:a, nil)
        expect(nil).to  eq  @cache[:a]
        expect(true).to eq  @cache.key?(:a)
        expect(nil).to  eq  @cache.get_and_set(:a, 1)
        expect(1).to    eq  @cache[:a]
      end
    end

    it '#key' do
      with_or_without_default_proc do
        expect(nil).to eq @cache.key(1)
        @cache[:a] = 1
        expect(:a).to  eq  @cache.key(1)
        expect(nil).to eq  @cache.key(0)
      end
    end

    it '#key?' do
      with_or_without_default_proc do
        expect(false).to eq @cache.key?(:a)
        @cache[:a] = 1
        expect(true).to  eq @cache.key?(:a)
      end
    end

    it '#value?' do
      with_or_without_default_proc do
        expect(false).to eq @cache.value?(1)
        @cache[:a] = 1
        expect(true).to  eq @cache.value?(1)
      end
    end

    it '#delete' do
      with_or_without_default_proc do |default_proc_set|
        expect_no_size_change do
          expect(nil).to   eq @cache.delete(:a)
        end
        @cache[:a] = 1
        expect_size_change(-1) do
          expect(1).to     eq @cache.delete(:a)
        end
        expect_no_size_change do
          expect(nil).to   eq @cache[:a] unless default_proc_set

          expect(false).to eq @cache.key?(:a)
          expect(nil).to   eq @cache.delete(:a)
        end
      end
    end

    it '#delete_pair' do
      with_or_without_default_proc do
        expect_no_size_change do
          expect(false).to eq @cache.delete_pair(:a, 2)
          expect(false).to eq @cache.delete_pair(:a, nil)
        end
        @cache[:a] = 1
        expect_no_size_change do
          expect(false).to eq @cache.delete_pair(:a, 2)
        end
        expect_size_change(-1) do
          expect(1).to     eq @cache[:a]
          expect(true).to  eq @cache.delete_pair(:a, 1)
          expect(false).to eq @cache.delete_pair(:a, 1)
          expect(false).to eq @cache.key?(:a)
        end
      end
    end

    specify 'default proc' do
      @cache = cache_with_default_proc(1)
      expect_no_size_change do
        expect(false).to eq @cache.key?(:a)
      end
      expect_size_change(1) do
        expect(1).to     eq @cache[:a]
        expect(true).to  eq @cache.key?(:a)
      end
    end

    specify 'falsy default proc' do
      @cache = cache_with_default_proc(nil)
      expect_no_size_change do
        expect(false).to eq @cache.key?(:a)
      end
      expect_size_change(1) do
        expect(nil).to   eq @cache[:a]
        expect(true).to  eq @cache.key?(:a)
      end
    end

    describe '#fetch' do
      it 'common' do
        with_or_without_default_proc do |default_proc_set|
          expect_no_size_change do
            expect(1).to     eq @cache.fetch(:a, 1)
            expect(1).to     eq @cache.fetch(:a) { 1 }
            expect(false).to eq @cache.key?(:a)

            expect(nil).to   eq @cache[:a] unless default_proc_set
          end

          @cache[:a] = 1
          expect_no_size_change do
            expect(1).to eq @cache.fetch(:a) { fail }
          end

          expect { @cache.fetch(:b) }.to raise_error(KeyError)

          expect_no_size_change do
            expect(1).to     eq @cache.fetch(:b, :c) {1} # assert block supersedes default value argument
            expect(false).to eq @cache.key?(:b)
          end
        end
      end

      it 'falsy' do
        with_or_without_default_proc do
          expect(false).to eq @cache.key?(:a)

          expect_no_size_change do
            expect(nil).to   eq @cache.fetch(:a, nil)
            expect(false).to eq @cache.fetch(:a, false)
            expect(nil).to   eq @cache.fetch(:a) {}
            expect(false).to eq @cache.fetch(:a) { false }
          end

          @cache[:a] = nil
          expect_no_size_change do
            expect(true).to eq @cache.key?(:a)
            expect(nil).to  eq @cache.fetch(:a) { fail }
          end
        end
      end

      it 'with return' do
        with_or_without_default_proc do
          r = -> do
            @cache.fetch(:a) { return 10 }
          end.call

          expect_no_size_change do
            expect(10).to    eq r
            expect(false).to eq @cache.key?(:a)
          end
        end
      end
    end

    describe '#fetch_or_store' do
      it 'common' do
        with_or_without_default_proc do |default_proc_set|
          expect_size_change(1) do
            expect(1).to eq @cache.fetch_or_store(:a, 1)
            expect(1).to eq @cache[:a]
          end

          @cache.delete(:a)

          expect_size_change 1 do
            expect(1).to eq @cache.fetch_or_store(:a) { 1 }
            expect(1).to eq @cache[:a]
          end

          expect_no_size_change do
            expect(1).to eq @cache.fetch_or_store(:a) { fail }
          end

          expect { @cache.fetch_or_store(:b) }.
            to raise_error(KeyError)

          expect_size_change(1) do
            expect(1).to eq @cache.fetch_or_store(:b, :c) { 1 } # assert block supersedes default value argument
            expect(1).to eq @cache[:b]
          end
        end
      end

      it 'falsy' do
        with_or_without_default_proc do
          expect(false).to eq @cache.key?(:a)

          expect_size_change(1) do
            expect(nil).to  eq @cache.fetch_or_store(:a, nil)
            expect(nil).to  eq @cache[:a]
            expect(true).to eq @cache.key?(:a)
          end
          @cache.delete(:a)

          expect_size_change(1) do
            expect(false).to eq @cache.fetch_or_store(:a, false)
            expect(false).to eq @cache[:a]
            expect(true).to  eq @cache.key?(:a)
          end
          @cache.delete(:a)

          expect_size_change(1) do
            expect(nil).to  eq @cache.fetch_or_store(:a) {}
            expect(nil).to  eq @cache[:a]
            expect(true).to eq @cache.key?(:a)
          end
          @cache.delete(:a)

          expect_size_change(1) do
            expect(false).to eq @cache.fetch_or_store(:a) { false }
            expect(false).to eq @cache[:a]
            expect(true).to  eq @cache.key?(:a)
          end

          @cache[:a] = nil
          expect_no_size_change do
            expect(nil).to eq @cache.fetch_or_store(:a) { fail }
          end
        end
      end

      it 'with return' do
        with_or_without_default_proc do
          r = -> do
            @cache.fetch_or_store(:a) { return 10 }
          end.call

          expect_no_size_change do
            expect(10).to    eq r
            expect(false).to eq @cache.key?(:a)
          end
        end
      end
    end

    it '#clear' do
      @cache[:a] = 1
      expect_size_change(-1) do
        expect(@cache).to eq @cache.clear
        expect(false).to  eq @cache.key?(:a)
        expect(nil).to    eq @cache[:a]
      end
    end

    describe '#each_pair' do
      it_should_behave_like :collection_each do
        let(:method) { :each_pair }
      end
    end

    describe '#each' do
      it_should_behave_like :collection_each do
        let(:method) { :each }
      end
    end

    it '#keys' do
      expect([]).to eq @cache.keys

      @cache[1] = 1
      expect([1]).to eq @cache.keys

      @cache[2] = 2
      expect([1, 2]).to eq @cache.keys.sort
    end

    it '#values' do
      expect([]).to eq @cache.values

      @cache[1] = 1
      expect([1]).to eq @cache.values

      @cache[2] = 2
      expect([1, 2]).to eq @cache.values.sort
    end

    it '#each_key' do
      expect(@cache).to eq @cache.each_key { fail }

      @cache[1] = 1
      arr = []
      @cache.each_key { |k| arr << k }
      expect([1]).to eq arr

      @cache[2] = 2
      arr = []
      @cache.each_key { |k| arr << k }
      expect([1, 2]).to eq arr.sort
    end

    it '#each_value' do
      expect(@cache).to eq @cache.each_value { fail }

      @cache[1] = 1
      arr = []
      @cache.each_value { |k| arr << k }
      expect([1]).to eq arr

      @cache[2] = 2
      arr = []
      @cache.each_value { |k| arr << k }
      expect([1, 2]).to eq arr.sort
    end

    it '#empty' do
      expect(true).to  eq @cache.empty?
      @cache[:a] = 1
      expect(false).to eq @cache.empty?
    end

    it 'options validation' do
      expect_valid_options(nil)
      expect_valid_options({})
      expect_valid_options(foo: :bar)
    end

    it 'initial capacity options validation' do
      expect_valid_option(:initial_capacity, nil)
      expect_valid_option(:initial_capacity, 1)
      expect_invalid_option(:initial_capacity, '')
      expect_invalid_option(:initial_capacity, 1.0)
      expect_invalid_option(:initial_capacity, -1)
    end

    it 'load factor options validation' do
      expect_valid_option(:load_factor, nil)
      expect_valid_option(:load_factor, 0.01)
      expect_valid_option(:load_factor, 0.75)
      expect_valid_option(:load_factor, 1)
      expect_invalid_option(:load_factor, '')
      expect_invalid_option(:load_factor, 0)
      expect_invalid_option(:load_factor, 1.1)
      expect_invalid_option(:load_factor, 2)
      expect_invalid_option(:load_factor, -1)
    end

    it '#size' do
      expect(0).to eq @cache.size
      @cache[:a] = 1
      expect(1).to eq @cache.size
      @cache[:b] = 1
      expect(2).to eq @cache.size
      @cache.delete(:a)
      expect(1).to eq @cache.size
      @cache.delete(:b)
      expect(0).to eq @cache.size
    end

    it '#get_or_default' do
      with_or_without_default_proc do
        expect(1).to     eq @cache.get_or_default(:a, 1)
        expect(nil).to   eq @cache.get_or_default(:a, nil)
        expect(false).to eq @cache.get_or_default(:a, false)
        expect(false).to eq @cache.key?(:a)

        @cache[:a] = 1
        expect(1).to eq @cache.get_or_default(:a, 2)
      end
    end

    it '#dup,#clone' do
      [:dup, :clone].each do |meth|
        cache = cache_with_default_proc(:default_value)
        cache[:a] = 1
        dupped = cache.send(meth)
        expect(1).to eq dupped[:a]
        expect(1).to eq dupped.size
        expect_size_change(1, cache) do
          expect_no_size_change(dupped) do
            cache[:b] = 1
          end
        end
        expect(false).to eq dupped.key?(:b)
        expect_no_size_change(cache) do
          expect_size_change(-1, dupped) do
            dupped.delete(:a)
          end
        end
        expect(false).to eq dupped.key?(:a)
        expect(true).to  eq cache.key?(:a)
        # test default proc
        expect_size_change(1, cache) do
          expect_no_size_change dupped do
            expect(:default_value).to eq cache[:c]
            expect(false).to          eq dupped.key?(:c)
          end
        end
        expect_no_size_change cache do
          expect_size_change 1, dupped do
            expect(dupped[:d]).to eq :default_value
            expect(cache.key?(:d)).to eq false
          end
        end
      end
    end

    it 'is unfreezable' do
      expect { @cache.freeze }.to raise_error(NoMethodError)
    end

    it 'marshal dump load' do
      new_cache = Marshal.load(Marshal.dump(@cache))
      expect(new_cache).to be_an_instance_of Concurrent::Map
      expect(0).to eq new_cache.size
      @cache[:a] = 1
      new_cache = Marshal.load(Marshal.dump(@cache))
      expect(1).to eq @cache[:a]
      expect(1).to eq new_cache.size
    end

    it 'marshal dump does not work with default proc' do
      expect { Marshal.dump(Concurrent::Map.new {}) }.to raise_error(TypeError)
    end

    it '#inspect' do
      regexp = /\A#<Concurrent::Map:0x[0-9a-f]+ entries=[0-9]+ default_proc=.*>\Z/i
      expect(Concurrent::Map.new.inspect).to match(regexp)
      expect((Concurrent::Map.new {}).inspect).to match(regexp)
      map = Concurrent::Map.new
      map[:foo] = :bar
      expect(map.inspect).to match(regexp)
    end

    private

    def with_or_without_default_proc(&block)
      block.call(false)
      @cache = Concurrent::Map.new { |h, k|
        expect(h).to be_kind_of(Concurrent::Map)
        h[k] = :default_value
      }
      block.call(true)
    end

    def cache_with_default_proc(default_value = 1)
      Concurrent::Map.new do |map, k|
        expect(map).to be_kind_of(Concurrent::Map)
        map[k] = default_value
      end
    end

    def expect_size_change(change, cache = @cache, &block)
      start = cache.size
      block.call
      expect(change).to eq cache.size - start
    end

    def expect_valid_option(option_name, value)
      expect_valid_options(option_name => value)
    end

    def expect_valid_options(options)
      c = Concurrent::Map.new(options)
      expect(c).to be_an_instance_of Concurrent::Map
    end

    def expect_invalid_option(option_name, value)
      expect_invalid_options(option_name => value)
    end

    def expect_invalid_options(options)
      expect { Concurrent::Map.new(options) }.to raise_error(ArgumentError)
    end

    def expect_no_size_change(cache = @cache, &block)
      expect_size_change(0, cache, &block)
    end

    def expect_handles_return_lambda(method, key, *args)
      before_had_key   = @cache.key?(key)
      before_had_value = before_had_key ? @cache[key] : nil

      returning_lambda = lambda do
        @cache.send(method, key, *args) { return :direct_return }
      end

      expect_no_size_change do
        expect(:direct_return).to   eq returning_lambda.call
        expect(before_had_key).to   eq @cache.key?(key)
        expect(before_had_value).to eq @cache[key] if before_had_value
      end
    end

    class TestException < Exception; end
    def expect_handles_exception(method, key, *args)
      before_had_key   = @cache.key?(key)
      before_had_value = before_had_key ? @cache[key] : nil

      expect_no_size_change do
        expect { @cache.send(method, key, *args) { raise TestException, '' } }.
          to raise_error(TestException)

        expect(before_had_key).to   eq @cache.key?(key)
        expect(before_had_value).to eq @cache[key] if before_had_value
      end
    end

    def expect_compute(key, expected_old_value, expected_result, &block)
      result = @cache.compute(:a) do |old_value|
        expect(expected_old_value).to eq old_value
        block.call
      end
      expect(expected_result).to eq result
    end

    def expect_merge_pair(key, value, expected_old_value, expected_result, &block)
      result = @cache.merge_pair(key, value) do |old_value|
        expect(expected_old_value).to eq old_value
        block.call
      end
      expect(expected_result).to eq result
    end

    def expect_collision_resistance(keys)
      keys.each { |k| @cache[k] = k.key }
      10.times do |i|
        size = keys.size
        while i < size
          k = keys[i]
          expect(k.key == @cache.delete(k) &&
                 [email protected]?(k) &&
                 (@cache[k] = k.key; @cache[k] == k.key)).to be_truthy
          i += 10
        end
      end
      expect(keys.all? { |k| @cache[k] == k.key }).to be_truthy
    end
  end
end