Back to Repositories

Testing SafeTaskExecutor Thread Synchronization in concurrent-ruby

This test suite validates the SafeTaskExecutor class in the concurrent-ruby library, focusing on safe execution of tasks with error handling and thread synchronization. The tests verify proper task execution, error handling, and thread-safe behavior.

Test Coverage Overview

The test suite provides comprehensive coverage of the SafeTaskExecutor functionality.

Key areas tested include:
  • Successful task execution and return value handling
  • Error handling and exception rescue behavior
  • Thread synchronization and mutex protection
  • Argument passing to executed tasks
  • CRuby-specific local jump error handling

Implementation Analysis

The testing approach uses RSpec’s context-based structure to organize test scenarios logically. The implementation leverages RSpec’s let blocks for setup and subject for test focus.

Testing patterns include:
  • Separate contexts for happy path and failure scenarios
  • Mock expectations for synchronization verification
  • Platform-specific test conditionals
  • Exception handling validation

Technical Details

Testing tools and configuration:
  • RSpec as the testing framework
  • Proc objects for task simulation
  • Thread-based execution testing
  • Platform detection for CRuby-specific tests
  • Version-specific conditional testing

Best Practices Demonstrated

The test suite exemplifies high-quality testing practices through organized and thorough test coverage.

Notable practices include:
  • Clear separation of concerns between test contexts
  • Comprehensive edge case coverage
  • Platform-specific test isolation
  • Explicit error condition testing
  • Clean and maintainable test structure

ruby-concurrency/concurrent-ruby

spec/concurrent/executor/safe_task_executor_spec.rb

            
require 'concurrent/executor/safe_task_executor'

module Concurrent

  RSpec.describe SafeTaskExecutor do

    describe '#execute' do

      context 'happy execution' do

        let(:task) { Proc.new { 42 } }
        subject { SafeTaskExecutor.new(task) }

        it 'should return success' do
          success, _value, _reason = subject.execute
          expect(success).to be_truthy
        end

        it 'should return task value' do
          _success, value, _reason = subject.execute
          expect(value).to eq 42
        end

        it 'should return a nil reason' do
          _success, _value, reason = subject.execute
          expect(reason).to be_nil
        end

        it 'passes all arguments to #execute to the task' do
          expected = nil
          task = proc {|*args| expected = args }
          SafeTaskExecutor.new(task).execute(1, 2, 3)
          expect(expected).to eq [1, 2, 3]
        end

        it 'protects #execute with a mutex' do
          expect(subject).to receive(:synchronize).with(no_args)
          subject.execute
        end
      end

      context 'failing execution' do

        let(:task) { Proc.new { raise StandardError.new('an error') } }
        subject { SafeTaskExecutor.new(task) }

        it 'should return false success' do
          success, _value, _reason = subject.execute
          expect(success).to be_falsey
        end

        it 'should return a nil value' do
          _success, value, _reason = subject.execute
          expect(value).to be_nil
        end

        it 'should return the reason' do
          _success, _value, reason = subject.execute
          expect(reason).to be_a(StandardError)
          expect(reason.message).to eq 'an error'
        end

        it 'rescues Exception when :rescue_exception is true' do
          task = proc { raise Exception }
          subject = SafeTaskExecutor.new(task, rescue_exception: true)
          expect {
            subject.execute
          }.to_not raise_error
        end

        it 'rescues StandardError when :rescue_exception is false' do
          task = proc { raise StandardError }
          subject = SafeTaskExecutor.new(task, rescue_exception: false)
          expect {
            subject.execute
          }.to_not raise_error

          task = proc { raise Exception }
          subject = SafeTaskExecutor.new(task, rescue_exception: false)
          expect {
            subject.execute
          }.to raise_error(Exception)
        end

        it 'rescues StandardError by default' do
          task = proc { raise StandardError }
          subject = SafeTaskExecutor.new(task)
          expect {
            subject.execute
          }.to_not raise_error

          task = proc { raise Exception }
          subject = SafeTaskExecutor.new(task)
          expect {
            subject.execute
          }.to raise_error(Exception)
        end
      end

      # These tests only make sense on CRuby as they test a workaround for CRuby bugs: https://github.com/ruby-concurrency/concurrent-ruby/issues/931
      if Concurrent.on_cruby? and Concurrent.ruby_version(:<, 3, 2, 0)
        context 'local jump error' do
          def execute
            Thread.new do
              executor = SafeTaskExecutor.new(-> { yield 42 })
              @result = executor.execute
            end.join
          end

          subject do
            to_enum(:execute).first
            @result
          end

          it 'should return success' do
            success, _value, _reason = subject
            expect(success).to be_truthy
          end

          it 'should return a nil value' do
            _success, value, _reason = subject
            expect(value).to be_nil
          end

          it 'should return a nil reason' do
            _success, _value, reason = subject
            expect(reason).to be_nil
          end
        end
      end
    end
  end
end