Back to Repositories

Testing Atomic Reference Operations in concurrent-ruby

This RSpec test suite thoroughly validates the AtomicReference implementation in the concurrent-ruby library, focusing on atomic operations and thread-safe value manipulation. The tests cover various reference types including mutex-based, C-extension, Java, and TruffleRuby implementations.

Test Coverage Overview

The test suite provides comprehensive coverage of AtomicReference functionality across different implementations.

Key areas tested include:
  • Basic construction and value operations
  • Atomic updates and swap operations
  • Compare-and-set (CAS) operations for various numeric types
  • Edge cases with different numeric ranges and types
  • Platform-specific implementations (JRuby, MRI, TruffleRuby)

Implementation Analysis

The testing approach uses RSpec shared examples to ensure consistent behavior across different AtomicReference implementations. The suite employs specific testing patterns for atomic operations, including retry mechanisms and concurrent update scenarios.

Technical implementation details:
  • Shared behavior testing using RSpec shared_examples
  • Platform-specific conditional testing
  • Explicit numeric range testing for different Ruby implementations

Technical Details

Testing infrastructure includes:
  • RSpec testing framework
  • Concurrent-ruby library integration
  • Platform-specific atomic implementations (C, Java, Mutex, TruffleRuby)
  • Complex number support
  • Rational number testing
  • Bignum operations validation

Best Practices Demonstrated

The test suite exemplifies several testing best practices:

  • Comprehensive edge case coverage
  • Cross-platform compatibility testing
  • Shared behavior specifications
  • Explicit failure scenario testing
  • Clear test organization and structure
  • Platform-specific implementation verification

ruby-concurrency/concurrent-ruby

spec/concurrent/atomic/atomic_reference_spec.rb

            
require 'concurrent/atomic/atomic_reference'

RSpec.shared_examples :atomic_reference do

  specify :test_construct do
    atomic = described_class.new
    expect(atomic.value).to be_nil

    atomic = described_class.new(0)
    expect(atomic.value).to eq 0
  end

  specify :test_value do
    atomic       = described_class.new(0)
    atomic.value = 1

    expect(atomic.value).to eq 1
  end

  specify :test_update do
    # use a number outside JRuby's fixnum cache range, to ensure identity is preserved
    atomic = described_class.new(1000)
    res    = atomic.update { |v| v + 1 }

    expect(atomic.value).to eq 1001
    expect(res).to eq 1001
  end

  specify :test_try_update do
    # use a number outside JRuby's fixnum cache range, to ensure identity is preserved
    atomic = described_class.new(1000)
    res    = atomic.try_update { |v| v + 1 }

    expect(atomic.value).to eq 1001
    expect(res).to eq 1001
  end

  specify :test_try_update_bang do
    # use a number outside JRuby's fixnum cache range, to ensure identity is preserved
    atomic = described_class.new(1000)
    res    = atomic.try_update! { |v| v + 1 }

    expect(atomic.value).to eq 1001
    expect(res).to eq 1001
  end

  specify :test_swap do
    atomic = described_class.new(1000)
    res    = atomic.swap(1001)

    expect(atomic.value).to eq 1001
    expect(res).to eq 1000
  end

  specify :test_try_update_fails do
    # use a number outside JRuby's fixnum cache range, to ensure identity is preserved
    atomic = described_class.new(1000)
    expect(
        # assigning within block exploits implementation detail for test
        atomic.try_update { |v| atomic.value = 1001; v + 1 }
    ).to be_falsey
  end

  specify :test_try_update_bang_fails do
    # use a number outside JRuby's fixnum cache range, to ensure identity is preserved
    atomic = described_class.new(1000)
    expect {
      # assigning within block exploits implementation detail for test
      atomic.try_update! { |v| atomic.value = 1001; v + 1 }
    }.to raise_error Concurrent::ConcurrentUpdateError
  end

  specify :test_update_retries do
    tries = 0
    # use a number outside JRuby's fixnum cache range, to ensure identity is preserved
    atomic = described_class.new(1000)
    # assigning within block exploits implementation detail for test
    atomic.update { |v| tries += 1; atomic.value = 1001; v + 1 }

    expect(tries).to eq 2
  end

  specify :test_numeric_cas do
    atomic = described_class.new(0)

    # 9-bit idempotent Fixnum (JRuby)
    max_8 = 2 ** 256 - 1
    min_8 = -(2 ** 256)

    atomic.set(max_8)
    max_8.upto(max_8 + 2) do |i|
      expect(atomic.compare_and_swap(i, i + 1)).to be_truthy, "CAS failed for numeric #{i} => #{i + 1}"
    end

    atomic.set(min_8)
    min_8.downto(min_8 - 2) do |i|
      expect(atomic.compare_and_swap(i, i - 1)).to be_truthy, "CAS failed for numeric #{i} => #{i - 1}"
    end

    # 64-bit idempotent Fixnum (MRI, TruffleRuby)
    max_64 = 2 ** 62 - 1
    min_64 = -(2 ** 62)

    atomic.set(max_64)
    max_64.upto(max_64 + 2) do |i|
      expect(atomic.compare_and_swap(i, i + 1)).to be_truthy, "CAS failed for numeric #{i} => #{i + 1}"
    end

    atomic.set(min_64)
    min_64.downto(min_64 - 2) do |i|
      expect(atomic.compare_and_swap(i, i - 1)).to be_truthy, "CAS failed for numeric #{i} => #{i - 1}"
    end

    ## 64-bit overflow into Bignum (JRuby)
    max_64 = 2 ** 63 - 1
    min_64 = (-2 ** 63)

    atomic.set(max_64)
    max_64.upto(max_64 + 2) do |i|
      expect(atomic.compare_and_swap(i, i + 1)).to be_truthy, "CAS failed for numeric #{i} => #{i + 1}"
    end

    atomic.set(min_64)
    min_64.downto(min_64 - 2) do |i|
      expect(atomic.compare_and_swap(i, i - 1)).to be_truthy, "CAS failed for numeric #{i} => #{i - 1}"
    end

    # non-idempotent Float (JRuby, MRI < 2.0.0 or 32-bit)
    atomic.set(1.0 + 0.1)
    expect(atomic.compare_and_set(1.0 + 0.1, 1.2)).to be_truthy, "CAS failed for #{1.0 + 0.1} => 1.2"

    # Bignum
    atomic.set(2 ** 100)
    expect(atomic.compare_and_set(2 ** 100, 0)).to be_truthy, "CAS failed for #{2 ** 100} => 0"

    # Rational
    require 'rational' unless ''.respond_to? :to_r
    atomic.set(Rational(1, 3))
    expect(atomic.compare_and_set(Rational(1, 3), 0)).to be_truthy, "CAS failed for #{Rational(1, 3)} => 0"

    # Complex
    require 'complex' unless ''.respond_to? :to_c
    atomic.set(Complex(1, 2))
    expect(atomic.compare_and_set(Complex(1, 2), 0)).to be_truthy, "CAS failed for #{Complex(1, 2)} => 0"
  end
end

module Concurrent

  RSpec.describe AtomicReference do
    it_should_behave_like :atomic_reference

    describe '#to_s and #inspect' do
      it 'includes the value' do
        subject = described_class.new('kajhsd')
        expect(subject.to_s).to include('kajhsd')
        expect(subject.inspect).to include('kajhsd')
      end
    end
  end

  RSpec.describe MutexAtomicReference do
    it_should_behave_like :atomic_reference
  end

  if Concurrent.allow_c_extensions?
    RSpec.describe CAtomicReference do
      it_should_behave_like :atomic_reference
    end
  end
  if Concurrent.on_jruby?
    RSpec.describe JavaAtomicReference do
      it_should_behave_like :atomic_reference
    end
  end
  if Concurrent.on_truffleruby?
    RSpec.describe TruffleRubyAtomicReference do
      it_should_behave_like :atomic_reference
    end
  end

  RSpec.describe AtomicReference do
    if RUBY_ENGINE != 'ruby'
      it 'does not load the C extension' do
        expect(defined?(Concurrent::CAtomicReference)).to be_falsey
      end
    end

    if Concurrent.on_jruby?
      it 'inherits from JavaAtomicReference' do
        expect(described_class.ancestors).to include(Concurrent::JavaAtomicReference)
      end
    elsif Concurrent.allow_c_extensions?
      it 'inherits from CAtomicReference' do
        expect(described_class.ancestors).to include(Concurrent::CAtomicReference)
      end
    elsif Concurrent.on_truffleruby?
      it 'inherits from TruffleRubyAtomicReference' do
        expect(described_class.ancestors).to include(Concurrent::TruffleRubyAtomicReference)
      end
    else
      it 'inherits from MutexAtomicReference' do
        expect(described_class.ancestors).to include(Concurrent::MutexAtomicReference)
      end
    end
  end
end