Testing Async Module Implementation in concurrent-ruby
This test suite validates the Concurrent::Async module in Ruby, focusing on asynchronous method execution and concurrent programming patterns. It ensures proper handling of async/await operations, method delegation, and thread-safe execution in concurrent Ruby applications.
Test Coverage Overview
Implementation Analysis
Technical Details
Best Practices Demonstrated
ruby-concurrency/concurrent-ruby
spec/concurrent/async_spec.rb
require 'concurrent/async'
module Concurrent
RSpec.describe Async do
let(:async_class) do
Class.new do
include Concurrent::Async
attr_accessor :accessor
def initialize(*args)
end
def echo(msg)
msg
end
def gather(first, second = nil)
return first, second
end
def boom(ex = StandardError.new)
raise ex
end
def wait(seconds)
sleep(seconds)
end
def with_block
yield
end
end
end
subject do
async_class.new
end
context 'object creation' do
it 'delegates to the original constructor' do
args = [:foo, 'bar', 42]
expect(async_class).to receive(:original_new).once.with(*args).and_call_original
async_class.new(*args)
end
specify 'passes all args to the original constructor' do
clazz = Class.new do
include Concurrent::Async
attr_reader :args
def initialize(*args)
@args = args
end
end
object = clazz.new(:foo, :bar)
expect(object.args).to eq [:foo, :bar]
end
specify 'passes a given block to the original constructor' do
clazz = Class.new do
include Concurrent::Async
attr_reader :block
def initialize(&block)
@block = yield
end
end
object = clazz.new{ 42 }
expect(object.block).to eq 42
end
specify 'initializes synchronization' do
mock = async_class.new
allow(async_class).to receive(:original_new).and_return(mock)
expect(mock).to receive(:init_synchronization).once.with(no_args)
async_class.new
end
end
context '#validate_argc' do
subject do
Class.new {
def zero() nil; end
def three(a, b, c, &block) nil; end
def two_plus_two(a, b, c=nil, d=nil, &block) nil; end
def many(*args, &block) nil; end
}.new
end
it 'raises an exception when the method is not defined' do
expect {
Async::validate_argc(subject, :bogus)
}.to raise_error(StandardError)
end
it 'raises an exception for too many args on a zero arity method' do
expect {
Async::validate_argc(subject, :zero, 1, 2, 3)
}.to raise_error(ArgumentError)
end
it 'does not raise an exception for correct zero arity' do
expect {
Async::validate_argc(subject, :zero)
}.not_to raise_error
end
it 'raises an exception for too many args on a method with positive arity' do
expect {
Async::validate_argc(subject, :three, 1, 2, 3, 4)
}.to raise_error(ArgumentError)
end
it 'raises an exception for too few args on a method with positive arity' do
expect {
Async::validate_argc(subject, :three, 1, 2)
}.to raise_error(ArgumentError)
end
it 'does not raise an exception for correct positive arity' do
expect {
Async::validate_argc(subject, :three, 1, 2, 3)
}.not_to raise_error
end
it 'raises an exception for too few args on a method with negative arity' do
expect {
Async::validate_argc(subject, :two_plus_two, 1)
}.to raise_error(ArgumentError)
end
it 'does not raise an exception for correct negative arity' do
expect {
Async::validate_argc(subject, :two_plus_two, 1, 2)
Async::validate_argc(subject, :two_plus_two, 1, 2, 3, 4)
Async::validate_argc(subject, :two_plus_two, 1, 2, 3, 4, 5, 6)
Async::validate_argc(subject, :many)
Async::validate_argc(subject, :many, 1, 2)
Async::validate_argc(subject, :many, 1, 2, 3, 4)
}.not_to raise_error
end
end
context '#async' do
it 'raises an error when calling a method that does not exist' do
expect {
subject.async.bogus
}.to raise_error(StandardError)
end
it 'raises an error when passing too few arguments' do
expect {
subject.async.gather
}.to raise_error(ArgumentError)
end
it 'raises an error when passing too many arguments (arity >= 0)' do
expect {
subject.async.echo(1, 2, 3, 4, 5)
}.to raise_error(StandardError)
end
it 'returns the existence of the method' do
expect(subject.async.respond_to?(:echo)).to be_truthy
expect(subject.async.respond_to?(:not_exist_method)).to be_falsy
end
it 'returns a :pending IVar' do
val = subject.async.wait(1)
expect(val).to be_a Concurrent::IVar
expect(val).to be_pending
end
it 'runs the future on the global executor' do
expect(Concurrent.global_io_executor).to receive(:post).with(any_args).
and_call_original
subject.async.echo(:foo)
end
it 'sets the value on success' do
val = subject.async.echo(:foo)
expect(val.value).to eq :foo
expect(val).to be_fulfilled
end
it 'sets the reason on failure' do
ex = ArgumentError.new
val = subject.async.boom(ex)
val.wait
expect(val.reason).to eq ex
expect(val).to be_rejected
end
it 'sets the reason when giving too many optional arguments' do
val = subject.async.gather(1, 2, 3, 4, 5)
val.wait
expect(val.reason).to be_a StandardError
expect(val).to be_rejected
end
it 'supports attribute accessors' do
subject.async.accessor = :foo
val = subject.async.accessor
expect(val.value).to eq :foo
expect(subject.accessor).to eq :foo
end
it 'supports methods with blocks' do
val = subject.async.with_block{ :foo }
expect(val.value).to eq :foo
end
end
context '#await' do
it 'raises an error when calling a method that does not exist' do
expect {
subject.await.bogus
}.to raise_error(StandardError)
end
it 'raises an error when passing too few arguments' do
expect {
subject.await.gather
}.to raise_error(ArgumentError)
end
it 'raises an error when passing too many arguments (arity >= 0)' do
expect {
subject.await.echo(1, 2, 3, 4, 5)
}.to raise_error(StandardError)
end
it 'returns the existence of the method' do
expect(subject.await.respond_to?(:echo)).to be_truthy
expect(subject.await.respond_to?(:not_exist_method)).to be_falsy
end
it 'returns a :fulfilled IVar' do
val = subject.await.echo(5)
expect(val).to be_a Concurrent::IVar
expect(val).to be_fulfilled
end
it 'runs the future on the global executor' do
expect(Concurrent.global_io_executor).to receive(:post).with(any_args).
and_call_original
subject.await.echo(:foo)
end
it 'sets the value on success' do
val = subject.await.echo(:foo)
expect(val.value).to eq :foo
expect(val).to be_fulfilled
end
it 'sets the reason on failure' do
ex = ArgumentError.new
val = subject.await.boom(ex)
expect(val.reason).to eq ex
expect(val).to be_rejected
end
it 'sets the reason when giving too many optional arguments' do
val = subject.await.gather(1, 2, 3, 4, 5)
expect(val.reason).to be_a StandardError
expect(val).to be_rejected
end
it 'supports attribute accessors' do
subject.await.accessor = :foo
val = subject.await.accessor
expect(val.value).to eq :foo
expect(subject.accessor).to eq :foo
end
it 'supports methods with blocks' do
val = subject.await.with_block{ :foo }
expect(val.value).to eq :foo
end
end
context 'locking' do
it 'uses the same lock for both #async and #await' do
object = Class.new {
include Concurrent::Async
attr_reader :bucket
def gather(seconds, first, *rest)
sleep(seconds)
(@bucket ||= []).concat([first])
@bucket.concat(rest)
end
}.new
object.async.gather(0.5, :a, :b)
object.await.gather(0, :c, :d)
expect(object.bucket).to eq [:a, :b, :c, :d]
end
end
context 'fork safety' do
it 'does not hang when forked' do
skip "Platform does not support fork" unless Process.respond_to?(:fork)
object = Class.new {
include Concurrent::Async
def foo; end
}.new
object.async.foo
_, status = Process.waitpid2(fork {object.await.foo})
expect(status.exitstatus).to eq 0
end
end
end
end